diff --git a/README.md b/README.md index 63f5e55f..a6be1e4e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Discord -![Demo](https://raw.githubusercontent.com/KSJaay/Lunalytics/refs/heads/main/docs/public/demo.gif) +![Demo](https://raw.githubusercontent.com/KSJaay/Lunalytics/refs/heads/main/public/demo.gif) ## 📔 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: '',