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 6aed01bcd..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: @@ -112,7 +117,7 @@ If you love this project, please consider giving me a ⭐. # Contribute -If you want to report a bug or request a new featue. Free feel to open a new issue. +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 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/package-lock.json b/package-lock.json index 92c91a935..99dd01b6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.0.5", + "version": "1.0.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -392,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", @@ -2093,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", @@ -3698,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", diff --git a/package.json b/package.json index cd416f77d..d4fe68885 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.0.5", + "version": "1.0.6", "license": "MIT", "repository": { "type": "git", @@ -12,10 +12,10 @@ "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": "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.5 && npm install && npm run build", + "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" }, @@ -38,6 +38,7 @@ "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", 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/heartbeat.js b/server/model/heartbeat.js index 74e329811..01fb71ff9 100644 --- a/server/model/heartbeat.js +++ b/server/model/heartbeat.js @@ -3,8 +3,6 @@ const utc = require('dayjs/plugin/utc') var timezone = require('dayjs/plugin/timezone') dayjs.extend(utc) dayjs.extend(timezone) -const axios = require("axios"); -const {R} = require("redbean-node"); const {BeanModel} = require("redbean-node/dist/bean-model"); diff --git a/server/model/monitor.js b/server/model/monitor.js index 162772875..04feea6b0 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 @@ -123,8 +121,6 @@ class Monitor extends BeanModel { this.id ]) - let promiseList = []; - let text; if (bean.status === 1) { text = "✅ Up" @@ -135,16 +131,24 @@ class Monitor extends BeanModel { let msg = `[${this.name}] [${text}] ${bean.msg}`; for(let notification of notificationList) { - promiseList.push(Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())); + try { + await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()) + } catch (e) { + console.error("Cannot send notification to " + notification.name) + } } - - await Promise.all(promiseList); } } else { 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 79c01f6db..3c0e9f2a4 100644 --- a/server/notification.js +++ b/server/notification.js @@ -72,7 +72,7 @@ class Notification { finalData = data; } - let res = await axios.post(notification.webhookURL, finalData, config) + await axios.post(notification.webhookURL, finalData, config) return okMsg; } catch (error) { @@ -90,7 +90,7 @@ class Notification { username: 'Uptime-Kuma', content: msg } - let res = await axios.post(notification.discordWebhookUrl, data) + await axios.post(notification.discordWebhookUrl, data) return okMsg; } // If heartbeatJSON is not null, we go into the normal alerting loop. @@ -116,7 +116,7 @@ class Notification { ] }] } - let res = await axios.post(notification.discordWebhookUrl, data) + await axios.post(notification.discordWebhookUrl, data) return okMsg; } catch(error) { throwGeneralAxiosError(error) @@ -131,7 +131,7 @@ class Notification { }; let config = {}; - let res = await axios.post(notification.signalURL, data, config) + await axios.post(notification.signalURL, data, config) return okMsg; } catch (error) { throwGeneralAxiosError(error) @@ -141,7 +141,7 @@ class Notification { try { if (heartbeatJSON == null) { let data = {'text': "Uptime Kuma Slack testing successful.", 'channel': notification.slackchannel, 'username': notification.slackusername, 'icon_emoji': notification.slackiconemo} - let res = await axios.post(notification.slackwebhookURL, data) + await axios.post(notification.slackwebhookURL, data) return okMsg; } @@ -186,7 +186,7 @@ class Notification { } ] } - let res = await axios.post(notification.slackwebhookURL, data) + await axios.post(notification.slackwebhookURL, data) return okMsg; } catch (error) { throwGeneralAxiosError(error) @@ -199,7 +199,7 @@ class Notification { 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) + await axios.post(pushoverlink, data) return okMsg; } @@ -214,7 +214,7 @@ class Notification { "expire": "3600", "html": 1 } - let res = await axios.post(pushoverlink, data) + await axios.post(pushoverlink, data) return okMsg; } catch (error) { throwGeneralAxiosError(error) @@ -278,7 +278,7 @@ class Notification { }); // send mail with defined transport object - let info = await transporter.sendMail({ + await transporter.sendMail({ from: `"Uptime Kuma" <${notification.smtpFrom}>`, to: notification.smtpTo, subject: msg, diff --git a/server/server.js b/server/server.js index bd5894775..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,9 +28,28 @@ 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 () => { @@ -50,7 +70,6 @@ let needSetup = false; version, }) - console.log('a user connected'); totalClient++; if (needSetup) { @@ -59,7 +78,6 @@ let needSetup = false; } socket.on('disconnect', () => { - console.log('user disconnected'); totalClient--; }); @@ -165,10 +183,6 @@ let needSetup = false; msg: e.message }); } - - - - }); // Auth Only API @@ -557,19 +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"); @@ -589,6 +605,7 @@ async function initDatabase() { 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; @@ -707,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); @@ -720,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/Datetime.vue b/src/components/Datetime.vue index 47702236a..e84c877bc 100644 --- a/src/components/Datetime.vue +++ b/src/components/Datetime.vue @@ -4,9 +4,9 @@ diff --git a/src/pages/Details.vue b/src/pages/Details.vue index f925c2849..f8c4879ad 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -12,7 +12,7 @@
- + Edit
@@ -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() { diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 01af50610..7a72cd139 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -61,7 +61,7 @@

Notifications

Not available, please setup.

-
+
@@ -59,7 +59,7 @@

Please assign the notification to monitor(s) to get it works.

    -
  • +
  • {{ notification.name }}
    Edit
  • @@ -77,8 +77,8 @@