mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-10-31 19:39:20 +08:00 
			
		
		
		
	Merge branch 'master' into notification_component
This commit is contained in:
		| @@ -2,10 +2,12 @@ | ||||
| /dist | ||||
| /node_modules | ||||
| /data | ||||
| /out | ||||
| /test | ||||
| /kubernetes | ||||
| /.do | ||||
| **/.dockerignore | ||||
| /private | ||||
| **/.git | ||||
| **/.gitignore | ||||
| **/docker-compose* | ||||
|   | ||||
| @@ -17,6 +17,7 @@ module.exports = { | ||||
|         requireConfigFile: false, | ||||
|     }, | ||||
|     rules: { | ||||
|         "linebreak-style": ["error", "unix"], | ||||
|         "camelcase": ["warn", { | ||||
|             "properties": "never", | ||||
|             "ignoreImports": true | ||||
| @@ -33,11 +34,12 @@ module.exports = { | ||||
|             }, | ||||
|         ], | ||||
|         quotes: ["warn", "double"], | ||||
|         //semi: ['off', 'never'], | ||||
|         semi: "warn", | ||||
|         "vue/html-indent": ["warn", 4], // default: 2 | ||||
|         "vue/max-attributes-per-line": "off", | ||||
|         "vue/singleline-html-element-content-newline": "off", | ||||
|         "vue/html-self-closing": "off", | ||||
|         "vue/attribute-hyphenation": "off",     // This change noNL to "no-n-l" unexpectedly | ||||
|         "no-multi-spaces": ["error", { | ||||
|             ignoreEOLComments: true, | ||||
|         }], | ||||
| @@ -85,10 +87,10 @@ module.exports = { | ||||
|     }, | ||||
|     "overrides": [ | ||||
|         { | ||||
|             "files": [ "src/languages/*.js" ], | ||||
|             "files": [ "src/languages/*.js", "src/icon.js" ], | ||||
|             "rules": { | ||||
|                 "comma-dangle": ["error", "always-multiline"], | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -8,3 +8,6 @@ dist-ssr | ||||
| /data | ||||
| !/data/.gitkeep | ||||
| .vscode | ||||
|  | ||||
| /private | ||||
| /out | ||||
|   | ||||
| @@ -82,12 +82,10 @@ npm install --legacy-peer-deps --dev | ||||
|  | ||||
| # Backend Dev | ||||
|  | ||||
| (2021-09-23 Update) | ||||
|  | ||||
| ```bash | ||||
| npm run start-server | ||||
|  | ||||
| # Or | ||||
|  | ||||
| node server/server.js | ||||
| npm run start-server-dev | ||||
| ``` | ||||
|  | ||||
| It binds to `0.0.0.0:3001` by default. | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								db/demo_kuma.db
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db/demo_kuma.db
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										30
									
								
								db/patch-group-table.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								db/patch-group-table.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | ||||
| BEGIN TRANSACTION; | ||||
|  | ||||
| create table `group` | ||||
| ( | ||||
|     id           INTEGER      not null | ||||
|         constraint group_pk | ||||
|             primary key autoincrement, | ||||
|     name         VARCHAR(255) not null, | ||||
|     created_date DATETIME              default (DATETIME('now')) not null, | ||||
|     public       BOOLEAN               default 0 not null, | ||||
|     active       BOOLEAN               default 1 not null, | ||||
|     weight       BOOLEAN      NOT NULL DEFAULT 1000 | ||||
| ); | ||||
|  | ||||
| CREATE TABLE [monitor_group] | ||||
| ( | ||||
|     [id]         INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, | ||||
|     [monitor_id] INTEGER                           NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, | ||||
|     [group_id]   INTEGER                           NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, | ||||
|     weight BOOLEAN NOT NULL DEFAULT 1000 | ||||
| ); | ||||
|  | ||||
| CREATE INDEX [fk] | ||||
|     ON [monitor_group] ( | ||||
|                         [monitor_id], | ||||
|                         [group_id]); | ||||
|  | ||||
|  | ||||
| COMMIT; | ||||
							
								
								
									
										18
									
								
								db/patch-incident-table.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								db/patch-incident-table.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| -- You should not modify if this have pushed to Github, unless it does serious wrong with the db. | ||||
| BEGIN TRANSACTION; | ||||
|  | ||||
| create table incident | ||||
| ( | ||||
|     id INTEGER not null | ||||
|         constraint incident_pk | ||||
|             primary key autoincrement, | ||||
|     title VARCHAR(255) not null, | ||||
|     content TEXT not null, | ||||
|     style VARCHAR(30) default 'warning' not null, | ||||
|     created_date DATETIME default (DATETIME('now')) not null, | ||||
|     last_updated_date DATETIME, | ||||
|     pin BOOLEAN default 1 not null, | ||||
|     active BOOLEAN default 1 not null | ||||
| ); | ||||
|  | ||||
| COMMIT; | ||||
							
								
								
									
										10
									
								
								dockerfile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								dockerfile
									
									
									
									
									
								
							| @@ -1,14 +1,8 @@ | ||||
| # DON'T UPDATE TO node:14-bullseye-slim, see #372. | ||||
| # If the image changed, the second stage image should be changed too | ||||
| 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 && \ | ||||
| @@ -16,7 +10,7 @@ RUN npm install --legacy-peer-deps && \ | ||||
|     chmod +x /app/extra/entrypoint.sh | ||||
|  | ||||
|  | ||||
| FROM node:14-bullseye-slim AS release | ||||
| FROM node:14-buster-slim AS release | ||||
| WORKDIR /app | ||||
|  | ||||
| # Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv | ||||
|   | ||||
| @@ -2,13 +2,6 @@ | ||||
| FROM node:14-alpine3.12 AS build | ||||
| WORKDIR /app | ||||
|  | ||||
| # split the sqlite install here, so that it can caches the arm prebuilt | ||||
| RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \ | ||||
|     ln -s /usr/bin/python3 /usr/bin/python && \ | ||||
|     npm install mapbox/node-sqlite3#593c9d && \ | ||||
|     apk del .build-deps && \ | ||||
|     rm -f /usr/bin/python | ||||
|  | ||||
| COPY . . | ||||
| RUN npm install --legacy-peer-deps && \ | ||||
|     npm run build && \ | ||||
|   | ||||
| @@ -6,12 +6,14 @@ const Database = require("../server/database"); | ||||
| const { R } = require("redbean-node"); | ||||
| const readline = require("readline"); | ||||
| const { initJWTSecret } = require("../server/util-server"); | ||||
| const args = require("args-parser")(process.argv); | ||||
| const rl = readline.createInterface({ | ||||
|     input: process.stdin, | ||||
|     output: process.stdout | ||||
| }); | ||||
|  | ||||
| (async () => { | ||||
|     Database.init(args); | ||||
|     await Database.connect(); | ||||
|  | ||||
|     try { | ||||
|   | ||||
							
								
								
									
										1073
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1073
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										47
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "uptime-kuma", | ||||
|     "version": "1.6.0", | ||||
|     "version": "1.7.0", | ||||
|     "license": "MIT", | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
| @@ -10,22 +10,25 @@ | ||||
|         "node": "14.*" | ||||
|     }, | ||||
|     "scripts": { | ||||
|         "install-legacy-peer-deps": "npm install --legacy-peer-deps", | ||||
|         "update-legacy-peer-deps": "npm update --legacy-peer-deps", | ||||
|         "install-legacy": "npm install --legacy-peer-deps", | ||||
|         "update-legacy": "npm update --legacy-peer-deps", | ||||
|         "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", | ||||
|         "lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore", | ||||
|         "lint": "npm run lint:js && npm run lint:style", | ||||
|         "dev": "vite --host", | ||||
|         "start": "npm run start-server", | ||||
|         "start-server": "node server/server.js", | ||||
|         "start-server-dev": "cross-env NODE_ENV=development node server/server.js", | ||||
|         "build": "vite build", | ||||
|         "tsc": "tsc", | ||||
|         "vite-preview-dist": "vite preview --host", | ||||
|         "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-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.7.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.7.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.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-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --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.6.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune", | ||||
|         "setup": "git checkout 1.7.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", | ||||
| @@ -36,7 +39,7 @@ | ||||
|         "test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .", | ||||
|         "test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .", | ||||
|         "simple-dns-server": "node extra/simple-dns-server.js", | ||||
|         "update-language-files_old": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix", | ||||
|         "update-language-files-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix", | ||||
|         "update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix" | ||||
|     }, | ||||
|     "dependencies": { | ||||
| @@ -44,6 +47,7 @@ | ||||
|         "@fortawesome/free-regular-svg-icons": "^5.15.4", | ||||
|         "@fortawesome/free-solid-svg-icons": "^5.15.4", | ||||
|         "@fortawesome/vue-fontawesome": "^3.0.0-4", | ||||
|         "@louislam/sqlite3": "^5.0.6", | ||||
|         "@popperjs/core": "^2.10.1", | ||||
|         "args-parser": "^1.3.0", | ||||
|         "axios": "^0.21.4", | ||||
| @@ -59,7 +63,7 @@ | ||||
|         "form-data": "^4.0.0", | ||||
|         "http-graceful-shutdown": "^3.1.4", | ||||
|         "jsonwebtoken": "^8.5.1", | ||||
|         "nodemailer": "^6.6.3", | ||||
|         "nodemailer": "^6.6.5", | ||||
|         "notp": "^2.0.3", | ||||
|         "password-hash": "^1.2.2", | ||||
|         "prom-client": "^13.2.0", | ||||
| @@ -68,34 +72,37 @@ | ||||
|         "redbean-node": "0.1.2", | ||||
|         "socket.io": "^4.2.0", | ||||
|         "socket.io-client": "^4.2.0", | ||||
|         "sqlite3": "github:mapbox/node-sqlite3#593c9d", | ||||
|         "tcp-ping": "^0.1.1", | ||||
|         "timezones-list": "^3.0.1", | ||||
|         "thirty-two": "^1.0.2", | ||||
|         "timezones-list": "^3.0.1", | ||||
|         "v-pagination-3": "^0.1.6", | ||||
|         "vue": "^3.2.8", | ||||
|         "vue": "next", | ||||
|         "vue-chart-3": "^0.5.8", | ||||
|         "vue-confirm-dialog": "^1.0.2", | ||||
|         "vue-contenteditable": "^3.0.4", | ||||
|         "vue-i18n": "^9.1.7", | ||||
|         "vue-image-crop-upload": "^3.0.3", | ||||
|         "vue-multiselect": "^3.0.0-alpha.2", | ||||
|         "vue-qrcode": "^1.0.0", | ||||
|         "vue-router": "^4.0.11", | ||||
|         "vue-toastification": "^2.0.0-rc.1" | ||||
|         "vue-toastification": "^2.0.0-rc.1", | ||||
|         "vuedraggable": "^4.1.0" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@babel/eslint-parser": "^7.15.4", | ||||
|         "@types/bootstrap": "^5.1.4", | ||||
|         "@babel/eslint-parser": "^7.15.7", | ||||
|         "@types/bootstrap": "^5.1.6", | ||||
|         "@vitejs/plugin-legacy": "^1.5.3", | ||||
|         "@vitejs/plugin-vue": "^1.6.2", | ||||
|         "@vue/compiler-sfc": "^3.2.11", | ||||
|         "core-js": "^3.17.3", | ||||
|         "@vitejs/plugin-vue": "^1.9.1", | ||||
|         "@vue/compiler-sfc": "^3.2.16", | ||||
|         "core-js": "^3.18.0", | ||||
|         "cross-env": "^7.0.3", | ||||
|         "dns2": "^2.0.1", | ||||
|         "eslint": "^7.32.0", | ||||
|         "eslint-plugin-vue": "^7.17.0", | ||||
|         "sass": "^1.39.2", | ||||
|         "eslint-plugin-vue": "^7.18.0", | ||||
|         "sass": "^1.42.1", | ||||
|         "stylelint": "^13.13.1", | ||||
|         "stylelint-config-standard": "^22.0.0", | ||||
|         "typescript": "^4.4.3", | ||||
|         "vite": "^2.5.7" | ||||
|         "vite": "^2.5.10" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ exports.startInterval = () => { | ||||
|  | ||||
|             // For debug | ||||
|             if (process.env.TEST_CHECK_VERSION === "1") { | ||||
|                 res.data.version = "1000.0.0" | ||||
|                 res.data.version = "1000.0.0"; | ||||
|             } | ||||
|  | ||||
|             exports.latestVersion = res.data.version; | ||||
|   | ||||
| @@ -3,11 +3,25 @@ const { R } = require("redbean-node"); | ||||
| const { setSetting, setting } = require("./util-server"); | ||||
| const { debug, sleep } = require("../src/util"); | ||||
| const dayjs = require("dayjs"); | ||||
| const knex = require("knex"); | ||||
|  | ||||
| /** | ||||
|  * Database & App Data Folder | ||||
|  */ | ||||
| class Database { | ||||
|  | ||||
|     static templatePath = "./db/kuma.db"; | ||||
|  | ||||
|     /** | ||||
|      * Data Dir (Default: ./data) | ||||
|      */ | ||||
|     static dataDir; | ||||
|  | ||||
|     /** | ||||
|      * User Upload Dir (Default: ./data/upload) | ||||
|      */ | ||||
|     static uploadDir; | ||||
|  | ||||
|     static path; | ||||
|  | ||||
|     /** | ||||
| @@ -32,6 +46,8 @@ class Database { | ||||
|         "patch-improve-performance.sql": true, | ||||
|         "patch-2fa.sql": true, | ||||
|         "patch-add-retry-interval-monitor.sql": true, | ||||
|         "patch-incident-table.sql": true, | ||||
|         "patch-group-table.sql": true, | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -42,27 +58,53 @@ class Database { | ||||
|  | ||||
|     static noReject = true; | ||||
|  | ||||
|     static init(args) { | ||||
|         // Data Directory (must be end with "/") | ||||
|         Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/"; | ||||
|         Database.path = Database.dataDir + "kuma.db"; | ||||
|         if (! fs.existsSync(Database.dataDir)) { | ||||
|             fs.mkdirSync(Database.dataDir, { recursive: true }); | ||||
|         } | ||||
|  | ||||
|         Database.uploadDir = Database.dataDir + "upload/"; | ||||
|  | ||||
|         if (! fs.existsSync(Database.uploadDir)) { | ||||
|             fs.mkdirSync(Database.uploadDir, { recursive: true }); | ||||
|         } | ||||
|  | ||||
|         console.log(`Data Dir: ${Database.dataDir}`); | ||||
|     } | ||||
|  | ||||
|     static async connect() { | ||||
|         const acquireConnectionTimeout = 120 * 1000; | ||||
|  | ||||
|         R.setup("sqlite", { | ||||
|             filename: Database.path, | ||||
|         const Dialect = require("knex/lib/dialects/sqlite3/index.js"); | ||||
|         Dialect.prototype._driver = () => require("@louislam/sqlite3"); | ||||
|  | ||||
|         const knexInstance = knex({ | ||||
|             client: Dialect, | ||||
|             connection: { | ||||
|                 filename: Database.path, | ||||
|                 acquireConnectionTimeout: acquireConnectionTimeout, | ||||
|             }, | ||||
|             useNullAsDefault: true, | ||||
|             acquireConnectionTimeout: acquireConnectionTimeout, | ||||
|         }, { | ||||
|             min: 1, | ||||
|             max: 1, | ||||
|             idleTimeoutMillis: 120 * 1000, | ||||
|             propagateCreateError: false, | ||||
|             acquireTimeoutMillis: acquireConnectionTimeout, | ||||
|             pool: { | ||||
|                 min: 1, | ||||
|                 max: 1, | ||||
|                 idleTimeoutMillis: 120 * 1000, | ||||
|                 propagateCreateError: false, | ||||
|                 acquireTimeoutMillis: acquireConnectionTimeout, | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         R.setup(knexInstance); | ||||
|  | ||||
|         if (process.env.SQL_LOG === "1") { | ||||
|             R.debug(true); | ||||
|         } | ||||
|  | ||||
|         // Auto map the model to a bean object | ||||
|         R.freeze(true) | ||||
|         R.freeze(true); | ||||
|         await R.autoloadModels("./server/model"); | ||||
|  | ||||
|         // Change to WAL | ||||
| @@ -72,6 +114,7 @@ class Database { | ||||
|         console.log("SQLite config:"); | ||||
|         console.log(await R.getAll("PRAGMA journal_mode")); | ||||
|         console.log(await R.getAll("PRAGMA cache_size")); | ||||
|         console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()")); | ||||
|     } | ||||
|  | ||||
|     static async patch() { | ||||
| @@ -89,7 +132,7 @@ class Database { | ||||
|         } else if (version > this.latestVersion) { | ||||
|             console.info("Warning: Database version is newer than expected"); | ||||
|         } else { | ||||
|             console.info("Database patch is needed") | ||||
|             console.info("Database patch is needed"); | ||||
|  | ||||
|             this.backup(version); | ||||
|  | ||||
| @@ -104,11 +147,12 @@ class Database { | ||||
|                 } | ||||
|             } 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") | ||||
|                 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"); | ||||
|  | ||||
|                 this.restore(); | ||||
|                 process.exit(1); | ||||
|             } | ||||
|         } | ||||
| @@ -133,7 +177,7 @@ class Database { | ||||
|  | ||||
|         try { | ||||
|             for (let sqlFilename in this.patchList) { | ||||
|                 await this.patch2Recursion(sqlFilename, databasePatchedFiles) | ||||
|                 await this.patch2Recursion(sqlFilename, databasePatchedFiles); | ||||
|             } | ||||
|  | ||||
|             if (this.patched) { | ||||
| @@ -142,11 +186,13 @@ class Database { | ||||
|  | ||||
|         } catch (ex) { | ||||
|             await Database.close(); | ||||
|             this.restore(); | ||||
|  | ||||
|             console.error(ex) | ||||
|             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"); | ||||
|  | ||||
|             this.restore(); | ||||
|  | ||||
|             process.exit(1); | ||||
|         } | ||||
|  | ||||
| @@ -186,7 +232,7 @@ class Database { | ||||
|             console.log(sqlFilename + " is patched successfully"); | ||||
|  | ||||
|         } else { | ||||
|             console.log(sqlFilename + " is already patched, skip"); | ||||
|             debug(sqlFilename + " is already patched, skip"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -204,12 +250,12 @@ class Database { | ||||
|         // Remove all comments (--) | ||||
|         let lines = text.split("\n"); | ||||
|         lines = lines.filter((line) => { | ||||
|             return ! line.startsWith("--") | ||||
|             return ! line.startsWith("--"); | ||||
|         }); | ||||
|  | ||||
|         // Split statements by semicolon | ||||
|         // Filter out empty line | ||||
|         text = lines.join("\n") | ||||
|         text = lines.join("\n"); | ||||
|  | ||||
|         let statements = text.split(";") | ||||
|             .map((statement) => { | ||||
| @@ -217,7 +263,7 @@ class Database { | ||||
|             }) | ||||
|             .filter((statement) => { | ||||
|                 return statement !== ""; | ||||
|             }) | ||||
|             }); | ||||
|  | ||||
|         for (let statement of statements) { | ||||
|             await R.exec(statement); | ||||
| @@ -263,7 +309,7 @@ class Database { | ||||
|      */ | ||||
|     static backup(version) { | ||||
|         if (! this.backupPath) { | ||||
|             console.info("Backup the db") | ||||
|             console.info("Backup the db"); | ||||
|             this.backupPath = this.dataDir + "kuma.db.bak" + version; | ||||
|             fs.copyFileSync(Database.path, this.backupPath); | ||||
|  | ||||
|   | ||||
							
								
								
									
										57
									
								
								server/image-data-uri.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								server/image-data-uri.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| /* | ||||
|     From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js | ||||
|     Modified with 0 dependencies | ||||
|  */ | ||||
| let fs = require("fs"); | ||||
|  | ||||
| let ImageDataURI = (() => { | ||||
|  | ||||
|     function decode(dataURI) { | ||||
|         if (!/data:image\//.test(dataURI)) { | ||||
|             console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\""); | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         let regExMatches = dataURI.match("data:(image/.*);base64,(.*)"); | ||||
|         return { | ||||
|             imageType: regExMatches[1], | ||||
|             dataBase64: regExMatches[2], | ||||
|             dataBuffer: new Buffer(regExMatches[2], "base64") | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     function encode(data, mediaType) { | ||||
|         if (!data || !mediaType) { | ||||
|             console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType "); | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType; | ||||
|         let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64"); | ||||
|         let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64; | ||||
|  | ||||
|         return dataImgBase64; | ||||
|     } | ||||
|  | ||||
|     function outputFile(dataURI, filePath) { | ||||
|         filePath = filePath || "./"; | ||||
|         return new Promise((resolve, reject) => { | ||||
|             let imageDecoded = decode(dataURI); | ||||
|  | ||||
|             fs.writeFile(filePath, imageDecoded.dataBuffer, err => { | ||||
|                 if (err) { | ||||
|                     return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4)); | ||||
|                 } | ||||
|                 resolve(filePath); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         decode: decode, | ||||
|         encode: encode, | ||||
|         outputFile: outputFile, | ||||
|     }; | ||||
| })(); | ||||
|  | ||||
| module.exports = ImageDataURI; | ||||
							
								
								
									
										34
									
								
								server/model/group.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								server/model/group.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| const { BeanModel } = require("redbean-node/dist/bean-model"); | ||||
| const { R } = require("redbean-node"); | ||||
|  | ||||
| class Group extends BeanModel { | ||||
|  | ||||
|     async toPublicJSON() { | ||||
|         let monitorBeanList = await this.getMonitorList(); | ||||
|         let monitorList = []; | ||||
|  | ||||
|         for (let bean of monitorBeanList) { | ||||
|             monitorList.push(await bean.toPublicJSON()); | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             id: this.id, | ||||
|             name: this.name, | ||||
|             weight: this.weight, | ||||
|             monitorList, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async getMonitorList() { | ||||
|         return R.convertToBeans("monitor", await R.getAll(` | ||||
|             SELECT monitor.* FROM monitor, monitor_group | ||||
|             WHERE monitor.id = monitor_group.monitor_id | ||||
|             AND group_id = ? | ||||
|             ORDER BY monitor_group.weight | ||||
|         `, [ | ||||
|             this.id, | ||||
|         ])); | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = Group; | ||||
| @@ -1,8 +1,8 @@ | ||||
| const dayjs = require("dayjs"); | ||||
| const utc = require("dayjs/plugin/utc") | ||||
| let timezone = require("dayjs/plugin/timezone") | ||||
| dayjs.extend(utc) | ||||
| dayjs.extend(timezone) | ||||
| const utc = require("dayjs/plugin/utc"); | ||||
| let timezone = require("dayjs/plugin/timezone"); | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
| const { BeanModel } = require("redbean-node/dist/bean-model"); | ||||
|  | ||||
| /** | ||||
| @@ -13,6 +13,15 @@ const { BeanModel } = require("redbean-node/dist/bean-model"); | ||||
|  */ | ||||
| class Heartbeat extends BeanModel { | ||||
|  | ||||
|     toPublicJSON() { | ||||
|         return { | ||||
|             status: this.status, | ||||
|             time: this.time, | ||||
|             msg: "",        // Hide for public | ||||
|             ping: this.ping, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     toJSON() { | ||||
|         return { | ||||
|             monitorID: this.monitor_id, | ||||
|   | ||||
							
								
								
									
										18
									
								
								server/model/incident.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								server/model/incident.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| const { BeanModel } = require("redbean-node/dist/bean-model"); | ||||
|  | ||||
| class Incident extends BeanModel { | ||||
|  | ||||
|     toPublicJSON() { | ||||
|         return { | ||||
|             id: this.id, | ||||
|             style: this.style, | ||||
|             title: this.title, | ||||
|             content: this.content, | ||||
|             pin: this.pin, | ||||
|             createdDate: this.createdDate, | ||||
|             lastUpdatedDate: this.lastUpdatedDate, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = Incident; | ||||
| @@ -1,16 +1,16 @@ | ||||
| const https = require("https"); | ||||
| const dayjs = require("dayjs"); | ||||
| const utc = require("dayjs/plugin/utc") | ||||
| let timezone = require("dayjs/plugin/timezone") | ||||
| dayjs.extend(utc) | ||||
| dayjs.extend(timezone) | ||||
| const utc = require("dayjs/plugin/utc"); | ||||
| let timezone = require("dayjs/plugin/timezone"); | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
| const axios = require("axios"); | ||||
| const { Prometheus } = require("../prometheus"); | ||||
| const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); | ||||
| const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server"); | ||||
| const { R } = require("redbean-node"); | ||||
| const { BeanModel } = require("redbean-node/dist/bean-model"); | ||||
| const { Notification } = require("../notification") | ||||
| const { Notification } = require("../notification"); | ||||
| const version = require("../../package.json").version; | ||||
|  | ||||
| /** | ||||
| @@ -20,13 +20,28 @@ const version = require("../../package.json").version; | ||||
|  *      2 = PENDING | ||||
|  */ | ||||
| class Monitor extends BeanModel { | ||||
|  | ||||
|     /** | ||||
|      * Return a object that ready to parse to JSON for public | ||||
|      * Only show necessary data to public | ||||
|      */ | ||||
|     async toPublicJSON() { | ||||
|         return { | ||||
|             id: this.id, | ||||
|             name: this.name, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return a object that ready to parse to JSON | ||||
|      */ | ||||
|     async toJSON() { | ||||
|  | ||||
|         let notificationIDList = {}; | ||||
|  | ||||
|         let list = await R.find("monitor_notification", " monitor_id = ? ", [ | ||||
|             this.id, | ||||
|         ]) | ||||
|         ]); | ||||
|  | ||||
|         for (let bean of list) { | ||||
|             notificationIDList[bean.notification_id] = true; | ||||
| @@ -64,7 +79,7 @@ class Monitor extends BeanModel { | ||||
|      * @returns {boolean} | ||||
|      */ | ||||
|     getIgnoreTls() { | ||||
|         return Boolean(this.ignoreTls) | ||||
|         return Boolean(this.ignoreTls); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -94,12 +109,12 @@ class Monitor extends BeanModel { | ||||
|             if (! previousBeat) { | ||||
|                 previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ | ||||
|                     this.id, | ||||
|                 ]) | ||||
|                 ]); | ||||
|             } | ||||
|  | ||||
|             const isFirstBeat = !previousBeat; | ||||
|  | ||||
|             let bean = R.dispense("heartbeat") | ||||
|             let bean = R.dispense("heartbeat"); | ||||
|             bean.monitor_id = this.id; | ||||
|             bean.time = R.isoDateTime(dayjs.utc()); | ||||
|             bean.status = DOWN; | ||||
| @@ -135,7 +150,7 @@ class Monitor extends BeanModel { | ||||
|                             return checkStatusCode(status, this.getAcceptedStatuscodes()); | ||||
|                         }, | ||||
|                     }); | ||||
|                     bean.msg = `${res.status} - ${res.statusText}` | ||||
|                     bean.msg = `${res.status} - ${res.statusText}`; | ||||
|                     bean.ping = dayjs().valueOf() - startTime; | ||||
|  | ||||
|                     // Check certificate if https is used | ||||
| @@ -145,12 +160,12 @@ class Monitor extends BeanModel { | ||||
|                             tlsInfo = await this.updateTlsInfo(checkCertificate(res)); | ||||
|                         } catch (e) { | ||||
|                             if (e.message !== "No TLS certificate in response") { | ||||
|                                 console.error(e.message) | ||||
|                                 console.error(e.message); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms") | ||||
|                     debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms"); | ||||
|  | ||||
|                     if (this.type === "http") { | ||||
|                         bean.status = UP; | ||||
| @@ -160,26 +175,26 @@ class Monitor extends BeanModel { | ||||
|  | ||||
|                         // Convert to string for object/array | ||||
|                         if (typeof data !== "string") { | ||||
|                             data = JSON.stringify(data) | ||||
|                             data = JSON.stringify(data); | ||||
|                         } | ||||
|  | ||||
|                         if (data.includes(this.keyword)) { | ||||
|                             bean.msg += ", keyword is found" | ||||
|                             bean.msg += ", keyword is found"; | ||||
|                             bean.status = UP; | ||||
|                         } else { | ||||
|                             throw new Error(bean.msg + ", but keyword is not found") | ||||
|                             throw new Error(bean.msg + ", but keyword is not found"); | ||||
|                         } | ||||
|  | ||||
|                     } | ||||
|  | ||||
|                 } else if (this.type === "port") { | ||||
|                     bean.ping = await tcping(this.hostname, this.port); | ||||
|                     bean.msg = "" | ||||
|                     bean.msg = ""; | ||||
|                     bean.status = UP; | ||||
|  | ||||
|                 } else if (this.type === "ping") { | ||||
|                     bean.ping = await ping(this.hostname); | ||||
|                     bean.msg = "" | ||||
|                     bean.msg = ""; | ||||
|                     bean.status = UP; | ||||
|                 } else if (this.type === "dns") { | ||||
|                     let startTime = dayjs().valueOf(); | ||||
| @@ -199,7 +214,7 @@ class Monitor extends BeanModel { | ||||
|                         dnsRes.forEach(record => { | ||||
|                             dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `; | ||||
|                         }); | ||||
|                         dnsMessage = dnsMessage.slice(0, -2) | ||||
|                         dnsMessage = dnsMessage.slice(0, -2); | ||||
|                     } else if (this.dns_resolve_type == "NS") { | ||||
|                         dnsMessage += "Servers: "; | ||||
|                         dnsMessage += dnsRes.join(" | "); | ||||
| @@ -209,7 +224,7 @@ class Monitor extends BeanModel { | ||||
|                         dnsRes.forEach(record => { | ||||
|                             dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `; | ||||
|                         }); | ||||
|                         dnsMessage = dnsMessage.slice(0, -2) | ||||
|                         dnsMessage = dnsMessage.slice(0, -2); | ||||
|                     } | ||||
|  | ||||
|                     if (this.dnsLastResult !== dnsMessage) { | ||||
| @@ -272,20 +287,20 @@ class Monitor extends BeanModel { | ||||
|                 if (!isFirstBeat || bean.status === DOWN) { | ||||
|                     let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ | ||||
|                         this.id, | ||||
|                     ]) | ||||
|                     ]); | ||||
|  | ||||
|                     let text; | ||||
|                     if (bean.status === UP) { | ||||
|                         text = "✅ Up" | ||||
|                         text = "✅ Up"; | ||||
|                     } else { | ||||
|                         text = "🔴 Down" | ||||
|                         text = "🔴 Down"; | ||||
|                     } | ||||
|  | ||||
|                     let msg = `[${this.name}] [${text}] ${bean.msg}`; | ||||
|  | ||||
|                     for (let notification of notificationList) { | ||||
|                         try { | ||||
|                             await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()) | ||||
|                             await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()); | ||||
|                         } catch (e) { | ||||
|                             console.error("Cannot send notification to " + notification.name); | ||||
|                             console.log(e); | ||||
| @@ -300,18 +315,18 @@ class Monitor extends BeanModel { | ||||
|             let beatInterval = this.interval; | ||||
|  | ||||
|             if (bean.status === UP) { | ||||
|                 console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`) | ||||
|                 console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`); | ||||
|             } else if (bean.status === PENDING) { | ||||
|                 if (this.retryInterval !== this.interval) { | ||||
|                     beatInterval = this.retryInterval; | ||||
|                 } | ||||
|                 console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`) | ||||
|                 console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`); | ||||
|             } else { | ||||
|                 console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`) | ||||
|                 console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`); | ||||
|             } | ||||
|  | ||||
|             io.to(this.user_id).emit("heartbeat", bean.toJSON()); | ||||
|             Monitor.sendStats(io, this.id, this.user_id) | ||||
|             Monitor.sendStats(io, this.id, this.user_id); | ||||
|  | ||||
|             await R.store(bean); | ||||
|             prometheus.update(bean, tlsInfo); | ||||
| @@ -322,7 +337,7 @@ class Monitor extends BeanModel { | ||||
|                 this.heartbeatInterval = setTimeout(beat, beatInterval * 1000); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|         }; | ||||
|  | ||||
|         beat(); | ||||
|     } | ||||
| @@ -415,7 +430,7 @@ class Monitor extends BeanModel { | ||||
|      * https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime | ||||
|      * @param duration : int Hours | ||||
|      */ | ||||
|     static async sendUptime(duration, io, monitorID, userID) { | ||||
|     static async calcUptime(duration, monitorID) { | ||||
|         const timeLogger = new TimeLogger(); | ||||
|  | ||||
|         const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour")); | ||||
| @@ -468,12 +483,21 @@ class Monitor extends BeanModel { | ||||
|         } else { | ||||
|             // 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; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return uptime; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Send Uptime | ||||
|      * @param duration : int Hours | ||||
|      */ | ||||
|     static async sendUptime(duration, io, monitorID, userID) { | ||||
|         const uptime = await this.calcUptime(duration, monitorID); | ||||
|         io.to(userID).emit("uptime", monitorID, duration, uptime); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										749
									
								
								server/modules/apicache/apicache.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										749
									
								
								server/modules/apicache/apicache.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,749 @@ | ||||
| let url = require("url"); | ||||
| let MemoryCache = require("./memory-cache"); | ||||
|  | ||||
| let t = { | ||||
|     ms: 1, | ||||
|     second: 1000, | ||||
|     minute: 60000, | ||||
|     hour: 3600000, | ||||
|     day: 3600000 * 24, | ||||
|     week: 3600000 * 24 * 7, | ||||
|     month: 3600000 * 24 * 30, | ||||
| }; | ||||
|  | ||||
| let instances = []; | ||||
|  | ||||
| let matches = function (a) { | ||||
|     return function (b) { | ||||
|         return a === b; | ||||
|     }; | ||||
| }; | ||||
|  | ||||
| let doesntMatch = function (a) { | ||||
|     return function (b) { | ||||
|         return !matches(a)(b); | ||||
|     }; | ||||
| }; | ||||
|  | ||||
| let logDuration = function (d, prefix) { | ||||
|     let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms"; | ||||
|     return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m"; | ||||
| }; | ||||
|  | ||||
| function getSafeHeaders(res) { | ||||
|     return res.getHeaders ? res.getHeaders() : res._headers; | ||||
| } | ||||
|  | ||||
| function ApiCache() { | ||||
|     let memCache = new MemoryCache(); | ||||
|  | ||||
|     let globalOptions = { | ||||
|         debug: false, | ||||
|         defaultDuration: 3600000, | ||||
|         enabled: true, | ||||
|         appendKey: [], | ||||
|         jsonp: false, | ||||
|         redisClient: false, | ||||
|         headerBlacklist: [], | ||||
|         statusCodes: { | ||||
|             include: [], | ||||
|             exclude: [], | ||||
|         }, | ||||
|         events: { | ||||
|             expire: undefined, | ||||
|         }, | ||||
|         headers: { | ||||
|             // 'cache-control':  'no-cache' // example of header overwrite | ||||
|         }, | ||||
|         trackPerformance: false, | ||||
|         respectCacheControl: false, | ||||
|     }; | ||||
|  | ||||
|     let middlewareOptions = []; | ||||
|     let instance = this; | ||||
|     let index = null; | ||||
|     let timers = {}; | ||||
|     let performanceArray = []; // for tracking cache hit rate | ||||
|  | ||||
|     instances.push(this); | ||||
|     this.id = instances.length; | ||||
|  | ||||
|     function debug(a, b, c, d) { | ||||
|         let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) { | ||||
|             return arg !== undefined; | ||||
|         }); | ||||
|         let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1; | ||||
|  | ||||
|         return (globalOptions.debug || debugEnv) && console.log.apply(null, arr); | ||||
|     } | ||||
|  | ||||
|     function shouldCacheResponse(request, response, toggle) { | ||||
|         let opt = globalOptions; | ||||
|         let codes = opt.statusCodes; | ||||
|  | ||||
|         if (!response) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (toggle && !toggle(request, response)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) { | ||||
|             return false; | ||||
|         } | ||||
|         if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     function addIndexEntries(key, req) { | ||||
|         let groupName = req.apicacheGroup; | ||||
|  | ||||
|         if (groupName) { | ||||
|             debug("group detected \"" + groupName + "\""); | ||||
|             let group = (index.groups[groupName] = index.groups[groupName] || []); | ||||
|             group.unshift(key); | ||||
|         } | ||||
|  | ||||
|         index.all.unshift(key); | ||||
|     } | ||||
|  | ||||
|     function filterBlacklistedHeaders(headers) { | ||||
|         return Object.keys(headers) | ||||
|             .filter(function (key) { | ||||
|                 return globalOptions.headerBlacklist.indexOf(key) === -1; | ||||
|             }) | ||||
|             .reduce(function (acc, header) { | ||||
|                 acc[header] = headers[header]; | ||||
|                 return acc; | ||||
|             }, {}); | ||||
|     } | ||||
|  | ||||
|     function createCacheObject(status, headers, data, encoding) { | ||||
|         return { | ||||
|             status: status, | ||||
|             headers: filterBlacklistedHeaders(headers), | ||||
|             data: data, | ||||
|             encoding: encoding, | ||||
|             timestamp: new Date().getTime() / 1000, // seconds since epoch.  This is used to properly decrement max-age headers in cached responses. | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     function cacheResponse(key, value, duration) { | ||||
|         let redis = globalOptions.redisClient; | ||||
|         let expireCallback = globalOptions.events.expire; | ||||
|  | ||||
|         if (redis && redis.connected) { | ||||
|             try { | ||||
|                 redis.hset(key, "response", JSON.stringify(value)); | ||||
|                 redis.hset(key, "duration", duration); | ||||
|                 redis.expire(key, duration / 1000, expireCallback || function () {}); | ||||
|             } catch (err) { | ||||
|                 debug("[apicache] error in redis.hset()"); | ||||
|             } | ||||
|         } else { | ||||
|             memCache.add(key, value, duration, expireCallback); | ||||
|         } | ||||
|  | ||||
|         // add automatic cache clearing from duration, includes max limit on setTimeout | ||||
|         timers[key] = setTimeout(function () { | ||||
|             instance.clear(key, true); | ||||
|         }, Math.min(duration, 2147483647)); | ||||
|     } | ||||
|  | ||||
|     function accumulateContent(res, content) { | ||||
|         if (content) { | ||||
|             if (typeof content == "string") { | ||||
|                 res._apicache.content = (res._apicache.content || "") + content; | ||||
|             } else if (Buffer.isBuffer(content)) { | ||||
|                 let oldContent = res._apicache.content; | ||||
|  | ||||
|                 if (typeof oldContent === "string") { | ||||
|                     oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent); | ||||
|                 } | ||||
|  | ||||
|                 if (!oldContent) { | ||||
|                     oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0); | ||||
|                 } | ||||
|  | ||||
|                 res._apicache.content = Buffer.concat( | ||||
|                     [oldContent, content], | ||||
|                     oldContent.length + content.length | ||||
|                 ); | ||||
|             } else { | ||||
|                 res._apicache.content = content; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) { | ||||
|     // monkeypatch res.end to create cache object | ||||
|         res._apicache = { | ||||
|             write: res.write, | ||||
|             writeHead: res.writeHead, | ||||
|             end: res.end, | ||||
|             cacheable: true, | ||||
|             content: undefined, | ||||
|         }; | ||||
|  | ||||
|         // append header overwrites if applicable | ||||
|         Object.keys(globalOptions.headers).forEach(function (name) { | ||||
|             res.setHeader(name, globalOptions.headers[name]); | ||||
|         }); | ||||
|  | ||||
|         res.writeHead = function () { | ||||
|             // add cache control headers | ||||
|             if (!globalOptions.headers["cache-control"]) { | ||||
|                 if (shouldCacheResponse(req, res, toggle)) { | ||||
|                     res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0)); | ||||
|                 } else { | ||||
|                     res.setHeader("cache-control", "no-cache, no-store, must-revalidate"); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             res._apicache.headers = Object.assign({}, getSafeHeaders(res)); | ||||
|             return res._apicache.writeHead.apply(this, arguments); | ||||
|         }; | ||||
|  | ||||
|         // patch res.write | ||||
|         res.write = function (content) { | ||||
|             accumulateContent(res, content); | ||||
|             return res._apicache.write.apply(this, arguments); | ||||
|         }; | ||||
|  | ||||
|         // patch res.end | ||||
|         res.end = function (content, encoding) { | ||||
|             if (shouldCacheResponse(req, res, toggle)) { | ||||
|                 accumulateContent(res, content); | ||||
|  | ||||
|                 if (res._apicache.cacheable && res._apicache.content) { | ||||
|                     addIndexEntries(key, req); | ||||
|                     let headers = res._apicache.headers || getSafeHeaders(res); | ||||
|                     let cacheObject = createCacheObject( | ||||
|                         res.statusCode, | ||||
|                         headers, | ||||
|                         res._apicache.content, | ||||
|                         encoding | ||||
|                     ); | ||||
|                     cacheResponse(key, cacheObject, duration); | ||||
|  | ||||
|                     // display log entry | ||||
|                     let elapsed = new Date() - req.apicacheTimer; | ||||
|                     debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed)); | ||||
|                     debug("_apicache.headers: ", res._apicache.headers); | ||||
|                     debug("res.getHeaders(): ", getSafeHeaders(res)); | ||||
|                     debug("cacheObject: ", cacheObject); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return res._apicache.end.apply(this, arguments); | ||||
|         }; | ||||
|  | ||||
|         next(); | ||||
|     } | ||||
|  | ||||
|     function sendCachedResponse(request, response, cacheObject, toggle, next, duration) { | ||||
|         if (toggle && !toggle(request, response)) { | ||||
|             return next(); | ||||
|         } | ||||
|  | ||||
|         let headers = getSafeHeaders(response); | ||||
|  | ||||
|         // Modified by @louislam, removed Cache-control, since I don't need client side cache! | ||||
|         // Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254 | ||||
|         Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {})); | ||||
|  | ||||
|         // only embed apicache headers when not in production environment | ||||
|         if (process.env.NODE_ENV !== "production") { | ||||
|             Object.assign(headers, { | ||||
|                 "apicache-store": globalOptions.redisClient ? "redis" : "memory", | ||||
|                 "apicache-version": "1.6.2-modified", | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // unstringify buffers | ||||
|         let data = cacheObject.data; | ||||
|         if (data && data.type === "Buffer") { | ||||
|             data = | ||||
|         typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data); | ||||
|         } | ||||
|  | ||||
|         // test Etag against If-None-Match for 304 | ||||
|         let cachedEtag = cacheObject.headers.etag; | ||||
|         let requestEtag = request.headers["if-none-match"]; | ||||
|  | ||||
|         if (requestEtag && cachedEtag === requestEtag) { | ||||
|             response.writeHead(304, headers); | ||||
|             return response.end(); | ||||
|         } | ||||
|  | ||||
|         response.writeHead(cacheObject.status || 200, headers); | ||||
|  | ||||
|         return response.end(data, cacheObject.encoding); | ||||
|     } | ||||
|  | ||||
|     function syncOptions() { | ||||
|         for (let i in middlewareOptions) { | ||||
|             Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     this.clear = function (target, isAutomatic) { | ||||
|         let group = index.groups[target]; | ||||
|         let redis = globalOptions.redisClient; | ||||
|  | ||||
|         if (group) { | ||||
|             debug("clearing group \"" + target + "\""); | ||||
|  | ||||
|             group.forEach(function (key) { | ||||
|                 debug("clearing cached entry for \"" + key + "\""); | ||||
|                 clearTimeout(timers[key]); | ||||
|                 delete timers[key]; | ||||
|                 if (!globalOptions.redisClient) { | ||||
|                     memCache.delete(key); | ||||
|                 } else { | ||||
|                     try { | ||||
|                         redis.del(key); | ||||
|                     } catch (err) { | ||||
|                         console.log("[apicache] error in redis.del(\"" + key + "\")"); | ||||
|                     } | ||||
|                 } | ||||
|                 index.all = index.all.filter(doesntMatch(key)); | ||||
|             }); | ||||
|  | ||||
|             delete index.groups[target]; | ||||
|         } else if (target) { | ||||
|             debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\""); | ||||
|             clearTimeout(timers[target]); | ||||
|             delete timers[target]; | ||||
|             // clear actual cached entry | ||||
|             if (!redis) { | ||||
|                 memCache.delete(target); | ||||
|             } else { | ||||
|                 try { | ||||
|                     redis.del(target); | ||||
|                 } catch (err) { | ||||
|                     console.log("[apicache] error in redis.del(\"" + target + "\")"); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // remove from global index | ||||
|             index.all = index.all.filter(doesntMatch(target)); | ||||
|  | ||||
|             // remove target from each group that it may exist in | ||||
|             Object.keys(index.groups).forEach(function (groupName) { | ||||
|                 index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target)); | ||||
|  | ||||
|                 // delete group if now empty | ||||
|                 if (!index.groups[groupName].length) { | ||||
|                     delete index.groups[groupName]; | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             debug("clearing entire index"); | ||||
|  | ||||
|             if (!redis) { | ||||
|                 memCache.clear(); | ||||
|             } else { | ||||
|                 // clear redis keys one by one from internal index to prevent clearing non-apicache entries | ||||
|                 index.all.forEach(function (key) { | ||||
|                     clearTimeout(timers[key]); | ||||
|                     delete timers[key]; | ||||
|                     try { | ||||
|                         redis.del(key); | ||||
|                     } catch (err) { | ||||
|                         console.log("[apicache] error in redis.del(\"" + key + "\")"); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             this.resetIndex(); | ||||
|         } | ||||
|  | ||||
|         return this.getIndex(); | ||||
|     }; | ||||
|  | ||||
|     function parseDuration(duration, defaultDuration) { | ||||
|         if (typeof duration === "number") { | ||||
|             return duration; | ||||
|         } | ||||
|  | ||||
|         if (typeof duration === "string") { | ||||
|             let split = duration.match(/^([\d\.,]+)\s?(\w+)$/); | ||||
|  | ||||
|             if (split.length === 3) { | ||||
|                 let len = parseFloat(split[1]); | ||||
|                 let unit = split[2].replace(/s$/i, "").toLowerCase(); | ||||
|                 if (unit === "m") { | ||||
|                     unit = "ms"; | ||||
|                 } | ||||
|  | ||||
|                 return (len || 1) * (t[unit] || 0); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return defaultDuration; | ||||
|     } | ||||
|  | ||||
|     this.getDuration = function (duration) { | ||||
|         return parseDuration(duration, globalOptions.defaultDuration); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|    * Return cache performance statistics (hit rate).  Suitable for putting into a route: | ||||
|    * <code> | ||||
|    * app.get('/api/cache/performance', (req, res) => { | ||||
|    *    res.json(apicache.getPerformance()) | ||||
|    * }) | ||||
|    * </code> | ||||
|    */ | ||||
|     this.getPerformance = function () { | ||||
|         return performanceArray.map(function (p) { | ||||
|             return p.report(); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     this.getIndex = function (group) { | ||||
|         if (group) { | ||||
|             return index.groups[group]; | ||||
|         } else { | ||||
|             return index; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     this.middleware = function cache(strDuration, middlewareToggle, localOptions) { | ||||
|         let duration = instance.getDuration(strDuration); | ||||
|         let opt = {}; | ||||
|  | ||||
|         middlewareOptions.push({ | ||||
|             options: opt, | ||||
|         }); | ||||
|  | ||||
|         let options = function (localOptions) { | ||||
|             if (localOptions) { | ||||
|                 middlewareOptions.find(function (middleware) { | ||||
|                     return middleware.options === opt; | ||||
|                 }).localOptions = localOptions; | ||||
|             } | ||||
|  | ||||
|             syncOptions(); | ||||
|  | ||||
|             return opt; | ||||
|         }; | ||||
|  | ||||
|         options(localOptions); | ||||
|  | ||||
|         /** | ||||
|      * A Function for non tracking performance | ||||
|      */ | ||||
|         function NOOPCachePerformance() { | ||||
|             this.report = this.hit = this.miss = function () {}; // noop; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|      * A function for tracking and reporting hit rate.  These statistics are returned by the getPerformance() call above. | ||||
|      */ | ||||
|         function CachePerformance() { | ||||
|             /** | ||||
|        * Tracks the hit rate for the last 100 requests. | ||||
|        * If there have been fewer than 100 requests, the hit rate just considers the requests that have happened. | ||||
|        */ | ||||
|             this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits | ||||
|  | ||||
|             /** | ||||
|        * Tracks the hit rate for the last 1000 requests. | ||||
|        * If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened. | ||||
|        */ | ||||
|             this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits | ||||
|  | ||||
|             /** | ||||
|        * Tracks the hit rate for the last 10000 requests. | ||||
|        * If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened. | ||||
|        */ | ||||
|             this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits | ||||
|  | ||||
|             /** | ||||
|        * Tracks the hit rate for the last 100000 requests. | ||||
|        * If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened. | ||||
|        */ | ||||
|             this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits | ||||
|  | ||||
|             /** | ||||
|        * The number of calls that have passed through the middleware since the server started. | ||||
|        */ | ||||
|             this.callCount = 0; | ||||
|  | ||||
|             /** | ||||
|        * The total number of hits since the server started | ||||
|        */ | ||||
|             this.hitCount = 0; | ||||
|  | ||||
|             /** | ||||
|        * The key from the last cache hit.  This is useful in identifying which route these statistics apply to. | ||||
|        */ | ||||
|             this.lastCacheHit = null; | ||||
|  | ||||
|             /** | ||||
|        * The key from the last cache miss.  This is useful in identifying which route these statistics apply to. | ||||
|        */ | ||||
|             this.lastCacheMiss = null; | ||||
|  | ||||
|             /** | ||||
|        * Return performance statistics | ||||
|        */ | ||||
|             this.report = function () { | ||||
|                 return { | ||||
|                     lastCacheHit: this.lastCacheHit, | ||||
|                     lastCacheMiss: this.lastCacheMiss, | ||||
|                     callCount: this.callCount, | ||||
|                     hitCount: this.hitCount, | ||||
|                     missCount: this.callCount - this.hitCount, | ||||
|                     hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount, | ||||
|                     hitRateLast100: this.hitRate(this.hitsLast100), | ||||
|                     hitRateLast1000: this.hitRate(this.hitsLast1000), | ||||
|                     hitRateLast10000: this.hitRate(this.hitsLast10000), | ||||
|                     hitRateLast100000: this.hitRate(this.hitsLast100000), | ||||
|                 }; | ||||
|             }; | ||||
|  | ||||
|             /** | ||||
|        * Computes a cache hit rate from an array of hits and misses. | ||||
|        * @param {Uint8Array} array An array representing hits and misses. | ||||
|        * @returns a number between 0 and 1, or null if the array has no hits or misses | ||||
|        */ | ||||
|             this.hitRate = function (array) { | ||||
|                 let hits = 0; | ||||
|                 let misses = 0; | ||||
|                 for (let i = 0; i < array.length; i++) { | ||||
|                     let n8 = array[i]; | ||||
|                     for (let j = 0; j < 4; j++) { | ||||
|                         switch (n8 & 3) { | ||||
|                             case 1: | ||||
|                                 hits++; | ||||
|                                 break; | ||||
|                             case 2: | ||||
|                                 misses++; | ||||
|                                 break; | ||||
|                         } | ||||
|                         n8 >>= 2; | ||||
|                     } | ||||
|                 } | ||||
|                 let total = hits + misses; | ||||
|                 if (total == 0) { | ||||
|                     return null; | ||||
|                 } | ||||
|                 return hits / total; | ||||
|             }; | ||||
|  | ||||
|             /** | ||||
|        * Record a hit or miss in the given array.  It will be recorded at a position determined | ||||
|        * by the current value of the callCount variable. | ||||
|        * @param {Uint8Array} array An array representing hits and misses. | ||||
|        * @param {boolean} hit true for a hit, false for a miss | ||||
|        * Each element in the array is 8 bits, and encodes 4 hit/miss records. | ||||
|        * Each hit or miss is encoded as to bits as follows: | ||||
|        * 00 means no hit or miss has been recorded in these bits | ||||
|        * 01 encodes a hit | ||||
|        * 10 encodes a miss | ||||
|        */ | ||||
|             this.recordHitInArray = function (array, hit) { | ||||
|                 let arrayIndex = ~~(this.callCount / 4) % array.length; | ||||
|                 let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element | ||||
|                 let clearMask = ~(3 << bitOffset); | ||||
|                 let record = (hit ? 1 : 2) << bitOffset; | ||||
|                 array[arrayIndex] = (array[arrayIndex] & clearMask) | record; | ||||
|             }; | ||||
|  | ||||
|             /** | ||||
|        * Records the hit or miss in the tracking arrays and increments the call count. | ||||
|        * @param {boolean} hit true records a hit, false records a miss | ||||
|        */ | ||||
|             this.recordHit = function (hit) { | ||||
|                 this.recordHitInArray(this.hitsLast100, hit); | ||||
|                 this.recordHitInArray(this.hitsLast1000, hit); | ||||
|                 this.recordHitInArray(this.hitsLast10000, hit); | ||||
|                 this.recordHitInArray(this.hitsLast100000, hit); | ||||
|                 if (hit) { | ||||
|                     this.hitCount++; | ||||
|                 } | ||||
|                 this.callCount++; | ||||
|             }; | ||||
|  | ||||
|             /** | ||||
|        * Records a hit event, setting lastCacheMiss to the given key | ||||
|        * @param {string} key The key that had the cache hit | ||||
|        */ | ||||
|             this.hit = function (key) { | ||||
|                 this.recordHit(true); | ||||
|                 this.lastCacheHit = key; | ||||
|             }; | ||||
|  | ||||
|             /** | ||||
|        * Records a miss event, setting lastCacheMiss to the given key | ||||
|        * @param {string} key The key that had the cache miss | ||||
|        */ | ||||
|             this.miss = function (key) { | ||||
|                 this.recordHit(false); | ||||
|                 this.lastCacheMiss = key; | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance(); | ||||
|  | ||||
|         performanceArray.push(perf); | ||||
|  | ||||
|         let cache = function (req, res, next) { | ||||
|             function bypass() { | ||||
|                 debug("bypass detected, skipping cache."); | ||||
|                 return next(); | ||||
|             } | ||||
|  | ||||
|             // initial bypass chances | ||||
|             if (!opt.enabled) { | ||||
|                 return bypass(); | ||||
|             } | ||||
|             if ( | ||||
|                 req.headers["x-apicache-bypass"] || | ||||
|         req.headers["x-apicache-force-fetch"] || | ||||
|         (opt.respectCacheControl && req.headers["cache-control"] == "no-cache") | ||||
|             ) { | ||||
|                 return bypass(); | ||||
|             } | ||||
|  | ||||
|             // REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER | ||||
|             // if (typeof middlewareToggle === 'function') { | ||||
|             //   if (!middlewareToggle(req, res)) return bypass() | ||||
|             // } else if (middlewareToggle !== undefined && !middlewareToggle) { | ||||
|             //   return bypass() | ||||
|             // } | ||||
|  | ||||
|             // embed timer | ||||
|             req.apicacheTimer = new Date(); | ||||
|  | ||||
|             // In Express 4.x the url is ambigious based on where a router is mounted.  originalUrl will give the full Url | ||||
|             let key = req.originalUrl || req.url; | ||||
|  | ||||
|             // Remove querystring from key if jsonp option is enabled | ||||
|             if (opt.jsonp) { | ||||
|                 key = url.parse(key).pathname; | ||||
|             } | ||||
|  | ||||
|             // add appendKey (either custom function or response path) | ||||
|             if (typeof opt.appendKey === "function") { | ||||
|                 key += "$$appendKey=" + opt.appendKey(req, res); | ||||
|             } else if (opt.appendKey.length > 0) { | ||||
|                 let appendKey = req; | ||||
|  | ||||
|                 for (let i = 0; i < opt.appendKey.length; i++) { | ||||
|                     appendKey = appendKey[opt.appendKey[i]]; | ||||
|                 } | ||||
|                 key += "$$appendKey=" + appendKey; | ||||
|             } | ||||
|  | ||||
|             // attempt cache hit | ||||
|             let redis = opt.redisClient; | ||||
|             let cached = !redis ? memCache.getValue(key) : null; | ||||
|  | ||||
|             // send if cache hit from memory-cache | ||||
|             if (cached) { | ||||
|                 let elapsed = new Date() - req.apicacheTimer; | ||||
|                 debug("sending cached (memory-cache) version of", key, logDuration(elapsed)); | ||||
|  | ||||
|                 perf.hit(key); | ||||
|                 return sendCachedResponse(req, res, cached, middlewareToggle, next, duration); | ||||
|             } | ||||
|  | ||||
|             // send if cache hit from redis | ||||
|             if (redis && redis.connected) { | ||||
|                 try { | ||||
|                     redis.hgetall(key, function (err, obj) { | ||||
|                         if (!err && obj && obj.response) { | ||||
|                             let elapsed = new Date() - req.apicacheTimer; | ||||
|                             debug("sending cached (redis) version of", key, logDuration(elapsed)); | ||||
|  | ||||
|                             perf.hit(key); | ||||
|                             return sendCachedResponse( | ||||
|                                 req, | ||||
|                                 res, | ||||
|                                 JSON.parse(obj.response), | ||||
|                                 middlewareToggle, | ||||
|                                 next, | ||||
|                                 duration | ||||
|                             ); | ||||
|                         } else { | ||||
|                             perf.miss(key); | ||||
|                             return makeResponseCacheable( | ||||
|                                 req, | ||||
|                                 res, | ||||
|                                 next, | ||||
|                                 key, | ||||
|                                 duration, | ||||
|                                 strDuration, | ||||
|                                 middlewareToggle | ||||
|                             ); | ||||
|                         } | ||||
|                     }); | ||||
|                 } catch (err) { | ||||
|                     // bypass redis on error | ||||
|                     perf.miss(key); | ||||
|                     return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); | ||||
|                 } | ||||
|             } else { | ||||
|                 perf.miss(key); | ||||
|                 return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         cache.options = options; | ||||
|  | ||||
|         return cache; | ||||
|     }; | ||||
|  | ||||
|     this.options = function (options) { | ||||
|         if (options) { | ||||
|             Object.assign(globalOptions, options); | ||||
|             syncOptions(); | ||||
|  | ||||
|             if ("defaultDuration" in options) { | ||||
|                 // Convert the default duration to a number in milliseconds (if needed) | ||||
|                 globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000); | ||||
|             } | ||||
|  | ||||
|             if (globalOptions.trackPerformance) { | ||||
|                 debug("WARNING: using trackPerformance flag can cause high memory usage!"); | ||||
|             } | ||||
|  | ||||
|             return this; | ||||
|         } else { | ||||
|             return globalOptions; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     this.resetIndex = function () { | ||||
|         index = { | ||||
|             all: [], | ||||
|             groups: {}, | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     this.newInstance = function (config) { | ||||
|         let instance = new ApiCache(); | ||||
|  | ||||
|         if (config) { | ||||
|             instance.options(config); | ||||
|         } | ||||
|  | ||||
|         return instance; | ||||
|     }; | ||||
|  | ||||
|     this.clone = function () { | ||||
|         return this.newInstance(this.options()); | ||||
|     }; | ||||
|  | ||||
|     // initialize index | ||||
|     this.resetIndex(); | ||||
| } | ||||
|  | ||||
| module.exports = new ApiCache(); | ||||
							
								
								
									
										14
									
								
								server/modules/apicache/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								server/modules/apicache/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| const apicache = require("./apicache"); | ||||
|  | ||||
| apicache.options({ | ||||
|     headerBlacklist: [ | ||||
|         "cache-control" | ||||
|     ], | ||||
|     headers: { | ||||
|         // Disable client side cache, only server side cache. | ||||
|         // BUG! Not working for the second request | ||||
|         "cache-control": "no-cache", | ||||
|     }, | ||||
| }); | ||||
|  | ||||
| module.exports = apicache; | ||||
							
								
								
									
										59
									
								
								server/modules/apicache/memory-cache.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								server/modules/apicache/memory-cache.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| function MemoryCache() { | ||||
|     this.cache = {}; | ||||
|     this.size = 0; | ||||
| } | ||||
|  | ||||
| MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { | ||||
|     let old = this.cache[key]; | ||||
|     let instance = this; | ||||
|  | ||||
|     let entry = { | ||||
|         value: value, | ||||
|         expire: time + Date.now(), | ||||
|         timeout: setTimeout(function () { | ||||
|             instance.delete(key); | ||||
|             return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key); | ||||
|         }, time) | ||||
|     }; | ||||
|  | ||||
|     this.cache[key] = entry; | ||||
|     this.size = Object.keys(this.cache).length; | ||||
|  | ||||
|     return entry; | ||||
| }; | ||||
|  | ||||
| MemoryCache.prototype.delete = function (key) { | ||||
|     let entry = this.cache[key]; | ||||
|  | ||||
|     if (entry) { | ||||
|         clearTimeout(entry.timeout); | ||||
|     } | ||||
|  | ||||
|     delete this.cache[key]; | ||||
|  | ||||
|     this.size = Object.keys(this.cache).length; | ||||
|  | ||||
|     return null; | ||||
| }; | ||||
|  | ||||
| MemoryCache.prototype.get = function (key) { | ||||
|     let entry = this.cache[key]; | ||||
|  | ||||
|     return entry; | ||||
| }; | ||||
|  | ||||
| MemoryCache.prototype.getValue = function (key) { | ||||
|     let entry = this.get(key); | ||||
|  | ||||
|     return entry && entry.value; | ||||
| }; | ||||
|  | ||||
| MemoryCache.prototype.clear = function () { | ||||
|     Object.keys(this.cache).forEach(function (key) { | ||||
|         this.delete(key); | ||||
|     }, this); | ||||
|  | ||||
|     return true; | ||||
| }; | ||||
|  | ||||
| module.exports = MemoryCache; | ||||
							
								
								
									
										151
									
								
								server/routers/api-router.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								server/routers/api-router.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| let express = require("express"); | ||||
| const { allowDevAllOrigin, getSettings, setting } = require("../util-server"); | ||||
| const { R } = require("redbean-node"); | ||||
| const server = require("../server"); | ||||
| const apicache = require("../modules/apicache"); | ||||
| const Monitor = require("../model/monitor"); | ||||
| let router = express.Router(); | ||||
|  | ||||
| let cache = apicache.middleware; | ||||
|  | ||||
| router.get("/api/entry-page", async (_, response) => { | ||||
|     allowDevAllOrigin(response); | ||||
|     response.json(server.entryPage); | ||||
| }); | ||||
|  | ||||
| // Status Page Config | ||||
| router.get("/api/status-page/config", async (_request, response) => { | ||||
|     allowDevAllOrigin(response); | ||||
|  | ||||
|     let config = await getSettings("statusPage"); | ||||
|  | ||||
|     if (! config.statusPageTheme) { | ||||
|         config.statusPageTheme = "light"; | ||||
|     } | ||||
|  | ||||
|     if (! config.statusPagePublished) { | ||||
|         config.statusPagePublished = true; | ||||
|     } | ||||
|  | ||||
|     if (! config.title) { | ||||
|         config.title = "Uptime Kuma"; | ||||
|     } | ||||
|  | ||||
|     response.json(config); | ||||
| }); | ||||
|  | ||||
| // Status Page - Get the current Incident | ||||
| // Can fetch only if published | ||||
| router.get("/api/status-page/incident", async (_, response) => { | ||||
|     allowDevAllOrigin(response); | ||||
|  | ||||
|     try { | ||||
|         await checkPublished(); | ||||
|  | ||||
|         let incident = await R.findOne("incident", " pin = 1 AND active = 1"); | ||||
|  | ||||
|         if (incident) { | ||||
|             incident = incident.toPublicJSON(); | ||||
|         } | ||||
|  | ||||
|         response.json({ | ||||
|             ok: true, | ||||
|             incident, | ||||
|         }); | ||||
|  | ||||
|     } catch (error) { | ||||
|         send403(response, error.message); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| // Status Page - Monitor List | ||||
| // Can fetch only if published | ||||
| router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => { | ||||
|     allowDevAllOrigin(response); | ||||
|  | ||||
|     try { | ||||
|         await checkPublished(); | ||||
|         const publicGroupList = []; | ||||
|         let list = await R.find("group", " public = 1 ORDER BY weight "); | ||||
|  | ||||
|         for (let groupBean of list) { | ||||
|             publicGroupList.push(await groupBean.toPublicJSON()); | ||||
|         } | ||||
|  | ||||
|         response.json(publicGroupList); | ||||
|  | ||||
|     } catch (error) { | ||||
|         send403(response, error.message); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| // Status Page Polling Data | ||||
| // Can fetch only if published | ||||
| router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => { | ||||
|     allowDevAllOrigin(response); | ||||
|  | ||||
|     try { | ||||
|         await checkPublished(); | ||||
|  | ||||
|         let heartbeatList = {}; | ||||
|         let uptimeList = {}; | ||||
|  | ||||
|         let monitorIDList = await R.getCol(` | ||||
|             SELECT monitor_group.monitor_id FROM monitor_group, \`group\` | ||||
|             WHERE monitor_group.group_id = \`group\`.id | ||||
|             AND public = 1 | ||||
|         `); | ||||
|  | ||||
|         for (let monitorID of monitorIDList) { | ||||
|             let list = await R.getAll(` | ||||
|                     SELECT * FROM heartbeat | ||||
|                     WHERE monitor_id = ? | ||||
|                     ORDER BY time DESC | ||||
|                     LIMIT 50 | ||||
|             `, [ | ||||
|                 monitorID, | ||||
|             ]); | ||||
|  | ||||
|             list = R.convertToBeans("heartbeat", list); | ||||
|             heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON()); | ||||
|  | ||||
|             const type = 24; | ||||
|             uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID); | ||||
|         } | ||||
|  | ||||
|         response.json({ | ||||
|             heartbeatList, | ||||
|             uptimeList | ||||
|         }); | ||||
|  | ||||
|     } catch (error) { | ||||
|         send403(response, error.message); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| async function checkPublished() { | ||||
|     if (! await isPublished()) { | ||||
|         throw new Error("The status page is not published"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Default is published | ||||
|  * @returns {Promise<boolean>} | ||||
|  */ | ||||
| async function isPublished() { | ||||
|     const value = await setting("statusPagePublished"); | ||||
|     if (value === null) { | ||||
|         return true; | ||||
|     } | ||||
|     return value; | ||||
| } | ||||
|  | ||||
| function send403(res, msg = "") { | ||||
|     res.status(403).json({ | ||||
|         "status": "fail", | ||||
|         "msg": msg, | ||||
|     }); | ||||
| } | ||||
|  | ||||
| module.exports = router; | ||||
							
								
								
									
										440
									
								
								server/server.js
									
									
									
									
									
								
							
							
						
						
									
										440
									
								
								server/server.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										161
									
								
								server/socket-handlers/status-page-socket-handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								server/socket-handlers/status-page-socket-handler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| const { R } = require("redbean-node"); | ||||
| const { checkLogin, setSettings } = require("../util-server"); | ||||
| const dayjs = require("dayjs"); | ||||
| const { debug } = require("../../src/util"); | ||||
| const ImageDataURI = require("../image-data-uri"); | ||||
| const Database = require("../database"); | ||||
| const apicache = require("../modules/apicache"); | ||||
|  | ||||
| module.exports.statusPageSocketHandler = (socket) => { | ||||
|  | ||||
|     // Post or edit incident | ||||
|     socket.on("postIncident", async (incident, callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|  | ||||
|             await R.exec("UPDATE incident SET pin = 0 "); | ||||
|  | ||||
|             let incidentBean; | ||||
|  | ||||
|             if (incident.id) { | ||||
|                 incidentBean = await R.findOne("incident", " id = ?", [ | ||||
|                     incident.id | ||||
|                 ]); | ||||
|             } | ||||
|  | ||||
|             if (incidentBean == null) { | ||||
|                 incidentBean = R.dispense("incident"); | ||||
|             } | ||||
|  | ||||
|             incidentBean.title = incident.title; | ||||
|             incidentBean.content = incident.content; | ||||
|             incidentBean.style = incident.style; | ||||
|             incidentBean.pin = true; | ||||
|  | ||||
|             if (incident.id) { | ||||
|                 incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc()); | ||||
|             } else { | ||||
|                 incidentBean.createdDate = R.isoDateTime(dayjs.utc()); | ||||
|             } | ||||
|  | ||||
|             await R.store(incidentBean); | ||||
|  | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|                 incident: incidentBean.toPublicJSON(), | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: error.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     socket.on("unpinIncident", async (callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|  | ||||
|             await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1"); | ||||
|  | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: error.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Save Status Page | ||||
|     // imgDataUrl Only Accept PNG! | ||||
|     socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => { | ||||
|  | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|  | ||||
|             apicache.clear(); | ||||
|  | ||||
|             const header = "data:image/png;base64,"; | ||||
|  | ||||
|             // Check logo format | ||||
|             // If is image data url, convert to png file | ||||
|             // Else assume it is a url, nothing to do | ||||
|             if (imgDataUrl.startsWith("data:")) { | ||||
|                 if (! imgDataUrl.startsWith(header)) { | ||||
|                     throw new Error("Only allowed PNG logo."); | ||||
|                 } | ||||
|  | ||||
|                 // Convert to file | ||||
|                 await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png"); | ||||
|                 config.logo = "/upload/logo.png?t=" + Date.now(); | ||||
|  | ||||
|             } else { | ||||
|                 config.icon = imgDataUrl; | ||||
|             } | ||||
|  | ||||
|             // Save Config | ||||
|             await setSettings("statusPage", config); | ||||
|  | ||||
|             // Save Public Group List | ||||
|             const groupIDList = []; | ||||
|             let groupOrder = 1; | ||||
|  | ||||
|             for (let group of publicGroupList) { | ||||
|                 let groupBean; | ||||
|                 if (group.id) { | ||||
|                     groupBean = await R.findOne("group", " id = ? AND public = 1 ", [ | ||||
|                         group.id | ||||
|                     ]); | ||||
|                 } else { | ||||
|                     groupBean = R.dispense("group"); | ||||
|                 } | ||||
|  | ||||
|                 groupBean.name = group.name; | ||||
|                 groupBean.public = true; | ||||
|                 groupBean.weight = groupOrder++; | ||||
|  | ||||
|                 await R.store(groupBean); | ||||
|  | ||||
|                 await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [ | ||||
|                     groupBean.id | ||||
|                 ]); | ||||
|  | ||||
|                 let monitorOrder = 1; | ||||
|                 console.log(group.monitorList); | ||||
|  | ||||
|                 for (let monitor of group.monitorList) { | ||||
|                     let relationBean = R.dispense("monitor_group"); | ||||
|                     relationBean.weight = monitorOrder++; | ||||
|                     relationBean.group_id = groupBean.id; | ||||
|                     relationBean.monitor_id = monitor.id; | ||||
|                     await R.store(relationBean); | ||||
|                 } | ||||
|  | ||||
|                 groupIDList.push(groupBean.id); | ||||
|                 group.id = groupBean.id; | ||||
|             } | ||||
|  | ||||
|             // Delete groups that not in the list | ||||
|             debug("Delete groups that not in the list"); | ||||
|             const slots = groupIDList.map(() => "?").join(","); | ||||
|             await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList); | ||||
|  | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|                 publicGroupList, | ||||
|             }); | ||||
|  | ||||
|         } catch (error) { | ||||
|             console.log(error); | ||||
|  | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: error.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
| }; | ||||
| @@ -23,7 +23,7 @@ exports.initJWTSecret = async () => { | ||||
|     jwtSecretBean.value = passwordHash.generate(dayjs() + ""); | ||||
|     await R.store(jwtSecretBean); | ||||
|     return jwtSecretBean; | ||||
| } | ||||
| }; | ||||
|  | ||||
| exports.tcping = function (hostname, port) { | ||||
|     return new Promise((resolve, reject) => { | ||||
| @@ -44,7 +44,7 @@ exports.tcping = function (hostname, port) { | ||||
|             resolve(Math.round(data.max)); | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
| }; | ||||
|  | ||||
| exports.ping = async (hostname) => { | ||||
|     try { | ||||
| @@ -57,7 +57,7 @@ exports.ping = async (hostname) => { | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| }; | ||||
|  | ||||
| exports.pingAsync = function (hostname, ipv6 = false) { | ||||
|     return new Promise((resolve, reject) => { | ||||
| @@ -69,13 +69,13 @@ exports.pingAsync = function (hostname, ipv6 = false) { | ||||
|             if (err) { | ||||
|                 reject(err); | ||||
|             } else if (ms === null) { | ||||
|                 reject(new Error(stdout)) | ||||
|                 reject(new Error(stdout)); | ||||
|             } else { | ||||
|                 resolve(Math.round(ms)) | ||||
|                 resolve(Math.round(ms)); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
| }; | ||||
|  | ||||
| exports.dnsResolve = function (hostname, resolver_server, rrtype) { | ||||
|     const resolver = new Resolver(); | ||||
| @@ -98,8 +98,8 @@ exports.dnsResolve = function (hostname, resolver_server, rrtype) { | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| exports.setting = async function (key) { | ||||
|     let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ | ||||
| @@ -108,29 +108,29 @@ exports.setting = async function (key) { | ||||
|  | ||||
|     try { | ||||
|         const v = JSON.parse(value); | ||||
|         debug(`Get Setting: ${key}: ${v}`) | ||||
|         debug(`Get Setting: ${key}: ${v}`); | ||||
|         return v; | ||||
|     } catch (e) { | ||||
|         return value; | ||||
|     } | ||||
| } | ||||
| }; | ||||
|  | ||||
| exports.setSetting = async function (key, value) { | ||||
|     let bean = await R.findOne("setting", " `key` = ? ", [ | ||||
|         key, | ||||
|     ]) | ||||
|     ]); | ||||
|     if (!bean) { | ||||
|         bean = R.dispense("setting") | ||||
|         bean = R.dispense("setting"); | ||||
|         bean.key = key; | ||||
|     } | ||||
|     bean.value = JSON.stringify(value); | ||||
|     await R.store(bean) | ||||
| } | ||||
|     await R.store(bean); | ||||
| }; | ||||
|  | ||||
| exports.getSettings = async function (type) { | ||||
|     let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [ | ||||
|         type, | ||||
|     ]) | ||||
|     ]); | ||||
|  | ||||
|     let result = {}; | ||||
|  | ||||
| @@ -143,7 +143,7 @@ exports.getSettings = async function (type) { | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
| } | ||||
| }; | ||||
|  | ||||
| exports.setSettings = async function (type, data) { | ||||
|     let keyList = Object.keys(data); | ||||
| @@ -163,12 +163,12 @@ exports.setSettings = async function (type, data) { | ||||
|  | ||||
|         if (bean.type === type) { | ||||
|             bean.value = JSON.stringify(data[key]); | ||||
|             promiseList.push(R.store(bean)) | ||||
|             promiseList.push(R.store(bean)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     await Promise.all(promiseList); | ||||
| } | ||||
| }; | ||||
|  | ||||
| // ssl-checker by @dyaa | ||||
| // param: res - response object from axios | ||||
| @@ -218,7 +218,7 @@ exports.checkCertificate = function (res) { | ||||
|         issuer, | ||||
|         fingerprint, | ||||
|     }; | ||||
| } | ||||
| }; | ||||
|  | ||||
| // Check if the provided status code is within the accepted ranges | ||||
| // Param: status - the status code to check | ||||
| @@ -247,7 +247,7 @@ exports.checkStatusCode = function (status, accepted_codes) { | ||||
|     } | ||||
|  | ||||
|     return false; | ||||
| } | ||||
| }; | ||||
|  | ||||
| exports.getTotalClientInRoom = (io, roomName) => { | ||||
|  | ||||
| @@ -270,7 +270,7 @@ exports.getTotalClientInRoom = (io, roomName) => { | ||||
|     } else { | ||||
|         return 0; | ||||
|     } | ||||
| } | ||||
| }; | ||||
|  | ||||
| exports.genSecret = () => { | ||||
|     let secret = ""; | ||||
| @@ -280,4 +280,21 @@ exports.genSecret = () => { | ||||
|         secret += chars.charAt(Math.floor(Math.random() * charsLength)); | ||||
|     } | ||||
|     return secret; | ||||
| } | ||||
| }; | ||||
|  | ||||
| exports.allowDevAllOrigin = (res) => { | ||||
|     if (process.env.NODE_ENV === "development") { | ||||
|         exports.allowAllOrigin(res); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| exports.allowAllOrigin = (res) => { | ||||
|     res.header("Access-Control-Allow-Origin", "*"); | ||||
|     res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); | ||||
| }; | ||||
|  | ||||
| exports.checkLogin = (socket) => { | ||||
|     if (! socket.userID) { | ||||
|         throw new Error("You are not logged in."); | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -144,7 +144,9 @@ h2 { | ||||
|     } | ||||
|  | ||||
|     .shadow-box { | ||||
|         background-color: $dark-bg; | ||||
|         &:not(.alert) { | ||||
|             background-color: $dark-bg; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .form-check-input { | ||||
| @@ -255,6 +257,18 @@ h2 { | ||||
|         background-color: $dark-bg; | ||||
|     } | ||||
|  | ||||
|     .monitor-list { | ||||
|         .item { | ||||
|             &:hover { | ||||
|                 background-color: $dark-bg2; | ||||
|             } | ||||
|  | ||||
|             &.active { | ||||
|                 background-color: $dark-bg2; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @media (max-width: 550px) { | ||||
|         .table-shadow-box { | ||||
|             tbody { | ||||
| @@ -268,6 +282,16 @@ h2 { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .alert { | ||||
|         &.bg-info, | ||||
|         &.bg-warning, | ||||
|         &.bg-danger, | ||||
|         &.bg-light { | ||||
|             color: $dark-font-color2; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| /* | ||||
| @@ -288,3 +312,119 @@ h2 { | ||||
|     transform: translateY(50px); | ||||
|     opacity: 0; | ||||
| } | ||||
|  | ||||
| .slide-fade-right-enter-active { | ||||
|     transition: all 0.2s $easing-in; | ||||
| } | ||||
|  | ||||
| .slide-fade-right-leave-active { | ||||
|     transition: all 0.2s $easing-in; | ||||
| } | ||||
|  | ||||
| .slide-fade-right-enter-from, | ||||
| .slide-fade-right-leave-to { | ||||
|     transform: translateX(50px); | ||||
|     opacity: 0; | ||||
| } | ||||
|  | ||||
| .monitor-list { | ||||
|     &.scrollbar { | ||||
|         min-height: calc(100vh - 240px); | ||||
|         max-height: calc(100vh - 30px); | ||||
|         overflow-y: auto; | ||||
|         position: sticky; | ||||
|         top: 10px; | ||||
|     } | ||||
|  | ||||
|     .item { | ||||
|         display: block; | ||||
|         text-decoration: none; | ||||
|         padding: 13px 15px 10px 15px; | ||||
|         border-radius: 10px; | ||||
|         transition: all ease-in-out 0.15s; | ||||
|  | ||||
|         &.disabled { | ||||
|             opacity: 0.3; | ||||
|         } | ||||
|  | ||||
|         .info { | ||||
|             white-space: nowrap; | ||||
|             overflow: hidden; | ||||
|             text-overflow: ellipsis; | ||||
|         } | ||||
|  | ||||
|         &:hover { | ||||
|             background-color: $highlight-white; | ||||
|         } | ||||
|  | ||||
|         &.active { | ||||
|             background-color: #cdf8f4; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .alert-success { | ||||
|     color: #122f21; | ||||
|     background-color: $primary; | ||||
|     border-color: $primary; | ||||
| } | ||||
|  | ||||
| .alert-info { | ||||
|     color: #055160; | ||||
|     background-color: #cff4fc; | ||||
|     border-color: #cff4fc; | ||||
| } | ||||
|  | ||||
| .alert-danger { | ||||
|     color: #842029; | ||||
|     background-color: #f8d7da; | ||||
|     border-color: #f8d7da; | ||||
| } | ||||
|  | ||||
| .btn-success { | ||||
|     color: #fff; | ||||
|     background-color: #4caf50; | ||||
|     border-color: #4caf50; | ||||
| } | ||||
|  | ||||
| [contenteditable=true] { | ||||
|     transition: all $easing-in 0.2s; | ||||
|     background-color: rgba(239, 239, 239, 0.7); | ||||
|     border-radius: 8px; | ||||
|  | ||||
|     &:focus { | ||||
|         outline: 0 solid #eee; | ||||
|         background-color: rgba(245, 245, 245, 0.9); | ||||
|     } | ||||
|  | ||||
|     &:hover { | ||||
|         background-color: rgba(239, 239, 239, 0.8); | ||||
|     } | ||||
|  | ||||
|     .dark & { | ||||
|         background-color: rgba(239, 239, 239, 0.2); | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|     &::after { | ||||
|         margin-left: 5px; | ||||
|         content: "🖊️"; | ||||
|         font-size: 13px; | ||||
|         color: #eee; | ||||
|     } | ||||
|     */ | ||||
|  | ||||
| } | ||||
|  | ||||
| .action { | ||||
|     transition: all $easing-in 0.2s; | ||||
|  | ||||
|     &:hover { | ||||
|         cursor: pointer; | ||||
|         transform: scale(1.2); | ||||
|     } | ||||
| } | ||||
|  | ||||
| .vue-image-crop-upload .vicp-wrap { | ||||
|     border-radius: 10px !important; | ||||
| } | ||||
|   | ||||
| @@ -25,6 +25,10 @@ export default { | ||||
|             type: Number, | ||||
|             required: true, | ||||
|         }, | ||||
|         heartbeatList: { | ||||
|             type: Array, | ||||
|             default: null, | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
| @@ -38,8 +42,15 @@ export default { | ||||
|     }, | ||||
|     computed: { | ||||
|  | ||||
|         /** | ||||
|          * If heartbeatList is null, get it from $root.heartbeatList | ||||
|          */ | ||||
|         beatList() { | ||||
|             return this.$root.heartbeatList[this.monitorId] | ||||
|             if (this.heartbeatList === null) { | ||||
|                 return this.$root.heartbeatList[this.monitorId]; | ||||
|             } else { | ||||
|                 return this.heartbeatList; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         shortBeatList() { | ||||
| @@ -118,8 +129,10 @@ export default { | ||||
|         window.removeEventListener("resize", this.resize); | ||||
|     }, | ||||
|     beforeMount() { | ||||
|         if (! (this.monitorId in this.$root.heartbeatList)) { | ||||
|             this.$root.heartbeatList[this.monitorId] = []; | ||||
|         if (this.heartbeatList === null) { | ||||
|             if (! (this.monitorId in this.$root.heartbeatList)) { | ||||
|                 this.$root.heartbeatList[this.monitorId] = []; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
|                 <input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" /> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="list" :class="{ scrollbar: scrollbar }"> | ||||
|         <div class="monitor-list" :class="{ scrollbar: scrollbar }"> | ||||
|             <div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3"> | ||||
|                 {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link> | ||||
|             </div> | ||||
| @@ -163,56 +163,6 @@ export default { | ||||
|     max-width: 15em; | ||||
| } | ||||
|  | ||||
| .list { | ||||
|     &.scrollbar { | ||||
|         min-height: calc(100vh - 240px); | ||||
|         max-height: calc(100vh - 30px); | ||||
|         overflow-y: auto; | ||||
|         position: sticky; | ||||
|         top: 10px; | ||||
|     } | ||||
|  | ||||
|     .item { | ||||
|         display: block; | ||||
|         text-decoration: none; | ||||
|         padding: 13px 15px 10px 15px; | ||||
|         border-radius: 10px; | ||||
|         transition: all ease-in-out 0.15s; | ||||
|  | ||||
|         &.disabled { | ||||
|             opacity: 0.3; | ||||
|         } | ||||
|  | ||||
|         .info { | ||||
|             white-space: nowrap; | ||||
|             overflow: hidden; | ||||
|             text-overflow: ellipsis; | ||||
|         } | ||||
|  | ||||
|         &:hover { | ||||
|             background-color: $highlight-white; | ||||
|         } | ||||
|  | ||||
|         &.active { | ||||
|             background-color: #cdf8f4; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .dark { | ||||
|     .list { | ||||
|         .item { | ||||
|             &:hover { | ||||
|                 background-color: $dark-bg2; | ||||
|             } | ||||
|  | ||||
|             &.active { | ||||
|                 background-color: $dark-bg2; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .monitorItem { | ||||
|     width: 100%; | ||||
| } | ||||
|   | ||||
							
								
								
									
										144
									
								
								src/components/PublicGroupList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/components/PublicGroupList.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| <template> | ||||
|     <!-- Group List --> | ||||
|     <Draggable | ||||
|         v-model="$root.publicGroupList" | ||||
|         :disabled="!editMode" | ||||
|         item-key="id" | ||||
|         :animation="100" | ||||
|     > | ||||
|         <template #item="group"> | ||||
|             <div class="mb-5 "> | ||||
|                 <!-- Group Title --> | ||||
|                 <h2 class="group-title"> | ||||
|                     <font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" /> | ||||
|                     <font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" /> | ||||
|                     <Editable v-model="group.element.name" :contenteditable="editMode" tag="span" /> | ||||
|                 </h2> | ||||
|  | ||||
|                 <div class="shadow-box monitor-list mt-4 position-relative"> | ||||
|                     <div v-if="group.element.monitorList.length === 0" class="text-center no-monitor-msg"> | ||||
|                         {{ $t("No Monitors") }} | ||||
|                     </div> | ||||
|  | ||||
|                     <!-- Monitor List --> | ||||
|                     <!-- animation is not working, no idea why --> | ||||
|                     <Draggable | ||||
|                         v-model="group.element.monitorList" | ||||
|                         class="monitor-list" | ||||
|                         group="same-group" | ||||
|                         :disabled="!editMode" | ||||
|                         :animation="100" | ||||
|                         item-key="id" | ||||
|                     > | ||||
|                         <template #item="monitor"> | ||||
|                             <div class="item"> | ||||
|                                 <div class="row"> | ||||
|                                     <div class="col-9 col-md-8 small-padding"> | ||||
|                                         <div class="info"> | ||||
|                                             <font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" /> | ||||
|                                             <font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" /> | ||||
|  | ||||
|                                             <Uptime :monitor="monitor.element" type="24" :pill="true" /> | ||||
|                                             {{ monitor.element.name }} | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                                     <div :key="$root.userHeartbeatBar" class="col-3 col-md-4"> | ||||
|                                         <HeartbeatBar size="small" :monitor-id="monitor.element.id" /> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </template> | ||||
|                     </Draggable> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </template> | ||||
|     </Draggable> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Draggable from "vuedraggable"; | ||||
| import HeartbeatBar from "./HeartbeatBar.vue"; | ||||
| import Uptime from "./Uptime.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Draggable, | ||||
|         HeartbeatBar, | ||||
|         Uptime, | ||||
|     }, | ||||
|     props: { | ||||
|         editMode: { | ||||
|             type: Boolean, | ||||
|             required: true, | ||||
|         }, | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|  | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         showGroupDrag() { | ||||
|             return (this.$root.publicGroupList.length >= 2); | ||||
|         } | ||||
|     }, | ||||
|     created() { | ||||
|  | ||||
|     }, | ||||
|     methods: { | ||||
|         removeGroup(index) { | ||||
|             this.$root.publicGroupList.splice(index, 1); | ||||
|         }, | ||||
|  | ||||
|         removeMonitor(groupIndex, index) { | ||||
|             this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1); | ||||
|         }, | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../assets/vars"; | ||||
|  | ||||
| .no-monitor-msg { | ||||
|     position: absolute; | ||||
|     width: 100%; | ||||
|     top: 20px; | ||||
|     left: 0; | ||||
| } | ||||
|  | ||||
| .monitor-list { | ||||
|     min-height: 46px; | ||||
| } | ||||
|  | ||||
| .flip-list-move { | ||||
|     transition: transform 0.5s; | ||||
| } | ||||
|  | ||||
| .no-move { | ||||
|     transition: transform 0s; | ||||
| } | ||||
|  | ||||
| .drag { | ||||
|     color: #bbb; | ||||
|     cursor: grab; | ||||
| } | ||||
|  | ||||
| .remove { | ||||
|     color: $danger; | ||||
| } | ||||
|  | ||||
| .group-title { | ||||
|     span { | ||||
|         display: inline-block; | ||||
|         min-width: 15px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .mobile { | ||||
|     .item { | ||||
|         padding: 13px 0 10px 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| </style> | ||||
| @@ -3,6 +3,7 @@ import daDK from "./languages/da-DK"; | ||||
| import deDE from "./languages/de-DE"; | ||||
| import en from "./languages/en"; | ||||
| import esEs from "./languages/es-ES"; | ||||
| import ptBR from "./languages/pt-BR"; | ||||
| import etEE from "./languages/et-EE"; | ||||
| import frFR from "./languages/fr-FR"; | ||||
| import itIT from "./languages/it-IT"; | ||||
| @@ -24,6 +25,7 @@ const languageList = { | ||||
|     "de-DE": deDE, | ||||
|     "nl-NL": nlNL, | ||||
|     "es-ES": esEs, | ||||
|     "pt-BR": ptBR, | ||||
|     "fr-FR": frFR, | ||||
|     "it-IT": itIT, | ||||
|     "ja": ja, | ||||
| @@ -43,6 +45,6 @@ export const i18n = createI18n({ | ||||
|     locale: localStorage.locale || "en", | ||||
|     fallbackLocale: "en", | ||||
|     silentFallbackWarn: true, | ||||
|     silentTranslationWarn: false, | ||||
|     silentTranslationWarn: true, | ||||
|     messages: languageList, | ||||
| }); | ||||
|   | ||||
							
								
								
									
										31
									
								
								src/icon.js
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								src/icon.js
									
									
									
									
									
								
							| @@ -1,4 +1,8 @@ | ||||
| import { library } from "@fortawesome/fontawesome-svg-core"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; | ||||
|  | ||||
| // Add Free Font Awesome Icons | ||||
| // https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free | ||||
| import { | ||||
|     faArrowAltCircleUp, | ||||
|     faCog, | ||||
| @@ -12,13 +16,19 @@ import { | ||||
|     faSearch, | ||||
|     faTachometerAlt, | ||||
|     faTimes, | ||||
|     faTrash | ||||
|     faTimesCircle, | ||||
|     faTrash, | ||||
|     faCheckCircle, | ||||
|     faStream, | ||||
|     faSave, | ||||
|     faExclamationCircle, | ||||
|     faBullhorn, | ||||
|     faArrowsAltV, | ||||
|     faUnlink, | ||||
|     faQuestionCircle, | ||||
|     faImages, faUpload, | ||||
| } from "@fortawesome/free-solid-svg-icons"; | ||||
| //import { fa } from '@fortawesome/free-regular-svg-icons' | ||||
| import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; | ||||
|  | ||||
| // Add Free Font Awesome Icons here | ||||
| // https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free | ||||
| library.add( | ||||
|     faArrowAltCircleUp, | ||||
|     faCog, | ||||
| @@ -32,7 +42,18 @@ library.add( | ||||
|     faSearch, | ||||
|     faTachometerAlt, | ||||
|     faTimes, | ||||
|     faTimesCircle, | ||||
|     faTrash, | ||||
|     faCheckCircle, | ||||
|     faStream, | ||||
|     faSave, | ||||
|     faExclamationCircle, | ||||
|     faBullhorn, | ||||
|     faArrowsAltV, | ||||
|     faUnlink, | ||||
|     faQuestionCircle, | ||||
|     faImages, | ||||
|     faUpload, | ||||
| ); | ||||
|  | ||||
| export { FontAwesomeIcon }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -168,4 +168,14 @@ export default { | ||||
|     "Export Backup": "Export Backup", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -168,6 +168,16 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
|     "telegram": "Telegram", | ||||
|     "webhook": "Webhook", | ||||
|     "smtp": "Email (SMTP)", | ||||
| @@ -185,4 +195,4 @@ export default { | ||||
|     "pushbullet": "Pushbullet", | ||||
|     "line": "Line Messenger", | ||||
|     "mattermost": "Mattermost", | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -168,4 +168,14 @@ export default { | ||||
|     "Search...": "Cerca...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Szukaj...", | ||||
|     "Avg. Ping": "Średni ping", | ||||
|     "Avg. Response": "Średnia odpowiedź", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
							
								
								
									
										182
									
								
								src/languages/pt-BR.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								src/languages/pt-BR.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| export default { | ||||
|     languageName: "Português (Brasileiro)", | ||||
|     checkEverySecond: "Verificar cada {0} segundos.", | ||||
|     retryCheckEverySecond: "Tentar novamente a cada {0} segundos.", | ||||
|     retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada", | ||||
|     ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS", | ||||
|     upsideDownModeDescription: "Inverta o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.", | ||||
|     maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Defina como 0 para desativar redirecionamentos.", | ||||
|     acceptedStatusCodesDescription: "Selecione os códigos de status que são considerados uma resposta bem-sucedida.", | ||||
|     passwordNotMatchMsg: "A senha repetida não corresponde.", | ||||
|     notificationDescription: "Atribua uma notificação ao (s) monitor (es) para que funcione.", | ||||
|     keywordDescription: "Pesquise a palavra-chave em html simples ou resposta JSON e diferencia maiúsculas de minúsculas", | ||||
|     pauseDashboardHome: "Pausar", | ||||
|     deleteMonitorMsg: "Tem certeza de que deseja excluir este monitor?", | ||||
|     deleteNotificationMsg: "Tem certeza de que deseja excluir esta notificação para todos os monitores?", | ||||
|     resoverserverDescription: "Cloudflare é o servidor padrão, você pode alterar o servidor resolvedor a qualquer momento.", | ||||
|     rrtypeDescription: "Selecione o RR-Type que você deseja monitorar", | ||||
|     pauseMonitorMsg: "Tem certeza que deseja fazer uma pausa?", | ||||
|     enableDefaultNotificationDescription: "Para cada novo monitor, esta notificação será habilitada por padrão. Você ainda pode desativar a notificação separadamente para cada monitor.", | ||||
|     clearEventsMsg: "Tem certeza de que deseja excluir todos os eventos deste monitor?", | ||||
|     clearHeartbeatsMsg: "Tem certeza de que deseja excluir todos os heartbeats deste monitor?", | ||||
|     confirmClearStatisticsMsg: "Tem certeza que deseja excluir TODAS as estatísticas?", | ||||
|     importHandleDescription: "Escolha 'Ignorar existente' se quiser ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.", | ||||
|     confirmImportMsg: "Tem certeza que deseja importar o backup? Certifique-se de que selecionou a opção de importação correta.", | ||||
|     twoFAVerifyLabel: "Digite seu token para verificar se 2FA está funcionando", | ||||
|     tokenValidSettingsMsg: "O token é válido! Agora você pode salvar as configurações 2FA.", | ||||
|     confirmEnableTwoFAMsg: "Tem certeza de que deseja habilitar 2FA?", | ||||
|     confirmDisableTwoFAMsg: "Tem certeza de que deseja desativar 2FA?", | ||||
|     Settings: "Configurações", | ||||
|     Dashboard: "Dashboard", | ||||
|     "New Update": "Nova Atualização", | ||||
|     Language: "Linguagem", | ||||
|     Appearance: "Aparência", | ||||
|     Theme: "Tema", | ||||
|     General: "Geral", | ||||
|     Version: "Versão", | ||||
|     "Check Update On GitHub": "Verificar atualização no Github", | ||||
|     List: "Lista", | ||||
|     Add: "Adicionar", | ||||
|     "Add New Monitor": "Adicionar novo monitor", | ||||
|     "Quick Stats": "Estatísticas rápidas", | ||||
|     Up: "On", | ||||
|     Down: "Off", | ||||
|     Pending: "Pendente", | ||||
|     Unknown: "Desconhecido", | ||||
|     Pause: "Pausar", | ||||
|     Name: "Nome", | ||||
|     Status: "Status", | ||||
|     DateTime: "Data hora", | ||||
|     Message: "Mensagem", | ||||
|     "No important events": "Nenhum evento importante", | ||||
|     Resume: "Resumo", | ||||
|     Edit: "Editar", | ||||
|     Delete: "Deletar", | ||||
|     Current: "Atual", | ||||
|     Uptime: "Tempo de atividade", | ||||
|     "Cert Exp.": "Cert Exp.", | ||||
|     days: "dias", | ||||
|     day: "dia", | ||||
|     "-day": "-dia", | ||||
|     hour: "hora", | ||||
|     "-hour": "-hora", | ||||
|     Response: "Resposta", | ||||
|     Ping: "Ping", | ||||
|     "Monitor Type": "Tipo de Monitor", | ||||
|     Keyword: "Palavra-Chave", | ||||
|     "Friendly Name": "Nome Amigável", | ||||
|     URL: "URL", | ||||
|     Hostname: "Hostname", | ||||
|     Port: "Porta", | ||||
|     "Heartbeat Interval": "Intervalo de Heartbeat", | ||||
|     Retries: "Novas tentativas", | ||||
|     "Heartbeat Retry Interval": "Intervalo de repetição de Heartbeat", | ||||
|     Advanced: "Avançado", | ||||
|     "Upside Down Mode": "Modo de cabeça para baixo", | ||||
|     "Max. Redirects": "Redirecionamento Máx.", | ||||
|     "Accepted Status Codes": "Status Code Aceitáveis", | ||||
|     Save: "Salvar", | ||||
|     Notifications: "Notificações", | ||||
|     "Not available, please setup.": "Não disponível, por favor configure.", | ||||
|     "Setup Notification": "Configurar Notificação", | ||||
|     Light: "Claro", | ||||
|     Dark: "Escuro", | ||||
|     Auto: "Auto", | ||||
|     "Theme - Heartbeat Bar": "Tema - Barra de Heartbeat", | ||||
|     Normal: "Normal", | ||||
|     Bottom: "Inferior", | ||||
|     None: "Nenhum", | ||||
|     Timezone: "Fuso horário", | ||||
|     "Search Engine Visibility": "Visibilidade do mecanismo de pesquisa", | ||||
|     "Allow indexing": "Permitir Indexação", | ||||
|     "Discourage search engines from indexing site": "Desencoraje os motores de busca de indexar o site", | ||||
|     "Change Password": "Mudar senha", | ||||
|     "Current Password": "Senha atual", | ||||
|     "New Password": "Nova Senha", | ||||
|     "Repeat New Password": "Repetir Nova Senha", | ||||
|     "Update Password": "Atualizar Senha", | ||||
|     "Disable Auth": "Desativar Autenticação", | ||||
|     "Enable Auth": "Ativar Autenticação", | ||||
|     Logout: "Deslogar", | ||||
|     Leave: "Sair", | ||||
|     "I understand, please disable": "Eu entendo, por favor desative.", | ||||
|     Confirm: "Confirmar", | ||||
|     Yes: "Sim", | ||||
|     No: "Não", | ||||
|     Username: "Usuário", | ||||
|     Password: "Senha", | ||||
|     "Remember me": "Lembre-me", | ||||
|     Login: "Autenticar", | ||||
|     "No Monitors, please": "Nenhum monitor, por favor", | ||||
|     "add one": "adicionar um", | ||||
|     "Notification Type": "Tipo de Notificação", | ||||
|     Email: "Email", | ||||
|     Test: "Testar", | ||||
|     "Certificate Info": "Info. do Certificado ", | ||||
|     "Resolver Server": "Resolver Servidor", | ||||
|     "Resource Record Type": "Tipo de registro de aplicação", | ||||
|     "Last Result": "Último resultado", | ||||
|     "Create your admin account": "Crie sua conta de admin", | ||||
|     "Repeat Password": "Repita a senha", | ||||
|     "Import Backup": "Importar Backup", | ||||
|     "Export Backup": "Exportar Backup", | ||||
|     Export: "Exportar", | ||||
|     Import: "Importar", | ||||
|     respTime: "Tempo de Resp. (ms)", | ||||
|     notAvailableShort: "N/A", | ||||
|     "Default enabled": "Padrão habilitado", | ||||
|     "Apply on all existing monitors": "Aplicar em todos os monitores existentes", | ||||
|     Create: "Criar", | ||||
|     "Clear Data": "Limpar Dados", | ||||
|     Events: "Eventos", | ||||
|     Heartbeats: "Heartbeats", | ||||
|     "Auto Get": "Obter Automático", | ||||
|     backupDescription: "Você pode fazer backup de todos os monitores e todas as notificações em um arquivo JSON.", | ||||
|     backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.", | ||||
|     backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantenha-o com cuidado.", | ||||
|     alertNoFile: "Selecione um arquivo para importar.", | ||||
|     alertWrongFileType: "Selecione um arquivo JSON.", | ||||
|     "Clear all statistics": "Limpar todas as estatísticas", | ||||
|     "Skip existing": "Pular existente", | ||||
|     Overwrite: "Sobrescrever", | ||||
|     Options: "Opções", | ||||
|     "Keep both": "Manter os dois", | ||||
|     "Verify Token": "Verificar Token", | ||||
|     "Setup 2FA": "Configurar 2FA", | ||||
|     "Enable 2FA": "Ativar 2FA", | ||||
|     "Disable 2FA": "Desativar 2FA", | ||||
|     "2FA Settings": "Configurações do 2FA ", | ||||
|     "Two Factor Authentication": "Autenticação e Dois Fatores", | ||||
|     Active: "Ativo", | ||||
|     Inactive: "Inativo", | ||||
|     Token: "Token", | ||||
|     "Show URI": "Mostrar URI", | ||||
|     Tags: "Tag", | ||||
|     "Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...", | ||||
|     "Tag with this name already exist.": "Já existe uma etiqueta com este nome.", | ||||
|     "Tag with this value already exist.": "Já existe uma etiqueta com este valor.", | ||||
|     color: "cor", | ||||
|     "value (optional)": "valor (opcional)", | ||||
|     Gray: "Cinza", | ||||
|     Red: "Vermelho", | ||||
|     Orange: "Laranja", | ||||
|     Green: "Verde", | ||||
|     Blue: "Azul", | ||||
|     Indigo: "Índigo", | ||||
|     Purple: "Roxo", | ||||
|     Pink: "Rosa", | ||||
|     "Search...": "Buscar...", | ||||
|     "Avg. Ping": "Ping Médio.", | ||||
|     "Avg. Response": "Resposta Média. ", | ||||
|     "Status Page": "Página de Status", | ||||
|     "Entry Page": "Página de entrada", | ||||
|     "statusPageNothing": "Nada aqui, por favor, adicione um grupo ou monitor.", | ||||
|     "No Services": "Nenhum Serviço", | ||||
|     "All Systems Operational": "Todos os Serviços Operacionais", | ||||
|     "Partially Degraded Service": "Serviço parcialmente degradado", | ||||
|     "Degraded Service": "Serviço Degradado", | ||||
|     "Add Group": "Adicionar Grupo", | ||||
|     "Add a monitor": "Adicionar um monitor", | ||||
|     "Edit Status Page": "Editar Página de Status", | ||||
|     "Go to Dashboard": "Ir para a dashboard", | ||||
| }; | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Поиск...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -168,4 +168,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -169,4 +169,14 @@ export default { | ||||
|     "Search...": "Search...", | ||||
|     "Avg. Ping": "Avg. Ping", | ||||
|     "Avg. Response": "Avg. Response", | ||||
| } | ||||
|     "Entry Page": "Entry Page", | ||||
|     "statusPageNothing": "Nothing here, please add a group or a monitor.", | ||||
|     "No Services": "No Services", | ||||
|     "All Systems Operational": "All Systems Operational", | ||||
|     "Partially Degraded Service": "Partially Degraded Service", | ||||
|     "Degraded Service": "Degraded Service", | ||||
|     "Add Group": "Add Group", | ||||
|     "Add a monitor": "Add a monitor", | ||||
|     "Edit Status Page": "Edit Status Page", | ||||
|     "Go to Dashboard": "Go to Dashboard", | ||||
| }; | ||||
|   | ||||
| @@ -18,7 +18,12 @@ | ||||
|             </a> | ||||
|  | ||||
|             <ul class="nav nav-pills"> | ||||
|                 <li class="nav-item"> | ||||
|                 <li class="nav-item me-2"> | ||||
|                     <a href="/status-page" class="nav-link status-page"> | ||||
|                         <font-awesome-icon icon="stream" /> {{ $t("Status Page") }} | ||||
|                     </a> | ||||
|                 </li> | ||||
|                 <li class="nav-item me-2"> | ||||
|                     <router-link to="/dashboard" class="nav-link"> | ||||
|                         <font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }} | ||||
|                     </router-link> | ||||
| @@ -81,7 +86,7 @@ export default { | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|         return {} | ||||
|         return {}; | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
| @@ -105,29 +110,29 @@ export default { | ||||
|     }, | ||||
|  | ||||
|     watch: { | ||||
|         $route(to, from) { | ||||
|             this.init(); | ||||
|         }, | ||||
|  | ||||
|     }, | ||||
|  | ||||
|     mounted() { | ||||
|         this.init(); | ||||
|  | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|         init() { | ||||
|             if (this.$route.name === "root") { | ||||
|                 this.$router.push("/dashboard") | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|     }, | ||||
|  | ||||
| } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../assets/vars.scss"; | ||||
|  | ||||
| .nav-link { | ||||
|     &.status-page { | ||||
|         background-color: rgba(255, 255, 255, 0.1); | ||||
|     } | ||||
| } | ||||
|  | ||||
| .bottom-nav { | ||||
|     z-index: 1000; | ||||
|     position: fixed; | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/main.js
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/main.js
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| import "bootstrap"; | ||||
| import { createApp, h } from "vue"; | ||||
| import Toast from "vue-toastification"; | ||||
| import contenteditable from "vue-contenteditable" | ||||
| import "vue-toastification/dist/index.css"; | ||||
| import App from "./App.vue"; | ||||
| import "./assets/app.scss"; | ||||
| @@ -10,6 +11,8 @@ import datetime from "./mixins/datetime"; | ||||
| import mobile from "./mixins/mobile"; | ||||
| import socket from "./mixins/socket"; | ||||
| import theme from "./mixins/theme"; | ||||
| import publicMixin from "./mixins/public"; | ||||
|  | ||||
| import { router } from "./router"; | ||||
| import { appName } from "./util.ts"; | ||||
|  | ||||
| @@ -18,7 +21,8 @@ const app = createApp({ | ||||
|         socket, | ||||
|         theme, | ||||
|         mobile, | ||||
|         datetime | ||||
|         datetime, | ||||
|         publicMixin, | ||||
|     ], | ||||
|     data() { | ||||
|         return { | ||||
| @@ -36,7 +40,7 @@ const options = { | ||||
| }; | ||||
|  | ||||
| app.use(Toast, options); | ||||
| app.component("Editable", contenteditable); | ||||
| app.component("FontAwesomeIcon", FontAwesomeIcon); | ||||
|  | ||||
| app.component("FontAwesomeIcon", FontAwesomeIcon) | ||||
|  | ||||
| app.mount("#app") | ||||
| app.mount("#app"); | ||||
|   | ||||
| @@ -3,23 +3,34 @@ export default { | ||||
|     data() { | ||||
|         return { | ||||
|             windowWidth: window.innerWidth, | ||||
|         } | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     created() { | ||||
|         window.addEventListener("resize", this.onResize); | ||||
|         this.updateBody(); | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|         onResize() { | ||||
|             this.windowWidth = window.innerWidth; | ||||
|             this.updateBody(); | ||||
|         }, | ||||
|  | ||||
|         updateBody() { | ||||
|             if (this.isMobile) { | ||||
|                 document.body.classList.add("mobile"); | ||||
|             } else { | ||||
|                 document.body.classList.remove("mobile"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|         isMobile() { | ||||
|             return this.windowWidth <= 767.98; | ||||
|         }, | ||||
|     } | ||||
|     }, | ||||
|  | ||||
| } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										40
									
								
								src/mixins/public.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/mixins/public.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import axios from "axios"; | ||||
|  | ||||
| const env = process.env.NODE_ENV || "production"; | ||||
|  | ||||
| // change the axios base url for development | ||||
| if (env === "development" || localStorage.dev === "dev") { | ||||
|     axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001"; | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     data() { | ||||
|         return { | ||||
|             publicGroupList: [], | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         publicMonitorList() { | ||||
|             let result = {}; | ||||
|  | ||||
|             for (let group of this.publicGroupList) { | ||||
|                 for (let monitor of group.monitorList) { | ||||
|                     result[monitor.id] = monitor; | ||||
|                 } | ||||
|             } | ||||
|             return result; | ||||
|         }, | ||||
|  | ||||
|         publicLastHeartbeatList() { | ||||
|             let result = {}; | ||||
|  | ||||
|             for (let monitorID in this.publicMonitorList) { | ||||
|                 if (this.lastHeartbeatList[monitorID]) { | ||||
|                     result[monitorID] = this.lastHeartbeatList[monitorID]; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         }, | ||||
|     } | ||||
| }; | ||||
| @@ -1,9 +1,14 @@ | ||||
| import { io } from "socket.io-client"; | ||||
| import { useToast } from "vue-toastification"; | ||||
| const toast = useToast() | ||||
| const toast = useToast(); | ||||
|  | ||||
| let socket; | ||||
|  | ||||
| const noSocketIOPages = [ | ||||
|     "/status-page", | ||||
|     "/" | ||||
| ]; | ||||
|  | ||||
| export default { | ||||
|  | ||||
|     data() { | ||||
| @@ -14,6 +19,7 @@ export default { | ||||
|                 firstConnect: true, | ||||
|                 connected: false, | ||||
|                 connectCount: 0, | ||||
|                 initedSocketIO: false, | ||||
|             }, | ||||
|             remember: (localStorage.remember !== "0"), | ||||
|             allowLoginDialog: false,        // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed. | ||||
| @@ -26,167 +32,186 @@ export default { | ||||
|             certInfoList: {}, | ||||
|             notificationList: [], | ||||
|             connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...", | ||||
|         } | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     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 = protocol + location.hostname + ":3001"; | ||||
|         } else { | ||||
|             wsHost = protocol + location.host; | ||||
|         } | ||||
|  | ||||
|         socket = io(wsHost, { | ||||
|             transports: ["websocket"], | ||||
|         }); | ||||
|  | ||||
|         socket.on("info", (info) => { | ||||
|             this.info = info; | ||||
|         }); | ||||
|  | ||||
|         socket.on("setup", (monitorID, data) => { | ||||
|             this.$router.push("/setup") | ||||
|         }); | ||||
|  | ||||
|         socket.on("autoLogin", (monitorID, data) => { | ||||
|             this.loggedIn = true; | ||||
|             this.storage().token = "autoLogin"; | ||||
|             this.allowLoginDialog = false; | ||||
|         }); | ||||
|  | ||||
|         socket.on("monitorList", (data) => { | ||||
|             // Add Helper function | ||||
|             Object.entries(data).forEach(([monitorID, monitor]) => { | ||||
|                 monitor.getUrl = () => { | ||||
|                     try { | ||||
|                         return new URL(monitor.url); | ||||
|                     } catch (_) { | ||||
|                         return null; | ||||
|                     } | ||||
|                 }; | ||||
|             }); | ||||
|             this.monitorList = data; | ||||
|         }); | ||||
|  | ||||
|         socket.on("notificationList", (data) => { | ||||
|             this.notificationList = data; | ||||
|         }); | ||||
|  | ||||
|         socket.on("heartbeat", (data) => { | ||||
|             if (! (data.monitorID in this.heartbeatList)) { | ||||
|                 this.heartbeatList[data.monitorID] = []; | ||||
|             } | ||||
|  | ||||
|             this.heartbeatList[data.monitorID].push(data) | ||||
|  | ||||
|             // Add to important list if it is important | ||||
|             // Also toast | ||||
|             if (data.important) { | ||||
|  | ||||
|                 if (data.status === 0) { | ||||
|                     toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, { | ||||
|                         timeout: false, | ||||
|                     }); | ||||
|                 } else if (data.status === 1) { | ||||
|                     toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, { | ||||
|                         timeout: 20000, | ||||
|                     }); | ||||
|                 } else { | ||||
|                     toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`); | ||||
|                 } | ||||
|  | ||||
|                 if (! (data.monitorID in this.importantHeartbeatList)) { | ||||
|                     this.importantHeartbeatList[data.monitorID] = []; | ||||
|                 } | ||||
|  | ||||
|                 this.importantHeartbeatList[data.monitorID].unshift(data) | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         socket.on("heartbeatList", (monitorID, data, overwrite = false) => { | ||||
|             if (! (monitorID in this.heartbeatList) || overwrite) { | ||||
|                 this.heartbeatList[monitorID] = data; | ||||
|             } else { | ||||
|                 this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID]) | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         socket.on("avgPing", (monitorID, data) => { | ||||
|             this.avgPingList[monitorID] = data | ||||
|         }); | ||||
|  | ||||
|         socket.on("uptime", (monitorID, type, data) => { | ||||
|             this.uptimeList[`${monitorID}_${type}`] = data | ||||
|         }); | ||||
|  | ||||
|         socket.on("certInfo", (monitorID, data) => { | ||||
|             this.certInfoList[monitorID] = JSON.parse(data) | ||||
|         }); | ||||
|  | ||||
|         socket.on("importantHeartbeatList", (monitorID, data, overwrite) => { | ||||
|             if (! (monitorID in this.importantHeartbeatList) || overwrite) { | ||||
|                 this.importantHeartbeatList[monitorID] = data; | ||||
|             } else { | ||||
|                 this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID]) | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         socket.on("connect_error", (err) => { | ||||
|             console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`); | ||||
|             this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`; | ||||
|             this.socket.connected = false; | ||||
|             this.socket.firstConnect = false; | ||||
|         }); | ||||
|  | ||||
|         socket.on("disconnect", () => { | ||||
|             console.log("disconnect") | ||||
|             this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting..."; | ||||
|             this.socket.connected = false; | ||||
|         }); | ||||
|  | ||||
|         socket.on("connect", () => { | ||||
|             console.log("connect") | ||||
|             this.socket.connectCount++; | ||||
|             this.socket.connected = true; | ||||
|  | ||||
|             // Reset Heartbeat list if it is re-connect | ||||
|             if (this.socket.connectCount >= 2) { | ||||
|                 this.clearData() | ||||
|             } | ||||
|  | ||||
|             let token = this.storage().token; | ||||
|  | ||||
|             if (token) { | ||||
|                 if (token !== "autoLogin") { | ||||
|                     this.loginByToken(token) | ||||
|                 } else { | ||||
|  | ||||
|                     // Timeout if it is not actually auto login | ||||
|                     setTimeout(() => { | ||||
|                         if (! this.loggedIn) { | ||||
|                             this.allowLoginDialog = true; | ||||
|                             this.$root.storage().removeItem("token"); | ||||
|                         } | ||||
|                     }, 5000); | ||||
|  | ||||
|                 } | ||||
|             } else { | ||||
|                 this.allowLoginDialog = true; | ||||
|             } | ||||
|  | ||||
|             this.socket.firstConnect = false; | ||||
|         }); | ||||
|  | ||||
|         this.initSocketIO(); | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|  | ||||
|         initSocketIO(bypass = false) { | ||||
|             // No need to re-init | ||||
|             if (this.socket.initedSocketIO) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // No need to connect to the socket.io for status page | ||||
|             if (! bypass && noSocketIOPages.includes(location.pathname)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.socket.initedSocketIO = true; | ||||
|  | ||||
|             let protocol = (location.protocol === "https:") ? "wss://" : "ws://"; | ||||
|  | ||||
|             let wsHost; | ||||
|             const env = process.env.NODE_ENV || "production"; | ||||
|             if (env === "development" || localStorage.dev === "dev") { | ||||
|                 wsHost = protocol + location.hostname + ":3001"; | ||||
|             } else { | ||||
|                 wsHost = protocol + location.host; | ||||
|             } | ||||
|  | ||||
|             socket = io(wsHost, { | ||||
|                 transports: ["websocket"], | ||||
|             }); | ||||
|  | ||||
|             socket.on("info", (info) => { | ||||
|                 this.info = info; | ||||
|             }); | ||||
|  | ||||
|             socket.on("setup", (monitorID, data) => { | ||||
|                 this.$router.push("/setup"); | ||||
|             }); | ||||
|  | ||||
|             socket.on("autoLogin", (monitorID, data) => { | ||||
|                 this.loggedIn = true; | ||||
|                 this.storage().token = "autoLogin"; | ||||
|                 this.allowLoginDialog = false; | ||||
|             }); | ||||
|  | ||||
|             socket.on("monitorList", (data) => { | ||||
|                 // Add Helper function | ||||
|                 Object.entries(data).forEach(([monitorID, monitor]) => { | ||||
|                     monitor.getUrl = () => { | ||||
|                         try { | ||||
|                             return new URL(monitor.url); | ||||
|                         } catch (_) { | ||||
|                             return null; | ||||
|                         } | ||||
|                     }; | ||||
|                 }); | ||||
|                 this.monitorList = data; | ||||
|             }); | ||||
|  | ||||
|             socket.on("notificationList", (data) => { | ||||
|                 this.notificationList = data; | ||||
|             }); | ||||
|  | ||||
|             socket.on("heartbeat", (data) => { | ||||
|                 if (! (data.monitorID in this.heartbeatList)) { | ||||
|                     this.heartbeatList[data.monitorID] = []; | ||||
|                 } | ||||
|  | ||||
|                 this.heartbeatList[data.monitorID].push(data); | ||||
|  | ||||
|                 if (this.heartbeatList[data.monitorID].length >= 150) { | ||||
|                     this.heartbeatList[data.monitorID].shift(); | ||||
|                 } | ||||
|  | ||||
|                 // Add to important list if it is important | ||||
|                 // Also toast | ||||
|                 if (data.important) { | ||||
|  | ||||
|                     if (data.status === 0) { | ||||
|                         toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, { | ||||
|                             timeout: false, | ||||
|                         }); | ||||
|                     } else if (data.status === 1) { | ||||
|                         toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, { | ||||
|                             timeout: 20000, | ||||
|                         }); | ||||
|                     } else { | ||||
|                         toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`); | ||||
|                     } | ||||
|  | ||||
|                     if (! (data.monitorID in this.importantHeartbeatList)) { | ||||
|                         this.importantHeartbeatList[data.monitorID] = []; | ||||
|                     } | ||||
|  | ||||
|                     this.importantHeartbeatList[data.monitorID].unshift(data); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             socket.on("heartbeatList", (monitorID, data, overwrite = false) => { | ||||
|                 if (! (monitorID in this.heartbeatList) || overwrite) { | ||||
|                     this.heartbeatList[monitorID] = data; | ||||
|                 } else { | ||||
|                     this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID]); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             socket.on("avgPing", (monitorID, data) => { | ||||
|                 this.avgPingList[monitorID] = data; | ||||
|             }); | ||||
|  | ||||
|             socket.on("uptime", (monitorID, type, data) => { | ||||
|                 this.uptimeList[`${monitorID}_${type}`] = data; | ||||
|             }); | ||||
|  | ||||
|             socket.on("certInfo", (monitorID, data) => { | ||||
|                 this.certInfoList[monitorID] = JSON.parse(data); | ||||
|             }); | ||||
|  | ||||
|             socket.on("importantHeartbeatList", (monitorID, data, overwrite) => { | ||||
|                 if (! (monitorID in this.importantHeartbeatList) || overwrite) { | ||||
|                     this.importantHeartbeatList[monitorID] = data; | ||||
|                 } else { | ||||
|                     this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID]); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             socket.on("connect_error", (err) => { | ||||
|                 console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`); | ||||
|                 this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`; | ||||
|                 this.socket.connected = false; | ||||
|                 this.socket.firstConnect = false; | ||||
|             }); | ||||
|  | ||||
|             socket.on("disconnect", () => { | ||||
|                 console.log("disconnect"); | ||||
|                 this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting..."; | ||||
|                 this.socket.connected = false; | ||||
|             }); | ||||
|  | ||||
|             socket.on("connect", () => { | ||||
|                 console.log("connect"); | ||||
|                 this.socket.connectCount++; | ||||
|                 this.socket.connected = true; | ||||
|  | ||||
|                 // Reset Heartbeat list if it is re-connect | ||||
|                 if (this.socket.connectCount >= 2) { | ||||
|                     this.clearData(); | ||||
|                 } | ||||
|  | ||||
|                 let token = this.storage().token; | ||||
|  | ||||
|                 if (token) { | ||||
|                     if (token !== "autoLogin") { | ||||
|                         this.loginByToken(token); | ||||
|                     } else { | ||||
|  | ||||
|                         // Timeout if it is not actually auto login | ||||
|                         setTimeout(() => { | ||||
|                             if (! this.loggedIn) { | ||||
|                                 this.allowLoginDialog = true; | ||||
|                                 this.$root.storage().removeItem("token"); | ||||
|                             } | ||||
|                         }, 5000); | ||||
|  | ||||
|                     } | ||||
|                 } else { | ||||
|                     this.allowLoginDialog = true; | ||||
|                 } | ||||
|  | ||||
|                 this.socket.firstConnect = false; | ||||
|             }); | ||||
|  | ||||
|         }, | ||||
|  | ||||
|         storage() { | ||||
|             return (this.remember) ? localStorage : sessionStorage; | ||||
|         }, | ||||
| @@ -210,7 +235,7 @@ export default { | ||||
|                 token, | ||||
|             }, (res) => { | ||||
|                 if (res.tokenRequired) { | ||||
|                     callback(res) | ||||
|                     callback(res); | ||||
|                 } | ||||
|  | ||||
|                 if (res.ok) { | ||||
| @@ -219,11 +244,11 @@ export default { | ||||
|                     this.loggedIn = true; | ||||
|  | ||||
|                     // Trigger Chrome Save Password | ||||
|                     history.pushState({}, "") | ||||
|                     history.pushState({}, ""); | ||||
|                 } | ||||
|  | ||||
|                 callback(res) | ||||
|             }) | ||||
|                 callback(res); | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         loginByToken(token) { | ||||
| @@ -231,11 +256,11 @@ export default { | ||||
|                 this.allowLoginDialog = true; | ||||
|  | ||||
|                 if (! res.ok) { | ||||
|                     this.logout() | ||||
|                     this.logout(); | ||||
|                 } else { | ||||
|                     this.loggedIn = true; | ||||
|                 } | ||||
|             }) | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         logout() { | ||||
| @@ -243,68 +268,68 @@ export default { | ||||
|             this.socket.token = null; | ||||
|             this.loggedIn = false; | ||||
|  | ||||
|             this.clearData() | ||||
|             this.clearData(); | ||||
|         }, | ||||
|  | ||||
|         prepare2FA(callback) { | ||||
|             socket.emit("prepare2FA", callback) | ||||
|             socket.emit("prepare2FA", callback); | ||||
|         }, | ||||
|  | ||||
|         save2FA(secret, callback) { | ||||
|             socket.emit("save2FA", callback) | ||||
|             socket.emit("save2FA", callback); | ||||
|         }, | ||||
|  | ||||
|         disable2FA(callback) { | ||||
|             socket.emit("disable2FA", callback) | ||||
|             socket.emit("disable2FA", callback); | ||||
|         }, | ||||
|  | ||||
|         verifyToken(token, callback) { | ||||
|             socket.emit("verifyToken", token, callback) | ||||
|             socket.emit("verifyToken", token, callback); | ||||
|         }, | ||||
|  | ||||
|         twoFAStatus(callback) { | ||||
|             socket.emit("twoFAStatus", callback) | ||||
|             socket.emit("twoFAStatus", callback); | ||||
|         }, | ||||
|  | ||||
|         getMonitorList(callback) { | ||||
|             socket.emit("getMonitorList", callback) | ||||
|             socket.emit("getMonitorList", callback); | ||||
|         }, | ||||
|  | ||||
|         add(monitor, callback) { | ||||
|             socket.emit("add", monitor, callback) | ||||
|             socket.emit("add", monitor, callback); | ||||
|         }, | ||||
|  | ||||
|         deleteMonitor(monitorID, callback) { | ||||
|             socket.emit("deleteMonitor", monitorID, callback) | ||||
|             socket.emit("deleteMonitor", monitorID, callback); | ||||
|         }, | ||||
|  | ||||
|         clearData() { | ||||
|             console.log("reset heartbeat list") | ||||
|             this.heartbeatList = {} | ||||
|             this.importantHeartbeatList = {} | ||||
|             console.log("reset heartbeat list"); | ||||
|             this.heartbeatList = {}; | ||||
|             this.importantHeartbeatList = {}; | ||||
|         }, | ||||
|  | ||||
|         uploadBackup(uploadedJSON, importHandle, callback) { | ||||
|             socket.emit("uploadBackup", uploadedJSON, importHandle, callback) | ||||
|             socket.emit("uploadBackup", uploadedJSON, importHandle, callback); | ||||
|         }, | ||||
|  | ||||
|         clearEvents(monitorID, callback) { | ||||
|             socket.emit("clearEvents", monitorID, callback) | ||||
|             socket.emit("clearEvents", monitorID, callback); | ||||
|         }, | ||||
|  | ||||
|         clearHeartbeats(monitorID, callback) { | ||||
|             socket.emit("clearHeartbeats", monitorID, callback) | ||||
|             socket.emit("clearHeartbeats", monitorID, callback); | ||||
|         }, | ||||
|  | ||||
|         clearStatistics(callback) { | ||||
|             socket.emit("clearStatistics", callback) | ||||
|             socket.emit("clearStatistics", callback); | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|  | ||||
|         lastHeartbeatList() { | ||||
|             let result = {} | ||||
|             let result = {}; | ||||
|  | ||||
|             for (let monitorID in this.heartbeatList) { | ||||
|                 let index = this.heartbeatList[monitorID].length - 1; | ||||
| @@ -315,15 +340,15 @@ export default { | ||||
|         }, | ||||
|  | ||||
|         statusList() { | ||||
|             let result = {} | ||||
|             let result = {}; | ||||
|  | ||||
|             let unknown = { | ||||
|                 text: "Unknown", | ||||
|                 color: "secondary", | ||||
|             } | ||||
|             }; | ||||
|  | ||||
|             for (let monitorID in this.lastHeartbeatList) { | ||||
|                 let lastHeartBeat = this.lastHeartbeatList[monitorID] | ||||
|                 let lastHeartBeat = this.lastHeartbeatList[monitorID]; | ||||
|  | ||||
|                 if (! lastHeartBeat) { | ||||
|                     result[monitorID] = unknown; | ||||
| @@ -356,14 +381,22 @@ export default { | ||||
|         // Reload the SPA if the server version is changed. | ||||
|         "info.version"(to, from) { | ||||
|             if (from && from !== to) { | ||||
|                 window.location.reload() | ||||
|                 window.location.reload(); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         remember() { | ||||
|             localStorage.remember = (this.remember) ? "1" : "0" | ||||
|             localStorage.remember = (this.remember) ? "1" : "0"; | ||||
|         }, | ||||
|  | ||||
|         // Reconnect the socket io, if status-page to dashboard | ||||
|         "$route.fullPath"(newValue, oldValue) { | ||||
|             if (noSocketIOPages.includes(newValue)) { | ||||
|                 return; | ||||
|             } | ||||
|             this.initSocketIO(); | ||||
|         }, | ||||
|  | ||||
|     }, | ||||
|  | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -5,6 +5,8 @@ export default { | ||||
|             system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light", | ||||
|             userTheme: localStorage.theme, | ||||
|             userHeartbeatBar: localStorage.heartbeatBarTheme, | ||||
|             statusPageTheme: "light", | ||||
|             path: "", | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
| @@ -25,14 +27,28 @@ export default { | ||||
|  | ||||
|     computed: { | ||||
|         theme() { | ||||
|             if (this.userTheme === "auto") { | ||||
|                 return this.system; | ||||
|  | ||||
|             // Entry no need dark | ||||
|             if (this.path === "") { | ||||
|                 return "light"; | ||||
|             } | ||||
|  | ||||
|             if (this.path === "/status-page") { | ||||
|                 return this.statusPageTheme; | ||||
|             } else { | ||||
|                 if (this.userTheme === "auto") { | ||||
|                     return this.system; | ||||
|                 } | ||||
|                 return this.userTheme; | ||||
|             } | ||||
|             return this.userTheme; | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     watch: { | ||||
|         "$route.fullPath"(path) { | ||||
|             this.path = path; | ||||
|         }, | ||||
|  | ||||
|         userTheme(to, from) { | ||||
|             localStorage.theme = to; | ||||
|         }, | ||||
| @@ -62,5 +78,5 @@ export default { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -50,7 +50,7 @@ | ||||
|                             <!-- TCP Port / Ping / DNS only --> | ||||
|                             <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' " class="my-3"> | ||||
|                                 <label for="hostname" class="form-label">{{ $t("Hostname") }}</label> | ||||
|                                 <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="ipRegexPattern || hostnameRegexPattern" required> | ||||
|                                 <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required> | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- For TCP Port Type --> | ||||
| @@ -233,11 +233,9 @@ export default { | ||||
|             dnsresolvetypeOptions: [], | ||||
|  | ||||
|             // Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/ | ||||
|             // eslint-disable-next-line | ||||
|             ipRegexPattern: "((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))", | ||||
|             ipRegexPattern: "((^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))", | ||||
|             // Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address | ||||
|             // eslint-disable-next-line | ||||
|             hostnameRegexPattern: "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" | ||||
|             hostnameRegexPattern: "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$" | ||||
|         } | ||||
|     }, | ||||
|  | ||||
| @@ -333,6 +331,11 @@ export default { | ||||
|                 this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { | ||||
|                     if (res.ok) { | ||||
|                         this.monitor = res.monitor; | ||||
|  | ||||
|                         // Handling for monitors that are created before 1.7.0 | ||||
|                         if (this.monitor.retryInterval === 0) { | ||||
|                             this.monitor.retryInterval = this.monitor.interval; | ||||
|                         } | ||||
|                     } else { | ||||
|                         toast.error(res.msg) | ||||
|                     } | ||||
|   | ||||
							
								
								
									
										20
									
								
								src/pages/Entry.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/pages/Entry.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| <template> | ||||
|     <div></div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import axios from "axios"; | ||||
|  | ||||
| export default { | ||||
|     async mounted() { | ||||
|         let entryPage = (await axios.get("/api/entry-page")).data; | ||||
|  | ||||
|         if (entryPage === "statusPage") { | ||||
|             this.$router.push("/status-page"); | ||||
|         } else { | ||||
|             this.$router.push("/dashboard"); | ||||
|         } | ||||
|     }, | ||||
|  | ||||
| }; | ||||
| </script> | ||||
| @@ -83,6 +83,24 @@ | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <div class="mb-3"> | ||||
|                                 <label class="form-label">{{ $t("Entry Page") }}</label> | ||||
|  | ||||
|                                 <div class="form-check"> | ||||
|                                     <input id="entryPageYes" v-model="settings.entryPage" class="form-check-input" type="radio" name="statusPage" value="dashboard" required> | ||||
|                                     <label class="form-check-label" for="entryPageYes"> | ||||
|                                         {{ $t("Dashboard") }} | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <div class="form-check"> | ||||
|                                     <input id="entryPageNo" v-model="settings.entryPage" class="form-check-input" type="radio" name="statusPage" value="statusPage" required> | ||||
|                                     <label class="form-check-label" for="entryPageNo"> | ||||
|                                         {{ $t("Status Page") }} | ||||
|                                     </label> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <div> | ||||
|                                 <button class="btn btn-primary" type="submit"> | ||||
|                                     {{ $t("Save") }} | ||||
| @@ -207,18 +225,15 @@ | ||||
|                         <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()"> | ||||
|                             {{ $t("Setup Notification") }} | ||||
|                         </button> | ||||
|  | ||||
|                         <h2 class="mt-5">Info</h2> | ||||
|  | ||||
|                         {{ $t("Version") }}: {{ $root.info.version }} <br /> | ||||
|                         <a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <footer> | ||||
|                 <div class="container-fluid"> | ||||
|                     Uptime Kuma - | ||||
|                     {{ $t("Version") }}: {{ $root.info.version }} - | ||||
|                     <a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a> | ||||
|                 </div> | ||||
|             </footer> | ||||
|  | ||||
|             <NotificationDialog ref="notificationDialog" /> | ||||
|             <TwoFADialog ref="TwoFADialog" /> | ||||
|  | ||||
| @@ -229,6 +244,12 @@ | ||||
|                     <p>Por favor usar con cuidado.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'pt-BR' "> | ||||
|                     <p>Você tem certeza que deseja <strong>desativar a autenticação</strong>?</p> | ||||
|                     <p>Isso é para <strong>alguém que tem autenticação de terceiros</strong> na frente do 'UpTime Kuma' como o Cloudflare Access.</p> | ||||
|                     <p>Por favor, utilize isso com cautela.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'zh-HK' "> | ||||
|                     <p>你是否確認<strong>取消登入認証</strong>?</p> | ||||
|                     <p>這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。</p> | ||||
| @@ -261,8 +282,8 @@ | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'tr-TR' "> | ||||
|                     <p><strong>Şifreli girişi devre dışı bırakmak istediğinizden</strong>emin misiniz?</p> | ||||
|                      <p>Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.</p> | ||||
|                      <p>Lütfen dikkatli kullanın.</p> | ||||
|                     <p>Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.</p> | ||||
|                     <p>Lütfen dikkatli kullanın.</p> | ||||
|                 </template> | ||||
|  | ||||
|                 <template v-else-if="$i18n.locale === 'ko-KR' "> | ||||
| @@ -316,16 +337,16 @@ | ||||
| <script> | ||||
| import Confirm from "../components/Confirm.vue"; | ||||
| import dayjs from "dayjs"; | ||||
| import utc from "dayjs/plugin/utc" | ||||
| import timezone from "dayjs/plugin/timezone" | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import timezone from "dayjs/plugin/timezone"; | ||||
| import NotificationDialog from "../components/NotificationDialog.vue"; | ||||
| import TwoFADialog from "../components/TwoFADialog.vue"; | ||||
| dayjs.extend(utc) | ||||
| dayjs.extend(timezone) | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
|  | ||||
| import { timezoneList } from "../util-frontend"; | ||||
| import { useToast } from "vue-toastification" | ||||
| const toast = useToast() | ||||
| import { useToast } from "vue-toastification"; | ||||
| const toast = useToast(); | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
| @@ -351,7 +372,7 @@ export default { | ||||
|             importAlert: null, | ||||
|             importHandle: "skip", | ||||
|             processing: false, | ||||
|         } | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         "password.repeatNewPassword"() { | ||||
| @@ -379,13 +400,13 @@ export default { | ||||
|                 this.invalidPassword = true; | ||||
|             } else { | ||||
|                 this.$root.getSocket().emit("changePassword", this.password, (res) => { | ||||
|                     this.$root.toastRes(res) | ||||
|                     this.$root.toastRes(res); | ||||
|                     if (res.ok) { | ||||
|                         this.password.currentPassword = "" | ||||
|                         this.password.newPassword = "" | ||||
|                         this.password.repeatNewPassword = "" | ||||
|                         this.password.currentPassword = ""; | ||||
|                         this.password.newPassword = ""; | ||||
|                         this.password.repeatNewPassword = ""; | ||||
|                     } | ||||
|                 }) | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
| @@ -397,15 +418,19 @@ export default { | ||||
|                     this.settings.searchEngineIndex = false; | ||||
|                 } | ||||
|  | ||||
|                 if (this.settings.entryPage === undefined) { | ||||
|                     this.settings.entryPage = "dashboard"; | ||||
|                 } | ||||
|  | ||||
|                 this.loaded = true; | ||||
|             }) | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         saveSettings() { | ||||
|             this.$root.getSocket().emit("setSettings", this.settings, (res) => { | ||||
|                 this.$root.toastRes(res); | ||||
|                 this.loadSettings(); | ||||
|             }) | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         confirmDisableAuth() { | ||||
| @@ -439,7 +464,7 @@ export default { | ||||
|                 version: this.$root.info.version, | ||||
|                 notificationList: this.$root.notificationList, | ||||
|                 monitorList: monitorList, | ||||
|             } | ||||
|             }; | ||||
|             exportData = JSON.stringify(exportData, null, 4); | ||||
|             let downloadItem = document.createElement("a"); | ||||
|             downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURIComponent(exportData)); | ||||
| @@ -453,12 +478,12 @@ export default { | ||||
|  | ||||
|             if (uploadItem.length <= 0) { | ||||
|                 this.processing = false; | ||||
|                 return this.importAlert = this.$t("alertNoFile") | ||||
|                 return this.importAlert = this.$t("alertNoFile"); | ||||
|             } | ||||
|  | ||||
|             if (uploadItem.item(0).type !== "application/json") { | ||||
|                 this.processing = false; | ||||
|                 return this.importAlert = this.$t("alertWrongFileType") | ||||
|                 return this.importAlert = this.$t("alertWrongFileType"); | ||||
|             } | ||||
|  | ||||
|             let fileReader = new FileReader(); | ||||
| @@ -473,8 +498,8 @@ export default { | ||||
|                     } else { | ||||
|                         toast.error(res.msg); | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
|                 }); | ||||
|             }; | ||||
|         }, | ||||
|  | ||||
|         clearStatistics() { | ||||
| @@ -484,10 +509,10 @@ export default { | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }) | ||||
|             }); | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   | ||||
							
								
								
									
										653
									
								
								src/pages/StatusPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										653
									
								
								src/pages/StatusPage.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,653 @@ | ||||
| <template> | ||||
|     <div v-if="loadedTheme" class="container mt-3"> | ||||
|         <!-- Logo & Title --> | ||||
|         <h1 class="mb-4"> | ||||
|             <!-- Logo --> | ||||
|             <span class="logo-wrapper" @click="showImageCropUploadMethod"> | ||||
|                 <img :src="logoURL" alt class="logo me-2" :class="logoClass" /> | ||||
|                 <font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" /> | ||||
|             </span> | ||||
|  | ||||
|             <!-- Uploader --> | ||||
|             <!--    url="/api/status-page/upload-logo" --> | ||||
|             <ImageCropUpload v-model="showImageCropUpload" | ||||
|                              field="img" | ||||
|                              :width="128" | ||||
|                              :height="128" | ||||
|                              :langType="$i18n.locale" | ||||
|                              img-format="png" | ||||
|                              :noCircle="true" | ||||
|                              :noSquare="false" | ||||
|                              @crop-success="cropSuccess" | ||||
|             /> | ||||
|  | ||||
|             <!-- Title --> | ||||
|             <Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" /> | ||||
|         </h1> | ||||
|  | ||||
|         <!-- Admin functions --> | ||||
|         <div v-if="hasToken" class="mb-4"> | ||||
|             <div v-if="!enableEditMode"> | ||||
|                 <button class="btn btn-info me-2" @click="edit"> | ||||
|                     <font-awesome-icon icon="edit" /> | ||||
|                     {{ $t("Edit Status Page") }} | ||||
|                 </button> | ||||
|  | ||||
|                 <a href="/dashboard" class="btn btn-info"> | ||||
|                     <font-awesome-icon icon="tachometer-alt" /> | ||||
|                     {{ $t("Go to Dashboard") }} | ||||
|                 </a> | ||||
|             </div> | ||||
|  | ||||
|             <div v-else> | ||||
|                 <button class="btn btn-success me-2" @click="save"> | ||||
|                     <font-awesome-icon icon="save" /> | ||||
|                     {{ $t("Save") }} | ||||
|                 </button> | ||||
|  | ||||
|                 <button class="btn btn-danger me-2" @click="discard"> | ||||
|                     <font-awesome-icon icon="save" /> | ||||
|                     {{ $t("Discard") }} | ||||
|                 </button> | ||||
|  | ||||
|                 <button class="btn btn-primary btn-add-group me-2" @click="createIncident"> | ||||
|                     <font-awesome-icon icon="bullhorn" /> | ||||
|                     {{ $t("Create Incident") }} | ||||
|                 </button> | ||||
|  | ||||
|                 <!-- | ||||
|                 <button v-if="isPublished" class="btn btn-light me-2" @click=""> | ||||
|                     <font-awesome-icon icon="save" /> | ||||
|                     {{ $t("Unpublish") }} | ||||
|                 </button> | ||||
|  | ||||
|                 <button v-if="!isPublished" class="btn btn-info me-2" @click=""> | ||||
|                     <font-awesome-icon icon="save" /> | ||||
|                     {{ $t("Publish") }} | ||||
|                 </button>--> | ||||
|  | ||||
|                 <!-- Set Default Language --> | ||||
|                 <!-- Set theme --> | ||||
|                 <button v-if="theme == 'dark'" class="btn btn-light me-2" @click="changeTheme('light')"> | ||||
|                     <font-awesome-icon icon="save" /> | ||||
|                     {{ $t("Switch to Light Theme") }} | ||||
|                 </button> | ||||
|  | ||||
|                 <button v-if="theme == 'light'" class="btn btn-dark me-2" @click="changeTheme('dark')"> | ||||
|                     <font-awesome-icon icon="save" /> | ||||
|                     {{ $t("Switch to Dark Theme") }} | ||||
|                 </button> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Incident --> | ||||
|         <div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass"> | ||||
|             <strong v-if="editIncidentMode">{{ $t("Title") }}:</strong> | ||||
|             <Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" /> | ||||
|  | ||||
|             <strong v-if="editIncidentMode">{{ $t("Content") }}:</strong> | ||||
|             <Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" /> | ||||
|  | ||||
|             <!-- Incident Date --> | ||||
|             <div class="date mt-3"> | ||||
|                 Created: {{ incident.createdDate }} ({{ createdDateFromNow }})<br /> | ||||
|                 <span v-if="incident.lastUpdatedDate"> | ||||
|                     Last Updated: {{ incident.lastUpdatedDate }} ({{ lastUpdatedDateFromNow }}) | ||||
|                 </span> | ||||
|             </div> | ||||
|  | ||||
|             <div v-if="editMode" class="mt-3"> | ||||
|                 <button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident"> | ||||
|                     <font-awesome-icon icon="bullhorn" /> | ||||
|                     {{ $t("Post") }} | ||||
|                 </button> | ||||
|  | ||||
|                 <button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident"> | ||||
|                     <font-awesome-icon icon="edit" /> | ||||
|                     {{ $t("Edit") }} | ||||
|                 </button> | ||||
|  | ||||
|                 <button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident"> | ||||
|                     <font-awesome-icon icon="times" /> | ||||
|                     {{ $t("Cancel") }} | ||||
|                 </button> | ||||
|  | ||||
|                 <div v-if="editIncidentMode" class="dropdown d-inline-block me-2"> | ||||
|                     <button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> | ||||
|                         Style: {{ incident.style }} | ||||
|                     </button> | ||||
|                     <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1"> | ||||
|                         <li><a class="dropdown-item" href="#" @click="incident.style = 'info'">info</a></li> | ||||
|                         <li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">warning</a></li> | ||||
|                         <li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">danger</a></li> | ||||
|                         <li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">primary</a></li> | ||||
|                         <li><a class="dropdown-item" href="#" @click="incident.style = 'light'">light</a></li> | ||||
|                         <li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">dark</a></li> | ||||
|                     </ul> | ||||
|                 </div> | ||||
|  | ||||
|                 <button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident"> | ||||
|                     <font-awesome-icon icon="unlink" /> | ||||
|                     {{ $t("Unpin") }} | ||||
|                 </button> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Overall Status --> | ||||
|         <div class="shadow-box list  p-4 overall-status mb-4"> | ||||
|             <div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData"> | ||||
|                 <font-awesome-icon icon="question-circle" class="ok" /> | ||||
|                 {{ $t("No Services") }} | ||||
|             </div> | ||||
|  | ||||
|             <template v-else> | ||||
|                 <div v-if="allUp"> | ||||
|                     <font-awesome-icon icon="check-circle" class="ok" /> | ||||
|                     {{ $t("All Systems Operational") }} | ||||
|                 </div> | ||||
|  | ||||
|                 <div v-else-if="partialDown"> | ||||
|                     <font-awesome-icon icon="exclamation-circle" class="warning" /> | ||||
|                     {{ $t("Partially Degraded Service") }} | ||||
|                 </div> | ||||
|  | ||||
|                 <div v-else-if="allDown"> | ||||
|                     <font-awesome-icon icon="times-circle" class="danger" /> | ||||
|                     {{ $t("Degraded Service") }} | ||||
|                 </div> | ||||
|  | ||||
|                 <div v-else> | ||||
|                     <font-awesome-icon icon="question-circle" style="color: #efefef" /> | ||||
|                 </div> | ||||
|             </template> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Description --> | ||||
|         <strong v-if="editMode">{{ $t("Description") }}:</strong> | ||||
|         <Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" /> | ||||
|  | ||||
|         <div v-if="editMode" class="mb-4"> | ||||
|             <div> | ||||
|                 <button class="btn btn-primary btn-add-group me-2" @click="addGroup"> | ||||
|                     <font-awesome-icon icon="plus" /> | ||||
|                     {{ $t("Add Group") }} | ||||
|                 </button> | ||||
|             </div> | ||||
|  | ||||
|             <div class="mt-3"> | ||||
|                 <div v-if="allMonitorList.length > 0 && loadedData"> | ||||
|                     <label>{{ $t("Add a monitor") }}:</label> | ||||
|                     <select v-model="selectedMonitor" class="form-control"> | ||||
|                         <option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option> | ||||
|                     </select> | ||||
|                 </div> | ||||
|                 <div v-else class="text-center"> | ||||
|                     {{ $t("No monitors available.") }}  <router-link to="/add">{{ $t("Add one") }}</router-link> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="mb-4"> | ||||
|             <div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center"> | ||||
|                 <!-- 👀 Nothing here, please add a group or a monitor. --> | ||||
|                 👀 {{ $t("statusPageNothing") }} | ||||
|             </div> | ||||
|  | ||||
|             <PublicGroupList :edit-mode="enableEditMode" /> | ||||
|         </div> | ||||
|  | ||||
|         <footer class="mt-5 mb-4"> | ||||
|             Powered by <a target="_blank" href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a> | ||||
|         </footer> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import axios from "axios"; | ||||
| import PublicGroupList from "../components/PublicGroupList.vue"; | ||||
| import ImageCropUpload from "vue-image-crop-upload"; | ||||
| import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts"; | ||||
| import { useToast } from "vue-toastification"; | ||||
| import dayjs from "dayjs"; | ||||
| const toast = useToast(); | ||||
|  | ||||
| const leavePageMsg = "Do you really want to leave? you have unsaved changes!"; | ||||
|  | ||||
| let feedInterval; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         PublicGroupList, | ||||
|         ImageCropUpload | ||||
|     }, | ||||
|  | ||||
|     // Leave Page for vue route change | ||||
|     beforeRouteLeave(to, from, next) { | ||||
|         if (this.editMode) { | ||||
|             const answer = window.confirm(leavePageMsg); | ||||
|             if (answer) { | ||||
|                 next(); | ||||
|             } else { | ||||
|                 next(false); | ||||
|             } | ||||
|         } | ||||
|         next(); | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|         return { | ||||
|             enableEditMode: false, | ||||
|             enableEditIncidentMode: false, | ||||
|             hasToken: false, | ||||
|             config: {}, | ||||
|             selectedMonitor: null, | ||||
|             incident: null, | ||||
|             previousIncident: null, | ||||
|             showImageCropUpload: false, | ||||
|             imgDataUrl: "/icon.svg", | ||||
|             loadedTheme: false, | ||||
|             loadedData: false, | ||||
|             baseURL: "", | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|  | ||||
|         logoURL() { | ||||
|             if (this.imgDataUrl.startsWith("data:")) { | ||||
|                 return this.imgDataUrl; | ||||
|             } else { | ||||
|                 return this.baseURL + this.imgDataUrl; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * If the monitor is added to public list, which will not be in this list. | ||||
|          */ | ||||
|         allMonitorList() { | ||||
|             let result = []; | ||||
|  | ||||
|             for (let id in this.$root.monitorList) { | ||||
|                 if (this.$root.monitorList[id] && ! (id in this.$root.publicMonitorList)) { | ||||
|                     let monitor = this.$root.monitorList[id]; | ||||
|                     result.push(monitor); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         }, | ||||
|  | ||||
|         editMode() { | ||||
|             return this.enableEditMode && this.$root.socket.connected; | ||||
|         }, | ||||
|  | ||||
|         editIncidentMode() { | ||||
|             return this.enableEditIncidentMode; | ||||
|         }, | ||||
|  | ||||
|         isPublished() { | ||||
|             return this.config.statusPagePublished; | ||||
|         }, | ||||
|  | ||||
|         theme() { | ||||
|             return this.config.statusPageTheme; | ||||
|         }, | ||||
|  | ||||
|         logoClass() { | ||||
|             if (this.editMode) { | ||||
|                 return { | ||||
|                     "edit-mode": true, | ||||
|                 }; | ||||
|             } | ||||
|             return {}; | ||||
|         }, | ||||
|  | ||||
|         incidentClass() { | ||||
|             return "bg-" + this.incident.style; | ||||
|         }, | ||||
|  | ||||
|         overallStatus() { | ||||
|  | ||||
|             if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) { | ||||
|                 return -1; | ||||
|             } | ||||
|  | ||||
|             let status = STATUS_PAGE_ALL_UP; | ||||
|             let hasUp = false; | ||||
|  | ||||
|             for (let id in this.$root.publicLastHeartbeatList) { | ||||
|                 let beat = this.$root.publicLastHeartbeatList[id]; | ||||
|  | ||||
|                 if (beat.status === UP) { | ||||
|                     hasUp = true; | ||||
|                 } else { | ||||
|                     status = STATUS_PAGE_PARTIAL_DOWN; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (! hasUp) { | ||||
|                 status = STATUS_PAGE_ALL_DOWN; | ||||
|             } | ||||
|  | ||||
|             return status; | ||||
|         }, | ||||
|  | ||||
|         allUp() { | ||||
|             return this.overallStatus === STATUS_PAGE_ALL_UP; | ||||
|         }, | ||||
|  | ||||
|         partialDown() { | ||||
|             return this.overallStatus === STATUS_PAGE_PARTIAL_DOWN; | ||||
|         }, | ||||
|  | ||||
|         allDown() { | ||||
|             return this.overallStatus === STATUS_PAGE_ALL_DOWN; | ||||
|         }, | ||||
|  | ||||
|         createdDateFromNow() { | ||||
|             return dayjs.utc(this.incident.createdDate).fromNow(); | ||||
|         }, | ||||
|  | ||||
|         lastUpdatedDateFromNow() { | ||||
|             return dayjs.utc(this.incident. lastUpdatedDate).fromNow(); | ||||
|         } | ||||
|  | ||||
|     }, | ||||
|     watch: { | ||||
|  | ||||
|         /** | ||||
|          * Selected a monitor and add to the list. | ||||
|          */ | ||||
|         selectedMonitor(monitor) { | ||||
|             if (monitor) { | ||||
|                 if (this.$root.publicGroupList.length === 0) { | ||||
|                     this.addGroup(); | ||||
|                 } | ||||
|  | ||||
|                 const firstGroup = this.$root.publicGroupList[0]; | ||||
|  | ||||
|                 firstGroup.monitorList.push(monitor); | ||||
|                 this.selectedMonitor = null; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         // Set Theme | ||||
|         "config.statusPageTheme"() { | ||||
|             this.$root.statusPageTheme = this.config.statusPageTheme; | ||||
|             this.loadedTheme = true; | ||||
|         }, | ||||
|  | ||||
|         "config.title"(title) { | ||||
|             document.title = title; | ||||
|         } | ||||
|  | ||||
|     }, | ||||
|     async created() { | ||||
|         this.hasToken = ("token" in this.$root.storage()); | ||||
|  | ||||
|         // Browser change page | ||||
|         // https://stackoverflow.com/questions/7317273/warn-user-before-leaving-web-page-with-unsaved-changes | ||||
|         window.addEventListener("beforeunload", (e) => { | ||||
|             if (this.editMode) { | ||||
|                 (e || window.event).returnValue = leavePageMsg; | ||||
|                 return leavePageMsg; | ||||
|             } else { | ||||
|                 return null; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Special handle for dev | ||||
|         const env = process.env.NODE_ENV; | ||||
|         if (env === "development" || localStorage.dev === "dev") { | ||||
|             this.baseURL = location.protocol + "//" + location.hostname + ":3001"; | ||||
|         } | ||||
|     }, | ||||
|     async mounted() { | ||||
|         axios.get("/api/status-page/config").then((res) => { | ||||
|             this.config = res.data; | ||||
|  | ||||
|             if (this.config.logo) { | ||||
|                 this.imgDataUrl = this.config.logo; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         axios.get("/api/status-page/incident").then((res) => { | ||||
|             if (res.data.ok) { | ||||
|                 this.incident = res.data.incident; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         axios.get("/api/status-page/monitor-list").then((res) => { | ||||
|             this.$root.publicGroupList = res.data; | ||||
|         }); | ||||
|  | ||||
|         // 5mins a loop | ||||
|         this.updateHeartbeatList(); | ||||
|         feedInterval = setInterval(() => { | ||||
|             this.updateHeartbeatList(); | ||||
|         }, (300 + 10) * 1000); | ||||
|     }, | ||||
|     methods: { | ||||
|  | ||||
|         updateHeartbeatList() { | ||||
|             // If editMode, it will use the data from websocket. | ||||
|             if (! this.editMode) { | ||||
|                 axios.get("/api/status-page/heartbeat").then((res) => { | ||||
|                     this.$root.heartbeatList = res.data.heartbeatList; | ||||
|                     this.$root.uptimeList = res.data.uptimeList; | ||||
|                     this.loadedData = true; | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         edit() { | ||||
|             this.$root.initSocketIO(true); | ||||
|             this.enableEditMode = true; | ||||
|         }, | ||||
|  | ||||
|         save() { | ||||
|             this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.enableEditMode = false; | ||||
|                     this.$root.publicGroupList = res.publicGroupList; | ||||
|                     location.reload(); | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         monitorSelectorLabel(monitor) { | ||||
|             return `${monitor.name}`; | ||||
|         }, | ||||
|  | ||||
|         addGroup() { | ||||
|             let groupName = "Untitled Group"; | ||||
|  | ||||
|             if (this.$root.publicGroupList.length === 0) { | ||||
|                 groupName = "Services"; | ||||
|             } | ||||
|  | ||||
|             this.$root.publicGroupList.push({ | ||||
|                 name: groupName, | ||||
|                 monitorList: [], | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         discard() { | ||||
|             location.reload(); | ||||
|         }, | ||||
|  | ||||
|         changeTheme(name) { | ||||
|             this.config.statusPageTheme = name; | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Crop Success | ||||
|          */ | ||||
|         cropSuccess(imgDataUrl) { | ||||
|             this.imgDataUrl = imgDataUrl; | ||||
|         }, | ||||
|  | ||||
|         showImageCropUploadMethod() { | ||||
|             if (this.editMode) { | ||||
|                 this.showImageCropUpload = true; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         createIncident() { | ||||
|             this.enableEditIncidentMode = true; | ||||
|  | ||||
|             if (this.incident) { | ||||
|                 this.previousIncident = this.incident; | ||||
|             } | ||||
|  | ||||
|             this.incident = { | ||||
|                 title: "", | ||||
|                 content: "", | ||||
|                 style: "primary", | ||||
|             }; | ||||
|         }, | ||||
|  | ||||
|         postIncident() { | ||||
|             if (this.incident.title == "" || this.incident.content == "") { | ||||
|                 toast.error("Please input title and content."); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.$root.getSocket().emit("postIncident", this.incident, (res) => { | ||||
|  | ||||
|                 if (res.ok) { | ||||
|                     this.enableEditIncidentMode = false; | ||||
|                     this.incident = res.incident; | ||||
|                 } else { | ||||
|                     toast.error(res.msg); | ||||
|                 } | ||||
|  | ||||
|             }); | ||||
|  | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Click Edit Button | ||||
|          */ | ||||
|         editIncident() { | ||||
|             this.enableEditIncidentMode = true; | ||||
|             this.previousIncident = Object.assign({}, this.incident); | ||||
|         }, | ||||
|  | ||||
|         cancelIncident() { | ||||
|             this.enableEditIncidentMode = false; | ||||
|  | ||||
|             if (this.previousIncident) { | ||||
|                 this.incident = this.previousIncident; | ||||
|                 this.previousIncident = null; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         unpinIncident() { | ||||
|             this.$root.getSocket().emit("unpinIncident", () => { | ||||
|                 this.incident = null; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../assets/vars.scss"; | ||||
|  | ||||
| .overall-status { | ||||
|     font-weight: bold; | ||||
|     font-size: 25px; | ||||
|  | ||||
|     .ok { | ||||
|         color: $primary; | ||||
|     } | ||||
|  | ||||
|     .warning { | ||||
|         color: $warning; | ||||
|     } | ||||
|  | ||||
|     .danger { | ||||
|         color: $danger; | ||||
|     } | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|     font-size: 30px; | ||||
|  | ||||
|     img { | ||||
|         vertical-align: middle; | ||||
|         height: 60px; | ||||
|         width: 60px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| footer { | ||||
|     text-align: center; | ||||
|     font-size: 14px; | ||||
| } | ||||
|  | ||||
| .description span { | ||||
|     min-width: 50px; | ||||
| } | ||||
|  | ||||
| .logo-wrapper { | ||||
|     display: inline-block; | ||||
|     position: relative; | ||||
|  | ||||
|     &:hover { | ||||
|         .icon-upload { | ||||
|             transform: scale(1.2); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .icon-upload { | ||||
|         transition: all $easing-in 0.2s; | ||||
|         position: absolute; | ||||
|         bottom: 6px; | ||||
|         font-size: 20px; | ||||
|         left: -14px; | ||||
|         background-color: white; | ||||
|         padding: 5px; | ||||
|         border-radius: 10px; | ||||
|         cursor: pointer; | ||||
|         box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9); | ||||
|     } | ||||
| } | ||||
|  | ||||
| .logo { | ||||
|     transition: all $easing-in 0.2s; | ||||
|  | ||||
|     &.edit-mode { | ||||
|         cursor: pointer; | ||||
|  | ||||
|         &:hover { | ||||
|             transform: scale(1.2); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .incident { | ||||
|     .content { | ||||
|         &[contenteditable=true] { | ||||
|             min-height: 60px; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .date { | ||||
|         font-size: 12px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .mobile { | ||||
|     h1 { | ||||
|         font-size: 22px; | ||||
|     } | ||||
|  | ||||
|     .overall-status { | ||||
|         font-size: 20px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| </style> | ||||
| @@ -8,14 +8,22 @@ import EditMonitor from "./pages/EditMonitor.vue"; | ||||
| import List from "./pages/List.vue"; | ||||
| import Settings from "./pages/Settings.vue"; | ||||
| import Setup from "./pages/Setup.vue"; | ||||
| import StatusPage from "./pages/StatusPage.vue"; | ||||
| import Entry from "./pages/Entry.vue"; | ||||
|  | ||||
| const routes = [ | ||||
|     { | ||||
|         path: "/", | ||||
|         component: Entry, | ||||
|     }, | ||||
|     { | ||||
|         // If it is "/dashboard", the active link is not working | ||||
|         // If it is "", it overrides the "/" unexpectedly | ||||
|         // Give a random name to solve the problem. | ||||
|         path: "/empty", | ||||
|         component: Layout, | ||||
|         children: [ | ||||
|             { | ||||
|                 name: "root", | ||||
|                 path: "", | ||||
|                 component: Dashboard, | ||||
|                 children: [ | ||||
| @@ -54,15 +62,17 @@ const routes = [ | ||||
|                     }, | ||||
|                 ], | ||||
|             }, | ||||
|  | ||||
|         ], | ||||
|  | ||||
|     }, | ||||
|     { | ||||
|         path: "/setup", | ||||
|         component: Setup, | ||||
|     }, | ||||
| ] | ||||
|     { | ||||
|         path: "/status-page", | ||||
|         component: StatusPage, | ||||
|     }, | ||||
| ]; | ||||
|  | ||||
| export const router = createRouter({ | ||||
|     linkActiveClass: "active", | ||||
|   | ||||
| @@ -3,8 +3,8 @@ import timezone from "dayjs/plugin/timezone"; | ||||
| import utc from "dayjs/plugin/utc"; | ||||
| import timezones from "timezones-list"; | ||||
|  | ||||
| dayjs.extend(utc) | ||||
| dayjs.extend(timezone) | ||||
| dayjs.extend(utc); | ||||
| dayjs.extend(timezone); | ||||
|  | ||||
| function getTimezoneOffset(timeZone) { | ||||
|     const now = new Date(); | ||||
| @@ -28,7 +28,7 @@ export function timezoneList() { | ||||
|                 name: `(UTC${display}) ${timezone.tzCode}`, | ||||
|                 value: timezone.tzCode, | ||||
|                 time: getTimezoneOffset(timezone.tzCode), | ||||
|             }) | ||||
|             }); | ||||
|         } catch (e) { | ||||
|             console.log("Skip Timezone: " + timezone.tzCode); | ||||
|         } | ||||
| @@ -44,7 +44,7 @@ export function timezoneList() { | ||||
|         } | ||||
|  | ||||
|         return 0; | ||||
|     }) | ||||
|     }); | ||||
|  | ||||
|     return result; | ||||
| } | ||||
|   | ||||
							
								
								
									
										174
									
								
								src/util.js
									
									
									
									
									
								
							
							
						
						
									
										174
									
								
								src/util.js
									
									
									
									
									
								
							| @@ -1,70 +1,104 @@ | ||||
| "use strict"; | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; | ||||
| const _dayjs = require("dayjs"); | ||||
| const dayjs = _dayjs; | ||||
| exports.isDev = process.env.NODE_ENV === "development"; | ||||
| exports.appName = "Uptime Kuma"; | ||||
| exports.DOWN = 0; | ||||
| exports.UP = 1; | ||||
| exports.PENDING = 2; | ||||
| function flipStatus(s) { | ||||
|     if (s === exports.UP) { | ||||
|         return exports.DOWN; | ||||
|     } | ||||
|     if (s === exports.DOWN) { | ||||
|         return exports.UP; | ||||
|     } | ||||
|     return s; | ||||
| } | ||||
| exports.flipStatus = flipStatus; | ||||
| function sleep(ms) { | ||||
|     return new Promise(resolve => setTimeout(resolve, ms)); | ||||
| } | ||||
| exports.sleep = sleep; | ||||
| function ucfirst(str) { | ||||
|     if (!str) { | ||||
|         return str; | ||||
|     } | ||||
|     const firstLetter = str.substr(0, 1); | ||||
|     return firstLetter.toUpperCase() + str.substr(1); | ||||
| } | ||||
| exports.ucfirst = ucfirst; | ||||
| function debug(msg) { | ||||
|     if (exports.isDev) { | ||||
|         console.log(msg); | ||||
|     } | ||||
| } | ||||
| exports.debug = debug; | ||||
| function polyfill() { | ||||
|     if (!String.prototype.replaceAll) { | ||||
|         String.prototype.replaceAll = function (str, newStr) { | ||||
|             if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { | ||||
|                 return this.replace(str, newStr); | ||||
|             } | ||||
|             return this.replace(new RegExp(str, "g"), newStr); | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| exports.polyfill = polyfill; | ||||
| class TimeLogger { | ||||
|     constructor() { | ||||
|         this.startTime = dayjs().valueOf(); | ||||
|     } | ||||
|     print(name) { | ||||
|         if (exports.isDev) { | ||||
|             console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| exports.TimeLogger = TimeLogger; | ||||
| function getRandomArbitrary(min, max) { | ||||
|     return Math.random() * (max - min) + min; | ||||
| } | ||||
| exports.getRandomArbitrary = getRandomArbitrary; | ||||
| function getRandomInt(min, max) { | ||||
|     min = Math.ceil(min); | ||||
|     max = Math.floor(max); | ||||
|     return Math.floor(Math.random() * (max - min + 1)) + min; | ||||
| } | ||||
| exports.getRandomInt = getRandomInt; | ||||
| "use strict"; | ||||
| // Common Util for frontend and backend | ||||
| // | ||||
| // DOT NOT MODIFY util.js! | ||||
| // Need to run "tsc" to compile if there are any changes. | ||||
| // | ||||
| // Backend uses the compiled file util.js | ||||
| // Frontend uses util.ts | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; | ||||
| const _dayjs = require("dayjs"); | ||||
| const dayjs = _dayjs; | ||||
| exports.isDev = process.env.NODE_ENV === "development"; | ||||
| exports.appName = "Uptime Kuma"; | ||||
| exports.DOWN = 0; | ||||
| exports.UP = 1; | ||||
| exports.PENDING = 2; | ||||
| exports.STATUS_PAGE_ALL_DOWN = 0; | ||||
| exports.STATUS_PAGE_ALL_UP = 1; | ||||
| exports.STATUS_PAGE_PARTIAL_DOWN = 2; | ||||
| function flipStatus(s) { | ||||
|     if (s === exports.UP) { | ||||
|         return exports.DOWN; | ||||
|     } | ||||
|     if (s === exports.DOWN) { | ||||
|         return exports.UP; | ||||
|     } | ||||
|     return s; | ||||
| } | ||||
| exports.flipStatus = flipStatus; | ||||
| function sleep(ms) { | ||||
|     return new Promise(resolve => setTimeout(resolve, ms)); | ||||
| } | ||||
| exports.sleep = sleep; | ||||
| /** | ||||
|  * PHP's ucfirst | ||||
|  * @param str | ||||
|  */ | ||||
| function ucfirst(str) { | ||||
|     if (!str) { | ||||
|         return str; | ||||
|     } | ||||
|     const firstLetter = str.substr(0, 1); | ||||
|     return firstLetter.toUpperCase() + str.substr(1); | ||||
| } | ||||
| exports.ucfirst = ucfirst; | ||||
| function debug(msg) { | ||||
|     if (exports.isDev) { | ||||
|         console.log(msg); | ||||
|     } | ||||
| } | ||||
| exports.debug = debug; | ||||
| function polyfill() { | ||||
|     /** | ||||
|      * String.prototype.replaceAll() polyfill | ||||
|      * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/ | ||||
|      * @author Chris Ferdinandi | ||||
|      * @license MIT | ||||
|      */ | ||||
|     if (!String.prototype.replaceAll) { | ||||
|         String.prototype.replaceAll = function (str, newStr) { | ||||
|             // If a regex pattern | ||||
|             if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { | ||||
|                 return this.replace(str, newStr); | ||||
|             } | ||||
|             // If a string | ||||
|             return this.replace(new RegExp(str, "g"), newStr); | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| exports.polyfill = polyfill; | ||||
| class TimeLogger { | ||||
|     constructor() { | ||||
|         this.startTime = dayjs().valueOf(); | ||||
|     } | ||||
|     print(name) { | ||||
|         if (exports.isDev) { | ||||
|             console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| exports.TimeLogger = TimeLogger; | ||||
| /** | ||||
|  * Returns a random number between min (inclusive) and max (exclusive) | ||||
|  */ | ||||
| function getRandomArbitrary(min, max) { | ||||
|     return Math.random() * (max - min) + min; | ||||
| } | ||||
| exports.getRandomArbitrary = getRandomArbitrary; | ||||
| /** | ||||
|  * From: https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range | ||||
|  * | ||||
|  * Returns a random integer between min (inclusive) and max (inclusive). | ||||
|  * The value is no lower than min (or the next integer greater than min | ||||
|  * if min isn't an integer) and no greater than max (or the next integer | ||||
|  * lower than max if max isn't an integer). | ||||
|  * Using Math.round() will give you a non-uniform distribution! | ||||
|  */ | ||||
| function getRandomInt(min, max) { | ||||
|     min = Math.ceil(min); | ||||
|     max = Math.floor(max); | ||||
|     return Math.floor(Math.random() * (max - min + 1)) + min; | ||||
| } | ||||
| exports.getRandomInt = getRandomInt; | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/util.ts
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/util.ts
									
									
									
									
									
								
							| @@ -1,7 +1,10 @@ | ||||
| // Common Util for frontend and backend | ||||
| // | ||||
| // DOT NOT MODIFY util.js! | ||||
| // Need to run "tsc" to compile if there are any changes. | ||||
| // | ||||
| // Backend uses the compiled file util.js | ||||
| // Frontend uses util.ts | ||||
| // Need to run "tsc" to compile if there are any changes. | ||||
|  | ||||
| import * as _dayjs from "dayjs"; | ||||
| const dayjs = _dayjs; | ||||
| @@ -12,6 +15,11 @@ export const DOWN = 0; | ||||
| export const UP = 1; | ||||
| export const PENDING = 2; | ||||
|  | ||||
| export const STATUS_PAGE_ALL_DOWN = 0; | ||||
| export const STATUS_PAGE_ALL_UP = 1; | ||||
| export const STATUS_PAGE_PARTIAL_DOWN = 2; | ||||
|  | ||||
|  | ||||
| export function flipStatus(s: number) { | ||||
|     if (s === UP) { | ||||
|         return DOWN; | ||||
| @@ -59,7 +67,6 @@ export function polyfill() { | ||||
|      */ | ||||
|     if (!String.prototype.replaceAll) { | ||||
|         String.prototype.replaceAll = function (str: string, newStr: string) { | ||||
|  | ||||
|             // If a regex pattern | ||||
|             if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { | ||||
|                 return this.replace(str, newStr); | ||||
| @@ -67,7 +74,6 @@ export function polyfill() { | ||||
|  | ||||
|             // If a string | ||||
|             return this.replace(new RegExp(str, "g"), newStr); | ||||
|  | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|             "es2020", | ||||
|             "DOM", | ||||
|         ], | ||||
|         "removeComments": true, | ||||
|         "removeComments": false, | ||||
|         "preserveConstEnums": true, | ||||
|         "sourceMap": false, | ||||
|         "strict": true | ||||
|   | ||||
		Reference in New Issue
	
	Block a user