diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.md b/.github/ISSUE_TEMPLATE/ask-for-help.md index a89d94218..f1ea1ac77 100644 --- a/.github/ISSUE_TEMPLATE/ask-for-help.md +++ b/.github/ISSUE_TEMPLATE/ask-for-help.md @@ -12,6 +12,8 @@ Please search in Issues without filters: https://github.com/louislam/uptime-kuma **Info** Uptime Kuma Version: Using Docker?: Yes/No +Docker Version: +Node.js Version (Without Docker only): OS: Browser: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index da89d154b..158b3e7e9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -25,10 +25,13 @@ A clear and concise description of what you expected to happen. **Info** -- Uptime Kuma Version: -- Using Docker?: Yes/No -- OS: -- Browser: +Uptime Kuma Version: +Using Docker?: Yes/No +Docker Version: +Node.js Version (Without Docker only): +OS: +Browser: + **Screenshots** If applicable, add screenshots to help explain your problem. @@ -36,3 +39,6 @@ If applicable, add screenshots to help explain your problem. **Error Log** It is easier for us to find out the problem. +Docker: "docker logs " +PM2: "~/.pm2/logs/" (e.g. /home/ubuntu/.pm2/logs) + diff --git a/db/patch11.sql b/db/patch-improve-performance.sql similarity index 100% rename from db/patch11.sql rename to db/patch-improve-performance.sql diff --git a/db/patch-setting-value-type.sql b/db/patch-setting-value-type.sql new file mode 100644 index 000000000..18d6390c3 --- /dev/null +++ b/db/patch-setting-value-type.sql @@ -0,0 +1,22 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +-- Generated by Intellij IDEA +create table setting_dg_tmp +( + id INTEGER + primary key autoincrement, + key VARCHAR(200) not null + unique, + value TEXT, + type VARCHAR(20) +); + +insert into setting_dg_tmp(id, key, value, type) select id, key, value, type from setting; + +drop table setting; + +alter table setting_dg_tmp rename to setting; + + +COMMIT; diff --git a/dockerfile b/dockerfile index ddb5f4e8c..6622def81 100644 --- a/dockerfile +++ b/dockerfile @@ -1,26 +1,32 @@ +# DON'T UPDATE TO node:14-bullseye-slim, see #372. +FROM node:14-buster-slim AS build +WORKDIR /app + +# split the sqlite install here, so that it can caches the arm prebuilt +# do not modify it, since we don't want to re-compile the arm prebuilt again +RUN apt update && \ + apt --yes install python3 python3-pip python3-dev git g++ make && \ + ln -s /usr/bin/python3 /usr/bin/python && \ + npm install mapbox/node-sqlite3#593c9d --build-from-source + +COPY . . +RUN npm install --legacy-peer-deps && npm run build && npm prune --production + FROM node:14-bullseye-slim AS release WORKDIR /app -# install dependencies -RUN apt update && apt --yes install python3 python3-pip python3-dev git g++ make iputils-ping -RUN ln -s /usr/bin/python3 /usr/bin/python - -# split the sqlite install here, so that it can caches the arm prebuilt -RUN npm install mapbox/node-sqlite3#593c9d - -# Install apprise -RUN apt --yes install python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib -RUN pip3 --no-cache-dir install apprise && \ - rm -rf /root/.cache - -# additional package should be added here, since we don't want to re-compile the arm prebuilt again - +# Install Apprise, # add sqlite3 cli for debugging in the future -RUN apt --yes install sqlite3 +# iputils-ping for ping +RUN apt update && \ + apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ + sqlite3 \ + iputils-ping && \ + pip3 --no-cache-dir install apprise && \ + rm -rf /var/lib/apt/lists/* - -COPY . . -RUN npm install --legacy-peer-deps && npm run build && npm prune +# Copy app files from build layer +COPY --from=build /app /app EXPOSE 3001 VOLUME ["/app/data"] diff --git a/dockerfile-alpine b/dockerfile-alpine index c8bead8bb..998204682 100644 --- a/dockerfile-alpine +++ b/dockerfile-alpine @@ -1,5 +1,5 @@ # DON'T UPDATE TO alpine3.13, 1.14, see #41. -FROM node:14-alpine3.12 AS release +FROM node:14-alpine3.12 AS build WORKDIR /app # split the sqlite install here, so that it can caches the arm prebuilt @@ -9,13 +9,20 @@ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && apk del .build-deps && \ rm -f /usr/bin/python +COPY . . +RUN npm install --legacy-peer-deps && npm run build && npm prune --production + + +FROM node:14-alpine3.12 AS release +WORKDIR /app + # Install apprise -RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib -RUN pip3 --no-cache-dir install apprise && \ +RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \ + pip3 --no-cache-dir install apprise && \ rm -rf /root/.cache -COPY . . -RUN npm install --legacy-peer-deps && npm run build && npm prune +# Copy app files from build layer +COPY --from=build /app /app EXPOSE 3001 VOLUME ["/app/data"] diff --git a/extra/healthcheck.js b/extra/healthcheck.js index ba3569db7..99f748fb5 100644 --- a/extra/healthcheck.js +++ b/extra/healthcheck.js @@ -1,3 +1,6 @@ +/* + * This script should be run after a period of time (180s), because the server may need some time to prepare. + */ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; let client; diff --git a/kubernetes/README.md b/kubernetes/README.md index 26ab2f6a4..3057a5349 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -1,4 +1,7 @@ # Uptime-Kuma K8s Deployment + +⚠ Warning: K8s deployment is provided by contributors. I have no experience with K8s and I can't fix error in the future. I only test Docker and Node.js. Use at your own risk. + ## How does it work? Kustomize is a tool which builds a complete deployment file for all config elements. @@ -25,4 +28,4 @@ This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.g - run ```kustomize build > apply.yml``` - run ```kubectl apply -f apply.yml``` -Now you should see some k8s magic and Uptime-Kuma should be available at the specified address. \ No newline at end of file +Now you should see some k8s magic and Uptime-Kuma should be available at the specified address. diff --git a/kubernetes/uptime-kuma/deployment.yml b/kubernetes/uptime-kuma/deployment.yml index a122509b0..b97ece210 100644 --- a/kubernetes/uptime-kuma/deployment.yml +++ b/kubernetes/uptime-kuma/deployment.yml @@ -30,6 +30,9 @@ spec: command: - node - extra/healthcheck.js + initialDelaySeconds: 180 + periodSeconds: 60 + timeoutSeconds: 30 readinessProbe: httpGet: path: / diff --git a/package.json b/package.json index e43e00944..e71fde810 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.5.3", + "version": "1.6.0", "license": "MIT", "repository": { "type": "git", @@ -18,12 +18,12 @@ "start-server": "node server/server.js", "build": "vite build", "vite-preview-dist": "vite preview --host", - "build-docker": "npm run build-docker-alpine && npm run build-docker-debian", - "build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.5.3-alpine --target release . --push", - "build-docker-debian": "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.5.3 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.5.3-debian --target release . --push", + "build-docker": "npm run build-docker-debian && npm run build-docker-alpine", + "build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.6.0-alpine --target release . --push", + "build-docker-debian": "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.6.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.6.0-debian --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 --progress plain", - "setup": "git checkout 1.5.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune", + "setup": "git checkout 1.6.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune", "update-version": "node extra/update-version.js", "mark-as-nightly": "node extra/mark-as-nightly.js", "reset-password": "node extra/reset-password.js", diff --git a/server/database.js b/server/database.js index b76b38713..e0bb0c9b8 100644 --- a/server/database.js +++ b/server/database.js @@ -1,15 +1,44 @@ const fs = require("fs"); const { R } = require("redbean-node"); const { setSetting, setting } = require("./util-server"); +const { debug, sleep } = require("../src/util"); +const dayjs = require("dayjs"); class Database { - static templatePath = "./db/kuma.db" + static templatePath = "./db/kuma.db"; static dataDir; static path; + + /** + * @type {boolean} + */ + static patched = false; + + /** + * For Backup only + */ + static backupPath = null; + + /** + * Add patch filename in key + * Values: + * true: Add it regardless of order + * false: Do nothing + * { parents: []}: Need parents before add it + */ + static patchList = { + "patch-setting-value-type.sql": true, + "patch-improve-performance.sql": true, + } + + /** + * The finally version should be 10 after merged tag feature + * @deprecated Use patchList for any new feature + */ static latestVersion = 9; + static noReject = true; - static sqliteInstance = null; static async connect() { const acquireConnectionTimeout = 120 * 1000; @@ -60,19 +89,7 @@ class Database { } else { console.info("Database patch is needed") - console.info("Backup the db") - const backupPath = this.dataDir + "kuma.db.bak" + version; - fs.copyFileSync(Database.path, backupPath); - - const shmPath = Database.path + "-shm"; - if (fs.existsSync(shmPath)) { - fs.copyFileSync(shmPath, shmPath + ".bak" + version); - } - - const walPath = Database.path + "-wal"; - if (fs.existsSync(walPath)) { - fs.copyFileSync(walPath, walPath + ".bak" + version); - } + this.backup(version); // Try catch anything here, if gone wrong, restore the backup try { @@ -83,18 +100,92 @@ class Database { 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) + this.restore(); + 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); } } + + await this.patch2(); + } + + /** + * Call it from patch() only + * @returns {Promise} + */ + static async patch2() { + console.log("Database Patch 2.0 Process"); + let databasePatchedFiles = await setting("databasePatchedFiles"); + + if (! databasePatchedFiles) { + databasePatchedFiles = {}; + } + + debug("Patched files:"); + debug(databasePatchedFiles); + + try { + for (let sqlFilename in this.patchList) { + await this.patch2Recursion(sqlFilename, databasePatchedFiles) + } + + if (this.patched) { + console.log("Database Patched Successfully"); + } + + } catch (ex) { + await Database.close(); + this.restore(); + + 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); + } + + await setSetting("databasePatchedFiles", databasePatchedFiles); + } + + /** + * Used it patch2() only + * @param sqlFilename + * @param databasePatchedFiles + */ + static async patch2Recursion(sqlFilename, databasePatchedFiles) { + let value = this.patchList[sqlFilename]; + + if (! value) { + console.log(sqlFilename + " skip"); + return; + } + + // Check if patched + if (! databasePatchedFiles[sqlFilename]) { + console.log(sqlFilename + " is not patched"); + + if (value.parents) { + console.log(sqlFilename + " need parents"); + for (let parentSQLFilename of value.parents) { + await this.patch2Recursion(parentSQLFilename, databasePatchedFiles); + } + } + + this.backup(dayjs().format("YYYYMMDDHHmmss")); + + console.log(sqlFilename + " is patching"); + this.patched = true; + await this.importSQLFile("./db/" + sqlFilename); + databasePatchedFiles[sqlFilename] = true; + console.log(sqlFilename + " is patched successfully"); + + } else { + console.log(sqlFilename + " is already patched, skip"); + } } /** @@ -140,10 +231,96 @@ class Database { * @returns {Promise} */ static async close() { - if (this.sqliteInstance) { - this.sqliteInstance.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); + } + + /** + * One backup one time in this process. + * Reset this.backupPath if you want to backup again + * @param version + */ + static backup(version) { + if (! this.backupPath) { + console.info("Backup the db") + this.backupPath = this.dataDir + "kuma.db.bak" + version; + fs.copyFileSync(Database.path, this.backupPath); + + const shmPath = Database.path + "-shm"; + if (fs.existsSync(shmPath)) { + this.backupShmPath = shmPath + ".bak" + version; + fs.copyFileSync(shmPath, this.backupShmPath); + } + + const walPath = Database.path + "-wal"; + if (fs.existsSync(walPath)) { + this.backupWalPath = walPath + ".bak" + version; + fs.copyFileSync(walPath, this.backupWalPath); + } + } + } + + /** + * + */ + static restore() { + if (this.backupPath) { + console.error("Patch db failed!!! Restoring the backup"); + + const shmPath = Database.path + "-shm"; + const walPath = Database.path + "-wal"; + + // Delete patch failed db + try { + if (fs.existsSync(Database.path)) { + fs.unlinkSync(Database.path); + } + + if (fs.existsSync(shmPath)) { + fs.unlinkSync(shmPath); + } + + if (fs.existsSync(walPath)) { + fs.unlinkSync(walPath); + } + } catch (e) { + console.log("Restore failed, you may need to restore the backup manually"); + process.exit(1); + } + + // Restore backup + fs.copyFileSync(this.backupPath, Database.path); + + if (this.backupShmPath) { + fs.copyFileSync(this.backupShmPath, shmPath); + } + + if (this.backupWalPath) { + fs.copyFileSync(this.backupWalPath, walPath); + } + + } else { + console.log("Nothing to restore"); } - console.log("Stopped database"); } } diff --git a/src/languages/de-DE.js b/src/languages/de-DE.js index 7f46e770e..f154066da 100644 --- a/src/languages/de-DE.js +++ b/src/languages/de-DE.js @@ -123,7 +123,7 @@ export default { enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.", Create: "Erstellen", "Auto Get": "Auto Get", - backupDescription: "Es können alle Monitore und alle Benachrichtigungen in einer JSON-Datei gesichert werden.", + backupDescription: "Es können alle Monitore und Benachrichtigungen in einer JSON-Datei gesichert werden.", backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.", backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.", alertNoFile: "Bitte wähle eine Datei zum importieren aus.", @@ -141,5 +141,6 @@ export default { Active: "Aktiv", Inactive: "Inaktiv", Token: "Token", - "Show URI": "URI Anzeigen" + "Show URI": "URI Anzeigen", + "Clear all statistics": "Lösche alle Statistiken" } diff --git a/src/languages/en.js b/src/languages/en.js index 6afa16557..8ad7efbdb 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -141,5 +141,6 @@ export default { Active: "Active", Inactive: "Inactive", Token: "Token", - "Show URI": "Show URI" + "Show URI": "Show URI", + "Clear all statistics": "Clear all Statistics" } diff --git a/src/languages/zh-CN.js b/src/languages/zh-CN.js index 2f55c8b3a..1d15c3d95 100644 --- a/src/languages/zh-CN.js +++ b/src/languages/zh-CN.js @@ -13,7 +13,7 @@ export default { pauseDashboardHome: "暂停", deleteMonitorMsg: "确定要删除此监控吗?", deleteNotificationMsg: "确定要删除此消息通知吗?这将对所有监控生效。", - resoverserverDescription: "默认服务器 Cloudflare,可以修改为任意你想要使用的DNS服务器", + resoverserverDescription: "可自定义要使用的DNS服务器", rrtypeDescription: "选择要监控的资源记录类型", pauseMonitorMsg: "确定要暂停吗?", Settings: "设置", @@ -109,23 +109,23 @@ export default { "Repeat Password": "重复密码", respTime: "Resp. Time (ms)", notAvailableShort: "N/A", - Create: "Create", - clearEventsMsg: "Are you sure want to delete all events for this monitor?", - clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", - confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", - "Clear Data": "Clear Data", - Events: "Events", - Heartbeats: "Heartbeats", - "Auto Get": "Auto Get", - enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", - "Default enabled": "Default enabled", - "Also apply to existing monitors": "Also apply to existing monitors", - "Import/Export Backup": "Import/Export Backup", - Export: "Export", - Import: "Import", - backupDescription: "You can backup all monitors and all notifications into a JSON file.", - backupDescription2: "PS: History and event data is not included.", - backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", - alertNoFile: "Please select a file to import.", - alertWrongFileType: "Please select a JSON file." + Create: "创建", + clearEventsMsg: "确定要删除此监控项的所有事件吗?", + clearHeartbeatsMsg: "确定要删除此监控项的所有状态吗?", + confirmClearStatisticsMsg: "确定要删除所有统计信息吗?", + "Clear Data": "清除数据", + Events: "事件", + Heartbeats: "心跳", + "Auto Get": "自动获取", + enableDefaultNotificationDescription: "新的监控项将默认启用,你也可以在每个监控项中分别设置", + "Default enabled": "默认开启", + "Also apply to existing monitors": "应用到所有监控项", + "Import/Export Backup": "导入/导出备份", + Export: "导出", + Import: "导入", + backupDescription: "你可以将所有的监控项和消息通知备份到一个 JSON 文件中", + backupDescription2: "注意: 不包括历史状态和事件数据", + backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!", + alertNoFile: "请选择一个文件导入", + alertWrongFileType: "请选择一个 JSON 格式的文件" } diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 0cffbdc56..ea55f5a88 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -32,12 +32,14 @@ export default { created() { window.addEventListener("resize", this.onResize); + let protocol = (location.protocol === "https:") ? "wss://" : "ws://"; + let wsHost; const env = process.env.NODE_ENV || "production"; if (env === "development" || localStorage.dev === "dev") { - wsHost = ":3001" + wsHost = protocol + location.hostname + ":3001"; } else { - wsHost = "" + wsHost = protocol + location.host; } socket = io(wsHost, { diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index 33992a439..80e043811 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -155,7 +155,7 @@ - + @@ -394,7 +394,7 @@ export default { notificationList: this.$root.notificationList, monitorList: monitorList, } - exportData = JSON.stringify(exportData); + exportData = JSON.stringify(exportData, null, 4); let downloadItem = document.createElement("a"); downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURI(exportData)); downloadItem.setAttribute("download", fileName);