diff --git a/.dockerignore b/.dockerignore index cdd61ffc2..9c16887bd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -20,6 +20,11 @@ yarn.lock app.json CODE_OF_CONDUCT.md CONTRIBUTING.md +CNAME +install.sh +SECURITY.md +tsconfig.json + ### .gitignore content (commented rules are duplicated) diff --git a/README.md b/README.md index c15d79db0..e5424b62f 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,21 @@ Telegram Notification Sample: If you love this project, please consider giving me a ⭐. + +## 🗣️ Discussion + +You can also discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues). + +I think the real "Discussion" tab is hard to use, as it is reddit-like flow, I always missed new comments. + + ## Contribute -If you want to report a bug or request a new feature. 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](https://github.com/louislam/uptime-kuma/issues). + +If you want to translate Uptime Kuma into your langauge, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md 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/patch11.sql b/db/patch11.sql new file mode 100644 index 000000000..c07d62981 --- /dev/null +++ b/db/patch11.sql @@ -0,0 +1,10 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +-- For sendHeartbeatList +CREATE INDEX monitor_time_index ON heartbeat (monitor_id, time); + +-- For sendImportantHeartbeatList +CREATE INDEX monitor_important_time_index ON heartbeat (monitor_id, important,time); + +COMMIT; diff --git a/dockerfile b/dockerfile index 3dabf513e..ddb5f4e8c 100644 --- a/dockerfile +++ b/dockerfile @@ -24,7 +24,7 @@ RUN npm install --legacy-peer-deps && npm run build && npm prune EXPOSE 3001 VOLUME ["/app/data"] -HEALTHCHECK --interval=600s --timeout=130s --start-period=300s CMD node extra/healthcheck.js +HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js CMD ["node", "server/server.js"] FROM release AS nightly diff --git a/dockerfile-alpine b/dockerfile-alpine index 1dd6b6b53..c8bead8bb 100644 --- a/dockerfile-alpine +++ b/dockerfile-alpine @@ -19,7 +19,7 @@ RUN npm install --legacy-peer-deps && npm run build && npm prune EXPOSE 3001 VOLUME ["/app/data"] -HEALTHCHECK --interval=600s --timeout=130s --start-period=300s CMD node extra/healthcheck.js +HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js CMD ["node", "server/server.js"] FROM release AS nightly diff --git a/extra/healthcheck.js b/extra/healthcheck.js index ed4e3eb23..ba3569db7 100644 --- a/extra/healthcheck.js +++ b/extra/healthcheck.js @@ -11,7 +11,7 @@ if (process.env.SSL_KEY && process.env.SSL_CERT) { let options = { host: process.env.HOST || "127.0.0.1", port: parseInt(process.env.PORT) || 3001, - timeout: 120 * 1000, + timeout: 28 * 1000, }; let request = client.request(options, (res) => { diff --git a/server/client.js b/server/client.js index 4f28a2fa0..e83d1f59a 100644 --- a/server/client.js +++ b/server/client.js @@ -32,19 +32,16 @@ async function sendNotificationList(socket) { async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) { const timeLogger = new TimeLogger(); - let list = await R.find("heartbeat", ` - monitor_id = ? + let list = await R.getAll(` + SELECT * FROM heartbeat + WHERE monitor_id = ? ORDER BY time DESC LIMIT 100 `, [ monitorID, ]) - let result = []; - - for (let bean of list) { - result.unshift(bean.toJSON()); - } + let result = list.reverse(); if (toUser) { io.to(socket.userID).emit("heartbeatList", monitorID, result, overwrite); diff --git a/server/database.js b/server/database.js index 832166d2c..b76b38713 100644 --- a/server/database.js +++ b/server/database.js @@ -36,7 +36,11 @@ class Database { // Change to WAL await R.exec("PRAGMA journal_mode = WAL"); + await R.exec("PRAGMA cache_size = -12000"); + + console.log("SQLite config:"); console.log(await R.getAll("PRAGMA journal_mode")); + console.log(await R.getAll("PRAGMA cache_size")); } static async patch() { diff --git a/server/model/monitor.js b/server/model/monitor.js index cc9ab4544..89208a3fd 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -409,58 +409,59 @@ class Monitor extends BeanModel { static async sendUptime(duration, io, monitorID, userID) { const timeLogger = new TimeLogger(); - let sec = duration * 3600; - - let heartbeatList = await R.getAll(` - SELECT duration, time, status + const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour")); + + // Handle if heartbeat duration longer than the target duration + // e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL) + let result = await R.getRow(` + SELECT + -- SUM all duration, also trim off the beat out of time window + SUM( + CASE + WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration + THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 + ELSE duration + END + ) AS total_duration, + + -- SUM all uptime duration, also trim off the beat out of time window + SUM( + CASE + WHEN (status = 1) + THEN + CASE + WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration + THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 + ELSE duration + END + END + ) AS uptime_duration FROM heartbeat - WHERE time > DATETIME('now', ? || ' hours') - AND monitor_id = ? `, [ - -duration, + WHERE time > ? + AND monitor_id = ? + `, [ + startTime, startTime, startTime, startTime, startTime, monitorID, ]); timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`); - let downtime = 0; - let total = 0; - let uptime; - - // Special handle for the first heartbeat only - if (heartbeatList.length === 1) { + let totalDuration = result.total_duration; + let uptimeDuration = result.uptime_duration; + let uptime = 0; - if (heartbeatList[0].status === 1) { - uptime = 1; - } else { + if (totalDuration > 0) { + uptime = uptimeDuration / totalDuration; + if (uptime < 0) { uptime = 0; } } else { - for (let row of heartbeatList) { - let value = parseInt(row.duration) - let time = row.time - - // Handle if heartbeat duration longer than the target duration - // e.g. Heartbeat duration = 28hrs, but target duration = 24hrs - if (value > sec) { - let trim = dayjs.utc().diff(dayjs(time), "second"); - value = sec - trim; - - if (value < 0) { - value = 0; - } - } - - total += value; - if (row.status === 0 || row.status === 2) { - downtime += value; - } - } - - uptime = (total - downtime) / total; - - if (uptime < 0) { - uptime = 0; + // Handle new monitor with only one beat, because the beat's duration = 0 + let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ])); + console.log("here???" + status); + if (status === UP) { + uptime = 1; } } diff --git a/server/notification-providers/smtp.js b/server/notification-providers/smtp.js index 4914c0748..ecb583eb7 100644 --- a/server/notification-providers/smtp.js +++ b/server/notification-providers/smtp.js @@ -30,10 +30,15 @@ class SMTP extends NotificationProvider { // send mail with defined transport object await transporter.sendMail({ - from: `"Uptime Kuma" <${notification.smtpFrom}>`, + from: notification.smtpFrom, + cc: notification.smtpCC, + bcc: notification.smtpBCC, to: notification.smtpTo, subject: msg, text: bodyTextContent, + tls: { + rejectUnauthorized: notification.smtpIgnoreTLSError || false, + }, }); return "Sent Successfully."; diff --git a/server/server.js b/server/server.js index 29e0857a0..bfebb89fc 100644 --- a/server/server.js +++ b/server/server.js @@ -593,6 +593,82 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); } }); + socket.on("uploadBackup", async (uploadedJSON, callback) => { + try { + checkLogin(socket) + + let backupData = JSON.parse(uploadedJSON); + + console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`) + + let notificationList = backupData.notificationList; + let monitorList = backupData.monitorList; + + if (notificationList.length >= 1) { + for (let i = 0; i < notificationList.length; i++) { + let notification = JSON.parse(notificationList[i].config); + await Notification.save(notification, null, socket.userID) + } + } + + if (monitorList.length >= 1) { + for (let i = 0; i < monitorList.length; i++) { + let monitor = { + name: monitorList[i].name, + type: monitorList[i].type, + url: monitorList[i].url, + interval: monitorList[i].interval, + hostname: monitorList[i].hostname, + maxretries: monitorList[i].maxretries, + port: monitorList[i].port, + keyword: monitorList[i].keyword, + ignoreTls: monitorList[i].ignoreTls, + upsideDown: monitorList[i].upsideDown, + maxredirects: monitorList[i].maxredirects, + accepted_statuscodes: monitorList[i].accepted_statuscodes, + dns_resolve_type: monitorList[i].dns_resolve_type, + dns_resolve_server: monitorList[i].dns_resolve_server, + notificationIDList: {}, + } + + let bean = R.dispense("monitor") + + let notificationIDList = monitor.notificationIDList; + delete monitor.notificationIDList; + + monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); + delete monitor.accepted_statuscodes; + + bean.import(monitor) + bean.user_id = socket.userID + await R.store(bean) + + await updateMonitorNotification(bean.id, notificationIDList) + + if (monitorList[i].active == 1) { + await startMonitor(socket.userID, bean.id); + } else { + await pauseMonitor(socket.userID, bean.id); + } + } + + await sendNotificationList(socket) + await sendMonitorList(socket); + } + + callback({ + ok: true, + msg: "Backup successfully restored.", + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + socket.on("clearEvents", async (monitorID, callback) => { try { checkLogin(socket) diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index cd8ee8173..447b19266 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -37,7 +37,7 @@ - + @@ -65,49 +65,7 @@ - + +

