diff --git a/.dockerignore b/.dockerignore index b68a8a694..d439b2db5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,11 @@ /node_modules /data/kuma.db /.do +**/.dockerignore +**/.git +**/.gitignore +**/docker-compose* +**/Dockerfile* +LICENSE +README.md +.editorconfig diff --git a/.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md b/.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md new file mode 100644 index 000000000..eb8623709 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md @@ -0,0 +1,10 @@ +--- +name: ⚠ Please go to "Discussions" Tab if you want to ask or share something +about: BUG REPORT ONLY HERE +title: '' +labels: '' +assignees: '' + +--- + +BUG REPORT ONLY HERE diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..cea1fc16e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - Uptime Kuma Version: + - Using Docker?: Yes/No + - OS: + - Browser: + + +**Additional context** +Add any other context about the problem here. diff --git a/README.md b/README.md index 35bfeb8ab..7777cb2c5 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ It is a self-hosted monitoring tool like "Uptime Robot". docker volume create uptime-kuma # Start the container -docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma +docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1 ``` Browse to http://localhost:3001 after started. @@ -35,7 +35,7 @@ Browse to http://localhost:3001 after started. Change Port and Volume ```bash -docker run -d --restart=always -p :3001 -v :/app/data --name uptime-kuma louislam/uptime-kuma +docker run -d --restart=always -p :3001 -v :/app/data --name uptime-kuma louislam/uptime-kuma:1 ``` ### Without Docker @@ -80,12 +80,17 @@ PS: For every new release, it takes some time to build the docker image, please ```bash git fetch --all -git checkout 1.0.5 --force +git checkout 1.0.6 --force npm install npm run build pm2 restart uptime-kuma ``` +# What's Next? + +I will mark requests/issues to the next milestone. +https://github.com/louislam/uptime-kuma/milestones + # More Screenshots Settings Page: @@ -109,3 +114,11 @@ Telegram Notification Sample: If you love this project, please consider giving me a ⭐. + +# Contribute + +If you want to report a bug or request a new feature. Free feel to open a new issue. + +If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/wiki/%5BDev%5D-Setup-Development-Environment + +English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki. diff --git a/db/patch1.sql b/db/patch1.sql new file mode 100644 index 000000000..6a31fa2f6 --- /dev/null +++ b/db/patch1.sql @@ -0,0 +1,37 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +-- Change Monitor.created_date from "TIMESTAMP" to "DATETIME" +-- SQL Generated by Intellij Idea +PRAGMA foreign_keys=off; + +BEGIN TRANSACTION; + +create table monitor_dg_tmp +( + id INTEGER not null + primary key autoincrement, + name VARCHAR(150), + active BOOLEAN default 1 not null, + user_id INTEGER + references user + on update cascade on delete set null, + interval INTEGER default 20 not null, + url TEXT, + type VARCHAR(20), + weight INTEGER default 2000, + hostname VARCHAR(255), + port INTEGER, + created_date DATETIME, + keyword VARCHAR(255) +); + +insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor; + +drop table monitor; + +alter table monitor_dg_tmp rename to monitor; + +create index user_id on monitor (user_id); + +COMMIT; + +PRAGMA foreign_keys=on; diff --git a/dockerfile b/dockerfile index 923d8b10a..1ebf00999 100644 --- a/dockerfile +++ b/dockerfile @@ -17,11 +17,14 @@ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \ # Compilation Fail 3 => Google Search "alpine opensslv.h" => Add openssl-dev # Compilation Fail 4 => Google Search "alpine opensslv.h" again => Change to libressl-dev musl-dev # Compilation Fail 5 => Google Search "ERROR: libressl3.3-libtls-3.3.3-r0: trying to overwrite usr/lib/libtls.so.20 owned by libretls-3.3.3-r0." again => Change back to openssl-dev with musl-dev +# Runtime Error => ModuleNotFoundError: No module named 'six' => pip3 install six +# Runtime Error 2 => ModuleNotFoundError: No module named 'six' => apk add py3-six ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1 -RUN apk add --no-cache python3 -RUN apk add --no-cache --virtual .build-deps libffi-dev musl-dev openssl-dev cargo py3-pip python3-dev && \ +RUN apk add --no-cache python3 py3-pip py3-six cargo +RUN apk add --no-cache --virtual .build-deps libffi-dev musl-dev openssl-dev python3-dev && \ pip3 install apprise && \ apk del .build-deps +RUN apprise --version # New things add here @@ -31,7 +34,7 @@ RUN npm run build EXPOSE 3001 VOLUME ["/app/data"] -HEALTHCHECK --interval=5s --timeout=3s --start-period=30s CMD node extra/healthcheck.js +HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js CMD ["npm", "run", "start-server"] FROM release AS nightly diff --git a/extra/mark-as-nightly.js b/extra/mark-as-nightly.js index c35455e87..28496511b 100644 --- a/extra/mark-as-nightly.js +++ b/extra/mark-as-nightly.js @@ -34,6 +34,7 @@ if (newVersion) { fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n") // Process README.md - fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion)) + if (fs.existsSync("README.md")) { + fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion)) + } } - diff --git a/package-lock.json b/package-lock.json index e7486fc81..99dd01b6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.0.4", + "version": "1.0.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -40,37 +40,43 @@ "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" }, "@types/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "@types/cors": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", - "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==" + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true }, "@types/node": { - "version": "15.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz", - "integrity": "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==" + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.3.tgz", + "integrity": "sha512-8h7k1YgQKxKXWckzFCMfsIwn0Y61UK6tlD6y2lOb3hTOIMlK3t9/QwHOhc81TwU+RMf0As5fj7NPjroERCnejQ==" }, "@vitejs/plugin-legacy": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.4.3.tgz", - "integrity": "sha512-lxZUJaMWYMQuqvZM1wPzDP6KABQgA/drVL5fnaygEPcz9adc2OHhfFNN/SvvHQ1V0rP8gybIc7uA+iI1gAdkVQ==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.4.4.tgz", + "integrity": "sha512-pVYeQUDPG5InWwrTu7acy187WWjGonJnL/GMqMLmeKCFiwkZ6UcsoUjojiKmCUI0nAJTrrKH5lhjTqkccY9Iow==", "dev": true, "requires": { "@babel/standalone": "^7.14.7", - "core-js": "^3.15.1", + "core-js": "^3.15.2", "magic-string": "^0.25.7", "regenerator-runtime": "^0.13.7", - "systemjs": "^6.10.1" + "systemjs": "^6.10.2" } }, "@vitejs/plugin-vue": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.2.3.tgz", - "integrity": "sha512-LlnLpObkGKZ+b7dcpL4T24l13nPSHLjo+6Oc7MbZiKz5PMAUzADfNJ3EKfYIQ0l0969nxf2jp/9vsfnuJ7h6fw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.2.5.tgz", + "integrity": "sha512-GIR31mdXTEfvElmBUaRhDc5v7lfdkEdawWQqJRiaRL/5qKsH+xusukglkvJz5y7+c6dEpxgmvcATv2BbB7+fzQ==", "dev": true }, "@vue/compiler-core": { @@ -95,17 +101,18 @@ } }, "@vue/compiler-sfc": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.1.tgz", - "integrity": "sha512-lSgMsZaYHF+bAgryq5aUqpvyfhu52GJI2/4LoiJCE5uaxc6FCZfxfgqgw/d9ltiZghv+HiISFtmQVAVvlsk+/w==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.5.tgz", + "integrity": "sha512-mtMY6xMvZeSRx9MTa1+NgJWndrkzVTdJ1pQAmAKQuxyb5LsHVvrgP7kcQFvxPHVpLVTORbTJWHaiqoKrJvi1iA==", "dev": true, "requires": { "@babel/parser": "^7.13.9", "@babel/types": "^7.13.0", - "@vue/compiler-core": "3.1.1", - "@vue/compiler-dom": "3.1.1", - "@vue/compiler-ssr": "3.1.1", - "@vue/shared": "3.1.1", + "@types/estree": "^0.0.48", + "@vue/compiler-core": "3.1.5", + "@vue/compiler-dom": "3.1.5", + "@vue/compiler-ssr": "3.1.5", + "@vue/shared": "3.1.5", "consolidate": "^0.16.0", "estree-walker": "^2.0.1", "hash-sum": "^2.0.0", @@ -116,16 +123,78 @@ "postcss-modules": "^4.0.0", "postcss-selector-parser": "^6.0.4", "source-map": "^0.6.1" + }, + "dependencies": { + "@vue/compiler-core": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.5.tgz", + "integrity": "sha512-TXBhFinoBaXKDykJzY26UEuQU1K07FOp/0Ie+OXySqqk0bS0ZO7Xvl7UmiTUPYcLrWbxWBR7Bs/y55AI0MNc2Q==", + "dev": true, + "requires": { + "@babel/parser": "^7.12.0", + "@babel/types": "^7.12.0", + "@vue/shared": "3.1.5", + "estree-walker": "^2.0.1", + "source-map": "^0.6.1" + } + }, + "@vue/compiler-dom": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.5.tgz", + "integrity": "sha512-ZsL3jqJ52OjGU/YiT/9XiuZAmWClKInZM2aFJh9gnsAPqOrj2JIELMbkIFpVKR/CrVO/f2VxfPiiQdQTr65jcQ==", + "dev": true, + "requires": { + "@vue/compiler-core": "3.1.5", + "@vue/shared": "3.1.5" + } + }, + "@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "dev": true + } } }, "@vue/compiler-ssr": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.1.tgz", - "integrity": "sha512-7H6krZtVt3h/YzfNp7eYK41hMDz8ZskiBy+Wby+EDRINX6BD9JQ5C8zyy2xAa7T6Iz2VrQzsaJ/Bb52lTPSS5A==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.5.tgz", + "integrity": "sha512-CU5N7Di/a4lyJ18LGJxJYZS2a8PlLdWpWHX9p/XcsjT2TngMpj3QvHVRkuik2u8QrIDZ8OpYmTyj1WDNsOV+Dg==", "dev": true, "requires": { - "@vue/compiler-dom": "3.1.1", - "@vue/shared": "3.1.1" + "@vue/compiler-dom": "3.1.5", + "@vue/shared": "3.1.5" + }, + "dependencies": { + "@vue/compiler-core": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.5.tgz", + "integrity": "sha512-TXBhFinoBaXKDykJzY26UEuQU1K07FOp/0Ie+OXySqqk0bS0ZO7Xvl7UmiTUPYcLrWbxWBR7Bs/y55AI0MNc2Q==", + "dev": true, + "requires": { + "@babel/parser": "^7.12.0", + "@babel/types": "^7.12.0", + "@vue/shared": "3.1.5", + "estree-walker": "^2.0.1", + "source-map": "^0.6.1" + } + }, + "@vue/compiler-dom": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.5.tgz", + "integrity": "sha512-ZsL3jqJ52OjGU/YiT/9XiuZAmWClKInZM2aFJh9gnsAPqOrj2JIELMbkIFpVKR/CrVO/f2VxfPiiQdQTr65jcQ==", + "dev": true, + "requires": { + "@vue/compiler-core": "3.1.5", + "@vue/shared": "3.1.5" + } + }, + "@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "dev": true + } } }, "@vue/devtools-api": { @@ -323,6 +392,11 @@ "follow-redirects": "^1.10.0" } }, + "babel-plugin-add-module-exports": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz", + "integrity": "sha1-mumh9KjcZ/DN7E9K7aHkOl/2XiU=" + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -531,9 +605,9 @@ } }, "bootstrap": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.0.1.tgz", - "integrity": "sha512-Fl79+wsLOZKoiU345KeEaWD0ik8WKRI5zm0YSPj2oF1Qr+BO7z0fco6GbUtqjoG1h4VI89PeKJnMsMMVQdKKTw==" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.0.2.tgz", + "integrity": "sha512-1Ge963tyEQWJJ+8qtXFU6wgmAVj9gweEjibUdbmcCEYsn38tVwRk8107rk2vzt6cfQcRr3SlZ8aQBqaD8aqf+Q==" }, "brace-expansion": { "version": "1.1.11", @@ -655,6 +729,11 @@ "delayed-stream": "~1.0.0" } }, + "command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==" + }, "commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -753,9 +832,9 @@ } }, "dayjs": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.5.tgz", - "integrity": "sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g==" + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", + "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==" }, "debug": { "version": "4.3.1", @@ -898,9 +977,9 @@ } }, "engine.io-client": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.1.1.tgz", - "integrity": "sha512-jPFpw2HLL0lhZ2KY0BpZhIJdleQcUO9W1xkIpo0h3d6s+5D6+EV/xgQw9qWOmymszv2WXef/6KUUehyxEKomlQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.1.2.tgz", + "integrity": "sha512-blRrgXIE0A/eurWXRzvfCLG7uUFJqfTGFsyJzXSK71srMMGJ2VraBLg8Mdw28uUxSpVicepBN9X7asqpD1mZcQ==", "requires": { "base64-arraybuffer": "0.1.4", "component-emitter": "~1.3.0", @@ -922,9 +1001,9 @@ } }, "esbuild": { - "version": "0.12.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.9.tgz", - "integrity": "sha512-MWRhAbMOJ9RJygCrt778rz/qNYgA4ZVj6aXnNPxFjs7PmIpb0fuB9Gmg5uWrr6n++XKwwm/RmSz6RR5JL2Ocsw==", + "version": "0.12.15", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.15.tgz", + "integrity": "sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==", "dev": true }, "escape-html": { @@ -2019,6 +2098,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "merge": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", + "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -2420,9 +2504,9 @@ } }, "nodemailer": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.2.tgz", - "integrity": "sha512-YSzu7TLbI+bsjCis/TZlAXBoM4y93HhlIgo0P5oiA2ua9Z4k+E2Fod//ybIzdJxOlXGRcHIh/WaeCBehvxZb/Q==" + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.3.tgz", + "integrity": "sha512-faZFufgTMrphYoDjvyVpbpJcYzwyFnbAMmQtj1lVBYAUSm3SOy2fIdd9+Mr4UxPosBa0JRw9bJoIwQn+nswiew==" }, "nopt": { "version": "3.0.6", @@ -2975,9 +3059,9 @@ } }, "rollup": { - "version": "2.52.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.2.tgz", - "integrity": "sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA==", + "version": "2.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.53.2.tgz", + "integrity": "sha512-1CtEYuS5CRCzFZ7SNW5528SlDlk4VDXIRGwbm/2POQxA/G4+7/crIqJwkmnj8Q/74hGx4oVlNvh4E1CJQ5hZ6w==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -3002,9 +3086,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.35.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.35.1.tgz", - "integrity": "sha512-oCisuQJstxMcacOPmxLNiLlj4cUyN2+8xJnG7VanRoh2GOLr9RqkvI4AxA4a6LHVg/rsu+PmxXeGhrdSF9jCiQ==", + "version": "1.35.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.35.2.tgz", + "integrity": "sha512-jhO5KAR+AMxCEwIH3v+4zbB2WB0z67V1X0jbapfVwQQdjHZUGUyukpnoM6+iCMfsIUC016w9OPKQ5jrNOS9uXw==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0" @@ -3225,19 +3309,19 @@ } }, "socket.io": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.1.2.tgz", - "integrity": "sha512-xK0SD1C7hFrh9+bYoYCdVt+ncixkSLKtNLCax5aEy1o3r5PaO5yQhVb97exIe67cE7lAK+EpyMytXWTWmyZY8w==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.1.3.tgz", + "integrity": "sha512-tLkaY13RcO4nIRh1K2hT5iuotfTaIQw7cVIe0FUykN3SuQi0cm7ALxuyT5/CtDswOMWUzMGTibxYNx/gU7In+Q==", "requires": { "@types/cookie": "^0.4.0", - "@types/cors": "^2.8.8", + "@types/cors": "^2.8.10", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "~2.0.0", "debug": "~4.3.1", - "engine.io": "~5.1.0", - "socket.io-adapter": "~2.3.0", - "socket.io-parser": "~4.0.3" + "engine.io": "~5.1.1", + "socket.io-adapter": "~2.3.1", + "socket.io-parser": "~4.0.4" } }, "socket.io-adapter": { @@ -3246,15 +3330,15 @@ "integrity": "sha512-8cVkRxI8Nt2wadkY6u60Y4rpW3ejA1rxgcK2JuyIhmF+RMNpTy1QRtkHIDUOf3B4HlQwakMsWbKftMv/71VMmw==" }, "socket.io-client": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.1.2.tgz", - "integrity": "sha512-RDpWJP4DQT1XeexmeDyDkm0vrFc0+bUsHDKiVGaNISJvJonhQQOMqV9Vwfg0ZpPJ27LCdan7iqTI92FRSOkFWQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.1.3.tgz", + "integrity": "sha512-hISFn6PDpgDifVUiNklLHVPTMv1LAk8poHArfIUdXa+gKgbr0MZbAlquDFqCqsF30yBqa+jg42wgos2FK50BHA==", "requires": { "@types/component-emitter": "^1.2.10", "backo2": "~1.0.2", "component-emitter": "~1.3.0", "debug": "~4.3.1", - "engine.io-client": "~5.1.1", + "engine.io-client": "~5.1.2", "parseuri": "0.0.6", "socket.io-parser": "~4.0.4" } @@ -3624,6 +3708,16 @@ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "optional": true }, + "v-pagination-3": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/v-pagination-3/-/v-pagination-3-0.1.6.tgz", + "integrity": "sha512-82J8HnEIYtZijn6F3xhyP/ildI5K7Rv4Yu74VNfQWQsiPWTKntgVvZgBH8UPh/lFEjgWxty/M4N+YHvS+YbGzg==", + "requires": { + "babel-plugin-add-module-exports": "^0.2.1", + "merge": "^2.1.1", + "vue": ">=3.0.0" + } + }, "v8flags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", @@ -3649,14 +3743,14 @@ } }, "vite": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.3.8.tgz", - "integrity": "sha512-QiEx+iqNnJntSgSF2fWRQvRey9pORIrtNJzNyBJXwc+BdzWs83FQolX84cTBo393cfhObrtWa6180dAa4NLDiQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.4.2.tgz", + "integrity": "sha512-2MifxD2I9fjyDmmEzbULOo3kOUoqX90A58cT6mECxoVQlMYFuijZsPQBuA14mqSwvV3ydUsqnq+BRWXyO9Qa+w==", "dev": true, "requires": { "esbuild": "^0.12.8", "fsevents": "~2.3.2", - "postcss": "^8.3.4", + "postcss": "^8.3.5", "resolve": "^1.20.0", "rollup": "^2.38.5" } diff --git a/package.json b/package.json index 8c03f9d3b..d4fe68885 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,21 @@ { "name": "uptime-kuma", - "version": "1.0.5", + "version": "1.0.6", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/louislam/uptime-kuma.git" + }, "scripts": { "dev": "vite --host", "start-server": "node server/server.js", "update": "", "build": "vite build", "vite-preview-dist": "vite preview --host", - "build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.5 --target release . --push", - "build-docker-nightly": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly --target nightly . --push", - "setup": "git checkout 1.0.5 && npm install && npm run build", + "build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.6 --target release . --push", + "build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", + "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push", + "setup": "git checkout 1.0.6 && npm install && npm run build", "version-global-replace": "node extra/version-global-replace.js", "mark-as-nightly": "node extra/mark-as-nightly.js" }, @@ -18,30 +24,32 @@ "args-parser": "^1.3.0", "axios": "^0.21.1", "bcrypt": "^5.0.1", - "bootstrap": "^5.0.0", - "dayjs": "^1.10.4", + "bootstrap": "^5.0.2", + "command-exists": "^1.2.9", + "dayjs": "^1.10.6", "express": "^4.17.1", "form-data": "^4.0.0", "http-graceful-shutdown": "^3.1.2", "jsonwebtoken": "^8.5.1", - "nodemailer": "^6.6.2", + "nodemailer": "^6.6.3", "password-hash": "^1.2.2", "redbean-node": "0.0.20", - "socket.io": "^4.0.2", - "socket.io-client": "^4.1.2", - "sqlite3": "^5.0.0", + "socket.io": "^4.1.3", + "socket.io-client": "^4.1.3", + "sqlite3": "^5.0.2", "tcp-ping": "^0.1.1", + "v-pagination-3": "^0.1.6", "vue": "^3.0.5", "vue-confirm-dialog": "^1.0.2", "vue-router": "^4.0.10", "vue-toastification": "^2.0.0-rc.1" }, "devDependencies": { - "@vitejs/plugin-legacy": "^1.4.3", - "@vitejs/plugin-vue": "^1.2.3", - "@vue/compiler-sfc": "^3.0.5", + "@vitejs/plugin-legacy": "^1.4.4", + "@vitejs/plugin-vue": "^1.2.5", + "@vue/compiler-sfc": "^3.1.5", "core-js": "^3.15.2", - "sass": "^1.35.1", - "vite": "^2.3.7" + "sass": "^1.35.2", + "vite": "^2.4.2" } } diff --git a/server/database.js b/server/database.js new file mode 100644 index 000000000..49659e613 --- /dev/null +++ b/server/database.js @@ -0,0 +1,119 @@ +const fs = require("fs"); +const {sleep} = require("./util"); +const {R} = require("redbean-node"); +const {setSetting, setting} = require("./util-server"); + + +class Database { + + static templatePath = "./db/kuma.db" + static path = './data/kuma.db'; + static latestVersion = 1; + static noReject = true; + + static async patch() { + let version = parseInt(await setting("database_version")); + + if (! version) { + version = 0; + } + + console.info("Your database version: " + version); + console.info("Latest database version: " + this.latestVersion); + + if (version === this.latestVersion) { + console.info("Database no need to patch"); + } else { + console.info("Database patch is needed") + + console.info("Backup the db") + const backupPath = "./data/kuma.db.bak" + version; + fs.copyFileSync(Database.path, backupPath); + + // Try catch anything here, if gone wrong, restore the backup + try { + for (let i = version + 1; i <= this.latestVersion; i++) { + const sqlFile = `./db/patch${i}.sql`; + console.info(`Patching ${sqlFile}`); + await Database.importSQLFile(sqlFile); + console.info(`Patched ${sqlFile}`); + await setSetting("database_version", i); + } + console.log("Database Patched Successfully"); + } catch (ex) { + await Database.close(); + console.error("Patch db failed!!! Restoring the backup") + fs.copyFileSync(backupPath, Database.path); + console.error(ex) + + console.error("Start Uptime-Kuma failed due to patch db failed") + console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues") + process.exit(1); + } + } + } + + /** + * Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself + * @param filename + * @returns {Promise} + */ + static async importSQLFile(filename) { + + await R.getCell("SELECT 1"); + + let text = fs.readFileSync(filename).toString(); + + // Remove all comments (--) + let lines = text.split("\n"); + lines = lines.filter((line) => { + return ! line.startsWith("--") + }); + + // Split statements by semicolon + // Filter out empty line + text = lines.join("\n") + + let statements = text.split(";") + .map((statement) => { + return statement.trim(); + }) + .filter((statement) => { + return statement !== ""; + }) + + for (let statement of statements) { + await R.exec(statement); + } + } + + /** + * Special handle, because tarn.js throw a promise reject that cannot be caught + * @returns {Promise} + */ + static async close() { + const listener = (reason, p) => { + Database.noReject = false; + }; + process.addListener('unhandledRejection', listener); + + console.log("Closing DB") + + while (true) { + Database.noReject = true; + await R.close() + await sleep(2000) + + if (Database.noReject) { + break; + } else { + console.log("Waiting to close the db") + } + } + console.log("SQLite closed") + + process.removeListener('unhandledRejection', listener); + } +} + +module.exports = Database; diff --git a/server/model/monitor.js b/server/model/monitor.js index 162772875..f81f6e00e 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -48,8 +48,6 @@ class Monitor extends BeanModel { let previousBeat = null; const beat = async () => { - console.log(`Monitor ${this.id}: Heartbeat`) - if (! previousBeat) { previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ this.id @@ -145,6 +143,12 @@ class Monitor extends BeanModel { bean.important = false; } + if (bean.status === 1) { + console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`) + } else { + console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`) + } + io.to(this.user_id).emit("heartbeat", bean.toJSON()); await R.store(bean) diff --git a/server/notification.js b/server/notification.js index 1435614aa..781f7dfde 100644 --- a/server/notification.js +++ b/server/notification.js @@ -2,9 +2,22 @@ const axios = require("axios"); const {R} = require("redbean-node"); const FormData = require('form-data'); const nodemailer = require("nodemailer"); +const child_process = require("child_process"); class Notification { + + /** + * + * @param notification + * @param msg + * @param monitorJSON + * @param heartbeatJSON + * @returns {Promise} Successful msg + * Throw Error with fail msg + */ static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let okMsg = "Sent Successfully. "; + if (notification.type === "telegram") { try { await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, { @@ -13,15 +26,16 @@ class Notification { text: msg, } }) - return true; + return okMsg; + } catch (error) { - console.error(error) - return false; + let msg = (error.response.data.description) ? error.response.data.description : "Error without description" + throw new Error(msg) } } else if (notification.type === "gotify") { try { - if (notification.gotifyserverurl.endsWith("/")) { + if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) { notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1); } await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, { @@ -29,15 +43,15 @@ class Notification { "priority": notification.gotifyPriority || 8, "title": "Uptime-Kuma" }) - return true; + + return okMsg; + } catch (error) { - console.error(error) - return false; + throwGeneralAxiosError(error) } } else if (notification.type === "webhook") { try { - let data = { heartbeat: heartbeatJSON, monitor: monitorJSON, @@ -58,11 +72,11 @@ class Notification { finalData = data; } - await axios.post(notification.webhookURL, finalData, config) - return true; + let res = await axios.post(notification.webhookURL, finalData, config) + return okMsg; + } catch (error) { - console.error(error) - return false; + throwGeneralAxiosError(error) } } else if (notification.type === "smtp") { @@ -77,7 +91,7 @@ class Notification { content: msg } let res = await axios.post(notification.discordWebhookUrl, data) - return true; + return okMsg; } // If heartbeatJSON is not null, we go into the normal alerting loop. if(heartbeatJSON['status'] == 0) { @@ -102,12 +116,10 @@ class Notification { ] }] } - - await axios.post(notification.discordWebhookUrl, data) - return true; + let res = await axios.post(notification.discordWebhookUrl, data) + return okMsg; } catch(error) { - console.error(error) - return false; + throwGeneralAxiosError(error) } } else if (notification.type === "signal") { @@ -119,19 +131,18 @@ class Notification { }; let config = {}; - await axios.post(notification.signalURL, data, config) - return true; + let res = await axios.post(notification.signalURL, data, config) + return okMsg; } catch (error) { - console.error(error) - return false; + throwGeneralAxiosError(error) } } else if (notification.type === "slack") { try { if (heartbeatJSON == null) { let data = {'text': "Uptime Kuma Slack testing successful.", 'channel': notification.slackchannel, 'username': notification.slackusername, 'icon_emoji': notification.slackiconemo} - await axios.post(notification.slackwebhookURL, data) - return true; + let res = await axios.post(notification.slackwebhookURL, data) + return okMsg; } const time = heartbeatJSON["time"]; @@ -175,22 +186,21 @@ class Notification { } ] } - await axios.post(notification.slackwebhookURL, data) - return true; + let res = await axios.post(notification.slackwebhookURL, data) + return okMsg; } catch (error) { - console.error(error) - return false; + throwGeneralAxiosError(error) } } else if (notification.type === "pushover") { var pushoverlink = 'https://api.pushover.net/1/messages.json' try { if (heartbeatJSON == null) { - let data = {'message': "Uptime Kuma Pushover testing successful.", + let data = {'message': "Uptime Kuma Pushover testing successful.", 'user': notification.pushoveruserkey, 'token': notification.pushoverapptoken, 'sound':notification.pushoversounds, 'priority': notification.pushoverpriority, 'title':notification.pushovertitle, 'retry': "30", 'expire':"3600", 'html': 1} let res = await axios.post(pushoverlink, data) - return true; + return okMsg; } let data = { @@ -205,12 +215,15 @@ class Notification { "html": 1 } let res = await axios.post(pushoverlink, data) - return true; + return okMsg; } catch (error) { - console.log(error) - return false; + throwGeneralAxiosError(error) } + } else if (notification.type === "apprise") { + + return Notification.apprise(notification, msg) + } else { throw new Error("Notification type is not supported") } @@ -272,20 +285,47 @@ class Notification { text: msg, }); - return true; + return "Sent Successfully."; + } + + static async apprise(notification, msg) { + let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL]) + + + let output = (s.stdout) ? s.stdout.toString() : 'ERROR: maybe apprise not found'; + + if (output) { + + if (! output.includes("ERROR")) { + return "Sent Successfully"; + } else { + throw new Error(output) + } + } else { + return "" + } } - static async discord(notification, msg) { - const client = new Discord.Client(); - await client.login(notification.discordToken) + static checkApprise() { + let commandExistsSync = require('command-exists').sync; + let exists = commandExistsSync('apprise'); + return exists; + } - const channel = await client.channels.fetch(notification.discordChannelID); - await channel.send(msg); +} - client.destroy() +function throwGeneralAxiosError(error) { + let msg = "Error: " + error + " "; - return true; + if (error.response && error.response.data) { + if (typeof error.response.data === "string") { + msg += error.response.data; + } else { + msg += JSON.stringify(error.response.data) + } } + + throw new Error(msg) } module.exports = { diff --git a/server/server.js b/server/server.js index 21d749b84..3c4d00295 100644 --- a/server/server.js +++ b/server/server.js @@ -12,6 +12,7 @@ const fs = require("fs"); const {getSettings} = require("./util-server"); const {Notification} = require("./notification") const gracefulShutdown = require('http-graceful-shutdown'); +const Database = require("./database"); const {sleep} = require("./util"); const args = require('args-parser')(process.argv); @@ -27,27 +28,48 @@ const server = http.createServer(app); const io = new Server(server); app.use(express.json()) +/** + * Total WebSocket client connected to server currently, no actual use + * @type {number} + */ let totalClient = 0; + +/** + * Use for decode the auth object + * @type {null} + */ let jwtSecret = null; + +/** + * Main monitor list + * @type {{}} + */ let monitorList = {}; + +/** + * Show Setup Page + * @type {boolean} + */ let needSetup = false; (async () => { await initDatabase(); + console.log("Adding route") app.use('/', express.static("dist")); app.get('*', function(request, response, next) { response.sendFile(process.cwd() + '/dist/index.html'); }); + + console.log("Adding socket handler") io.on('connection', async (socket) => { socket.emit("info", { version, }) - console.log('a user connected'); totalClient++; if (needSetup) { @@ -56,7 +78,6 @@ let needSetup = false; } socket.on('disconnect', () => { - console.log('user disconnected'); totalClient--; }); @@ -433,25 +454,36 @@ let needSetup = false; try { checkLogin(socket) - await Notification.send(notification, notification.name + " Testing") + let msg = await Notification.send(notification, notification.name + " Testing") callback({ ok: true, - msg: "Sent Successfully" + msg }); } catch (e) { + console.error(e) + callback({ ok: false, msg: e.message }); } }); + + socket.on("checkApprise", async (callback) => { + try { + checkLogin(socket) + callback(Notification.checkApprise()); + } catch (e) { + callback(false); + } + }); }); + console.log("Init") server.listen(port, hostname, () => { console.log(`Listening on ${hostname}:${port}`); - startMonitors(); }); @@ -539,18 +571,21 @@ function checkLogin(socket) { } async function initDatabase() { - const path = './data/kuma.db'; - - if (! fs.existsSync(path)) { + if (! fs.existsSync(Database.path)) { console.log("Copying Database") - fs.copyFileSync("./db/kuma.db", path); + fs.copyFileSync(Database.templatePath, Database.path); } console.log("Connecting to Database") - R.setup('sqlite', { - filename: path + filename: Database.path }); + console.log("Connected") + + // Patch the database + await Database.patch() + + // Auto map the model to a bean object R.freeze(true) await R.autoloadModels("./server/model"); @@ -565,10 +600,12 @@ async function initDatabase() { jwtSecretBean.value = passwordHash.generate(dayjs() + "") await R.store(jwtSecretBean) + console.log("Stored JWT secret into database") } else { console.log("Load JWT secret from database.") } + // If there is no record in user table, it is a new Uptime Kuma instance, need to setup if ((await R.count("user")) === 0) { console.log("No user, need setup") needSetup = true; @@ -687,11 +724,6 @@ const startGracefulShutdown = async () => { } -let noReject = true; -process.on('unhandledRejection', (reason, p) => { - noReject = false; -}); - async function shutdownFunction(signal) { console.log('Called signal: ' + signal); @@ -700,24 +732,8 @@ async function shutdownFunction(signal) { let monitor = monitorList[id] monitor.stop() } - await sleep(2000) - - console.log("Closing DB") - - // Special handle, because tarn.js throw a promise reject that cannot be caught - while (true) { - noReject = true; - await R.close() - await sleep(2000) - - if (noReject) { - break; - } else { - console.log("Waiting...") - } - } - - console.log("OK") + await sleep(2000); + await Database.close(); } function finalFunction() { diff --git a/server/util-server.js b/server/util-server.js index 6904a65a4..b387f4c7c 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -45,6 +45,18 @@ exports.setting = async function (key) { ]) } +exports.setSetting = async function (key, value) { + let bean = await R.findOne("setting", " `key` = ? ", [ + key + ]) + if (! bean) { + bean = R.dispense("setting") + bean.key = key; + } + bean.value = value; + await R.store(bean) +} + exports.getSettings = async function (type) { let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [ type diff --git a/src/components/Login.vue b/src/components/Login.vue index cc5cea576..017261ccd 100644 --- a/src/components/Login.vue +++ b/src/components/Login.vue @@ -15,14 +15,14 @@ -
-
@@ -68,8 +75,21 @@ diff --git a/src/pages/Details.vue b/src/pages/Details.vue index ba4f9478a..f8c4879ad 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -64,7 +64,7 @@ - + {{ beat.msg }} @@ -75,6 +75,13 @@ + +
+ +
@@ -95,6 +102,7 @@ import Status from "../components/Status.vue"; import Datetime from "../components/Datetime.vue"; import CountUp from "../components/CountUp.vue"; import Uptime from "../components/Uptime.vue"; +import Pagination from "v-pagination-3"; export default { components: { @@ -104,13 +112,16 @@ export default { HeartbeatBar, Confirm, Status, + Pagination, }, mounted() { }, data() { return { - + page: 1, + perPage: 25, + heartBeatList: [], } }, computed: { @@ -154,6 +165,7 @@ export default { importantHeartBeatList() { if (this.$root.importantHeartbeatList[this.monitor.id]) { + this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id]; return this.$root.importantHeartbeatList[this.monitor.id] } else { return []; @@ -166,8 +178,13 @@ export default { } else { return { } } - } + }, + displayedRecords() { + const startIndex = this.perPage * (this.page - 1); + const endIndex = startIndex + this.perPage; + return this.heartBeatList.slice(startIndex, endIndex); + }, }, methods: { testNotification() {