} The response from the axios GET request.
+ *
+ */
+ async getAggregateStatsById(authToken, monitorId, dateRange) {
+ const params = new URLSearchParams();
+ if (dateRange) params.append("dateRange", dateRange);
+
+ return this.axiosInstance.get(
+ `/monitors/aggregate/${monitorId}?${params.toString()}`,
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ }
+ );
+ }
+
/**
* ************************************
* Updates a single monitor
diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js
index 15df7c1b8..e54d9eaf4 100644
--- a/Client/src/Utils/Theme/darkTheme.js
+++ b/Client/src/Utils/Theme/darkTheme.js
@@ -4,16 +4,16 @@ const text = {
primary: "#fafafa",
secondary: "#e6e6e6",
tertiary: "#a1a1aa",
- accent: "#e6e6e6",
+ accent: "#8e8e8f",
disabled: "rgba(172, 172, 172, 0.3)",
};
const background = {
main: "#151518",
alt: "#09090b",
- fill: "#2e2e2e",
+ fill: "#2D2D33",
accent: "#18181a",
};
-const border = { light: "#27272a", dark: "#2c2c2c" };
+const border = { light: "#27272a", dark: "#36363e" };
const fontFamilyDefault =
'"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif';
@@ -25,7 +25,7 @@ const darkTheme = createTheme({
palette: {
mode: "dark",
primary: { main: "#1570ef" },
- secondary: { main: "#2e2e2e" },
+ secondary: { main: "#2D2D33" },
text: text,
background: background,
border: border,
@@ -39,24 +39,30 @@ const darkTheme = createTheme({
success: {
text: "#079455",
main: "#45bb7a",
- light: "#1e1e1e",
- bg: "#27272a",
+ light: "#1c4428",
+ bg: "#12261e",
},
error: {
text: "#f04438",
main: "#d32f2f",
- light: "#1e1e1e",
- bg: "#27272a",
+ light: "#542426",
+ bg: "#301a1f",
dark: "#932020",
border: "#f04438",
},
warning: {
text: "#e88c30",
main: "#FF9F00",
- light: "#27272a",
- bg: "#1E1E1E",
+ light: "#272115",
+ bg: "#624711",
border: "#e88c30",
},
+ percentage: {
+ uptimePoor: "#d32f2f",
+ uptimeFair: "#e88c30",
+ uptimeGood: "#ffd600",
+ uptimeExcellent: "#079455",
+ },
unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" },
divider: border.light,
other: {
@@ -92,6 +98,15 @@ const darkTheme = createTheme({
backgroundColor: theme.palette.secondary.main,
},
},
+ {
+ props: (props) =>
+ props.variant === "contained" && props.color === "secondary",
+ style: {
+ border: 1,
+ borderStyle: "solid",
+ borderColor: theme.palette.border.dark,
+ },
+ },
],
fontWeight: 400,
borderRadius: 4,
@@ -205,6 +220,13 @@ const darkTheme = createTheme({
},
},
},
+ MuiSkeleton: {
+ styleOverrides: {
+ root: {
+ backgroundColor: "#151518",
+ },
+ },
+ },
},
shape: {
borderRadius: 2,
diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js
index bb83d09b2..60a8e0b2d 100644
--- a/Client/src/Utils/Theme/lightTheme.js
+++ b/Client/src/Utils/Theme/lightTheme.js
@@ -54,6 +54,12 @@ const lightTheme = createTheme({
bg: "#ffecbc",
border: "#fec84b",
},
+ percentage: {
+ uptimePoor: "#d32f2f",
+ uptimeFair: "#ec8013",
+ uptimeGood: "#ffb800",
+ uptimeExcellent: "#079455",
+ },
unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" },
divider: border.light,
other: {
@@ -89,6 +95,15 @@ const lightTheme = createTheme({
backgroundColor: theme.palette.secondary.main,
},
},
+ {
+ props: (props) =>
+ props.variant === "contained" && props.color === "secondary",
+ style: {
+ border: 1,
+ borderStyle: "solid",
+ borderColor: theme.palette.border.light,
+ },
+ },
],
fontWeight: 400,
borderRadius: 4,
@@ -201,6 +216,13 @@ const lightTheme = createTheme({
},
},
},
+ MuiSkeleton: {
+ styleOverrides: {
+ root: {
+ backgroundColor: "#f2f4f7",
+ },
+ },
+ },
},
shape: {
borderRadius: 2,
diff --git a/Client/src/Utils/greeting.jsx b/Client/src/Utils/greeting.jsx
index cb878c991..7da08b340 100644
--- a/Client/src/Utils/greeting.jsx
+++ b/Client/src/Utils/greeting.jsx
@@ -160,6 +160,7 @@ const Greeting = ({ type = "" }) => {
lineHeight={1}
fontWeight={500}
color={theme.palette.text.primary}
+ mb={theme.spacing(1)}
>
{
{append} — Here’s an overview of your {type} monitors.
diff --git a/Client/src/Utils/timeUtils.js b/Client/src/Utils/timeUtils.js
index 2d1a2f180..f9f916d04 100644
--- a/Client/src/Utils/timeUtils.js
+++ b/Client/src/Utils/timeUtils.js
@@ -43,6 +43,23 @@ export const formatDurationRounded = (ms) => {
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 formatDate = (date, customOptions) => {
const options = {
year: "numeric",
@@ -51,9 +68,11 @@ export const formatDate = (date, customOptions) => {
hour: "numeric",
minute: "numeric",
hour12: true,
- ...customOptions
+ ...customOptions,
};
// Return the date using the specified options
- return date.toLocaleString("en-US", options);
+ return date
+ .toLocaleString("en-US", options)
+ .replace(/\b(AM|PM)\b/g, (match) => match.toLowerCase());
};
diff --git a/Client/src/assets/data/hostnames.json b/Client/src/assets/data/hostnames.json
new file mode 100644
index 000000000..b66818c3a
--- /dev/null
+++ b/Client/src/assets/data/hostnames.json
@@ -0,0 +1,1290 @@
+[
+ {
+ "Company Name": "0to255",
+ "Company Link ": "https://0to255.com"
+ },
+ {
+ "Company Name": "10015.io",
+ "Company Link ": "https://10015.io"
+ },
+ {
+ "Company Name": "3DIcons",
+ "Company Link ": "https://3dicons.co"
+ },
+ {
+ "Company Name": "About.me",
+ "Company Link ": "https://about.me"
+ },
+ {
+ "Company Name": "Alias",
+ "Company Link ": "https://alias.co"
+ },
+ {
+ "Company Name": "All About Berlin",
+ "Company Link ": "https://allaboutberlin.com"
+ },
+ {
+ "Company Name": "All Acronyms",
+ "Company Link ": "https://allacronyms.com"
+ },
+ {
+ "Company Name": "All You Can Read ",
+ "Company Link ": "https://allyoucanread.com"
+ },
+ {
+ "Company Name": "AllTrails",
+ "Company Link ": "https://alltrails.com"
+ },
+ {
+ "Company Name": "Anotepad",
+ "Company Link ": "https://anotepad.com"
+ },
+ {
+ "Company Name": "AnswerSocrates",
+ "Company Link ": "https://answersocrates.com"
+ },
+ {
+ "Company Name": "AnswerThePublic ",
+ "Company Link ": "https://answerthepublic.com"
+ },
+ {
+ "Company Name": "Apollo ",
+ "Company Link ": "https://apollo.io"
+ },
+ {
+ "Company Name": "ArrayList",
+ "Company Link ": "https://arraylist.org"
+ },
+ {
+ "Company Name": "Ask Difference",
+ "Company Link ": "https://askdifference.com"
+ },
+ {
+ "Company Name": "Audd.io",
+ "Company Link ": "https://audd.io"
+ },
+ {
+ "Company Name": "Audiocheck",
+ "Company Link ": "https://audiocheck.net"
+ },
+ {
+ "Company Name": "Audionautix",
+ "Company Link ": "https://audionautix.com"
+ },
+ {
+ "Company Name": "Authentic Jobs",
+ "Company Link ": "https://authenticjobs.com"
+ },
+ {
+ "Company Name": "Behind the Name",
+ "Company Link ": "https://behindthename.com"
+ },
+ {
+ "Company Name": "Bilim Terimleri",
+ "Company Link ": "https://terimler.org"
+ },
+ {
+ "Company Name": "BitBof",
+ "Company Link ": "https://bitbof.com"
+ },
+ {
+ "Company Name": "Blank Page",
+ "Company Link ": "https://blank.page"
+ },
+ {
+ "Company Name": "Bonanza",
+ "Company Link ": "https://bonanza.com"
+ },
+ {
+ "Company Name": "BookCrossing",
+ "Company Link ": "https://bookcrossing.com"
+ },
+ {
+ "Company Name": "Browse AI",
+ "Company Link ": "https://browse.ai"
+ },
+ {
+ "Company Name": "Bubbl.us",
+ "Company Link ": "https://bubbl.us"
+ },
+ {
+ "Company Name": "Business Model Toolbox",
+ "Company Link ": "https://bmtoolbox.net"
+ },
+ {
+ "Company Name": "ByClickDownloader",
+ "Company Link ": "https://byclickdownloader.com"
+ },
+ {
+ "Company Name": "Calligraphr",
+ "Company Link ": "https://calligraphr.com"
+ },
+ {
+ "Company Name": "CertificateClaim",
+ "Company Link ": "https://certificateclaim.com"
+ },
+ {
+ "Company Name": "Chosic",
+ "Company Link ": "https://chosic.com"
+ },
+ {
+ "Company Name": "ClipDrop",
+ "Company Link ": "https://clipdrop.co"
+ },
+ {
+ "Company Name": "CloudConvert",
+ "Company Link ": "https://cloudconvert.com"
+ },
+ {
+ "Company Name": "CodingFont",
+ "Company Link ": "https://codingfont.com"
+ },
+ {
+ "Company Name": "Color Hunt",
+ "Company Link ": "https://colorhunt.co"
+ },
+ {
+ "Company Name": "ColorHexa",
+ "Company Link ": "https://colorhexa.com"
+ },
+ {
+ "Company Name": "Conversion-Tool",
+ "Company Link ": "https://conversion-tool.com"
+ },
+ {
+ "Company Name": "Cool Startup Jobs",
+ "Company Link ": "https://coolstartupjobs.com"
+ },
+ {
+ "Company Name": "Coroflot",
+ "Company Link ": "https://coroflot.com"
+ },
+ {
+ "Company Name": "Corrupt-a-File",
+ "Company Link ": "https://corrupt-a-file.net"
+ },
+ {
+ "Company Name": "Couchsurfing",
+ "Company Link ": "https://couchsurfing.com"
+ },
+ {
+ "Company Name": "Countries Been",
+ "Company Link ": "https://countriesbeen.com"
+ },
+ {
+ "Company Name": "Country Code",
+ "Company Link ": "https://countrycode.org"
+ },
+ {
+ "Company Name": "Creately",
+ "Company Link ": "https://creately.com"
+ },
+ {
+ "Company Name": "Creately ",
+ "Company Link ": "https://creately.com"
+ },
+ {
+ "Company Name": "Crossfade.io",
+ "Company Link ": "https://crossfade.io"
+ },
+ {
+ "Company Name": "Crunchbase",
+ "Company Link ": "https://crunchbase.com"
+ },
+ {
+ "Company Name": "CVmkr",
+ "Company Link ": "https://cvwizard.com"
+ },
+ {
+ "Company Name": "Daily Remote",
+ "Company Link ": "https://dailyremote.com"
+ },
+ {
+ "Company Name": "David Li",
+ "Company Link ": "https://david.li"
+ },
+ {
+ "Company Name": "DemandHunt",
+ "Company Link ": "https://demandhunt.com"
+ },
+ {
+ "Company Name": "Designify",
+ "Company Link ": "https://designify.com"
+ },
+ {
+ "Company Name": "Diff Checker",
+ "Company Link ": "https://diffchecker.com"
+ },
+ {
+ "Company Name": "DifferenceBetween.info",
+ "Company Link ": "https://differencebetween.info"
+ },
+ {
+ "Company Name": "Digital Glossary",
+ "Company Link ": "https://digital-glossary.com"
+ },
+ {
+ "Company Name": "Dimensions",
+ "Company Link ": "https://dimensions.com"
+ },
+ {
+ "Company Name": "Discoverify Music",
+ "Company Link ": "https://discoverifymusic.com"
+ },
+ {
+ "Company Name": "discu.eu",
+ "Company Link ": "https://discu.eu"
+ },
+ {
+ "Company Name": "Do It Yourself",
+ "Company Link ": "https://doityourself.com"
+ },
+ {
+ "Company Name": "Donkey Car",
+ "Company Link ": "https://donkeycar.com"
+ },
+ {
+ "Company Name": "draw.io",
+ "Company Link ": "https://drawio.com"
+ },
+ {
+ "Company Name": "Drumeo",
+ "Company Link ": "https://drumeo.com"
+ },
+ {
+ "Company Name": "Dummies",
+ "Company Link ": "https://dummies.com"
+ },
+ {
+ "Company Name": "Easel.ly",
+ "Company Link ": "https://easel.ly"
+ },
+ {
+ "Company Name": "Educalingo",
+ "Company Link ": "https://educalingo.com"
+ },
+ {
+ "Company Name": "Emoji Combos",
+ "Company Link ": "https://emojicombos.com"
+ },
+ {
+ "Company Name": "EquityBee",
+ "Company Link ": "https://equitybee.com"
+ },
+ {
+ "Company Name": "EquityZen",
+ "Company Link ": "https://equityzen.com"
+ },
+ {
+ "Company Name": "Escape Room Tips",
+ "Company Link ": "https://escaperoomtips.com"
+ },
+ {
+ "Company Name": "Every Noise",
+ "Company Link ": "https://everynoise.com"
+ },
+ {
+ "Company Name": "Every Time Zone",
+ "Company Link ": "https://everytimezone.com"
+ },
+ {
+ "Company Name": "Excalideck",
+ "Company Link ": "https://excalideck.com"
+ },
+ {
+ "Company Name": "Excalidraw",
+ "Company Link ": "https://excalidraw.com"
+ },
+ {
+ "Company Name": "Extract pics",
+ "Company Link ": "https://extract.pics"
+ },
+ {
+ "Company Name": "EZGIF",
+ "Company Link ": "https://ezgif.com"
+ },
+ {
+ "Company Name": "FactSlides",
+ "Company Link ": "https://factslides.com"
+ },
+ {
+ "Company Name": "FIGR ",
+ "Company Link ": "https://figr.app"
+ },
+ {
+ "Company Name": "Fine Dictionary",
+ "Company Link ": "https://finedictionary.com"
+ },
+ {
+ "Company Name": "Fiverr",
+ "Company Link ": "https://fiverr.com"
+ },
+ {
+ "Company Name": "Fix It Club",
+ "Company Link ": "https://fixitclub.com"
+ },
+ {
+ "Company Name": "FlexJobs",
+ "Company Link ": "https://flexjobs.com"
+ },
+ {
+ "Company Name": "Flightradar24",
+ "Company Link ": "https://flightradar24.com"
+ },
+ {
+ "Company Name": "FlowCV ",
+ "Company Link ": "https://flowcv.com"
+ },
+ {
+ "Company Name": "Foco Clipping",
+ "Company Link ": "https://fococlipping.com"
+ },
+ {
+ "Company Name": "Font Squirrel",
+ "Company Link ": "https://fontsquirrel.com"
+ },
+ {
+ "Company Name": "FontAwesome",
+ "Company Link ": "https://fontawesome.com"
+ },
+ {
+ "Company Name": "Fontello ",
+ "Company Link ": "https://fontello.com"
+ },
+ {
+ "Company Name": "Form to Chatbot",
+ "Company Link ": "https://formtochatbot.com"
+ },
+ {
+ "Company Name": "Founder Resources",
+ "Company Link ": "https://founderresources.io"
+ },
+ {
+ "Company Name": "Franz",
+ "Company Link ": "https://meetfranz.com"
+ },
+ {
+ "Company Name": "Fraze It",
+ "Company Link ": "https://fraze.it"
+ },
+ {
+ "Company Name": "Freecycle",
+ "Company Link ": "https://freecycle.org"
+ },
+ {
+ "Company Name": "FreeType",
+ "Company Link ": "https://freetype.org"
+ },
+ {
+ "Company Name": "FutureM",
+ "Company Link ": "https://futureme.org"
+ },
+ {
+ "Company Name": "Generated.Photos",
+ "Company Link ": "https://generated.photos"
+ },
+ {
+ "Company Name": "Get Human",
+ "Company Link ": "https://gethuman.com"
+ },
+ {
+ "Company Name": "Go Bento",
+ "Company Link ": "https://gobento.com"
+ },
+ {
+ "Company Name": "Good CV",
+ "Company Link ": "https://goodcv.com"
+ },
+ {
+ "Company Name": "Grammar Monster",
+ "Company Link ": "https://grammar-monster.com"
+ },
+ {
+ "Company Name": "Grammar Book",
+ "Company Link ": "https://grammarbook.com"
+ },
+ {
+ "Company Name": "Gummy Search",
+ "Company Link ": "https://gummysearch.com"
+ },
+ {
+ "Company Name": "Gumroad",
+ "Company Link ": "https://gumroad.com"
+ },
+ {
+ "Company Name": "HealthIcons",
+ "Company Link ": "https://healthicons.org"
+ },
+ {
+ "Company Name": "HexColor",
+ "Company Link ": "https://hexcolor.co"
+ },
+ {
+ "Company Name": "Hidden Life Radio",
+ "Company Link ": "https://hiddenliferadio.com"
+ },
+ {
+ "Company Name": "Hired",
+ "Company Link ": "https://lhh.com"
+ },
+ {
+ "Company Name": "Honey",
+ "Company Link ": "https://joinhoney.com"
+ },
+ {
+ "Company Name": "HowStuffWorks",
+ "Company Link ": "https://howstuffworks.com"
+ },
+ {
+ "Company Name": "HugeIcons Pro",
+ "Company Link ": "https://hugeicons.com"
+ },
+ {
+ "Company Name": "Humble Bundle",
+ "Company Link ": "https://humblebundle.com"
+ },
+ {
+ "Company Name": "I Have No TV",
+ "Company Link ": "https://ihavenotv.com"
+ },
+ {
+ "Company Name": "I Miss the Office",
+ "Company Link ": "https://imisstheoffice.eu"
+ },
+ {
+ "Company Name": "IcoMoon",
+ "Company Link ": "https://icomoon.io"
+ },
+ {
+ "Company Name": "Iconfinder",
+ "Company Link ": "https://iconfinder.com"
+ },
+ {
+ "Company Name": "Icon Packs",
+ "Company Link ": "https://iconpacks.net"
+ },
+ {
+ "Company Name": "Iconshock",
+ "Company Link ": "https://iconshock.com"
+ },
+ {
+ "Company Name": "Iconz Design",
+ "Company Link ": "https://iconz.design"
+ },
+ {
+ "Company Name": "iFixit",
+ "Company Link ": "https://ifixit.com"
+ },
+ {
+ "Company Name": "IFTTT",
+ "Company Link ": "https://ifttt.com"
+ },
+ {
+ "Company Name": "Illlustrations",
+ "Company Link ": "https://illlustrations.co"
+ },
+ {
+ "Company Name": "Illustration Kit",
+ "Company Link ": "https://illustrationkit.com"
+ },
+ {
+ "Company Name": "IMSDB",
+ "Company Link ": "https://imsdb.com"
+ },
+ {
+ "Company Name": "Incompetech",
+ "Company Link ": "https://incompetech.com"
+ },
+ {
+ "Company Name": "Incredibox",
+ "Company Link ": "https://incredibox.com"
+ },
+ {
+ "Company Name": "InnerBod",
+ "Company Link ": "https://innerbody.com"
+ },
+ {
+ "Company Name": "Instructables",
+ "Company Link ": "https://instructables.com"
+ },
+ {
+ "Company Name": "Integromat",
+ "Company Link ": "https://make.com"
+ },
+ {
+ "Company Name": "Investopedia",
+ "Company Link ": "https://investopedia.com"
+ },
+ {
+ "Company Name": "Japanese Wiki Corpus",
+ "Company Link ": "https://japanesewiki.com"
+ },
+ {
+ "Company Name": "Jitter.Video",
+ "Company Link ": "https://jitter.video"
+ },
+ {
+ "Company Name": "Jobspresso",
+ "Company Link ": "https://jobspresso.co"
+ },
+ {
+ "Company Name": "JPEG-Optimizer",
+ "Company Link ": "https://jpeg-optimizer.com"
+ },
+ {
+ "Company Name": "JS Remotely",
+ "Company Link ": "https://jsremotely.com"
+ },
+ {
+ "Company Name": "JScreenFix",
+ "Company Link ": "https://jscreenfix.com"
+ },
+ {
+ "Company Name": "JSON Resume",
+ "Company Link ": "https://jsonresume.io"
+ },
+ {
+ "Company Name": "Just Join",
+ "Company Link ": "https://justjoin.it"
+ },
+ {
+ "Company Name": "Just the Recipe",
+ "Company Link ": "https://justtherecipe.com"
+ },
+ {
+ "Company Name": "JustRemote",
+ "Company Link ": "https://justremote.co"
+ },
+ {
+ "Company Name": "JustWatch",
+ "Company Link ": "https://justwatch.com"
+ },
+ {
+ "Company Name": "Kanopy",
+ "Company Link ": "https://kanopy.com"
+ },
+ {
+ "Company Name": "Kassellabs",
+ "Company Link ": "https://kassellabs.io"
+ },
+ {
+ "Company Name": "Key Differences",
+ "Company Link ": "https://keydifferences.com"
+ },
+ {
+ "Company Name": "Keybase",
+ "Company Link ": "https://keybase.io"
+ },
+ {
+ "Company Name": "KeyValues",
+ "Company Link ": "https://keyvalues.com"
+ },
+ {
+ "Company Name": "KHInsider",
+ "Company Link ": "https://khinsider.com"
+ },
+ {
+ "Company Name": "Killed by Google",
+ "Company Link ": "https://killedbygoogle.com"
+ },
+ {
+ "Company Name": "Kimovil",
+ "Company Link ": "https://kimovil.com"
+ },
+ {
+ "Company Name": "Lalal.ai",
+ "Company Link ": "https://www.lalal.ai"
+ },
+ {
+ "Company Name": "Learn Anything",
+ "Company Link ": "https://learn-anything.xyz"
+ },
+ {
+ "Company Name": "LendingTree",
+ "Company Link ": "https://lendingtree.com"
+ },
+ {
+ "Company Name": "Lightyear.fm",
+ "Company Link ": "https://lightyear.fm"
+ },
+ {
+ "Company Name": "LittleSis",
+ "Company Link ": "https://littlesis.org"
+ },
+ {
+ "Company Name": "Looria",
+ "Company Link ": "https://looria.com"
+ },
+ {
+ "Company Name": "Lucidchart",
+ "Company Link ": "https://lucidchart.com"
+ },
+ {
+ "Company Name": "Lunar",
+ "Company Link ": "https://lunar.fyi"
+ },
+ {
+ "Company Name": "Manuals Lib",
+ "Company Link ": "https://manualslib.com"
+ },
+ {
+ "Company Name": "Map Crunch",
+ "Company Link ": "https://mapcrunch.com"
+ },
+ {
+ "Company Name": "Masterworks",
+ "Company Link ": "https://masterworks.com"
+ },
+ {
+ "Company Name": "MediaFire",
+ "Company Link ": "https://mediafire.com"
+ },
+ {
+ "Company Name": "Mind Mup",
+ "Company Link ": "https://mindmup.com"
+ },
+ {
+ "Company Name": "Mixlr",
+ "Company Link ": "https://mixlr.com"
+ },
+ {
+ "Company Name": "Moises AI",
+ "Company Link ": "https://moises.ai"
+ },
+ {
+ "Company Name": "Money",
+ "Company Link ": "https://money.com"
+ },
+ {
+ "Company Name": "Mountain Project",
+ "Company Link ": "https://mountainproject.com"
+ },
+ {
+ "Company Name": "Movie Map",
+ "Company Link ": "https://movie-map.com"
+ },
+ {
+ "Company Name": "Movie Sounds",
+ "Company Link ": "https://movie-sounds.org"
+ },
+ {
+ "Company Name": "MP3Cut",
+ "Company Link ": "https://mp3cut.net"
+ },
+ {
+ "Company Name": "Murmel",
+ "Company Link ": "https://murmel.social"
+ },
+ {
+ "Company Name": "Muscle Wiki",
+ "Company Link ": "https://musclewiki.com"
+ },
+ {
+ "Company Name": "Music-Map",
+ "Company Link ": "https://music-map.com"
+ },
+ {
+ "Company Name": "MusicTheory.net",
+ "Company Link ": "https://musictheory.net"
+ },
+ {
+ "Company Name": "MyFonts",
+ "Company Link ": "https://myfonts.com"
+ },
+ {
+ "Company Name": "MyFridgeFood",
+ "Company Link ": "https://myfridgefood.com"
+ },
+ {
+ "Company Name": "Nameberry",
+ "Company Link ": "https://nameberry.com"
+ },
+ {
+ "Company Name": "Namechk",
+ "Company Link ": "https://namechk.com"
+ },
+ {
+ "Company Name": "Ncase",
+ "Company Link ": "https://ncase.me"
+ },
+ {
+ "Company Name": "News in Levels",
+ "Company Link ": "https://newsinlevels.com"
+ },
+ {
+ "Company Name": "Noisli",
+ "Company Link ": "https://noisli.com"
+ },
+ {
+ "Company Name": "Notes.io",
+ "Company Link ": "https://notes.io"
+ },
+ {
+ "Company Name": "Novoresume",
+ "Company Link ": "https://novoresume.com"
+ },
+ {
+ "Company Name": "Ocoya",
+ "Company Link ": "https://ocoya.com"
+ },
+ {
+ "Company Name": "Old Computers Museum",
+ "Company Link ": "https://oldcomputers.net"
+ },
+ {
+ "Company Name": "Online Tone Generator",
+ "Company Link ": "https://onlinetonegenerator.com"
+ },
+ {
+ "Company Name": "Online-Convert",
+ "Company Link ": "https://online-convert.com"
+ },
+ {
+ "Company Name": "OnlineConversion",
+ "Company Link ": "https://onlineconversion.com"
+ },
+ {
+ "Company Name": "Online OCR",
+ "Company Link ": "https://onlineocr.net"
+ },
+ {
+ "Company Name": "OpenWeatherMap",
+ "Company Link ": "https://openweathermap.org"
+ },
+ {
+ "Company Name": "OrgPad",
+ "Company Link ": "https://orgpad.com"
+ },
+ {
+ "Company Name": "Passport Index",
+ "Company Link ": "https://passportindex.org"
+ },
+ {
+ "Company Name": "PDF Candy",
+ "Company Link ": "https://pdfcandy.com"
+ },
+ {
+ "Company Name": "PDF2DOC",
+ "Company Link ": "https://pdf2doc.com"
+ },
+ {
+ "Company Name": "PDFescape",
+ "Company Link ": "https://pdfescape.com"
+ },
+ {
+ "Company Name": "PfpMaker",
+ "Company Link ": "https://pfpmaker.com"
+ },
+ {
+ "Company Name": "PIDGI Wiki ",
+ "Company Link ": "https://pidgi.net"
+ },
+ {
+ "Company Name": "PimEyes",
+ "Company Link ": "https://pimeyes.com"
+ },
+ {
+ "Company Name": "Pipl ",
+ "Company Link ": "https://pipl.com"
+ },
+ {
+ "Company Name": "PixelBazaar",
+ "Company Link ": "https://pixelbazaar.com"
+ },
+ {
+ "Company Name": "PixelPaper",
+ "Company Link ": "https://pixelpaper.io"
+ },
+ {
+ "Company Name": "Ponly",
+ "Company Link ": "https://ponly.com"
+ },
+ {
+ "Company Name": "PowerToFly",
+ "Company Link ": "https://powertofly.com"
+ },
+ {
+ "Company Name": "Pretzel Rocks",
+ "Company Link ": "https://pretzel.rocks"
+ },
+ {
+ "Company Name": "PrintIt",
+ "Company Link ": "https://printit.work"
+ },
+ {
+ "Company Name": "Prismatext",
+ "Company Link ": "https://prismatext.com"
+ },
+ {
+ "Company Name": "Puffin Maps",
+ "Company Link ": "https://puffinmaps.com"
+ },
+ {
+ "Company Name": "Puzzle Loop ",
+ "Company Link ": "https://puzzle-loop.com"
+ },
+ {
+ "Company Name": "QuoteMaster",
+ "Company Link ": "https://quotemaster.org"
+ },
+ {
+ "Company Name": "Radio Garden",
+ "Company Link ": "https://radio.garden"
+ },
+ {
+ "Company Name": "Radiooooo",
+ "Company Link ": "https://radiooooo.com"
+ },
+ {
+ "Company Name": "Radiosondy",
+ "Company Link ": "https://radiosondy.info"
+ },
+ {
+ "Company Name": "Rainy Mood",
+ "Company Link ": "https://rainymood.com"
+ },
+ {
+ "Company Name": "Random Street View",
+ "Company Link ": "https://randomstreetview.com"
+ },
+ {
+ "Company Name": "Rap4Ever",
+ "Company Link ": "https://rap4all.com"
+ },
+ {
+ "Company Name": "RareFilm",
+ "Company Link ": "https://rarefilm.net"
+ },
+ {
+ "Company Name": "Rattibha",
+ "Company Link ": "https://rattibha.com"
+ },
+ {
+ "Company Name": "Reddit List ",
+ "Company Link ": "https://redditlist.com"
+ },
+ {
+ "Company Name": "RedditSearch.io",
+ "Company Link ": "https://redditsearch.io"
+ },
+ {
+ "Company Name": "Reelgood",
+ "Company Link ": "https://reelgood.com"
+ },
+ {
+ "Company Name": "Reface",
+ "Company Link ": "https://reface.ai"
+ },
+ {
+ "Company Name": "Rejected.us",
+ "Company Link ": "https://rejected.us"
+ },
+ {
+ "Company Name": "Relanote",
+ "Company Link ": "https://relanote.com"
+ },
+ {
+ "Company Name": "Remote Leaf",
+ "Company Link ": "https://remoteleaf.com"
+ },
+ {
+ "Company Name": "Remote OK",
+ "Company Link ": "https://remoteok.com"
+ },
+ {
+ "Company Name": "Remote Starter Kit ",
+ "Company Link ": "https://remotestarterkit.com"
+ },
+ {
+ "Company Name": "Remote.co",
+ "Company Link ": "https://remote.co"
+ },
+ {
+ "Company Name": "Remote Base ",
+ "Company Link ": "https://remotebase.com"
+ },
+ {
+ "Company Name": "Remote Bear",
+ "Company Link ": "https://remotebear.io"
+ },
+ {
+ "Company Name": "Remove.bg",
+ "Company Link ": "https://remove.bg"
+ },
+ {
+ "Company Name": "Respresso",
+ "Company Link ": "https://respresso.io"
+ },
+ {
+ "Company Name": "Reveddit",
+ "Company Link ": "https://reveddit.com"
+ },
+ {
+ "Company Name": "Rhymer",
+ "Company Link ": "https://rhymer.com"
+ },
+ {
+ "Company Name": "RhymeZone",
+ "Company Link ": "https://rhymezone.com"
+ },
+ {
+ "Company Name": "Ribbet",
+ "Company Link ": "https://ribbet.com"
+ },
+ {
+ "Company Name": "Roadmap.sh",
+ "Company Link ": "https://roadmap.sh"
+ },
+ {
+ "Company Name": "Roadtrippers",
+ "Company Link ": "https://roadtrippers.com"
+ },
+ {
+ "Company Name": "RxResu.me",
+ "Company Link ": "https://rxresu.me"
+ },
+ {
+ "Company Name": "SchemeColor",
+ "Company Link ": "https://schemecolor.com"
+ },
+ {
+ "Company Name": "Screenshot.Guru",
+ "Company Link ": "https://screenshot.guru"
+ },
+ {
+ "Company Name": "SeatGuru",
+ "Company Link ": "https://seatguru.com"
+ },
+ {
+ "Company Name": "Sessions",
+ "Company Link ": "https://sessions.us"
+ },
+ {
+ "Company Name": "Shottr",
+ "Company Link ": "https://shottr.cc"
+ },
+ {
+ "Company Name": "Signature Maker",
+ "Company Link ": "https://signature-maker.net"
+ },
+ {
+ "Company Name": "Skip The Drive",
+ "Company Link ": "https://skipthedrive.com"
+ },
+ {
+ "Company Name": "Slowby",
+ "Company Link ": "https://slowby.travel"
+ },
+ {
+ "Company Name": "Small World",
+ "Company Link ": "https://smallworld.kiwi"
+ },
+ {
+ "Company Name": "SmallPDF",
+ "Company Link ": "https://smallpdf.com"
+ },
+ {
+ "Company Name": "Social Image Maker",
+ "Company Link ": "https://socialimagemaker.io"
+ },
+ {
+ "Company Name": "Social Sizes",
+ "Company Link ": "https://socialsizes.io"
+ },
+ {
+ "Company Name": "SoundLove",
+ "Company Link ": "https://soundlove.se"
+ },
+ {
+ "Company Name": "Spline",
+ "Company Link ": "https://spline.design"
+ },
+ {
+ "Company Name": "Starkey Comics",
+ "Company Link ": "https://starkeycomics.com"
+ },
+ {
+ "Company Name": "Startups List",
+ "Company Link ": "https://startups-list.com"
+ },
+ {
+ "Company Name": "Statista",
+ "Company Link ": "https://statista.com"
+ },
+ {
+ "Company Name": "Stolen Camera Finder",
+ "Company Link ": "https://stolencamerafinder.com"
+ },
+ {
+ "Company Name": "Strobe.Cool",
+ "Company Link ": "https://strobe.cool"
+ },
+ {
+ "Company Name": "Sumo",
+ "Company Link ": "https://sumo.app"
+ },
+ {
+ "Company Name": "SuperMeme AI",
+ "Company Link ": "https://supermeme.ai"
+ },
+ {
+ "Company Name": "Synthesia",
+ "Company Link ": "https://synthesia.io"
+ },
+ {
+ "Company Name": "TablerIcons",
+ "Company Link ": "https://tablericons.com"
+ },
+ {
+ "Company Name": "Tango",
+ "Company Link ": "https://tango.us"
+ },
+ {
+ "Company Name": "TasteDive",
+ "Company Link ": "https://tastedive.com"
+ },
+ {
+ "Company Name": "TechSpecs",
+ "Company Link ": "https://techspecs.io"
+ },
+ {
+ "Company Name": "Teoria",
+ "Company Link ": "https://teoria.com"
+ },
+ {
+ "Company Name": "Text Faces",
+ "Company Link ": "https://textfac.es"
+ },
+ {
+ "Company Name": "The Balance Money",
+ "Company Link ": "https://thebalancemoney.com"
+ },
+ {
+ "Company Name": "The Punctuation Guide",
+ "Company Link ": "https://thepunctuationguide.com"
+ },
+ {
+ "Company Name": "This to That",
+ "Company Link ": "https://thistothat.com"
+ },
+ {
+ "Company Name": "This vs That",
+ "Company Link ": "https://thisvsthat.io"
+ },
+ {
+ "Company Name": "ThreadReaderApp ",
+ "Company Link ": "https://threadreaderapp.com"
+ },
+ {
+ "Company Name": "Thumbly",
+ "Company Link ": "https://tokee.ai"
+ },
+ {
+ "Company Name": "Tiii.me",
+ "Company Link ": "https://tiii.me"
+ },
+ {
+ "Company Name": "TikTok Video Downloader",
+ "Company Link ": "https://ttvdl.com"
+ },
+ {
+ "Company Name": "Time and Date",
+ "Company Link ": "https://timeanddate.com"
+ },
+ {
+ "Company Name": "Time.is",
+ "Company Link ": "https://time.is"
+ },
+ {
+ "Company Name": "Title Case",
+ "Company Link ": "https://titlecase.com"
+ },
+ {
+ "Company Name": "Toaster Central",
+ "Company Link ": "https://toastercentral.com"
+ },
+ {
+ "Company Name": "Tongue-Twister ",
+ "Company Link ": "https://tongue-twister.net"
+ },
+ {
+ "Company Name": "TradingView",
+ "Company Link ": "https://tradingview.com"
+ },
+ {
+ "Company Name": "Transparent Textures",
+ "Company Link ": "https://transparenttextures.com"
+ },
+ {
+ "Company Name": "Tubi TV",
+ "Company Link ": "https://tubitv.com"
+ },
+ {
+ "Company Name": "Tunefind",
+ "Company Link ": "https://tunefind.com"
+ },
+ {
+ "Company Name": "TuneMyMusic",
+ "Company Link ": "https://tunemymusic.com"
+ },
+ {
+ "Company Name": "Tweepsmap",
+ "Company Link ": "https://fedica.com"
+ },
+ {
+ "Company Name": "Two Peas and Their Pod",
+ "Company Link ": "https://twopeasandtheirpod.com"
+ },
+ {
+ "Company Name": "Typatone",
+ "Company Link ": "https://typatone.com"
+ },
+ {
+ "Company Name": "Under Glass",
+ "Company Link ": "https://underglass.io"
+ },
+ {
+ "Company Name": "UniCorner",
+ "Company Link ": "https://unicorner.news"
+ },
+ {
+ "Company Name": "Unita",
+ "Company Link ": "https://unita.co"
+ },
+ {
+ "Company Name": "UnitConverters",
+ "Company Link ": "https://unitconverters.net"
+ },
+ {
+ "Company Name": "Unreadit",
+ "Company Link ": "https://unreadit.com"
+ },
+ {
+ "Company Name": "Unscreen",
+ "Company Link ": "https://unscreen.com"
+ },
+ {
+ "Company Name": "UnTools ",
+ "Company Link ": "https://untools.co"
+ },
+ {
+ "Company Name": "Upwork",
+ "Company Link ": "https://upwork.com"
+ },
+ {
+ "Company Name": "UTF8 Icons",
+ "Company Link ": "https://utf8icons.com"
+ },
+ {
+ "Company Name": "Vector Magic",
+ "Company Link ": "https://vectormagic.com"
+ },
+ {
+ "Company Name": "Virtual Vacation",
+ "Company Link ": "https://virtualvacation.us"
+ },
+ {
+ "Company Name": "Virtual Vocations",
+ "Company Link ": "https://virtualvocations.com"
+ },
+ {
+ "Company Name": "Visiwig",
+ "Company Link ": "https://visiwig.com"
+ },
+ {
+ "Company Name": "Visual CV",
+ "Company Link ": "https://visualcv.com"
+ },
+ {
+ "Company Name": "Vocus.io",
+ "Company Link ": "https://vocus.io"
+ },
+ {
+ "Company Name": "Voscreen",
+ "Company Link ": "https://voscreen.com"
+ },
+ {
+ "Company Name": "Wanderprep",
+ "Company Link ": "https://wanderprep.com"
+ },
+ {
+ "Company Name": "Warmshowers",
+ "Company Link ": "https:/warmshowers.org"
+ },
+ {
+ "Company Name": "Watch Documentaries",
+ "Company Link ": "https://watchdocumentaries.com"
+ },
+ {
+ "Company Name": "We Work Remotely",
+ "Company Link ": "https://weworkremotely.com"
+ },
+ {
+ "Company Name": "Web2PDFConvert",
+ "Company Link ": "https://web2pdfconvert.com"
+ },
+ {
+ "Company Name": "Welcome to My Garden",
+ "Company Link ": "https://welcometomygarden.org"
+ },
+ {
+ "Company Name": "When2meet ",
+ "Company Link ": "https://when2meet.com"
+ },
+ {
+ "Company Name": "Where's George",
+ "Company Link ": "https://wheresgeorge.com"
+ },
+ {
+ "Company Name": "Where's Willy",
+ "Company Link ": "https://whereswilly.com"
+ },
+ {
+ "Company Name": "WikiHow",
+ "Company Link ": "https://wikihow.com"
+ },
+ {
+ "Company Name": "Windy",
+ "Company Link ": "https://www.windy.com"
+ },
+ {
+ "Company Name": "WonderHowTo",
+ "Company Link ": "https://wonderhowto.com"
+ },
+ {
+ "Company Name": "Working Nomads",
+ "Company Link ": "https://workingnomads.com"
+ },
+ {
+ "Company Name": "Wormhole",
+ "Company Link ": "https://wormhole.app"
+ },
+ {
+ "Company Name": "Y Combinator Jobs",
+ "Company Link ": "https://ycombinator.com"
+ },
+ {
+ "Company Name": "Yes Promo",
+ "Company Link ": "https://yespromo.me"
+ },
+ {
+ "Company Name": "YouGlish",
+ "Company Link ": "https://youglish.com"
+ },
+ {
+ "Company Name": "Zamzar",
+ "Company Link ": "https://zamzar.com"
+ },
+ {
+ "Company Name": "Zippyshare",
+ "Company Link ": "https://zippyshare.com"
+ },
+ {
+ "Company Name": "Zoom Earth",
+ "Company Link ": "https://zoom.earth"
+ },
+ {
+ "Company Name": "Zoom.it",
+ "Company Link ": "https://zoom.it"
+ }
+]
\ No newline at end of file
diff --git a/Client/src/assets/icons/average-response-icon.svg b/Client/src/assets/icons/average-response-icon.svg
new file mode 100644
index 000000000..572e2002f
--- /dev/null
+++ b/Client/src/assets/icons/average-response-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/Client/src/assets/icons/certificate.svg b/Client/src/assets/icons/certificate.svg
new file mode 100644
index 000000000..a985bef9f
--- /dev/null
+++ b/Client/src/assets/icons/certificate.svg
@@ -0,0 +1,3 @@
+
diff --git a/Client/src/assets/icons/history-icon.svg b/Client/src/assets/icons/history-icon.svg
new file mode 100644
index 000000000..7343455ec
--- /dev/null
+++ b/Client/src/assets/icons/history-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/Client/src/assets/icons/response-time-icon.svg b/Client/src/assets/icons/response-time-icon.svg
new file mode 100644
index 000000000..db5b2af38
--- /dev/null
+++ b/Client/src/assets/icons/response-time-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/Client/src/assets/icons/top-right-arrow.svg b/Client/src/assets/icons/top-right-arrow.svg
index 471b9454c..d569cdd7e 100644
--- a/Client/src/assets/icons/top-right-arrow.svg
+++ b/Client/src/assets/icons/top-right-arrow.svg
@@ -1,3 +1,3 @@
diff --git a/Client/src/assets/icons/uptime-icon.svg b/Client/src/assets/icons/uptime-icon.svg
new file mode 100644
index 000000000..55706ae71
--- /dev/null
+++ b/Client/src/assets/icons/uptime-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/Client/src/index.css b/Client/src/index.css
index 105776bd8..1f3b9bee2 100644
--- a/Client/src/index.css
+++ b/Client/src/index.css
@@ -4,6 +4,10 @@
box-sizing: border-box;
}
+html {
+ scroll-behavior: smooth;
+}
+
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-weight: 400;
@@ -86,10 +90,6 @@ body {
display: none;
}
-body .MuiSkeleton-root {
- background-color: #f2f4f7;
-}
-
@keyframes ripple {
from {
opacity: 1;
diff --git a/README.md b/README.md
index 9cbd37355..e050b221e 100644
--- a/README.md
+++ b/README.md
@@ -11,9 +11,8 @@
An open source server monitoring application
-https://github.com/user-attachments/assets/c8234cce-edf8-4d7e-8f6b-f18b06669fcc
-
-
+
+(yes, we have a light theme as well, but this looks better on readme.md)
BlueWave Uptime is an open source server monitoring application used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
index f33ddcf10..4571a9a65 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -4,15 +4,20 @@ const {
getMonitorsByTeamIdValidation,
createMonitorBodyValidation,
editMonitorBodyValidation,
+ getMonitorsAndSummaryByTeamIdParamValidation,
+ getMonitorsAndSummaryByTeamIdQueryValidation,
getMonitorsByTeamIdQueryValidation,
pauseMonitorParamValidation,
+ getMonitorStatsByIdParamValidation,
+ getMonitorStatsByIdQueryValidation,
+ getCertificateParamValidation,
+ getMonitorAggregateStatsParamValidation,
+ getMonitorAggregateStatsQueryValidation,
} = require("../validation/joi");
const sslChecker = require("ssl-checker");
const SERVICE_NAME = "monitorController";
const { errorMessages, successMessages } = require("../utils/messages");
-const { runInNewContext } = require("vm");
-
/**
* Returns all monitors
* @async
@@ -35,6 +40,44 @@ const getAllMonitors = async (req, res, next) => {
}
};
+/**
+ * Returns agregate stats for a monitor
+ * @async
+ * @param {Express.Request} req
+ * @param {Express.Response} res
+ * @returns {Promise}
+ * @throws {Error}
+ */
+
+const getMonitorAggregateStats = async (req, res, next) => {
+ try {
+ await getMonitorAggregateStatsParamValidation.validateAsync(req.params);
+ await getMonitorAggregateStatsQueryValidation.validateAsync(req.query);
+ } catch (error) {
+ error.status = 422;
+ error.message =
+ error.details?.[0]?.message || error.message || "Validation Error";
+ next(error);
+ return;
+ }
+
+ try {
+ const { monitorId } = req.params;
+ const dateRange = req.query.dateRange;
+ const aggregateStats = await req.db.getMonitorAggregateStats(
+ monitorId,
+ dateRange
+ );
+ return res.json({
+ success: true,
+ msg: successMessages.MONTIOR_STATS_BY_ID,
+ data: aggregateStats,
+ });
+ } catch (error) {
+ next(error);
+ }
+};
+
/**
* Returns monitor stats for monitor with matching ID
* @async
@@ -45,7 +88,8 @@ const getAllMonitors = async (req, res, next) => {
*/
const getMonitorStatsById = async (req, res, next) => {
try {
- //Validation
+ await getMonitorStatsByIdParamValidation.validateAsync(req.params);
+ await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.message =
@@ -69,7 +113,7 @@ const getMonitorStatsById = async (req, res, next) => {
const getMonitorCertificate = async (req, res, next) => {
try {
- //validation
+ await getCertificateParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.message =
@@ -143,7 +187,54 @@ const getMonitorById = async (req, res, next) => {
};
/**
- * Returns all monitors belong to User with UserID
+ * Returns all monitors and a sumamry for a team with TeamID
+ * @async
+ * @param {Express.Request} req
+ * @param {Express.Response} res
+ * @returns {Promise}
+ * @throws {Error}
+ */
+
+const getMonitorsAndSummaryByTeamId = async (req, res, next) => {
+ try {
+ await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(
+ req.params
+ );
+ await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query);
+ //validation
+ } catch (error) {
+ error.status = 422;
+ error.service = SERVICE_NAME;
+ error.method === undefined &&
+ error.method === "getMonitorsAndSummaryByTeamId";
+ error.message =
+ error.details?.[0]?.message || error.message || "Validation Error";
+ next(error);
+ return;
+ }
+
+ try {
+ const { teamId } = req.params;
+ const { type } = req.query;
+ const monitorsSummary = await req.db.getMonitorsAndSummaryByTeamId(
+ teamId,
+ type
+ );
+ return res.status(200).json({
+ success: true,
+ msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
+ data: monitorsSummary,
+ });
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method === undefined &&
+ error.method === "getMonitorsAndSummaryByTeamId";
+ next(error);
+ }
+};
+
+/**
+ * Returns all monitors belong to team with TeamID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
@@ -359,6 +450,7 @@ const pauseMonitor = async (req, res, next) => {
await req.jobQueue.addJob(monitor._id, monitor);
}
monitor.isActive = !monitor.isActive;
+ monitor.status = undefined;
monitor.save();
return res.status(200).json({
success: true,
@@ -376,9 +468,11 @@ const pauseMonitor = async (req, res, next) => {
module.exports = {
getAllMonitors,
+ getMonitorAggregateStats,
getMonitorStatsById,
getMonitorCertificate,
getMonitorById,
+ getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
deleteMonitor,
diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js
index 912bfb438..d6a4a243f 100644
--- a/Server/db/mongo/MongoDB.js
+++ b/Server/db/mongo/MongoDB.js
@@ -66,6 +66,7 @@ const {
getAllMonitors,
getMonitorStatsById,
getMonitorById,
+ getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
deleteMonitor,
@@ -146,6 +147,7 @@ module.exports = {
getAllMonitors,
getMonitorStatsById,
getMonitorById,
+ getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
deleteMonitor,
diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js
index 79e8917ea..e77ccc696 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -35,7 +35,7 @@ const calculateUptimeDuration = (checks) => {
const latestCheck = new Date(checks[0].createdAt);
let latestDownCheck = 0;
- for (let i = 0; i < checks.length; i++) {
+ for (let i = checks.length; i <= 0; i--) {
if (checks[i].status === false) {
latestDownCheck = new Date(checks[i].createdAt);
break;
@@ -77,11 +77,11 @@ const getLatestResponseTime = (checks) => {
};
/**
- * Helper function to get average 24h response time
+ * Helper function to get average response time
* @param {Array} checks Array of check objects.
* @returns {number} Timestamp of the most recent check.
*/
-const getAverageResponseTime24Hours = (checks) => {
+const getAverageResponseTime = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
@@ -121,28 +121,7 @@ const getIncidents = (checks) => {
return check.status === false ? (acc += 1) : acc;
}, 0);
};
-/**
- * Helper function to get all incidents
- * @param {Array} checks Array of check objects.
- * @returns {Array} Array of booleans representing up/down.
- */
-const getStatusBarValues = (monitor, checks) => {
- const checksIn60Mins = Math.floor((60 * 60 * 1000) / monitor.interval);
- const noBlankChecks = checksIn60Mins - checks.length;
- const statusBarValues = checks.map((check) => {
- return {
- status: check.status,
- responseTime: check.responseTime,
- value: 75,
- };
- });
-
- for (let i = 0; i < noBlankChecks; i++) {
- statusBarValues.push({ status: undefined, responseTime: 0, value: 75 });
- }
- return statusBarValues.reverse();
-};
/**
* Get stats by monitor ID
* @async
@@ -152,42 +131,35 @@ const getStatusBarValues = (monitor, checks) => {
* @throws {Error}
*/
const getMonitorStatsById = async (req) => {
- const filterLookup = {
- hour: new Date(new Date().getTime() - 60 * 60 * 1000),
+ const startDates = {
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
};
-
+ const endDate = new Date();
try {
- const { monitorId } = req.params;
- let { status, limit, sortOrder, dateRange, numToDisplay, normalize } =
- req.query;
-
- // This effectively removes limit, returning all checks
- if (limit === undefined) limit = 0;
-
- // Default sort order is newest -> oldest
- sortOrder = sortOrder === "asc" ? 1 : -1;
-
// Get monitor
+ const { monitorId } = req.params;
+ let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
const monitor = await Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
}
+ // This effectively removes limit, returning all checks
+ if (limit === undefined) limit = 0;
+ // Default sort order is newest -> oldest
+ sortOrder = sortOrder === "asc" ? 1 : -1;
- // Determine if this is a pagespeed monitor or an http/ping monitor
let model =
monitor.type === "http" || monitor.type === "ping"
? Check
: PageSpeedCheck;
- // Build monitor stats object
const monitorStats = {
...monitor.toObject(),
};
- // Start building query
+ // Build checks query
const checksQuery = { monitorId: monitor._id };
// Get all checks
@@ -195,76 +167,101 @@ const getMonitorStatsById = async (req) => {
createdAt: sortOrder,
});
+ const checksQueryForDateRange = {
+ ...checksQuery,
+ createdAt: {
+ $gte: startDates[dateRange],
+ $lte: endDate,
+ },
+ };
+
+ const checksForDateRange = await model
+ .find(checksQueryForDateRange)
+ .sort({ createdAt: sortOrder });
+
if (monitor.type === "http" || monitor.type === "ping") {
- const checksQuery24Hours = {
- ...checksQuery,
- createdAt: { $gte: filterLookup.day },
- };
- const checksQuery30Days = {
- ...checksQuery,
- createdAt: { $gte: filterLookup.month },
- };
- const checksQuery60Mins = {
- ...checksQuery,
- createdAt: { $gte: filterLookup.hour },
- };
-
- const [checks24Hours, checks30Days, checks60Mins] = await Promise.all([
- model.find(checksQuery24Hours).sort({ createdAt: sortOrder }),
- model.find(checksQuery30Days).sort({ createdAt: sortOrder }),
- model.find(checksQuery60Mins).sort({ createdAt: sortOrder }),
- ]);
-
// HTTP/PING Specific stats
- monitorStats.avgResponseTime24hours =
- getAverageResponseTime24Hours(checks24Hours);
- monitorStats.uptime24Hours = getUptimePercentage(checks24Hours);
- monitorStats.uptime30Days = getUptimePercentage(checks30Days);
- monitorStats.statusBar = getStatusBarValues(monitor, checks60Mins);
+ monitorStats.periodAvgResponseTime =
+ getAverageResponseTime(checksForDateRange);
+ monitorStats.periodUptime = getUptimePercentage(checksForDateRange);
+
+ // Aggregate data
+ let groupedChecks;
+ // Group checks by hour if range is day
+ if (dateRange === "day") {
+ groupedChecks = checksForDateRange.reduce((acc, check) => {
+ const time = new Date(check.createdAt);
+ time.setMinutes(0, 0, 0);
+ if (!acc[time]) {
+ acc[time] = { time, checks: [] };
+ }
+ acc[time].checks.push(check);
+ return acc;
+ }, {});
+ } else {
+ groupedChecks = checksForDateRange.reduce((acc, check) => {
+ const time = new Date(check.createdAt).toISOString().split("T")[0]; // Extract the date part
+ if (!acc[time]) {
+ acc[time] = { time, checks: [] };
+ }
+ acc[time].checks.push(check);
+ return acc;
+ }, {});
+ }
+
+ // Map grouped checks to stats
+ const aggregateData = Object.values(groupedChecks).map((group) => {
+ const totalChecks = group.checks.length;
+ const uptimePercentage = getUptimePercentage(group.checks);
+ const totalIncidents = group.checks.filter(
+ (check) => check.status === false
+ ).length;
+ const avgResponseTime =
+ group.checks.reduce((sum, check) => sum + check.responseTime, 0) /
+ totalChecks;
+
+ return {
+ time: group.time,
+ uptimePercentage,
+ totalChecks,
+ totalIncidents,
+ avgResponseTime,
+ };
+ });
+ monitorStats.aggregateData = aggregateData;
}
- //Get checks for dateRange
- if (status !== undefined) {
- checksQuery.status = status;
- }
-
- // Filter checks by "day", "week", or "month"
- if (dateRange !== undefined) {
- checksQuery.createdAt = { $gte: filterLookup[dateRange] };
- }
-
- let dateRangeChecks = await model
- .find(checksQuery)
- .sort({
- createdAt: sortOrder,
- })
- .limit(limit);
-
- const incidents = getIncidents(dateRangeChecks);
+ monitorStats.periodIncidents = getIncidents(checksForDateRange);
+ monitorStats.periodTotalChecks = checksForDateRange.length;
// If more than numToDisplay checks, pick every nth check
+
+ let nthChecks = checksForDateRange;
+
if (
numToDisplay !== undefined &&
- dateRangeChecks &&
- dateRangeChecks.length > numToDisplay
+ checksForDateRange &&
+ checksForDateRange.length > numToDisplay
) {
- const n = Math.ceil(dateRangeChecks.length / numToDisplay);
- dateRangeChecks = dateRangeChecks.filter((_, index) => index % n === 0);
+ const n = Math.ceil(checksForDateRange.length / numToDisplay);
+ nthChecks = checksForDateRange.filter((_, index) => index % n === 0);
}
// Normalize checks if requested
if (normalize !== undefined) {
- dateRangeChecks = NormalizeData(dateRangeChecks, 1, 100);
+ const normailzedChecks = NormalizeData(nthChecks, 1, 100);
+ monitorStats.checks = normailzedChecks;
+ } else {
+ monitorStats.checks = nthChecks;
}
- // Add common stats and stats that depend on the dateRange
monitorStats.uptimeDuration = calculateUptimeDuration(checksAll);
monitorStats.lastChecked = getLastChecked(checksAll);
monitorStats.latestResponseTime = getLatestResponseTime(checksAll);
- monitorStats.incidents = incidents;
- monitorStats.checks = dateRangeChecks;
+
return monitorStats;
} catch (error) {
+ error.methodName = "getMonitorStatsById";
throw error;
}
};
@@ -295,6 +292,43 @@ const getMonitorById = async (monitorId) => {
}
};
+/**
+ * Get monitors and Summary by TeamID
+ * @async
+ * @param {Express.Request} req
+ * @param {Express.Response} res
+ * @returns {Promise>}
+ * @throws {Error}
+ */
+
+const getMonitorsAndSummaryByTeamId = async (teamId, type) => {
+ try {
+ const monitors = await Monitor.find({ teamId, type });
+ const monitorCounts = monitors.reduce(
+ (acc, monitor) => {
+ if (monitor.status === true) {
+ acc.up += 1;
+ }
+
+ if (monitor.status === false) {
+ acc.down += 1;
+ }
+
+ if (monitor.isActive === false) {
+ acc.paused += 1;
+ }
+ return acc;
+ },
+ { up: 0, down: 0, paused: 0 }
+ );
+ monitorCounts.total = monitors.length;
+ return { monitors, monitorCounts };
+ } catch (error) {
+ error.method = "getMonitorsAndSummaryByTeamId";
+ throw error;
+ }
+};
+
/**
* Get monitors by TeamID
* @async
@@ -305,8 +339,19 @@ const getMonitorById = async (monitorId) => {
*/
const getMonitorsByTeamId = async (req, res) => {
try {
- let { limit, type, status, sortOrder, normalize } = req.query || {};
+ let { limit, type, status, sortOrder, normalize, page, rowsPerPage } =
+ req.query || {};
const monitorQuery = { teamId: req.params.teamId };
+ const monitorsCount = await Monitor.countDocuments({
+ teamId: req.params.teamId,
+ type,
+ });
+
+ // Pagination
+ let skip = 0;
+ if (page && rowsPerPage) {
+ skip = page * rowsPerPage;
+ }
if (type !== undefined) {
const types = Array.isArray(type) ? type : [type];
@@ -323,7 +368,9 @@ const getMonitorsByTeamId = async (req, res) => {
// This effectively removes limit, returning all checks
if (limit === undefined) limit = 0;
- const monitors = await Monitor.find(monitorQuery);
+ const monitors = await Monitor.find(monitorQuery)
+ .skip(skip)
+ .limit(rowsPerPage);
// Map each monitor to include its associated checks
const monitorsWithChecks = await Promise.all(
monitors.map(async (monitor) => {
@@ -352,7 +399,7 @@ const getMonitorsByTeamId = async (req, res) => {
return { ...monitor.toObject(), checks };
})
);
- return monitorsWithChecks;
+ return { monitors: monitorsWithChecks, monitorCount: monitorsCount };
} catch (error) {
throw error;
}
@@ -454,6 +501,7 @@ module.exports = {
getAllMonitors,
getMonitorStatsById,
getMonitorById,
+ getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
deleteMonitor,
diff --git a/Server/middleware/handleErrors.js b/Server/middleware/handleErrors.js
index 1e1e955c5..db51c1af6 100644
--- a/Server/middleware/handleErrors.js
+++ b/Server/middleware/handleErrors.js
@@ -5,7 +5,10 @@ const handleErrors = (error, req, res, next) => {
const status = error.status || 500;
const message = error.message || errorMessages.FRIENDLY_ERROR;
const service = error.service || errorMessages.UNKNOWN_SERVICE;
- logger.error(error.message, { service: service });
+ logger.error(error.message, {
+ service: service,
+ methodName: error.methodName,
+ });
res.status(status).json({ success: false, msg: message });
};
diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js
index fdc147d1b..d91275e23 100644
--- a/Server/models/Monitor.js
+++ b/Server/models/Monitor.js
@@ -24,7 +24,7 @@ const MonitorSchema = mongoose.Schema(
},
status: {
type: Boolean,
- default: true,
+ default: undefined,
},
type: {
type: String,
@@ -46,7 +46,7 @@ const MonitorSchema = mongoose.Schema(
},
uptimePercentage: {
type: Number,
- default: undefined,
+ default: undefined,
},
notifications: [
{
diff --git a/Server/openapi.json b/Server/openapi.json
new file mode 100644
index 000000000..23c16521d
--- /dev/null
+++ b/Server/openapi.json
@@ -0,0 +1,835 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "BlueWave Uptime",
+ "summary": "BlueWave Uptime OpenAPI Specifications",
+ "description": "BlueWave Uptime is an open source server monitoring application used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.",
+ "contact": {
+ "name": "API Support",
+ "url": "mailto:support@bluewavelabs.ca",
+ "email": "support@bluewavelabs.ca"
+ },
+ "license": {
+ "name": "AGPLv3",
+ "url": "https://github.com/bluewave-labs/bluewave-uptime/tree/HEAD/LICENSE"
+ },
+ "version": "1.0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:{PORT}/{API_PATH}",
+ "description": "Local Development Server",
+ "variables": {
+ "PORT": {
+ "description": "API Port",
+ "enum": ["5000", "3000", "8080"],
+ "default": "5000"
+ },
+ "API_PATH": {
+ "description": "API Base Path",
+ "enum": ["api/v1", "api/v1.1", "api/v2"],
+ "default": "api/v1"
+ }
+ }
+ }
+ ],
+ "tags": [
+ {
+ "name": "auth",
+ "description": "Authentication"
+ },
+ {
+ "name": "invite",
+ "description": "Invite"
+ },
+ {
+ "name": "monitors",
+ "description": "Monitors"
+ },
+ {
+ "name": "checks",
+ "description": "Checks"
+ },
+ {
+ "name": "alerts",
+ "description": "Alerts"
+ },
+ {
+ "name": "pagespeed",
+ "description": "Page Speed"
+ },
+ {
+ "name": "maintenance-window",
+ "description": "Maintenance window"
+ },
+ {
+ "name": "healthy",
+ "description": "Health check"
+ },
+ {
+ "name": "mail",
+ "description": "Mail service"
+ }
+ ],
+ "paths": {
+ "/auth/register": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Register a new user",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["firstName", "lastName", "email", "password", "role", "teamId"],
+ "properties": {
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "format": "email"
+ },
+ "password": {
+ "type": "string",
+ "format": "password"
+ },
+ "profileImage": {
+ "type": "file",
+ "format": "file"
+ },
+ "role": {
+ "type": "array",
+ "enum": [["user"], ["admin"], ["superadmin"]],
+ "default": ["superadmin"]
+ },
+ "teamId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Login with credentials",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["email", "password"],
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email"
+ },
+ "password": {
+ "type": "string",
+ "format": "password"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/user/:userId": {
+ "put": {
+ "tags": ["auth"],
+ "description": "Change user informations",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": ["auth"],
+ "description": "Delete user",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/users/admin": {
+ "get": {
+ "tags": ["auth"],
+ "description": "Checks to see if an admin account exists",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/users": {
+ "get": {
+ "tags": ["auth"],
+ "description": "Get all users",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/invite": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Invite people to application",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/invite/verify": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Verify the invite",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/recovery/request": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Request a recovery token",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/recovery/validate": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Validate recovery token",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/recovery/reset": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Password reset",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/healthy": {
+ "get": {
+ "tags": ["healthy"],
+ "description": "Health check",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "ErrorResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ }
+ }
+ },
+ "SuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object"
+ }
+ }
+ },
+ "UserSuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "$ref": "#/components/schemas/UserDataType"
+ }
+ }
+ },
+ "MonitorSuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "$ref": "#/components/schemas/MonitorDataType"
+ }
+ }
+ },
+ "CheckDataSuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "$ref": "#/components/schemas/CheckDataType"
+ }
+ }
+ },
+ "AlertDataSuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "$ref": "#/components/schemas/AlertDataType"
+ }
+ }
+ },
+ "UserDataType": {
+ "type": "object",
+ "required": [
+ "firstname",
+ "lastname",
+ "email",
+ "profilePicUrl",
+ "isActive",
+ "isVerified",
+ "updatedAt",
+ "createdAt"
+ ],
+ "properties": {
+ "firstname": {
+ "type": "string"
+ },
+ "lastname": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "format": "email"
+ },
+ "profilePicUrl": {
+ "type": "string"
+ },
+ "isActive": {
+ "type": "boolean"
+ },
+ "isVerified": {
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ }
+ }
+ },
+ "MonitorDataType": {
+ "type": "object",
+ "required": [
+ "userId",
+ "name",
+ "description",
+ "url",
+ "isActive",
+ "interval",
+ "updatedAt",
+ "createdAt"
+ ],
+ "properties": {
+ "userId": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "isActive": {
+ "type": "boolean"
+ },
+ "interval": {
+ "type": "integer"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ }
+ }
+ },
+ "CheckDataType": {
+ "type": "object",
+ "required": [
+ "monitorId",
+ "status",
+ "responseTime",
+ "statusCode",
+ "message",
+ "updatedAt",
+ "createdAt"
+ ],
+ "properties": {
+ "monitorId": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "responseTime": {
+ "type": "integer"
+ },
+ "statusCode": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ }
+ }
+ },
+ "AlertDataType": {
+ "type": "object",
+ "required": [
+ "checkId",
+ "monitorId",
+ "userId",
+ "status",
+ "message",
+ "notifiedStatus",
+ "acknowledgedStatus",
+ "updatedAt",
+ "createdAt"
+ ],
+ "properties": {
+ "checkId": {
+ "type": "string"
+ },
+ "monitorId": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "notifiedStatus": {
+ "type": "boolean"
+ },
+ "acknowledgedStatus": {
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js
index ee1c08488..b29b7130a 100644
--- a/Server/routes/monitorRoute.js
+++ b/Server/routes/monitorRoute.js
@@ -3,9 +3,14 @@ const monitorController = require("../controllers/monitorController");
const { isAllowed } = require("../middleware/isAllowed");
router.get("/", monitorController.getAllMonitors);
+router.get("/aggregate/:monitorId", monitorController.getMonitorAggregateStats);
router.get("/stats/:monitorId", monitorController.getMonitorStatsById);
router.get("/certificate/:monitorId", monitorController.getMonitorCertificate);
router.get("/:monitorId", monitorController.getMonitorById);
+router.get(
+ "/team/summary/:teamId",
+ monitorController.getMonitorsAndSummaryByTeamId
+);
router.get("/team/:teamId", monitorController.getMonitorsByTeamId);
router.post(
diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js
index ed15e0f94..1c44c4062 100644
--- a/Server/service/jobQueue.js
+++ b/Server/service/jobQueue.js
@@ -39,7 +39,9 @@ class JobQueue {
queue.networkService = networkService;
const monitors = await db.getAllMonitors();
for (const monitor of monitors) {
- await queue.addJob(monitor.id, monitor);
+ if (monitor.active) {
+ await queue.addJob(monitor.id, monitor);
+ }
}
const workerStats = await queue.getWorkerStats();
await queue.scaleWorkers(workerStats);
@@ -238,7 +240,6 @@ class JobQueue {
console.log("Adding job", payload.url);
// Execute job immediately
await this.queue.add(jobName, payload);
-
await this.queue.add(jobName, payload, {
repeat: {
every: payload.interval,
diff --git a/Server/service/networkService.js b/Server/service/networkService.js
index f5c333a60..e25a63ce5 100644
--- a/Server/service/networkService.js
+++ b/Server/service/networkService.js
@@ -15,44 +15,60 @@ class NetworkService {
this.NETWORK_ERROR = 5000;
}
- async handleNotification(job, isAlive) {
+ async handleNotification(monitor, isAlive) {
try {
- const { _id } = job.data;
- const monitor = await this.db.getMonitorById(_id);
- if (monitor === null || monitor === undefined) {
- logger.error(`Null Monitor: ${_id}`, {
- method: "handleNotification",
- service: this.SERVICE_NAME,
- jobId: job.id,
- });
- return;
- }
+ let template =
+ isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
+ let status = isAlive === true ? "up" : "down";
- // If monitor status changes, update monitor status and send notification
- if (monitor.status !== isAlive) {
- monitor.status = !monitor.status;
- await monitor.save();
-
- let template =
- isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
- let status = isAlive === true ? "up" : "down";
-
- const notifications = await this.db.getNotificationsByMonitorId(_id);
- for (const notification of notifications) {
- if (notification.type === "email") {
- await this.emailService.buildAndSendEmail(
- template,
- { monitorName: monitor.name, monitorUrl: monitor.url },
- notification.address,
- `Monitor ${monitor.name} is ${status}`
- );
- }
+ const notifications = await this.db.getNotificationsByMonitorId(
+ monitor._id
+ );
+ for (const notification of notifications) {
+ if (notification.type === "email") {
+ await this.emailService.buildAndSendEmail(
+ template,
+ { monitorName: monitor.name, monitorUrl: monitor.url },
+ notification.address,
+ `Monitor ${monitor.name} is ${status}`
+ );
}
}
} catch (error) {
logger.error(error.message, {
method: "handleNotification",
service: this.SERVICE_NAME,
+ monitorId: monitor._id,
+ });
+ }
+ }
+
+ async handleStatusUpdate(job, isAlive) {
+ try {
+ const { _id } = job.data;
+ const monitor = await this.db.getMonitorById(_id);
+
+ if (monitor === null || monitor === undefined) {
+ logger.error(`Null Monitor: ${_id}`, {
+ method: "handleStatusUpdate",
+ service: this.SERVICE_NAME,
+ jobId: job.id,
+ });
+ return;
+ }
+ if (monitor.status === undefined || monitor.status !== isAlive) {
+ const oldStatus = monitor.status;
+ monitor.status = isAlive;
+ await monitor.save();
+
+ if (oldStatus !== undefined && oldStatus !== isAlive) {
+ this.handleNotification(monitor, isAlive);
+ }
+ }
+ } catch (error) {
+ logger.error(error.message, {
+ method: "handleStatusUpdate",
+ service: this.SERVICE_NAME,
jobId: job.id,
});
}
@@ -113,7 +129,7 @@ class NetworkService {
};
return await this.logAndStoreCheck(checkData, this.db.createCheck);
} finally {
- this.handleNotification(job, isAlive);
+ this.handleStatusUpdate(job, isAlive);
}
}
@@ -161,7 +177,7 @@ class NetworkService {
return await this.logAndStoreCheck(checkData, this.db.createCheck);
} finally {
- this.handleNotification(job, isAlive);
+ this.handleStatusUpdate(job, isAlive);
}
}
@@ -181,7 +197,6 @@ class NetworkService {
*/
async handlePagespeed(job) {
let isAlive;
-
try {
const url = job.data.url;
const response = await axios.get(
@@ -241,7 +256,7 @@ class NetworkService {
};
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
} finally {
- this.handleNotification(job, isAlive);
+ this.handleStatusUpdate(job, isAlive);
}
}
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index ccc8da9f6..417965b95 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -1,4 +1,5 @@
const joi = require("joi");
+const { normalize } = require("path");
//****************************************
// Custom Validators
@@ -160,6 +161,19 @@ const getMonitorByIdQueryValidation = joi.object({
normalize: joi.boolean(),
});
+const getMonitorsAndSummaryByTeamIdParamValidation = joi.object({
+ teamId: joi.string().required(),
+});
+
+const getMonitorsAndSummaryByTeamIdQueryValidation = joi.object({
+ type: joi
+ .alternatives()
+ .try(
+ joi.string().valid("http", "ping", "pagespeed"),
+ joi.array().items(joi.string().valid("http", "ping", "pagespeed"))
+ ),
+});
+
const getMonitorsByTeamIdValidation = joi.object({
teamId: joi.string().required(),
});
@@ -175,6 +189,24 @@ const getMonitorsByTeamIdQueryValidation = joi.object({
joi.string().valid("http", "ping", "pagespeed"),
joi.array().items(joi.string().valid("http", "ping", "pagespeed"))
),
+ page: joi.number(),
+ rowsPerPage: joi.number(),
+});
+
+const getMonitorStatsByIdParamValidation = joi.object({
+ monitorId: joi.string().required(),
+});
+const getMonitorStatsByIdQueryValidation = joi.object({
+ status: joi.string(),
+ limit: joi.number(),
+ sortOrder: joi.string().valid("asc", "desc"),
+ dateRange: joi.string().valid("day", "week", "month"),
+ numToDisplay: joi.number(),
+ normalize: joi.boolean(),
+});
+
+const getCertificateParamValidation = joi.object({
+ monitorId: joi.string().required(),
});
const createMonitorBodyValidation = joi.object({
@@ -201,6 +233,13 @@ const pauseMonitorParamValidation = joi.object({
monitorId: joi.string().required(),
});
+const getMonitorAggregateStatsParamValidation = joi.object({
+ monitorId: joi.string().required(),
+});
+const getMonitorAggregateStatsQueryValidation = joi.object({
+ dateRange: joi.string().valid("day", "week", "month"),
+});
+
//****************************************
// Alerts
//****************************************
@@ -355,8 +394,15 @@ module.exports = {
createMonitorBodyValidation,
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
+ getMonitorsAndSummaryByTeamIdParamValidation,
+ getMonitorsAndSummaryByTeamIdQueryValidation,
getMonitorsByTeamIdValidation,
getMonitorsByTeamIdQueryValidation,
+ getMonitorStatsByIdParamValidation,
+ getMonitorStatsByIdQueryValidation,
+ getCertificateParamValidation,
+ getMonitorAggregateStatsParamValidation,
+ getMonitorAggregateStatsQueryValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
editUserParamValidation,