{{ $t("Import/Export Backup") }}

+ +

+ {{ $t("backupDescription") }}
+ ({{ $t("backupDescription2") }})
+

+ +
+ + + +
+
+ {{ importAlert }} +
+ +

{{ $t("backupDescription3") }}

+

{{ $t("Advanced") }}

@@ -275,6 +296,8 @@ export default { }, loaded: false, + importAlert: null, + processing: false, } }, watch: { @@ -351,6 +374,52 @@ export default { this.$root.storage().removeItem("token"); }, + downloadBackup() { + let time = dayjs().format("YYYY_MM_DD-hh_mm_ss"); + let fileName = `Uptime_Kuma_Backup_${time}.json`; + let monitorList = Object.values(this.$root.monitorList); + let exportData = { + version: this.$root.info.version, + notificationList: this.$root.notificationList, + monitorList: monitorList, + } + exportData = JSON.stringify(exportData); + let downloadItem = document.createElement("a"); + downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURI(exportData)); + downloadItem.setAttribute("download", fileName); + downloadItem.click(); + }, + + importBackup() { + this.processing = true; + let uploadItem = document.getElementById("importBackup").files; + + if (uploadItem.length <= 0) { + this.processing = false; + return this.importAlert = this.$t("alertNoFile") + } + + if (uploadItem.item(0).type !== "application/json") { + this.processing = false; + return this.importAlert = this.$t("alertWrongFileType") + } + + let fileReader = new FileReader(); + fileReader.readAsText(uploadItem.item(0)); + + fileReader.onload = item => { + this.$root.uploadBackup(item.target.result, (res) => { + this.processing = false; + + if (res.ok) { + toast.success(res.msg); + } else { + toast.error(res.msg); + } + }) + } + }, + clearStatistics() { this.$root.clearStatistics((res) => { if (res.ok) { @@ -388,6 +457,18 @@ export default { .btn-check:hover + .btn-outline-primary { color: #000; } + + #importBackup { + &::file-selector-button { + color: $primary; + background-color: $dark-bg; + } + + &:hover:not(:disabled):not([readonly])::file-selector-button { + color: $dark-font-color2; + background-color: $primary; + } + } } footer {