diff --git a/README.md b/README.md
index 63f5e55f..a6be1e4e 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
Discord
-
+
## 📔 Features
diff --git a/app/components/incident/content/impact.tsx b/app/components/incident/content/impact.tsx
index d6f8175d..a77a4396 100644
--- a/app/components/incident/content/impact.tsx
+++ b/app/components/incident/content/impact.tsx
@@ -1,4 +1,5 @@
import { toast } from 'react-toastify';
+import { observer } from 'mobx-react-lite';
// import local files
import Dropdown from '../../ui/dropdown';
@@ -88,4 +89,4 @@ const IncidentContentImpact = () => {
IncidentContentImpact.displayName = 'IncidentContentImpact';
-export default IncidentContentImpact;
+export default observer(IncidentContentImpact);
diff --git a/app/components/modal/monitor/configure.tsx b/app/components/modal/monitor/configure.tsx
index 9fea2f9a..b2a226cb 100644
--- a/app/components/modal/monitor/configure.tsx
+++ b/app/components/modal/monitor/configure.tsx
@@ -17,6 +17,7 @@ import MonitorConfigureDockerModal from './configure/docker';
import MonitorConfigureJsonQueryModal from './configure/json';
import type { MonitorProps } from '../../../types/monitor';
import MonitorConfigurePushModal from './configure/push';
+import classNames from 'classnames';
const pages = [
{ id: 'basic', title: 'Basic', icon: },
@@ -43,11 +44,19 @@ const MonitorConfigureModal = ({
isEdit = false,
}: ModalProps) => {
const [pageId, setPageId] = useState('basic');
- const { errors, inputs, handleActionButtons, handleInput } = useMonitorForm(
+ const {
+ errors,
+ inputs,
+ handleActionButtons,
+ handleInput,
+ errorPages,
+ setErrorPages,
+ } = useMonitorForm(
monitor,
isEdit,
closeModal,
- handleMonitorSubmit
+ handleMonitorSubmit,
+ setPageId
);
return (
@@ -63,20 +72,30 @@ const MonitorConfigureModal = ({
settings to your liking.
- {pages.map((page) => (
-
setPageId(page.id)}
- key={page.id}
- >
- {page.icon}
-
{page.title}
-
- ))}
+ {pages.map((page) => {
+ const classes = classNames('monitor-configure-page-button', {
+ active: page.id === pageId,
+ 'error-circle': errorPages.has(page.id),
+ });
+
+ return (
+
{
+ setErrorPages((oldSet) => {
+ const trimmedErrorPages = new Set([...oldSet]);
+ trimmedErrorPages.delete(pageId);
+ return trimmedErrorPages;
+ });
+ setPageId(page.id);
+ }}
+ key={page.id}
+ >
+ {page.icon}
+
{page.title}
+
+ );
+ })}
diff --git a/app/components/modal/monitor/pages/initial/index.tsx b/app/components/modal/monitor/pages/initial/index.tsx
index 7e69b696..40d37ca2 100644
--- a/app/components/modal/monitor/pages/initial/index.tsx
+++ b/app/components/modal/monitor/pages/initial/index.tsx
@@ -4,6 +4,7 @@ import { Input } from '@lunalytics/ui';
// import local files
import MonitorInitialDropdown from './type';
import MonitorIconSelect from './icons';
+import MonitorParentSelect from './parent';
interface MonitorInitialTypeProps {
inputs: any;
@@ -51,6 +52,8 @@ const MonitorInitialType = ({
)}
+
+
);
};
diff --git a/app/components/modal/monitor/pages/initial/parent.tsx b/app/components/modal/monitor/pages/initial/parent.tsx
new file mode 100644
index 00000000..ea2bff9a
--- /dev/null
+++ b/app/components/modal/monitor/pages/initial/parent.tsx
@@ -0,0 +1,71 @@
+import { observer } from 'mobx-react-lite';
+import useDropdown from '../../../../../hooks/useDropdown';
+import Dropdown from '../../../../ui/dropdown';
+import useContextStore from '../../../../../context';
+
+const MonitorParentSelect = ({
+ inputs,
+ handleInput,
+}: {
+ inputs: any;
+ handleInput: (key: string, value: any) => void;
+}) => {
+ const {
+ globalStore: { allMonitors },
+ } = useContextStore();
+ const { toggleDropdown, dropdownIsOpen } = useDropdown(true);
+
+ const onSelect = (monitorId: string | null) => {
+ handleInput('parentId', monitorId);
+ toggleDropdown();
+ };
+
+ const monitors = allMonitors.filter(
+ (monitor) => monitor.monitorId !== inputs.monitorId
+ );
+
+ return (
+
+
+
+
+
+
+
+ {inputs.parentId
+ ? monitors.find(
+ (monitor) => monitor.monitorId === inputs.parentId
+ )?.name
+ : 'No parent'}
+
+
+ onSelect(null)}>
+ No parent
+
+ {monitors.map((monitor) => (
+ onSelect(monitor.monitorId)}
+ >
+ {monitor.name}
+
+ ))}
+
+
+
+
+ );
+};
+
+export default observer(MonitorParentSelect);
diff --git a/app/components/modal/monitor/styles.scss b/app/components/modal/monitor/styles.scss
index 41d9b3f4..fd7bed60 100644
--- a/app/components/modal/monitor/styles.scss
+++ b/app/components/modal/monitor/styles.scss
@@ -13,6 +13,7 @@
}
.monitor-configure-page-button {
+ position: relative;
padding: 12px;
background-color: var(--accent-800);
border-radius: 8px;
@@ -31,6 +32,17 @@
&:hover {
background-color: var(--accent-700);
}
+
+ &.error-circle::after {
+ content: "";
+ position: absolute;
+ top: 40%;
+ right: 10px;
+ width: 10px;
+ height: 10px;
+ background-color: red;
+ border-radius: 50%;
+ }
}
.monitor-configure-left-container {
diff --git a/app/handlers/monitor.ts b/app/handlers/monitor.ts
index 07a3cc01..08d1785a 100644
--- a/app/handlers/monitor.ts
+++ b/app/handlers/monitor.ts
@@ -33,6 +33,7 @@ const handleMonitor = async (
const {
name,
+ parentId,
type,
url,
method,
@@ -57,6 +58,7 @@ const handleMonitor = async (
const query = await createPostRequest(apiPath, {
name,
+ parentId,
type,
url,
method,
diff --git a/app/hooks/useMonitorForm.tsx b/app/hooks/useMonitorForm.tsx
index e26b542e..6eccde49 100644
--- a/app/hooks/useMonitorForm.tsx
+++ b/app/hooks/useMonitorForm.tsx
@@ -24,18 +24,50 @@ const useMonitorForm = (
values: Partial = defaultInputs,
isEdit: boolean = false,
closeModal: () => void,
- setMonitor: (monitor: Partial) => void
+ setMonitor: (monitor: Partial) => void,
+ setPageId: (id: string) => void,
) => {
const [inputs, setInput] = useState>({
...defaultInputs,
...values,
});
const [errors, setErrors] = useState({});
+ const [errorPages, setErrorPages] = useState>(new Set())
const handleInput = (name: string, value: any) => {
setInput((prev) => ({ ...prev, [name]: value }));
};
+ const getPagesWithErrors = (errorsObj: Record) => {
+
+ const associatedPage: Record = {
+ 'name': "basic",
+ 'type': "basic",
+ 'url': "basic",
+ 'port': "basic",
+ 'icon': "basic",
+ 'method': "basic",
+ 'json_query': "basic",
+ 'interval': "interval",
+ 'retry': "interval",
+ 'retryInterval': "interval",
+ 'requestTimeout': "interval",
+ 'notificationType': "notification",
+ 'headers': "advanced",
+ 'body': "advanced",
+ 'valid_status_codes': "advanced",
+ }
+ const pagesWithError = new Set();
+
+ Object.keys(errorsObj).forEach(error => {
+ const page = associatedPage[error]
+ if(error && page) pagesWithError.add(page)
+ })
+
+ return pagesWithError
+ }
+
+
const handleActionButtons = (action: string) => () => {
switch (action) {
case 'Create': {
@@ -43,15 +75,20 @@ const useMonitorForm = (
const validator = monitorValidators[type];
if (!validator) return console.log("Validator doesn't exist");
- const errorsObj = validator(inputs);
+ const errorsObj = validator(inputs) as Record | false;
+
+ if (errorsObj !== false) {
+ const pagesWithErrors = getPagesWithErrors(errorsObj)
+ setErrorPages(pagesWithErrors);
+ setPageId(Array.from(pagesWithErrors)[0])
- if (errorsObj) {
- setErrors(errorsObj);
+ setErrors(errorsObj);
break;
}
+ setErrorPages(new Set())
setErrors({});
-
+
handleMonitor(inputs, isEdit, closeModal, setMonitor);
break;
}
@@ -66,7 +103,7 @@ const useMonitorForm = (
}
};
- return { inputs, errors, handleActionButtons, handleInput };
+ return { inputs, errors, handleActionButtons, handleInput, errorPages, setErrorPages };
};
export default useMonitorForm;
diff --git a/docs/public/icon-192x192.png b/docs/public/icon-192x192.png
deleted file mode 100644
index 774f8b8a..00000000
Binary files a/docs/public/icon-192x192.png and /dev/null differ
diff --git a/docs/public/icon-512x512.png b/docs/public/icon-512x512.png
deleted file mode 100644
index 5a37c9ca..00000000
Binary files a/docs/public/icon-512x512.png and /dev/null differ
diff --git a/package-lock.json b/package-lock.json
index 1826c7a7..a213dab9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "lunalytics",
- "version": "0.10.12",
+ "version": "0.10.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lunalytics",
- "version": "0.10.12",
+ "version": "0.10.13",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@dnd-kit/core": "6.3.1",
@@ -50,6 +50,7 @@
"react-window": "1.8.11",
"recharts": "3.2.1",
"swapy": "1.0.5",
+ "systeminformation": "^5.27.11",
"ua-parser-js": "2.0.3",
"uuid": "11.1.0",
"winston": "3.17.0",
@@ -12851,6 +12852,32 @@
"integrity": "sha512-XEzy5HCw7yESb7QajKTBJ+NA/BL0kAKlE7XhSMPZawF740X4ws/5CFtXRsVDQ+EztISC759a9+DJM9mxjz4hXg==",
"license": "GPL-3.0"
},
+ "node_modules/systeminformation": {
+ "version": "5.27.11",
+ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.27.11.tgz",
+ "integrity": "sha512-K3Lto/2m3K2twmKHdgx5B+0in9qhXK4YnoT9rIlgwN/4v7OV5c8IjbeAUkuky/6VzCQC7iKCAqi8rZathCdjHg==",
+ "license": "MIT",
+ "os": [
+ "darwin",
+ "linux",
+ "win32",
+ "freebsd",
+ "openbsd",
+ "netbsd",
+ "sunos",
+ "android"
+ ],
+ "bin": {
+ "systeminformation": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "Buy me a coffee",
+ "url": "https://www.buymeacoffee.com/systeminfo"
+ }
+ },
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
diff --git a/package.json b/package.json
index f558e0c8..8ed274e5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "lunalytics",
- "version": "0.10.12",
+ "version": "0.10.13",
"description": "Open source Node.js server/website monitoring tool",
"private": true,
"author": "KSJaay ",
@@ -81,6 +81,7 @@
"react-window": "1.8.11",
"recharts": "3.2.1",
"swapy": "1.0.5",
+ "systeminformation": "^5.27.11",
"ua-parser-js": "2.0.3",
"uuid": "11.1.0",
"winston": "3.17.0",
diff --git a/docs/public/demo.gif b/public/demo.gif
similarity index 100%
rename from docs/public/demo.gif
rename to public/demo.gif
diff --git a/docs/public/header.png b/public/header.png
similarity index 100%
rename from docs/public/header.png
rename to public/header.png
diff --git a/scripts/migrations/0-10-13.js b/scripts/migrations/0-10-13.js
new file mode 100644
index 00000000..af6d13fa
--- /dev/null
+++ b/scripts/migrations/0-10-13.js
@@ -0,0 +1,22 @@
+// import local files
+import SQLite from '../../server/database/sqlite/setup.js';
+import logger from '../../server/utils/logger.js';
+
+const infomation = {
+ title: 'Add parentId to monitor table',
+ description: 'Adds parentId to monitor table',
+ version: '0.10.13',
+};
+
+const migrate = async () => {
+ const client = await SQLite.connect();
+
+ await client.schema.alterTable('monitor', (table) => {
+ table.string('parentId').nullable().defaultTo(null);
+ });
+
+ logger.info('Migrations', { message: '0.10.13 has been applied' });
+ return;
+};
+
+export { infomation, migrate };
diff --git a/scripts/migrations/index.js b/scripts/migrations/index.js
index c73d0bff..d76f0b29 100644
--- a/scripts/migrations/index.js
+++ b/scripts/migrations/index.js
@@ -11,6 +11,7 @@ import { migrate as migrateIncidentEmail } from './0-9-4.js';
import { migrate as migrateMonitor } from './0-9-5.js';
import { migrate as migrateMonitorJson } from './0-9-7.js';
import { migrate as migrateUiOverhaul } from './0-10-0.js';
+import { migrate as migrateMonitorParent } from './0-10-13.js';
const migrationList = {
'0.4.0': migrateTcpUpdate,
@@ -25,6 +26,7 @@ const migrationList = {
'0.9.5': migrateMonitor,
'0.9.7': migrateMonitorJson,
'0.10.0': migrateUiOverhaul,
+ '0.10.13': migrateMonitorParent,
};
export default migrationList;
diff --git a/server/cache/index.js b/server/cache/index.js
index 2057896a..82a70895 100644
--- a/server/cache/index.js
+++ b/server/cache/index.js
@@ -197,6 +197,21 @@ class Master {
if (!hasOutage && !hasRecovered) return;
if (!monitor.notificationId) return;
+ if (hasOutage && monitor.parentId) {
+ const parentMonitor = await fetchMonitor(monitor.parentId).catch(
+ () => false
+ );
+
+ if (parentMonitor) {
+ const isDown = await isMonitorDown(
+ parentMonitor.monitorId,
+ parentMonitor.retry
+ );
+
+ if (isDown) return;
+ }
+ }
+
const notification = await fetchNotificationById(monitor.notificationId);
if (
diff --git a/server/class/monitor/docker.js b/server/class/monitor/docker.js
index 35909f12..4ae2731e 100644
--- a/server/class/monitor/docker.js
+++ b/server/class/monitor/docker.js
@@ -2,6 +2,7 @@ import { parseJsonOrArray } from '../../utils/parser.js';
const clean = ({ heartbeats = [], ...monitor }, includeHeartbeats = true) => ({
monitorId: monitor.monitorId,
+ parentId: monitor.parentId || null,
name: monitor.name,
url: monitor.url,
retry: parseInt(monitor.retry),
diff --git a/server/class/monitor/http.js b/server/class/monitor/http.js
index faaae5ac..0b365026 100644
--- a/server/class/monitor/http.js
+++ b/server/class/monitor/http.js
@@ -7,6 +7,7 @@ const clean = (
includeCert = true
) => ({
monitorId: monitor.monitorId,
+ parentId: monitor.parentId || null,
name: monitor.name,
url: monitor.url,
retry: parseInt(monitor.retry),
diff --git a/server/class/monitor/json.js b/server/class/monitor/json.js
index af5c1c9b..4cb2ab21 100644
--- a/server/class/monitor/json.js
+++ b/server/class/monitor/json.js
@@ -7,6 +7,7 @@ export const clean = (
includeCert = true
) => ({
monitorId: monitor.monitorId,
+ parentId: monitor.parentId || null,
name: monitor.name,
url: monitor.url,
retry: parseInt(monitor.retry),
diff --git a/server/class/monitor/ping.js b/server/class/monitor/ping.js
index 93ce7483..ec36959b 100644
--- a/server/class/monitor/ping.js
+++ b/server/class/monitor/ping.js
@@ -5,6 +5,7 @@ export const clean = (
includeHeartbeats = true
) => ({
monitorId: monitor.monitorId,
+ parentId: monitor.parentId || null,
name: monitor.name,
url: monitor.url,
retry: parseInt(monitor.retry),
diff --git a/server/class/monitor/tcp.js b/server/class/monitor/tcp.js
index 31c727de..6f593122 100644
--- a/server/class/monitor/tcp.js
+++ b/server/class/monitor/tcp.js
@@ -2,6 +2,7 @@ import { parseJsonOrArray } from '../../utils/parser.js';
const clean = ({ heartbeats = [], ...monitor }, includeHeartbeats = true) => ({
monitorId: monitor.monitorId,
+ parentId: monitor.parentId || null,
name: monitor.name,
url: monitor.url,
retry: parseInt(monitor.retry),
diff --git a/server/database/sqlite/tables/monitor.js b/server/database/sqlite/tables/monitor.js
index 7bd9fb2a..5b42fad2 100644
--- a/server/database/sqlite/tables/monitor.js
+++ b/server/database/sqlite/tables/monitor.js
@@ -5,6 +5,7 @@ export const monitorTable = async (client) => {
await client.schema.createTable('monitor', (table) => {
table.increments('id');
table.string('monitorId').notNullable().primary();
+ table.string('parentId').defaultTo(null);
table.string('name').notNullable();
table.string('url').notNullable();
table.integer('port').defaultTo(null);
diff --git a/server/index.js b/server/index.js
index 41ac1879..28b73a9b 100644
--- a/server/index.js
+++ b/server/index.js
@@ -17,10 +17,7 @@ import initialiseCronJobs from './utils/cron.js';
import migrateDatabase from '../scripts/migrate.js';
import addInviteToCookie from './middleware/addInviteToCookie.js';
import { loadIcons } from './utils/icons.js';
-import {
- getVersionInfo,
- startVersionCheck
-} from './utils/checkVersion.js';
+import { getVersionInfo, startVersionCheck } from './utils/checkVersion.js';
const app = express();
diff --git a/server/middleware/monitor/add.js b/server/middleware/monitor/add.js
index 765b7e50..af82eb45 100644
--- a/server/middleware/monitor/add.js
+++ b/server/middleware/monitor/add.js
@@ -25,6 +25,7 @@ export const defaultMonitorData = (body) => ({
url: body.url,
interval: body.interval ?? 60,
monitorId: body.monitorId,
+ parentId: body.parentId ?? null,
retry: body.retry ?? 1,
retryInterval: body.retryInterval ?? 60,
requestTimeout: body.requestTimeout ?? 60,
@@ -51,6 +52,7 @@ export const formatMonitorData = (body, email) => {
url: body.url,
interval: body.interval,
monitorId: body.monitorId,
+ parentId: body.parentId,
retry: body.retry,
retryInterval: body.retryInterval,
requestTimeout: body.requestTimeout,
diff --git a/test/server/class/monitor.test.js b/test/server/class/monitor.test.js
index 87314480..a2db826e 100644
--- a/test/server/class/monitor.test.js
+++ b/test/server/class/monitor.test.js
@@ -4,6 +4,7 @@ import { cleanMonitor } from '../../../server/class/monitor/index';
describe('Monitor - Class', () => {
const monitor = {
monitorId: '4d048471-9e85-428b-8050-4238f6033478',
+ parentId: '4d048471-9e85-428b-8050-4238f6033479',
name: 'Lunalytics',
url: 'https://demo.lunalytics.xyz/api/status',
createdAt: '2025-08-23T17:00:00.000Z',
@@ -48,6 +49,7 @@ describe('Monitor - Class', () => {
it('should return valid monitor using cleanPartialMonitor', () => {
expect(cleanMonitor(monitor, false, false)).toEqual({
monitorId: '4d048471-9e85-428b-8050-4238f6033478',
+ parentId: '4d048471-9e85-428b-8050-4238f6033479',
name: 'Lunalytics',
url: 'https://demo.lunalytics.xyz/api/status',
createdAt: '2025-08-23T17:00:00.000Z',
@@ -87,6 +89,7 @@ describe('Monitor - Class', () => {
})
).toEqual({
monitorId: '4d048471-9e85-428b-8050-4238f6033478',
+ parentId: '4d048471-9e85-428b-8050-4238f6033479',
name: 'Lunalytics',
url: 'https://demo.lunalytics.xyz/api/status',
createdAt: '2025-08-23T17:00:00.000Z',
diff --git a/test/server/middleware/monitor/add.test.js b/test/server/middleware/monitor/add.test.js
index 53a96fbf..e51e677b 100644
--- a/test/server/middleware/monitor/add.test.js
+++ b/test/server/middleware/monitor/add.test.js
@@ -154,6 +154,7 @@ describe('Add Monitor - Middleware', () => {
requestTimeout: fakeRequest.body.requestTimeout,
notificationId: fakeRequest.body.notificationId,
notificationType: fakeRequest.body.notificationType,
+ parentId: null,
headers: JSON.stringify(fakeRequest.body.headers),
body: JSON.stringify(fakeRequest.body.body),
email: user.email,
@@ -276,6 +277,7 @@ describe('Add Monitor - Middleware', () => {
requestTimeout: fakeRequest.body.requestTimeout,
notificationId: fakeRequest.body.notificationId,
notificationType: fakeRequest.body.notificationType,
+ parentId: null,
email: user.email,
port: fakeRequest.body.port,
valid_status_codes: '',
diff --git a/test/server/middleware/monitor/edit.test.js b/test/server/middleware/monitor/edit.test.js
index b8498373..40aecb84 100644
--- a/test/server/middleware/monitor/edit.test.js
+++ b/test/server/middleware/monitor/edit.test.js
@@ -154,6 +154,7 @@ describe('Edit Monitor - Middleware', () => {
requestTimeout: fakeRequest.body.requestTimeout,
notificationId: fakeRequest.body.notificationId,
notificationType: fakeRequest.body.notificationType,
+ parentId: null,
body: JSON.stringify(fakeRequest.body.body),
headers: JSON.stringify(fakeRequest.body.headers),
email: user.email,
@@ -275,6 +276,7 @@ describe('Edit Monitor - Middleware', () => {
requestTimeout: fakeRequest.body.requestTimeout,
notificationId: fakeRequest.body.notificationId,
notificationType: fakeRequest.body.notificationType,
+ parentId: null,
email: user.email,
port: fakeRequest.body.port,
valid_status_codes: '',