diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f46b17e1a..3f4550928 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,8 +17,11 @@ The frontend code build into "dist" directory. The server (express.js) exposes t ## Directories +- config (dev config files) - data (App data) +- db (Base database and migration scripts) - dist (Frontend build) +- docker (Dockerfiles) - extra (Extra useful scripts) - public (Frontend resources for dev only) - server (Server source code) @@ -80,13 +83,13 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r ## Project Styles -I personally do not like it when something requires so much learning and configuration before you can finally start the app. +I personally do not like something that requires so many configurations before you can finally start the app. I hope Uptime Kuma installation could be as easy as like installing a mobile app. -- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort required to get it running +- Easy to install for non-Docker users, no native build dependency is needed (for x86_64/armv7/arm64), no extra config, no extra effort required to get it running - Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go -- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`. +- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR` - Easy to use -- The web UI styling should be consistent and nice. +- The web UI styling should be consistent and nice ## Coding Styles @@ -95,7 +98,7 @@ I personally do not like it when something requires so much learning and configu - Follow ESLint - Methods and functions should be documented with JSDoc -## Name convention +## Name Conventions - Javascript/Typescript: camelCaseType - SQLite: snake_case (Underscore) @@ -109,7 +112,7 @@ I personally do not like it when something requires so much learning and configu - IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA) - A SQLite GUI tool (SQLite Expert Personal is suggested) -## Install dependencies +## Install Dependencies for Development ```bash npm ci @@ -127,6 +130,12 @@ Port `3000` and port `3001` will be used. npm run dev ``` +But sometimes, you would like to keep restart the server, but not the frontend, you can run these command in two terminals: +``` +npm run start-frontend-dev +npm run start-server-dev +``` + ## Backend Server It binds to `0.0.0.0:3001` by default. @@ -142,12 +151,15 @@ express.js is used for: ### Structure in /server/ +- jobs/ (Jobs that are running in another process) - model/ (Object model, auto mapping to the database table name) - modules/ (Modified 3rd-party modules) +- monitor_types (Monitor Types) - notification-providers/ (individual notification logic) - routers/ (Express Routers) - socket-handler (Socket.io Handlers) -- server.js (Server entry point and main logic) +- server.js (Server entry point) +- uptime-kuma-server.js (UptimeKumaServer class, main logic should be here, but some still in `server.js`) ## Frontend Dev Server @@ -198,18 +210,12 @@ Both frontend and backend share the same package.json. However, the frontend dep ### Update Dependencies -Install `ncu` -https://github.com/raineorshine/npm-check-updates - -```bash -ncu -u -t patch -npm install -``` - Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only. Patch release = the third digit ([Semantic Versioning](https://semver.org/)) +If for maybe security reasons, a library must be updated. Then you must need to check if there are any breaking changes. + ## Translations Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages diff --git a/docker/alpine-base.dockerfile b/docker/alpine-base.dockerfile index 82bc7bb05..276d6e450 100644 --- a/docker/alpine-base.dockerfile +++ b/docker/alpine-base.dockerfile @@ -3,6 +3,6 @@ FROM node:16-alpine3.12 WORKDIR /app # Install apprise, iputils for non-root ping, setpriv -RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \ +RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib git && \ pip3 --no-cache-dir install apprise==1.2.1 && \ rm -rf /root/.cache diff --git a/docker/builder-go.dockerfile b/docker/builder-go.dockerfile index 79c1a95ba..1d25843bc 100644 --- a/docker/builder-go.dockerfile +++ b/docker/builder-go.dockerfile @@ -2,7 +2,7 @@ # Build in Golang # Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck ############################################ -FROM golang:1.19.4-buster +FROM golang:1.19-buster WORKDIR /app ARG TARGETPLATFORM COPY ./extra/ ./extra/ diff --git a/docker/debian-base.dockerfile b/docker/debian-base.dockerfile index d94b4c7fe..026189c47 100644 --- a/docker/debian-base.dockerfile +++ b/docker/debian-base.dockerfile @@ -10,7 +10,7 @@ WORKDIR /app # Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine! RUN apt update && \ apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ - sqlite3 iputils-ping util-linux dumb-init && \ + sqlite3 iputils-ping util-linux dumb-init git && \ pip3 --no-cache-dir install apprise==1.2.1 && \ rm -rf /var/lib/apt/lists/* && \ apt --yes autoremove diff --git a/extra/healthcheck.go b/extra/healthcheck.go index 302883d84..f79b3e65b 100644 --- a/extra/healthcheck.go +++ b/extra/healthcheck.go @@ -11,12 +11,17 @@ import ( "net/http" "os" "runtime" + "strings" "time" ) func main() { isFreeBSD := runtime.GOOS == "freebsd" + // Is K8S + uptime-kuma as the container name + // See #2083 + isK8s := strings.HasPrefix(os.Getenv("UPTIME_KUMA_PORT"), "tcp://") + // process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, @@ -44,7 +49,11 @@ func main() { hostname = "127.0.0.1" } - port := os.Getenv("UPTIME_KUMA_PORT") + port := "" + // UPTIME_KUMA_PORT is override by K8S unexpectedly, + if !isK8s { + port = os.Getenv("UPTIME_KUMA_PORT") + } if len(port) == 0 { port = os.Getenv("PORT") } diff --git a/extra/healthcheck.js b/extra/healthcheck.js index 9b95cf26a..40691418c 100644 --- a/extra/healthcheck.js +++ b/extra/healthcheck.js @@ -19,17 +19,17 @@ if (sslKey && sslCert) { // If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise. // Dual-stack support for (::) -let hostname = process.env.UPTIME_KUMA_HOST; +let hostname = process.env.UPTIME_KUMA_SERVICE_HOST || process.env.UPTIME_KUMA_HOST || "::"; // Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD if (!hostname && !FBSD) { hostname = process.env.HOST; } -const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001); +const port = parseInt(process.env.UPTIME_KUMA_SERVICE_PORT || process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001); let options = { - host: hostname || "127.0.0.1", + host: hostname, port: port, timeout: 28 * 1000, }; diff --git a/package-lock.json b/package-lock.json index 9da629687..c5429446f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "uptime-kuma", - "version": "1.19.5", + "version": "1.19.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "1.19.5", + "version": "1.19.6", "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.7.3", @@ -27,6 +27,7 @@ "compare-versions": "~3.6.0", "compression": "~1.7.4", "dayjs": "~1.11.5", + "dotenv": "~16.0.3", "express": "~4.17.3", "express-basic-auth": "~1.2.1", "express-static-gzip": "~2.1.7", @@ -7813,6 +7814,14 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", @@ -24881,6 +24890,11 @@ "domhandler": "^5.0.1" } }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" + }, "duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", diff --git a/package.json b/package.json index 9e635d1ba..901408363 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "compare-versions": "~3.6.0", "compression": "~1.7.4", "dayjs": "~1.11.5", + "dotenv": "~16.0.3", "express": "~4.17.3", "express-basic-auth": "~1.2.1", "express-static-gzip": "~2.1.7", diff --git a/server/config.js b/server/config.js index d46f24b75..0523e7078 100644 --- a/server/config.js +++ b/server/config.js @@ -5,6 +5,8 @@ const badgeConstants = { naColor: "#999", defaultUpColor: "#66c20a", defaultDownColor: "#c2290a", + defaultPendingColor: "#f8a306", + defaultMaintenanceColor: "#1747f5", defaultPingColor: "blue", // as defined by badge-maker / shields.io defaultStyle: "flat", defaultPingValueSuffix: "ms", diff --git a/server/plugins-manager.js b/server/plugins-manager.js index e48c53c89..674ab9691 100644 --- a/server/plugins-manager.js +++ b/server/plugins-manager.js @@ -72,6 +72,12 @@ class PluginsManager { * @param {string} name Directory name, also known as plugin unique name */ downloadPlugin(repoURL, name) { + if (fs.existsSync(this.pluginsDir + name)) { + log.info("plugin", "Plugin folder already exists? Removing..."); + fs.rmSync(this.pluginsDir + name, { + recursive: true + }); + } log.info("plugin", "Installing plugin: " + name + " " + repoURL); let result = Git.clone(repoURL, this.pluginsDir, name); log.info("plugin", "Install result: " + result); @@ -115,13 +121,19 @@ class PluginsManager { * @returns {Promise<[]>} */ async fetchPluginList() { - const res = await axios.get("https://uptime.kuma.pet/c/plugins.json"); - const list = res.data.pluginList; + let remotePluginList; + try { + const res = await axios.get("https://uptime.kuma.pet/c/plugins.json"); + remotePluginList = res.data.pluginList; + } catch (e) { + log.error("plugin", "Failed to fetch plugin list: " + e.message); + remotePluginList = []; + } for (let plugin of this.pluginList) { let find = false; // Try to merge - for (let remotePlugin of list) { + for (let remotePlugin of remotePluginList) { if (remotePlugin.name === plugin.info.name) { find = true; remotePlugin.installed = true; @@ -136,17 +148,17 @@ class PluginsManager { // Local plugin if (!find) { plugin.info.local = true; - list.push(plugin.info); + remotePluginList.push(plugin.info); } } // Sort Installed first, then sort by name - return list.sort((a, b) => { + return remotePluginList.sort((a, b) => { if (a.installed === b.installed) { - if ( a.fullName < b.fullName ) { + if (a.fullName < b.fullName) { return -1; } - if ( a.fullName > b.fullName ) { + if (a.fullName > b.fullName) { return 1; } return 0; @@ -191,15 +203,24 @@ class PluginWrapper { let indexFile = this.pluginDir + "/index.js"; let packageJSON = this.pluginDir + "/package.json"; + log.info("plugin", "Installing dependencies"); + if (fs.existsSync(indexFile)) { // Install dependencies - childProcess.execSync("npm install", { + let result = childProcess.spawnSync("npm", [ "install" ], { cwd: this.pluginDir, env: { + ...process.env, PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor } }); + if (result.stdout) { + log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8")); + } else { + log.warn("plugin", "Install dependencies result: no output"); + } + this.pluginClass = require(path.join(process.cwd(), indexFile)); let pluginClassType = typeof this.pluginClass; diff --git a/server/routers/api-router.js b/server/routers/api-router.js index bbecbced3..e95fd045e 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -4,7 +4,7 @@ const { R } = require("redbean-node"); const apicache = require("../modules/apicache"); const Monitor = require("../model/monitor"); const dayjs = require("dayjs"); -const { UP, MAINTENANCE, DOWN, flipStatus, log } = require("../../src/util"); +const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util"); const StatusPage = require("../model/status_page"); const { UptimeKumaServer } = require("../uptime-kuma-server"); const { makeBadge } = require("badge-maker"); @@ -111,8 +111,12 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response label, upLabel = "Up", downLabel = "Down", + pendingLabel = "Pending", + maintenanceLabel = "Maintenance", upColor = badgeConstants.defaultUpColor, downColor = badgeConstants.defaultDownColor, + pendingColor = badgeConstants.defaultPendingColor, + maintenanceColor = badgeConstants.defaultMaintenanceColor, style = badgeConstants.defaultStyle, value, // for demo purpose only } = request.query; @@ -139,11 +143,30 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response badgeValues.color = badgeConstants.naColor; } else { const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId); - const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1; + const state = overrideValue !== undefined ? overrideValue : heartbeat.status; - badgeValues.label = label ? label : ""; - badgeValues.color = state ? upColor : downColor; - badgeValues.message = label ?? state ? upLabel : downLabel; + badgeValues.label = label ?? ""; + switch (state) { + case DOWN: + badgeValues.color = downColor; + badgeValues.message = downLabel; + break; + case UP: + badgeValues.color = upColor; + badgeValues.message = upLabel; + break; + case PENDING: + badgeValues.color = pendingColor; + badgeValues.message = pendingLabel; + break; + case MAINTENANCE: + badgeValues.color = maintenanceColor; + badgeValues.message = maintenanceLabel; + break; + default: + badgeValues.color = badgeConstants.naColor; + badgeValues.message = "N/A"; + } } // build the svg based on given values diff --git a/server/server.js b/server/server.js index 2d065abea..841e7417b 100644 --- a/server/server.js +++ b/server/server.js @@ -11,6 +11,9 @@ dayjs.extend(require("dayjs/plugin/utc")); dayjs.extend(require("./modules/dayjs/plugin/timezone")); dayjs.extend(require("dayjs/plugin/customParseFormat")); +// Load environment variables from `.env` +require("dotenv").config(); + // Check Node.js Version const nodeVersion = parseInt(process.versions.node.split(".")[0]); const requiredVersion = 14; diff --git a/server/socket-handlers/plugins-handler.js b/server/socket-handlers/plugins-handler.js index 4ee712c79..533da309b 100644 --- a/server/socket-handlers/plugins-handler.js +++ b/server/socket-handlers/plugins-handler.js @@ -1,5 +1,6 @@ const { checkLogin } = require("../util-server"); -const { PluginManager } = require("../plugins-manager"); +const { PluginsManager } = require("../plugins-manager"); +const { log } = require("../../src/util.js"); /** * Handlers for plugins @@ -15,7 +16,9 @@ module.exports.pluginsHandler = (socket, server) => { try { checkLogin(socket); - if (PluginManager.disable) { + log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable); + + if (PluginsManager.disable) { throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/"); } @@ -25,6 +28,7 @@ module.exports.pluginsHandler = (socket, server) => { pluginList, }); } catch (error) { + log.warn("plugin", "Error: " + error.message); callback({ ok: false, msg: error.message, diff --git a/src/components/settings/Plugins.vue b/src/components/settings/Plugins.vue index ca39e7adc..614034fcb 100644 --- a/src/components/settings/Plugins.vue +++ b/src/components/settings/Plugins.vue @@ -48,7 +48,7 @@ export default { this.remotePluginList = res.pluginList; this.remotePluginListMsg = ""; } else { - this.remotePluginListMsg = this.$t("loadingError") + " " + res.message; + this.remotePluginListMsg = this.$t("loadingError") + " " + res.msg; } }); } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 7122ed39f..f3e2a88c3 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -74,12 +74,16 @@ + + @@ -425,10 +429,6 @@