mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-30 07:59:52 -08:00
Merge pull request #124 from chakflying/public-dashboard
Implement Public Dashboard
This commit is contained in:
commit
a9bb8ae6a1
|
@ -2,10 +2,12 @@
|
||||||
/dist
|
/dist
|
||||||
/node_modules
|
/node_modules
|
||||||
/data
|
/data
|
||||||
|
/out
|
||||||
/test
|
/test
|
||||||
/kubernetes
|
/kubernetes
|
||||||
/.do
|
/.do
|
||||||
**/.dockerignore
|
**/.dockerignore
|
||||||
|
/private
|
||||||
**/.git
|
**/.git
|
||||||
**/.gitignore
|
**/.gitignore
|
||||||
**/docker-compose*
|
**/docker-compose*
|
||||||
|
|
|
@ -17,6 +17,7 @@ module.exports = {
|
||||||
requireConfigFile: false,
|
requireConfigFile: false,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
"camelcase": ["warn", {
|
"camelcase": ["warn", {
|
||||||
"properties": "never",
|
"properties": "never",
|
||||||
"ignoreImports": true
|
"ignoreImports": true
|
||||||
|
@ -33,11 +34,12 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
quotes: ["warn", "double"],
|
quotes: ["warn", "double"],
|
||||||
//semi: ['off', 'never'],
|
semi: "warn",
|
||||||
"vue/html-indent": ["warn", 4], // default: 2
|
"vue/html-indent": ["warn", 4], // default: 2
|
||||||
"vue/max-attributes-per-line": "off",
|
"vue/max-attributes-per-line": "off",
|
||||||
"vue/singleline-html-element-content-newline": "off",
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
"vue/html-self-closing": "off",
|
"vue/html-self-closing": "off",
|
||||||
|
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
||||||
"no-multi-spaces": ["error", {
|
"no-multi-spaces": ["error", {
|
||||||
ignoreEOLComments: true,
|
ignoreEOLComments: true,
|
||||||
}],
|
}],
|
||||||
|
@ -85,10 +87,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": [ "src/languages/*.js" ],
|
"files": [ "src/languages/*.js", "src/icon.js" ],
|
||||||
"rules": {
|
"rules": {
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
};
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -8,3 +8,6 @@ dist-ssr
|
||||||
/data
|
/data
|
||||||
!/data/.gitkeep
|
!/data/.gitkeep
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
/private
|
||||||
|
/out
|
||||||
|
|
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;
|
623
package-lock.json
generated
623
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -19,6 +19,7 @@
|
||||||
"start": "npm run start-server",
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"tsc": "tsc",
|
||||||
"vite-preview-dist": "vite preview --host",
|
"vite-preview-dist": "vite preview --host",
|
||||||
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
"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-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",
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
"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 .",
|
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
"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"
|
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -71,17 +72,20 @@
|
||||||
"socket.io": "^4.2.0",
|
"socket.io": "^4.2.0",
|
||||||
"socket.io-client": "^4.2.0",
|
"socket.io-client": "^4.2.0",
|
||||||
"tcp-ping": "^0.1.1",
|
"tcp-ping": "^0.1.1",
|
||||||
"timezones-list": "^3.0.1",
|
|
||||||
"thirty-two": "^1.0.2",
|
"thirty-two": "^1.0.2",
|
||||||
|
"timezones-list": "^3.0.1",
|
||||||
"v-pagination-3": "^0.1.6",
|
"v-pagination-3": "^0.1.6",
|
||||||
"vue": "^3.2.8",
|
"vue": "next",
|
||||||
"vue-chart-3": "^0.5.8",
|
"vue-chart-3": "^0.5.8",
|
||||||
"vue-confirm-dialog": "^1.0.2",
|
"vue-confirm-dialog": "^1.0.2",
|
||||||
|
"vue-contenteditable": "^3.0.4",
|
||||||
"vue-i18n": "^9.1.7",
|
"vue-i18n": "^9.1.7",
|
||||||
|
"vue-image-crop-upload": "^3.0.3",
|
||||||
"vue-multiselect": "^3.0.0-alpha.2",
|
"vue-multiselect": "^3.0.0-alpha.2",
|
||||||
"vue-qrcode": "^1.0.0",
|
"vue-qrcode": "^1.0.0",
|
||||||
"vue-router": "^4.0.11",
|
"vue-router": "^4.0.11",
|
||||||
"vue-toastification": "^2.0.0-rc.1"
|
"vue-toastification": "^2.0.0-rc.1",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.15.4",
|
"@babel/eslint-parser": "^7.15.4",
|
||||||
|
@ -93,7 +97,7 @@
|
||||||
"dns2": "^2.0.1",
|
"dns2": "^2.0.1",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-vue": "^7.17.0",
|
"eslint-plugin-vue": "^7.17.0",
|
||||||
"sass": "^1.39.2",
|
"sass": "^1.41.0",
|
||||||
"stylelint": "^13.13.1",
|
"stylelint": "^13.13.1",
|
||||||
"stylelint-config-standard": "^22.0.0",
|
"stylelint-config-standard": "^22.0.0",
|
||||||
"typescript": "^4.4.3",
|
"typescript": "^4.4.3",
|
||||||
|
|
|
@ -18,7 +18,7 @@ exports.startInterval = () => {
|
||||||
|
|
||||||
// For debug
|
// For debug
|
||||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||||
res.data.version = "1000.0.0"
|
res.data.version = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.latestVersion = res.data.version;
|
exports.latestVersion = res.data.version;
|
||||||
|
|
|
@ -5,10 +5,23 @@ const { debug, sleep } = require("../src/util");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const knex = require("knex");
|
const knex = require("knex");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database & App Data Folder
|
||||||
|
*/
|
||||||
class Database {
|
class Database {
|
||||||
|
|
||||||
static templatePath = "./db/kuma.db";
|
static templatePath = "./db/kuma.db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Dir (Default: ./data)
|
||||||
|
*/
|
||||||
static dataDir;
|
static dataDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Upload Dir (Default: ./data/upload)
|
||||||
|
*/
|
||||||
|
static uploadDir;
|
||||||
|
|
||||||
static path;
|
static path;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,6 +46,8 @@ class Database {
|
||||||
"patch-improve-performance.sql": true,
|
"patch-improve-performance.sql": true,
|
||||||
"patch-2fa.sql": true,
|
"patch-2fa.sql": true,
|
||||||
"patch-add-retry-interval-monitor.sql": true,
|
"patch-add-retry-interval-monitor.sql": true,
|
||||||
|
"patch-incident-table.sql": true,
|
||||||
|
"patch-group-table.sql": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,6 +65,13 @@ class Database {
|
||||||
if (! fs.existsSync(Database.dataDir)) {
|
if (! fs.existsSync(Database.dataDir)) {
|
||||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
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}`);
|
console.log(`Data Dir: ${Database.dataDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +104,7 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto map the model to a bean object
|
// Auto map the model to a bean object
|
||||||
R.freeze(true)
|
R.freeze(true);
|
||||||
await R.autoloadModels("./server/model");
|
await R.autoloadModels("./server/model");
|
||||||
|
|
||||||
// Change to WAL
|
// Change to WAL
|
||||||
|
@ -110,7 +132,7 @@ class Database {
|
||||||
} else if (version > this.latestVersion) {
|
} else if (version > this.latestVersion) {
|
||||||
console.info("Warning: Database version is newer than expected");
|
console.info("Warning: Database version is newer than expected");
|
||||||
} else {
|
} else {
|
||||||
console.info("Database patch is needed")
|
console.info("Database patch is needed");
|
||||||
|
|
||||||
this.backup(version);
|
this.backup(version);
|
||||||
|
|
||||||
|
@ -125,11 +147,12 @@ class Database {
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
await Database.close();
|
await Database.close();
|
||||||
this.restore();
|
|
||||||
|
|
||||||
console.error(ex)
|
console.error(ex);
|
||||||
console.error("Start Uptime-Kuma failed due to patch db failed")
|
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("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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +177,7 @@ class Database {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let sqlFilename in this.patchList) {
|
for (let sqlFilename in this.patchList) {
|
||||||
await this.patch2Recursion(sqlFilename, databasePatchedFiles)
|
await this.patch2Recursion(sqlFilename, databasePatchedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.patched) {
|
if (this.patched) {
|
||||||
|
@ -163,11 +186,13 @@ class Database {
|
||||||
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
await Database.close();
|
await Database.close();
|
||||||
this.restore();
|
|
||||||
|
|
||||||
console.error(ex)
|
console.error(ex);
|
||||||
console.error("Start Uptime-Kuma failed due to patch db failed");
|
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("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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +232,7 @@ class Database {
|
||||||
console.log(sqlFilename + " is patched successfully");
|
console.log(sqlFilename + " is patched successfully");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log(sqlFilename + " is already patched, skip");
|
debug(sqlFilename + " is already patched, skip");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,12 +250,12 @@ class Database {
|
||||||
// Remove all comments (--)
|
// Remove all comments (--)
|
||||||
let lines = text.split("\n");
|
let lines = text.split("\n");
|
||||||
lines = lines.filter((line) => {
|
lines = lines.filter((line) => {
|
||||||
return ! line.startsWith("--")
|
return ! line.startsWith("--");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Split statements by semicolon
|
// Split statements by semicolon
|
||||||
// Filter out empty line
|
// Filter out empty line
|
||||||
text = lines.join("\n")
|
text = lines.join("\n");
|
||||||
|
|
||||||
let statements = text.split(";")
|
let statements = text.split(";")
|
||||||
.map((statement) => {
|
.map((statement) => {
|
||||||
|
@ -238,7 +263,7 @@ class Database {
|
||||||
})
|
})
|
||||||
.filter((statement) => {
|
.filter((statement) => {
|
||||||
return statement !== "";
|
return statement !== "";
|
||||||
})
|
});
|
||||||
|
|
||||||
for (let statement of statements) {
|
for (let statement of statements) {
|
||||||
await R.exec(statement);
|
await R.exec(statement);
|
||||||
|
@ -284,7 +309,7 @@ class Database {
|
||||||
*/
|
*/
|
||||||
static backup(version) {
|
static backup(version) {
|
||||||
if (! this.backupPath) {
|
if (! this.backupPath) {
|
||||||
console.info("Backup the db")
|
console.info("Backup the db");
|
||||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||||
fs.copyFileSync(Database.path, this.backupPath);
|
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 dayjs = require("dayjs");
|
||||||
const utc = require("dayjs/plugin/utc")
|
const utc = require("dayjs/plugin/utc");
|
||||||
let timezone = require("dayjs/plugin/timezone")
|
let timezone = require("dayjs/plugin/timezone");
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone);
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,6 +13,15 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
*/
|
*/
|
||||||
class Heartbeat extends BeanModel {
|
class Heartbeat extends BeanModel {
|
||||||
|
|
||||||
|
toPublicJSON() {
|
||||||
|
return {
|
||||||
|
status: this.status,
|
||||||
|
time: this.time,
|
||||||
|
msg: "", // Hide for public
|
||||||
|
ping: this.ping,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
monitorID: this.monitor_id,
|
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 https = require("https");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const utc = require("dayjs/plugin/utc")
|
const utc = require("dayjs/plugin/utc");
|
||||||
let timezone = require("dayjs/plugin/timezone")
|
let timezone = require("dayjs/plugin/timezone");
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone);
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification")
|
const { Notification } = require("../notification");
|
||||||
const version = require("../../package.json").version;
|
const version = require("../../package.json").version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,13 +20,28 @@ const version = require("../../package.json").version;
|
||||||
* 2 = PENDING
|
* 2 = PENDING
|
||||||
*/
|
*/
|
||||||
class Monitor extends BeanModel {
|
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() {
|
async toJSON() {
|
||||||
|
|
||||||
let notificationIDList = {};
|
let notificationIDList = {};
|
||||||
|
|
||||||
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
||||||
this.id,
|
this.id,
|
||||||
])
|
]);
|
||||||
|
|
||||||
for (let bean of list) {
|
for (let bean of list) {
|
||||||
notificationIDList[bean.notification_id] = true;
|
notificationIDList[bean.notification_id] = true;
|
||||||
|
@ -64,7 +79,7 @@ class Monitor extends BeanModel {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
getIgnoreTls() {
|
getIgnoreTls() {
|
||||||
return Boolean(this.ignoreTls)
|
return Boolean(this.ignoreTls);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,12 +109,12 @@ class Monitor extends BeanModel {
|
||||||
if (! previousBeat) {
|
if (! previousBeat) {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
this.id,
|
this.id,
|
||||||
])
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFirstBeat = !previousBeat;
|
const isFirstBeat = !previousBeat;
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat")
|
let bean = R.dispense("heartbeat");
|
||||||
bean.monitor_id = this.id;
|
bean.monitor_id = this.id;
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTime(dayjs.utc());
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
|
@ -135,7 +150,7 @@ class Monitor extends BeanModel {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
bean.msg = `${res.status} - ${res.statusText}`
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
// Check certificate if https is used
|
// Check certificate if https is used
|
||||||
|
@ -145,12 +160,12 @@ class Monitor extends BeanModel {
|
||||||
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message !== "No TLS certificate in response") {
|
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") {
|
if (this.type === "http") {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
@ -160,26 +175,26 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
// Convert to string for object/array
|
// Convert to string for object/array
|
||||||
if (typeof data !== "string") {
|
if (typeof data !== "string") {
|
||||||
data = JSON.stringify(data)
|
data = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.includes(this.keyword)) {
|
if (data.includes(this.keyword)) {
|
||||||
bean.msg += ", keyword is found"
|
bean.msg += ", keyword is found";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} 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") {
|
} else if (this.type === "port") {
|
||||||
bean.ping = await tcping(this.hostname, this.port);
|
bean.ping = await tcping(this.hostname, this.port);
|
||||||
bean.msg = ""
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
|
||||||
} else if (this.type === "ping") {
|
} else if (this.type === "ping") {
|
||||||
bean.ping = await ping(this.hostname);
|
bean.ping = await ping(this.hostname);
|
||||||
bean.msg = ""
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else if (this.type === "dns") {
|
} else if (this.type === "dns") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
@ -199,7 +214,7 @@ class Monitor extends BeanModel {
|
||||||
dnsRes.forEach(record => {
|
dnsRes.forEach(record => {
|
||||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||||
});
|
});
|
||||||
dnsMessage = dnsMessage.slice(0, -2)
|
dnsMessage = dnsMessage.slice(0, -2);
|
||||||
} else if (this.dns_resolve_type == "NS") {
|
} else if (this.dns_resolve_type == "NS") {
|
||||||
dnsMessage += "Servers: ";
|
dnsMessage += "Servers: ";
|
||||||
dnsMessage += dnsRes.join(" | ");
|
dnsMessage += dnsRes.join(" | ");
|
||||||
|
@ -209,7 +224,7 @@ class Monitor extends BeanModel {
|
||||||
dnsRes.forEach(record => {
|
dnsRes.forEach(record => {
|
||||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
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) {
|
if (this.dnsLastResult !== dnsMessage) {
|
||||||
|
@ -272,20 +287,20 @@ class Monitor extends BeanModel {
|
||||||
if (!isFirstBeat || bean.status === DOWN) {
|
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 ", [
|
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||||
this.id,
|
this.id,
|
||||||
])
|
]);
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
if (bean.status === UP) {
|
if (bean.status === UP) {
|
||||||
text = "✅ Up"
|
text = "✅ Up";
|
||||||
} else {
|
} else {
|
||||||
text = "🔴 Down"
|
text = "🔴 Down";
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||||
|
|
||||||
for (let notification of notificationList) {
|
for (let notification of notificationList) {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
console.error("Cannot send notification to " + notification.name);
|
console.error("Cannot send notification to " + notification.name);
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
@ -300,18 +315,18 @@ class Monitor extends BeanModel {
|
||||||
let beatInterval = this.interval;
|
let beatInterval = this.interval;
|
||||||
|
|
||||||
if (bean.status === UP) {
|
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) {
|
} else if (bean.status === PENDING) {
|
||||||
if (this.retryInterval !== this.interval) {
|
if (this.retryInterval !== this.interval) {
|
||||||
beatInterval = this.retryInterval;
|
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 {
|
} 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());
|
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);
|
await R.store(bean);
|
||||||
prometheus.update(bean, tlsInfo);
|
prometheus.update(bean, tlsInfo);
|
||||||
|
@ -322,7 +337,7 @@ class Monitor extends BeanModel {
|
||||||
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
};
|
||||||
|
|
||||||
beat();
|
beat();
|
||||||
}
|
}
|
||||||
|
@ -415,7 +430,7 @@ class Monitor extends BeanModel {
|
||||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||||
* @param duration : int Hours
|
* @param duration : int Hours
|
||||||
*/
|
*/
|
||||||
static async sendUptime(duration, io, monitorID, userID) {
|
static async calcUptime(duration, monitorID) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
||||||
|
@ -468,12 +483,21 @@ class Monitor extends BeanModel {
|
||||||
} else {
|
} else {
|
||||||
// Handle new monitor with only one beat, because the beat's duration = 0
|
// Handle new monitor with only one beat, because the beat's duration = 0
|
||||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
||||||
console.log("here???" + status);
|
|
||||||
if (status === UP) {
|
if (status === UP) {
|
||||||
uptime = 1;
|
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);
|
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;
|
150
server/routers/api-router.js
Normal file
150
server/routers/api-router.js
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
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;
|
375
server/server.js
375
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() + "");
|
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
||||||
await R.store(jwtSecretBean);
|
await R.store(jwtSecretBean);
|
||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.tcping = function (hostname, port) {
|
exports.tcping = function (hostname, port) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -44,7 +44,7 @@ exports.tcping = function (hostname, port) {
|
||||||
resolve(Math.round(data.max));
|
resolve(Math.round(data.max));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.ping = async (hostname) => {
|
exports.ping = async (hostname) => {
|
||||||
try {
|
try {
|
||||||
|
@ -57,7 +57,7 @@ exports.ping = async (hostname) => {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.pingAsync = function (hostname, ipv6 = false) {
|
exports.pingAsync = function (hostname, ipv6 = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -69,13 +69,13 @@ exports.pingAsync = function (hostname, ipv6 = false) {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else if (ms === null) {
|
} else if (ms === null) {
|
||||||
reject(new Error(stdout))
|
reject(new Error(stdout));
|
||||||
} else {
|
} else {
|
||||||
resolve(Math.round(ms))
|
resolve(Math.round(ms));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
|
@ -98,8 +98,8 @@ exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.setting = async function (key) {
|
exports.setting = async function (key) {
|
||||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||||
|
@ -108,29 +108,29 @@ exports.setting = async function (key) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const v = JSON.parse(value);
|
const v = JSON.parse(value);
|
||||||
debug(`Get Setting: ${key}: ${v}`)
|
debug(`Get Setting: ${key}: ${v}`);
|
||||||
return v;
|
return v;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.setSetting = async function (key, value) {
|
exports.setSetting = async function (key, value) {
|
||||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
key,
|
key,
|
||||||
])
|
]);
|
||||||
if (!bean) {
|
if (!bean) {
|
||||||
bean = R.dispense("setting")
|
bean = R.dispense("setting");
|
||||||
bean.key = key;
|
bean.key = key;
|
||||||
}
|
}
|
||||||
bean.value = JSON.stringify(value);
|
bean.value = JSON.stringify(value);
|
||||||
await R.store(bean)
|
await R.store(bean);
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.getSettings = async function (type) {
|
exports.getSettings = async function (type) {
|
||||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||||
type,
|
type,
|
||||||
])
|
]);
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ exports.getSettings = async function (type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.setSettings = async function (type, data) {
|
exports.setSettings = async function (type, data) {
|
||||||
let keyList = Object.keys(data);
|
let keyList = Object.keys(data);
|
||||||
|
@ -163,12 +163,12 @@ exports.setSettings = async function (type, data) {
|
||||||
|
|
||||||
if (bean.type === type) {
|
if (bean.type === type) {
|
||||||
bean.value = JSON.stringify(data[key]);
|
bean.value = JSON.stringify(data[key]);
|
||||||
promiseList.push(R.store(bean))
|
promiseList.push(R.store(bean));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promiseList);
|
await Promise.all(promiseList);
|
||||||
}
|
};
|
||||||
|
|
||||||
// ssl-checker by @dyaa
|
// ssl-checker by @dyaa
|
||||||
// param: res - response object from axios
|
// param: res - response object from axios
|
||||||
|
@ -218,7 +218,7 @@ exports.checkCertificate = function (res) {
|
||||||
issuer,
|
issuer,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
// Check if the provided status code is within the accepted ranges
|
// Check if the provided status code is within the accepted ranges
|
||||||
// Param: status - the status code to check
|
// Param: status - the status code to check
|
||||||
|
@ -247,7 +247,7 @@ exports.checkStatusCode = function (status, accepted_codes) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.getTotalClientInRoom = (io, roomName) => {
|
exports.getTotalClientInRoom = (io, roomName) => {
|
||||||
|
|
||||||
|
@ -270,7 +270,7 @@ exports.getTotalClientInRoom = (io, roomName) => {
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.genSecret = () => {
|
exports.genSecret = () => {
|
||||||
let secret = "";
|
let secret = "";
|
||||||
|
@ -280,4 +280,21 @@ exports.genSecret = () => {
|
||||||
secret += chars.charAt(Math.floor(Math.random() * charsLength));
|
secret += chars.charAt(Math.floor(Math.random() * charsLength));
|
||||||
}
|
}
|
||||||
return secret;
|
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 {
|
.shadow-box {
|
||||||
|
&:not(.alert) {
|
||||||
background-color: $dark-bg;
|
background-color: $dark-bg;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-check-input {
|
.form-check-input {
|
||||||
background-color: $dark-bg2;
|
background-color: $dark-bg2;
|
||||||
|
@ -255,6 +257,18 @@ h2 {
|
||||||
background-color: $dark-bg;
|
background-color: $dark-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monitor-list {
|
||||||
|
.item {
|
||||||
|
&:hover {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 550px) {
|
@media (max-width: 550px) {
|
||||||
.table-shadow-box {
|
.table-shadow-box {
|
||||||
tbody {
|
tbody {
|
||||||
|
@ -268,6 +282,16 @@ h2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
&.bg-info,
|
||||||
|
&.bg-warning,
|
||||||
|
&.bg-danger,
|
||||||
|
&.bg-light {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -288,3 +312,119 @@ h2 {
|
||||||
transform: translateY(50px);
|
transform: translateY(50px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-fade-right-enter-active {
|
||||||
|
transition: all 0.2s $easing-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-right-leave-active {
|
||||||
|
transition: all 0.2s $easing-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-right-enter-from,
|
||||||
|
.slide-fade-right-leave-to {
|
||||||
|
transform: translateX(50px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-list {
|
||||||
|
&.scrollbar {
|
||||||
|
min-height: calc(100vh - 240px);
|
||||||
|
max-height: calc(100vh - 30px);
|
||||||
|
overflow-y: auto;
|
||||||
|
position: sticky;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 13px 15px 10px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all ease-in-out 0.15s;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $highlight-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #cdf8f4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
color: #122f21;
|
||||||
|
background-color: $primary;
|
||||||
|
border-color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
color: #055160;
|
||||||
|
background-color: #cff4fc;
|
||||||
|
border-color: #cff4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
color: #842029;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border-color: #f8d7da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #4caf50;
|
||||||
|
border-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable=true] {
|
||||||
|
transition: all $easing-in 0.2s;
|
||||||
|
background-color: rgba(239, 239, 239, 0.7);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0 solid #eee;
|
||||||
|
background-color: rgba(245, 245, 245, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(239, 239, 239, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: rgba(239, 239, 239, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
&::after {
|
||||||
|
margin-left: 5px;
|
||||||
|
content: "🖊️";
|
||||||
|
font-size: 13px;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
transition: all $easing-in 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-image-crop-upload .vicp-wrap {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,10 @@ export default {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
heartbeatList: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -38,8 +42,15 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If heartbeatList is null, get it from $root.heartbeatList
|
||||||
|
*/
|
||||||
beatList() {
|
beatList() {
|
||||||
return this.$root.heartbeatList[this.monitorId]
|
if (this.heartbeatList === null) {
|
||||||
|
return this.$root.heartbeatList[this.monitorId];
|
||||||
|
} else {
|
||||||
|
return this.heartbeatList;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
shortBeatList() {
|
shortBeatList() {
|
||||||
|
@ -118,9 +129,11 @@ export default {
|
||||||
window.removeEventListener("resize", this.resize);
|
window.removeEventListener("resize", this.resize);
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
if (this.heartbeatList === null) {
|
||||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||||
this.$root.heartbeatList[this.monitorId] = [];
|
this.$root.heartbeatList[this.monitorId] = [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
|
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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>
|
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -163,56 +163,6 @@ export default {
|
||||||
max-width: 15em;
|
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 {
|
.monitorItem {
|
||||||
width: 100%;
|
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>
|
|
@ -43,6 +43,6 @@ export const i18n = createI18n({
|
||||||
locale: localStorage.locale || "en",
|
locale: localStorage.locale || "en",
|
||||||
fallbackLocale: "en",
|
fallbackLocale: "en",
|
||||||
silentFallbackWarn: true,
|
silentFallbackWarn: true,
|
||||||
silentTranslationWarn: false,
|
silentTranslationWarn: true,
|
||||||
messages: languageList,
|
messages: languageList,
|
||||||
});
|
});
|
||||||
|
|
31
src/icon.js
31
src/icon.js
|
@ -1,4 +1,8 @@
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
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 {
|
import {
|
||||||
faArrowAltCircleUp,
|
faArrowAltCircleUp,
|
||||||
faCog,
|
faCog,
|
||||||
|
@ -12,13 +16,19 @@ import {
|
||||||
faSearch,
|
faSearch,
|
||||||
faTachometerAlt,
|
faTachometerAlt,
|
||||||
faTimes,
|
faTimes,
|
||||||
faTrash
|
faTimesCircle,
|
||||||
|
faTrash,
|
||||||
|
faCheckCircle,
|
||||||
|
faStream,
|
||||||
|
faSave,
|
||||||
|
faExclamationCircle,
|
||||||
|
faBullhorn,
|
||||||
|
faArrowsAltV,
|
||||||
|
faUnlink,
|
||||||
|
faQuestionCircle,
|
||||||
|
faImages, faUpload,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} 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(
|
library.add(
|
||||||
faArrowAltCircleUp,
|
faArrowAltCircleUp,
|
||||||
faCog,
|
faCog,
|
||||||
|
@ -32,7 +42,18 @@ library.add(
|
||||||
faSearch,
|
faSearch,
|
||||||
faTachometerAlt,
|
faTachometerAlt,
|
||||||
faTimes,
|
faTimes,
|
||||||
|
faTimesCircle,
|
||||||
faTrash,
|
faTrash,
|
||||||
|
faCheckCircle,
|
||||||
|
faStream,
|
||||||
|
faSave,
|
||||||
|
faExclamationCircle,
|
||||||
|
faBullhorn,
|
||||||
|
faArrowsAltV,
|
||||||
|
faUnlink,
|
||||||
|
faQuestionCircle,
|
||||||
|
faImages,
|
||||||
|
faUpload,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
|
|
@ -18,6 +18,11 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
|
<li class="nav-item me-2">
|
||||||
|
<a href="/status-page" class="nav-link status-page">
|
||||||
|
<font-awesome-icon icon="stream" /> {{ $t("Status Page") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<router-link to="/dashboard" class="nav-link">
|
<router-link to="/dashboard" class="nav-link">
|
||||||
<font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }}
|
<font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }}
|
||||||
|
@ -81,7 +86,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -105,29 +110,29 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
|
||||||
if (this.$route.name === "root") {
|
|
||||||
this.$router.push("/dashboard")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
&.status-page {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bottom-nav {
|
.bottom-nav {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
12
src/main.js
12
src/main.js
|
@ -1,6 +1,7 @@
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
import { createApp, h } from "vue";
|
import { createApp, h } from "vue";
|
||||||
import Toast from "vue-toastification";
|
import Toast from "vue-toastification";
|
||||||
|
import contenteditable from "vue-contenteditable"
|
||||||
import "vue-toastification/dist/index.css";
|
import "vue-toastification/dist/index.css";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import "./assets/app.scss";
|
import "./assets/app.scss";
|
||||||
|
@ -10,6 +11,8 @@ import datetime from "./mixins/datetime";
|
||||||
import mobile from "./mixins/mobile";
|
import mobile from "./mixins/mobile";
|
||||||
import socket from "./mixins/socket";
|
import socket from "./mixins/socket";
|
||||||
import theme from "./mixins/theme";
|
import theme from "./mixins/theme";
|
||||||
|
import publicMixin from "./mixins/public";
|
||||||
|
|
||||||
import { router } from "./router";
|
import { router } from "./router";
|
||||||
import { appName } from "./util.ts";
|
import { appName } from "./util.ts";
|
||||||
|
|
||||||
|
@ -18,7 +21,8 @@ const app = createApp({
|
||||||
socket,
|
socket,
|
||||||
theme,
|
theme,
|
||||||
mobile,
|
mobile,
|
||||||
datetime
|
datetime,
|
||||||
|
publicMixin,
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -36,7 +40,7 @@ const options = {
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use(Toast, 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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
windowWidth: window.innerWidth,
|
windowWidth: window.innerWidth,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener("resize", this.onResize);
|
window.addEventListener("resize", this.onResize);
|
||||||
|
this.updateBody();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onResize() {
|
onResize() {
|
||||||
this.windowWidth = window.innerWidth;
|
this.windowWidth = window.innerWidth;
|
||||||
|
this.updateBody();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateBody() {
|
||||||
|
if (this.isMobile) {
|
||||||
|
document.body.classList.add("mobile");
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove("mobile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isMobile() {
|
isMobile() {
|
||||||
return this.windowWidth <= 767.98;
|
return this.windowWidth <= 767.98;
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
|
||||||
}
|
};
|
||||||
|
|
40
src/mixins/public.js
Normal file
40
src/mixins/public.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const env = process.env.NODE_ENV || "production";
|
||||||
|
|
||||||
|
// change the axios base url for development
|
||||||
|
if (env === "development" || localStorage.dev === "dev") {
|
||||||
|
axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
publicGroupList: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
publicMonitorList() {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
for (let group of this.publicGroupList) {
|
||||||
|
for (let monitor of group.monitorList) {
|
||||||
|
result[monitor.id] = monitor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
publicLastHeartbeatList() {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
for (let monitorID in this.publicMonitorList) {
|
||||||
|
if (this.lastHeartbeatList[monitorID]) {
|
||||||
|
result[monitorID] = this.lastHeartbeatList[monitorID];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,9 +1,14 @@
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
const toast = useToast()
|
const toast = useToast();
|
||||||
|
|
||||||
let socket;
|
let socket;
|
||||||
|
|
||||||
|
const noSocketIOPages = [
|
||||||
|
"/status-page",
|
||||||
|
"/"
|
||||||
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -14,6 +19,7 @@ export default {
|
||||||
firstConnect: true,
|
firstConnect: true,
|
||||||
connected: false,
|
connected: false,
|
||||||
connectCount: 0,
|
connectCount: 0,
|
||||||
|
initedSocketIO: false,
|
||||||
},
|
},
|
||||||
remember: (localStorage.remember !== "0"),
|
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.
|
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 +32,28 @@ export default {
|
||||||
certInfoList: {},
|
certInfoList: {},
|
||||||
notificationList: [],
|
notificationList: [],
|
||||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener("resize", this.onResize);
|
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://";
|
let protocol = (location.protocol === "https:") ? "wss://" : "ws://";
|
||||||
|
|
||||||
|
@ -51,7 +74,7 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("setup", (monitorID, data) => {
|
socket.on("setup", (monitorID, data) => {
|
||||||
this.$router.push("/setup")
|
this.$router.push("/setup");
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("autoLogin", (monitorID, data) => {
|
socket.on("autoLogin", (monitorID, data) => {
|
||||||
|
@ -83,7 +106,11 @@ export default {
|
||||||
this.heartbeatList[data.monitorID] = [];
|
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
|
// Add to important list if it is important
|
||||||
// Also toast
|
// Also toast
|
||||||
|
@ -105,7 +132,7 @@ export default {
|
||||||
this.importantHeartbeatList[data.monitorID] = [];
|
this.importantHeartbeatList[data.monitorID] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.importantHeartbeatList[data.monitorID].unshift(data)
|
this.importantHeartbeatList[data.monitorID].unshift(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -113,27 +140,27 @@ export default {
|
||||||
if (! (monitorID in this.heartbeatList) || overwrite) {
|
if (! (monitorID in this.heartbeatList) || overwrite) {
|
||||||
this.heartbeatList[monitorID] = data;
|
this.heartbeatList[monitorID] = data;
|
||||||
} else {
|
} else {
|
||||||
this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID])
|
this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("avgPing", (monitorID, data) => {
|
socket.on("avgPing", (monitorID, data) => {
|
||||||
this.avgPingList[monitorID] = data
|
this.avgPingList[monitorID] = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("uptime", (monitorID, type, data) => {
|
socket.on("uptime", (monitorID, type, data) => {
|
||||||
this.uptimeList[`${monitorID}_${type}`] = data
|
this.uptimeList[`${monitorID}_${type}`] = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("certInfo", (monitorID, data) => {
|
socket.on("certInfo", (monitorID, data) => {
|
||||||
this.certInfoList[monitorID] = JSON.parse(data)
|
this.certInfoList[monitorID] = JSON.parse(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("importantHeartbeatList", (monitorID, data, overwrite) => {
|
socket.on("importantHeartbeatList", (monitorID, data, overwrite) => {
|
||||||
if (! (monitorID in this.importantHeartbeatList) || overwrite) {
|
if (! (monitorID in this.importantHeartbeatList) || overwrite) {
|
||||||
this.importantHeartbeatList[monitorID] = data;
|
this.importantHeartbeatList[monitorID] = data;
|
||||||
} else {
|
} else {
|
||||||
this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID])
|
this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -145,26 +172,26 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
console.log("disconnect")
|
console.log("disconnect");
|
||||||
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||||
this.socket.connected = false;
|
this.socket.connected = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
console.log("connect")
|
console.log("connect");
|
||||||
this.socket.connectCount++;
|
this.socket.connectCount++;
|
||||||
this.socket.connected = true;
|
this.socket.connected = true;
|
||||||
|
|
||||||
// Reset Heartbeat list if it is re-connect
|
// Reset Heartbeat list if it is re-connect
|
||||||
if (this.socket.connectCount >= 2) {
|
if (this.socket.connectCount >= 2) {
|
||||||
this.clearData()
|
this.clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = this.storage().token;
|
let token = this.storage().token;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
if (token !== "autoLogin") {
|
if (token !== "autoLogin") {
|
||||||
this.loginByToken(token)
|
this.loginByToken(token);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Timeout if it is not actually auto login
|
// Timeout if it is not actually auto login
|
||||||
|
@ -185,8 +212,6 @@ export default {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
storage() {
|
storage() {
|
||||||
return (this.remember) ? localStorage : sessionStorage;
|
return (this.remember) ? localStorage : sessionStorage;
|
||||||
},
|
},
|
||||||
|
@ -210,7 +235,7 @@ export default {
|
||||||
token,
|
token,
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
if (res.tokenRequired) {
|
if (res.tokenRequired) {
|
||||||
callback(res)
|
callback(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@ -219,11 +244,11 @@ export default {
|
||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
|
|
||||||
// Trigger Chrome Save Password
|
// Trigger Chrome Save Password
|
||||||
history.pushState({}, "")
|
history.pushState({}, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(res)
|
callback(res);
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
loginByToken(token) {
|
loginByToken(token) {
|
||||||
|
@ -231,11 +256,11 @@ export default {
|
||||||
this.allowLoginDialog = true;
|
this.allowLoginDialog = true;
|
||||||
|
|
||||||
if (! res.ok) {
|
if (! res.ok) {
|
||||||
this.logout()
|
this.logout();
|
||||||
} else {
|
} else {
|
||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
@ -243,68 +268,68 @@ export default {
|
||||||
this.socket.token = null;
|
this.socket.token = null;
|
||||||
this.loggedIn = false;
|
this.loggedIn = false;
|
||||||
|
|
||||||
this.clearData()
|
this.clearData();
|
||||||
},
|
},
|
||||||
|
|
||||||
prepare2FA(callback) {
|
prepare2FA(callback) {
|
||||||
socket.emit("prepare2FA", callback)
|
socket.emit("prepare2FA", callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
save2FA(secret, callback) {
|
save2FA(secret, callback) {
|
||||||
socket.emit("save2FA", callback)
|
socket.emit("save2FA", callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
disable2FA(callback) {
|
disable2FA(callback) {
|
||||||
socket.emit("disable2FA", callback)
|
socket.emit("disable2FA", callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
verifyToken(token, callback) {
|
verifyToken(token, callback) {
|
||||||
socket.emit("verifyToken", token, callback)
|
socket.emit("verifyToken", token, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
twoFAStatus(callback) {
|
twoFAStatus(callback) {
|
||||||
socket.emit("twoFAStatus", callback)
|
socket.emit("twoFAStatus", callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
getMonitorList(callback) {
|
getMonitorList(callback) {
|
||||||
socket.emit("getMonitorList", callback)
|
socket.emit("getMonitorList", callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
add(monitor, callback) {
|
add(monitor, callback) {
|
||||||
socket.emit("add", monitor, callback)
|
socket.emit("add", monitor, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteMonitor(monitorID, callback) {
|
deleteMonitor(monitorID, callback) {
|
||||||
socket.emit("deleteMonitor", monitorID, callback)
|
socket.emit("deleteMonitor", monitorID, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearData() {
|
clearData() {
|
||||||
console.log("reset heartbeat list")
|
console.log("reset heartbeat list");
|
||||||
this.heartbeatList = {}
|
this.heartbeatList = {};
|
||||||
this.importantHeartbeatList = {}
|
this.importantHeartbeatList = {};
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadBackup(uploadedJSON, importHandle, callback) {
|
uploadBackup(uploadedJSON, importHandle, callback) {
|
||||||
socket.emit("uploadBackup", uploadedJSON, importHandle, callback)
|
socket.emit("uploadBackup", uploadedJSON, importHandle, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearEvents(monitorID, callback) {
|
clearEvents(monitorID, callback) {
|
||||||
socket.emit("clearEvents", monitorID, callback)
|
socket.emit("clearEvents", monitorID, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearHeartbeats(monitorID, callback) {
|
clearHeartbeats(monitorID, callback) {
|
||||||
socket.emit("clearHeartbeats", monitorID, callback)
|
socket.emit("clearHeartbeats", monitorID, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearStatistics(callback) {
|
clearStatistics(callback) {
|
||||||
socket.emit("clearStatistics", callback)
|
socket.emit("clearStatistics", callback);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
lastHeartbeatList() {
|
lastHeartbeatList() {
|
||||||
let result = {}
|
let result = {};
|
||||||
|
|
||||||
for (let monitorID in this.heartbeatList) {
|
for (let monitorID in this.heartbeatList) {
|
||||||
let index = this.heartbeatList[monitorID].length - 1;
|
let index = this.heartbeatList[monitorID].length - 1;
|
||||||
|
@ -315,15 +340,15 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
statusList() {
|
statusList() {
|
||||||
let result = {}
|
let result = {};
|
||||||
|
|
||||||
let unknown = {
|
let unknown = {
|
||||||
text: "Unknown",
|
text: "Unknown",
|
||||||
color: "secondary",
|
color: "secondary",
|
||||||
}
|
};
|
||||||
|
|
||||||
for (let monitorID in this.lastHeartbeatList) {
|
for (let monitorID in this.lastHeartbeatList) {
|
||||||
let lastHeartBeat = this.lastHeartbeatList[monitorID]
|
let lastHeartBeat = this.lastHeartbeatList[monitorID];
|
||||||
|
|
||||||
if (! lastHeartBeat) {
|
if (! lastHeartBeat) {
|
||||||
result[monitorID] = unknown;
|
result[monitorID] = unknown;
|
||||||
|
@ -356,14 +381,22 @@ export default {
|
||||||
// Reload the SPA if the server version is changed.
|
// Reload the SPA if the server version is changed.
|
||||||
"info.version"(to, from) {
|
"info.version"(to, from) {
|
||||||
if (from && from !== to) {
|
if (from && from !== to) {
|
||||||
window.location.reload()
|
window.location.reload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
remember() {
|
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",
|
system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
|
||||||
userTheme: localStorage.theme,
|
userTheme: localStorage.theme,
|
||||||
userHeartbeatBar: localStorage.heartbeatBarTheme,
|
userHeartbeatBar: localStorage.heartbeatBarTheme,
|
||||||
|
statusPageTheme: "light",
|
||||||
|
path: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -25,14 +27,28 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
theme() {
|
theme() {
|
||||||
|
|
||||||
|
// Entry no need dark
|
||||||
|
if (this.path === "") {
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.path === "/status-page") {
|
||||||
|
return this.statusPageTheme;
|
||||||
|
} else {
|
||||||
if (this.userTheme === "auto") {
|
if (this.userTheme === "auto") {
|
||||||
return this.system;
|
return this.system;
|
||||||
}
|
}
|
||||||
return this.userTheme;
|
return this.userTheme;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
"$route.fullPath"(path) {
|
||||||
|
this.path = path;
|
||||||
|
},
|
||||||
|
|
||||||
userTheme(to, from) {
|
userTheme(to, from) {
|
||||||
localStorage.theme = to;
|
localStorage.theme = to;
|
||||||
},
|
},
|
||||||
|
@ -62,5 +78,5 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
20
src/pages/Entry.vue
Normal file
20
src/pages/Entry.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async mounted() {
|
||||||
|
let entryPage = (await axios.get("/api/entry-page")).data;
|
||||||
|
|
||||||
|
if (entryPage === "statusPage") {
|
||||||
|
this.$router.push("/status-page");
|
||||||
|
} else {
|
||||||
|
this.$router.push("/dashboard");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -83,6 +83,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit">
|
||||||
{{ $t("Save") }}
|
{{ $t("Save") }}
|
||||||
|
@ -207,17 +225,14 @@
|
||||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||||
{{ $t("Setup Notification") }}
|
{{ $t("Setup Notification") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
<h2 class="mt-5">Info</h2>
|
||||||
<div class="container-fluid">
|
|
||||||
Uptime Kuma -
|
{{ $t("Version") }}: {{ $root.info.version }} <br />
|
||||||
{{ $t("Version") }}: {{ $root.info.version }} -
|
|
||||||
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
|
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<NotificationDialog ref="notificationDialog" />
|
<NotificationDialog ref="notificationDialog" />
|
||||||
<TwoFADialog ref="TwoFADialog" />
|
<TwoFADialog ref="TwoFADialog" />
|
||||||
|
@ -397,6 +412,10 @@ export default {
|
||||||
this.settings.searchEngineIndex = false;
|
this.settings.searchEngineIndex = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.settings.entryPage === undefined) {
|
||||||
|
this.settings.entryPage = "dashboard";
|
||||||
|
}
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
647
src/pages/StatusPage.vue
Normal file
647
src/pages/StatusPage.vue
Normal file
|
@ -0,0 +1,647 @@
|
||||||
|
<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" />
|
||||||
|
Edit Status Page
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="/dashboard" class="btn btn-info">
|
||||||
|
<font-awesome-icon icon="tachometer-alt" />
|
||||||
|
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" />
|
||||||
|
No Services
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="allUp">
|
||||||
|
<font-awesome-icon icon="check-circle" class="ok" />
|
||||||
|
All Systems Operational
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="partialDown">
|
||||||
|
<font-awesome-icon icon="exclamation-circle" class="warning" />
|
||||||
|
Partially Degraded Service
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="allDown">
|
||||||
|
<font-awesome-icon icon="times-circle" class="danger" />
|
||||||
|
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" />
|
||||||
|
Add Group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<label>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>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
|
||||||
|
👀 Nothing here, please add a group or a monitor.
|
||||||
|
</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 localStorage);
|
||||||
|
|
||||||
|
// 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,20 +8,25 @@ import EditMonitor from "./pages/EditMonitor.vue";
|
||||||
import List from "./pages/List.vue";
|
import List from "./pages/List.vue";
|
||||||
import Settings from "./pages/Settings.vue";
|
import Settings from "./pages/Settings.vue";
|
||||||
import Setup from "./pages/Setup.vue";
|
import Setup from "./pages/Setup.vue";
|
||||||
|
import StatusPage from "./pages/StatusPage.vue";
|
||||||
|
import Entry from "./pages/Entry.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
component: Entry,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/dashboard",
|
||||||
component: Layout,
|
component: Layout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: "root",
|
|
||||||
path: "",
|
path: "",
|
||||||
component: Dashboard,
|
component: Dashboard,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: "DashboardHome",
|
name: "DashboardHome",
|
||||||
path: "/dashboard",
|
path: "",
|
||||||
component: DashboardHome,
|
component: DashboardHome,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
@ -54,15 +59,17 @@ const routes = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/setup",
|
path: "/setup",
|
||||||
component: Setup,
|
component: Setup,
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
|
path: "/status-page",
|
||||||
|
component: StatusPage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
linkActiveClass: "active",
|
linkActiveClass: "active",
|
||||||
|
|
|
@ -3,8 +3,8 @@ import timezone from "dayjs/plugin/timezone";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import timezones from "timezones-list";
|
import timezones from "timezones-list";
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
function getTimezoneOffset(timeZone) {
|
function getTimezoneOffset(timeZone) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -28,7 +28,7 @@ export function timezoneList() {
|
||||||
name: `(UTC${display}) ${timezone.tzCode}`,
|
name: `(UTC${display}) ${timezone.tzCode}`,
|
||||||
value: timezone.tzCode,
|
value: timezone.tzCode,
|
||||||
time: getTimezoneOffset(timezone.tzCode),
|
time: getTimezoneOffset(timezone.tzCode),
|
||||||
})
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Skip Timezone: " + timezone.tzCode);
|
console.log("Skip Timezone: " + timezone.tzCode);
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ export function timezoneList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
})
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
10
src/util.js
10
src/util.js
|
@ -1,10 +1,13 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
// Common Util for frontend and backend
|
// 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
|
// Backend uses the compiled file util.js
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
// Need to run "tsc" to compile if there are any changes.
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
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 = require("dayjs");
|
||||||
const dayjs = _dayjs;
|
const dayjs = _dayjs;
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
|
@ -12,6 +15,9 @@ exports.appName = "Uptime Kuma";
|
||||||
exports.DOWN = 0;
|
exports.DOWN = 0;
|
||||||
exports.UP = 1;
|
exports.UP = 1;
|
||||||
exports.PENDING = 2;
|
exports.PENDING = 2;
|
||||||
|
exports.STATUS_PAGE_ALL_DOWN = 0;
|
||||||
|
exports.STATUS_PAGE_ALL_UP = 1;
|
||||||
|
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
|
||||||
function flipStatus(s) {
|
function flipStatus(s) {
|
||||||
if (s === exports.UP) {
|
if (s === exports.UP) {
|
||||||
return exports.DOWN;
|
return exports.DOWN;
|
||||||
|
|
10
src/util.ts
10
src/util.ts
|
@ -1,7 +1,10 @@
|
||||||
// Common Util for frontend and backend
|
// 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
|
// Backend uses the compiled file util.js
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
// Need to run "tsc" to compile if there are any changes.
|
|
||||||
|
|
||||||
import * as _dayjs from "dayjs";
|
import * as _dayjs from "dayjs";
|
||||||
const dayjs = _dayjs;
|
const dayjs = _dayjs;
|
||||||
|
@ -12,6 +15,11 @@ export const DOWN = 0;
|
||||||
export const UP = 1;
|
export const UP = 1;
|
||||||
export const PENDING = 2;
|
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) {
|
export function flipStatus(s: number) {
|
||||||
if (s === UP) {
|
if (s === UP) {
|
||||||
return DOWN;
|
return DOWN;
|
||||||
|
|
Loading…
Reference in a new issue