mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 05:36:13 +08:00 
			
		
		
		
	Merge Conflict Resolved
Farsi Lang Updated Some hardcoded words has been replaced with translations
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.
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,9 @@ Browse to http://localhost:3001 after started.
 | 
			
		||||
Required Tools: Node.js >= 14, git and pm2.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Update your npm to the latest version
 | 
			
		||||
npm install npm -g
 | 
			
		||||
 | 
			
		||||
git clone https://github.com/louislam/uptime-kuma.git
 | 
			
		||||
cd uptime-kuma
 | 
			
		||||
npm run setup
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											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 {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1075
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1075
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										45
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								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",
 | 
			
		||||
        "postcss-rtlcss": "^3.4.1",
 | 
			
		||||
@@ -70,34 +74,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",
 | 
			
		||||
        "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", {
 | 
			
		||||
        const Dialect = require("knex/lib/dialects/sqlite3/index.js");
 | 
			
		||||
        Dialect.prototype._driver = () => require("@louislam/sqlite3");
 | 
			
		||||
 | 
			
		||||
        const knexInstance = knex({
 | 
			
		||||
            client: Dialect,
 | 
			
		||||
            connection: {
 | 
			
		||||
                filename: Database.path,
 | 
			
		||||
            useNullAsDefault: true,
 | 
			
		||||
                acquireConnectionTimeout: acquireConnectionTimeout,
 | 
			
		||||
        }, {
 | 
			
		||||
            },
 | 
			
		||||
            useNullAsDefault: true,
 | 
			
		||||
            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;
 | 
			
		||||
							
								
								
									
										438
									
								
								server/server.js
									
									
									
									
									
								
							
							
						
						
									
										438
									
								
								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,8 +144,10 @@ h2 {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .shadow-box {
 | 
			
		||||
        &:not(.alert) {
 | 
			
		||||
            background-color: $dark-bg;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .form-check-input {
 | 
			
		||||
        background-color: $dark-bg2;
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@@ -289,6 +313,121 @@ h2 {
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Localization
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,9 +129,11 @@ export default {
 | 
			
		||||
        window.removeEventListener("resize", this.resize);
 | 
			
		||||
    },
 | 
			
		||||
    beforeMount() {
 | 
			
		||||
        if (this.heartbeatList === null) {
 | 
			
		||||
            if (! (this.monitorId in this.$root.heartbeatList)) {
 | 
			
		||||
                this.$root.heartbeatList[this.monitorId] = [];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    mounted() {
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -4,6 +4,7 @@ import deDE from "./languages/de-DE";
 | 
			
		||||
import en from "./languages/en";
 | 
			
		||||
import fa from "./languages/fa";
 | 
			
		||||
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";
 | 
			
		||||
@@ -26,6 +27,7 @@ const languageList = {
 | 
			
		||||
    "nl-NL": nlNL,
 | 
			
		||||
    "es-ES": esEs,
 | 
			
		||||
    "fa": fa,
 | 
			
		||||
    "pt-BR": ptBR,
 | 
			
		||||
    "fr-FR": frFR,
 | 
			
		||||
    "it-IT": itIT,
 | 
			
		||||
    "ja": ja,
 | 
			
		||||
@@ -52,6 +54,6 @@ export const i18n = createI18n({
 | 
			
		||||
    locale: currentLocale(),
 | 
			
		||||
    fallbackLocale: "en",
 | 
			
		||||
    silentFallbackWarn: true,
 | 
			
		||||
    silentTranslationWarn: false,
 | 
			
		||||
    messages: languageList
 | 
			
		||||
    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",
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -166,6 +166,16 @@ export default {
 | 
			
		||||
    retryCheckEverySecond: "Versuche alle {0} Sekunden",
 | 
			
		||||
    "Import Backup": "Import Backup",
 | 
			
		||||
    "Export Backup": "Export Backup",
 | 
			
		||||
    "Avg. Ping": "Avg. Ping",
 | 
			
		||||
    "Avg. Response": "Avg. Response",
 | 
			
		||||
}
 | 
			
		||||
    "Avg. Ping": "Durchsch. Ping",
 | 
			
		||||
    "Avg. Response": "Durchsch. Antwort",
 | 
			
		||||
    "Entry Page": "Einstiegsseite",
 | 
			
		||||
    "statusPageNothing": "Nichts ist hier, bitte füge eine Gruppe oder Monitor hinzu.",
 | 
			
		||||
    "No Services": "Keine Dienste",
 | 
			
		||||
    "All Systems Operational": "Alle Systeme Betriebsbereit",
 | 
			
		||||
    "Partially Degraded Service": "Teilweise beeinträchtigter Dienst",
 | 
			
		||||
    "Degraded Service": "Eingeschränkter Dienst",
 | 
			
		||||
    "Add Group": "Gruppe hinzufügen",
 | 
			
		||||
    "Add a monitor": "Monitor hinzufügen",
 | 
			
		||||
    "Edit Status Page": "Bearbeite Statusseite",
 | 
			
		||||
    "Go to Dashboard": "Gehe zum 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",
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -168,10 +168,23 @@ export default {
 | 
			
		||||
    "Search...": "جستجو...",
 | 
			
		||||
    "Avg. Ping": "متوسط پینگ",
 | 
			
		||||
    "Avg. Response": "متوسط زمان پاسخ",
 | 
			
		||||
    "Entry Page": "صفحه ورودی",
 | 
			
		||||
    "statusPageNothing": "چیزی اینجا نیست، لطفا یک گروه و یا یک مانیتور اضافه کنید!",
 | 
			
		||||
    "No Services": "هیچ سرویسی موجود نیست",
 | 
			
		||||
    "All Systems Operational": "تمامی سیستمها عملیاتی هستند!",
 | 
			
		||||
    "Partially Degraded Service": "افت نسبی کیفیت سرویس",
 | 
			
		||||
    "Degraded Service": "افت کامل کیفیت سرویس",
 | 
			
		||||
    "Add Group": "اضافه کردن گروه",
 | 
			
		||||
    "Add a monitor": "اضافه کردن مانیتور",
 | 
			
		||||
    "Edit Status Page": "ویرایش صفحه وضعیت",
 | 
			
		||||
    "Status Page":"صفحه وضعیت",
 | 
			
		||||
    "Go to Dashboard": "رفتن به پیشخوان",
 | 
			
		||||
    "Uptime Kuma": "آپتایم کوما",
 | 
			
		||||
    "records":"مورد",
 | 
			
		||||
    "One record":"یک مورد",
 | 
			
		||||
    "Showing {from} to {to} of {count} records":"نمایش از {from} تا {to} از {count} مورد",
 | 
			
		||||
    'First': 'اولین',
 | 
			
		||||
    'Last': 'آخرین'
 | 
			
		||||
    'Last': 'آخرین',
 | 
			
		||||
    'Info':'اطلاعات',
 | 
			
		||||
    'Powered By':'نیرو گرفته از'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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" 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,15 @@
 | 
			
		||||
import { io } from "socket.io-client";
 | 
			
		||||
import { useToast } from "vue-toastification";
 | 
			
		||||
const toast = useToast()
 | 
			
		||||
const toast = useToast();
 | 
			
		||||
 | 
			
		||||
let socket;
 | 
			
		||||
 | 
			
		||||
const noSocketIOPages = [
 | 
			
		||||
    "/status-page",
 | 
			
		||||
    "/status",
 | 
			
		||||
    "/"
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
@@ -14,6 +20,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,11 +33,28 @@ export default {
 | 
			
		||||
            certInfoList: {},
 | 
			
		||||
            notificationList: [],
 | 
			
		||||
            connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
 | 
			
		||||
        }
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    created() {
 | 
			
		||||
        window.addEventListener("resize", this.onResize);
 | 
			
		||||
        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://";
 | 
			
		||||
 | 
			
		||||
@@ -51,7 +75,7 @@ export default {
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("setup", (monitorID, data) => {
 | 
			
		||||
            this.$router.push("/setup")
 | 
			
		||||
                this.$router.push("/setup");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("autoLogin", (monitorID, data) => {
 | 
			
		||||
@@ -83,7 +107,11 @@ export default {
 | 
			
		||||
                    this.heartbeatList[data.monitorID] = [];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            this.heartbeatList[data.monitorID].push(data)
 | 
			
		||||
                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
 | 
			
		||||
@@ -105,7 +133,7 @@ export default {
 | 
			
		||||
                        this.importantHeartbeatList[data.monitorID] = [];
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                this.importantHeartbeatList[data.monitorID].unshift(data)
 | 
			
		||||
                    this.importantHeartbeatList[data.monitorID].unshift(data);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
@@ -113,27 +141,27 @@ export default {
 | 
			
		||||
                if (! (monitorID in this.heartbeatList) || overwrite) {
 | 
			
		||||
                    this.heartbeatList[monitorID] = data;
 | 
			
		||||
                } else {
 | 
			
		||||
                this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID])
 | 
			
		||||
                    this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID]);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("avgPing", (monitorID, data) => {
 | 
			
		||||
            this.avgPingList[monitorID] = data
 | 
			
		||||
                this.avgPingList[monitorID] = data;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("uptime", (monitorID, type, data) => {
 | 
			
		||||
            this.uptimeList[`${monitorID}_${type}`] = data
 | 
			
		||||
                this.uptimeList[`${monitorID}_${type}`] = data;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("certInfo", (monitorID, data) => {
 | 
			
		||||
            this.certInfoList[monitorID] = JSON.parse(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])
 | 
			
		||||
                    this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID]);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
@@ -145,26 +173,26 @@ export default {
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("disconnect", () => {
 | 
			
		||||
            console.log("disconnect")
 | 
			
		||||
                console.log("disconnect");
 | 
			
		||||
                this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
 | 
			
		||||
                this.socket.connected = false;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            socket.on("connect", () => {
 | 
			
		||||
            console.log("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()
 | 
			
		||||
                    this.clearData();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let token = this.storage().token;
 | 
			
		||||
 | 
			
		||||
                if (token) {
 | 
			
		||||
                    if (token !== "autoLogin") {
 | 
			
		||||
                    this.loginByToken(token)
 | 
			
		||||
                        this.loginByToken(token);
 | 
			
		||||
                    } else {
 | 
			
		||||
 | 
			
		||||
                        // Timeout if it is not actually auto login
 | 
			
		||||
@@ -185,8 +213,6 @@ export default {
 | 
			
		||||
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
 | 
			
		||||
        storage() {
 | 
			
		||||
            return (this.remember) ? localStorage : sessionStorage;
 | 
			
		||||
        },
 | 
			
		||||
@@ -210,7 +236,7 @@ export default {
 | 
			
		||||
                token,
 | 
			
		||||
            }, (res) => {
 | 
			
		||||
                if (res.tokenRequired) {
 | 
			
		||||
                    callback(res)
 | 
			
		||||
                    callback(res);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (res.ok) {
 | 
			
		||||
@@ -219,11 +245,11 @@ export default {
 | 
			
		||||
                    this.loggedIn = true;
 | 
			
		||||
 | 
			
		||||
                    // Trigger Chrome Save Password
 | 
			
		||||
                    history.pushState({}, "")
 | 
			
		||||
                    history.pushState({}, "");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                callback(res)
 | 
			
		||||
            })
 | 
			
		||||
                callback(res);
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        loginByToken(token) {
 | 
			
		||||
@@ -231,11 +257,11 @@ export default {
 | 
			
		||||
                this.allowLoginDialog = true;
 | 
			
		||||
 | 
			
		||||
                if (! res.ok) {
 | 
			
		||||
                    this.logout()
 | 
			
		||||
                    this.logout();
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.loggedIn = true;
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        logout() {
 | 
			
		||||
@@ -243,68 +269,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 +341,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 +382,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() {
 | 
			
		||||
 | 
			
		||||
            // Entry no need dark
 | 
			
		||||
            if (this.path === "") {
 | 
			
		||||
                return "light";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.path === "/status-page" || this.path === "/status") {
 | 
			
		||||
                return this.statusPageTheme;
 | 
			
		||||
            } else {
 | 
			
		||||
                if (this.userTheme === "auto") {
 | 
			
		||||
                    return this.system;
 | 
			
		||||
                }
 | 
			
		||||
                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");
 | 
			
		||||
        } 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,17 +225,14 @@
 | 
			
		||||
                        <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
 | 
			
		||||
                            {{ $t("Setup Notification") }}
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <footer>
 | 
			
		||||
                <div class="container-fluid">
 | 
			
		||||
                    Uptime Kuma -
 | 
			
		||||
                    {{ $t("Version") }}: {{ $root.info.version }} -
 | 
			
		||||
                        <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>
 | 
			
		||||
            </footer>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <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>
 | 
			
		||||
@@ -295,6 +316,12 @@
 | 
			
		||||
                    <p>Пожалуйста, используйте с осторожностью.</p>
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <template v-else-if="$i18n.locale === 'fa' ">
 | 
			
		||||
                    <p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
 | 
			
		||||
                    <p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
 | 
			
		||||
                    <p>لطفا از این امکان با دقت استفاده کنید.</p>
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <!-- English (en) -->
 | 
			
		||||
                <template v-else>
 | 
			
		||||
                    <p>Are you sure want to <strong>disable auth</strong>?</p>
 | 
			
		||||
@@ -316,16 +343,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 +378,7 @@ export default {
 | 
			
		||||
            importAlert: null,
 | 
			
		||||
            importHandle: "skip",
 | 
			
		||||
            processing: false,
 | 
			
		||||
        }
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
        "password.repeatNewPassword"() {
 | 
			
		||||
@@ -379,13 +406,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 +424,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 +470,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 +484,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 +504,8 @@ export default {
 | 
			
		||||
                    } else {
 | 
			
		||||
                        toast.error(res.msg);
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
                });
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        clearStatistics() {
 | 
			
		||||
@@ -484,10 +515,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,21 @@ const routes = [
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
        ],
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: "/setup",
 | 
			
		||||
        component: Setup,
 | 
			
		||||
    },
 | 
			
		||||
]
 | 
			
		||||
    {
 | 
			
		||||
        path: "/status-page",
 | 
			
		||||
        component: StatusPage,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: "/status",
 | 
			
		||||
        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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								src/util.js
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								src/util.js
									
									
									
									
									
								
							@@ -1,6 +1,13 @@
 | 
			
		||||
"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.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
 | 
			
		||||
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";
 | 
			
		||||
@@ -8,6 +15,9 @@ 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;
 | 
			
		||||
@@ -22,6 +32,10 @@ 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;
 | 
			
		||||
@@ -37,11 +51,19 @@ function debug(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);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
@@ -58,10 +80,22 @@ class TimeLogger {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
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);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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