commit
359fe52c2e
@ -0,0 +1,34 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: Auto Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [14.x, 15.x, 16.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
- run: npm test
|
||||||
|
env:
|
||||||
|
HEADLESS_TEST: 1
|
||||||
|
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
Binary file not shown.
@ -0,0 +1,7 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD retry_interval INTEGER default 0 not null;
|
||||||
|
|
||||||
|
COMMIT;
|
@ -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;
|
@ -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;
|
@ -0,0 +1,7 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD push_token VARCHAR(20) DEFAULT NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
@ -0,0 +1,19 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
CREATE TABLE tag (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
color VARCHAR(255) NOT NULL,
|
||||||
|
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE monitor_tag (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
monitor_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER NOT NULL,
|
||||||
|
value TEXT,
|
||||||
|
CONSTRAINT FK_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX monitor_tag_monitor_id_index ON monitor_tag (monitor_id);
|
||||||
|
CREATE INDEX monitor_tag_tag_id_index ON monitor_tag (tag_id);
|
@ -0,0 +1,8 @@
|
|||||||
|
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||||
|
FROM node:14-alpine3.12
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
|
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
|
||||||
|
pip3 --no-cache-dir install apprise && \
|
||||||
|
rm -rf /root/.cache
|
@ -0,0 +1,12 @@
|
|||||||
|
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||||
|
# If the image changed, the second stage image should be changed too
|
||||||
|
FROM node:14-buster-slim
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||||
|
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specific --no-install-recommends to skip them, make the base even smaller than alpine!
|
||||||
|
RUN apt update && \
|
||||||
|
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||||
|
sqlite3 iputils-ping util-linux dumb-init && \
|
||||||
|
pip3 --no-cache-dir install apprise && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# set -e Exit the script if an error happens
|
||||||
|
set -e
|
||||||
|
PUID=${PUID=0}
|
||||||
|
PGID=${PGID=0}
|
||||||
|
|
||||||
|
files_ownership () {
|
||||||
|
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
|
||||||
|
# -R Recursively descends the specified directories
|
||||||
|
# -c Like verbose but report only when a change is made
|
||||||
|
chown -hRc "$PUID":"$PGID" /app/data
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "==> Performing startup jobs and maintenance tasks"
|
||||||
|
files_ownership
|
||||||
|
|
||||||
|
echo "==> Starting application with user $PUID group $PGID"
|
||||||
|
|
||||||
|
# --clear-groups Clear supplementary groups.
|
||||||
|
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@"
|
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Author: Stefan Buck
|
||||||
|
# License: MIT
|
||||||
|
# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# This script accepts the following parameters:
|
||||||
|
#
|
||||||
|
# * owner
|
||||||
|
# * repo
|
||||||
|
# * tag
|
||||||
|
# * filename
|
||||||
|
# * github_api_token
|
||||||
|
#
|
||||||
|
# Script to upload a release asset using the GitHub API v3.
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
#
|
||||||
|
# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip
|
||||||
|
#
|
||||||
|
|
||||||
|
# Check dependencies.
|
||||||
|
set -e
|
||||||
|
xargs=$(which gxargs || which xargs)
|
||||||
|
|
||||||
|
# Validate settings.
|
||||||
|
[ "$TRACE" ] && set -x
|
||||||
|
|
||||||
|
CONFIG=$@
|
||||||
|
|
||||||
|
for line in $CONFIG; do
|
||||||
|
eval "$line"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Define variables.
|
||||||
|
GH_API="https://api.github.com"
|
||||||
|
GH_REPO="$GH_API/repos/$owner/$repo"
|
||||||
|
GH_TAGS="$GH_REPO/releases/tags/$tag"
|
||||||
|
AUTH="Authorization: token $github_api_token"
|
||||||
|
WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
|
||||||
|
CURL_ARGS="-LJO#"
|
||||||
|
|
||||||
|
if [[ "$tag" == 'LATEST' ]]; then
|
||||||
|
GH_TAGS="$GH_REPO/releases/latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate token.
|
||||||
|
curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
|
||||||
|
|
||||||
|
# Read asset tags.
|
||||||
|
response=$(curl -sH "$AUTH" $GH_TAGS)
|
||||||
|
|
||||||
|
# Get ID of the asset based on given filename.
|
||||||
|
eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
|
||||||
|
[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
|
||||||
|
|
||||||
|
# Upload asset
|
||||||
|
echo "Uploading asset... "
|
||||||
|
|
||||||
|
# Construct url
|
||||||
|
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
|
||||||
|
|
||||||
|
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET
|
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
"launch": {
|
||||||
|
"headless": process.env.HEADLESS_TEST || false,
|
||||||
|
"userDataDir": "./data/test-chrome-profile",
|
||||||
|
}
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 9.5 KiB |
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "Uptime Kuma",
|
||||||
|
"short_name": "Uptime Kuma",
|
||||||
|
"start_url": "/",
|
||||||
|
"background_color": "#fff",
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -0,0 +1,13 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class Tag extends BeanModel {
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this._id,
|
||||||
|
name: this._name,
|
||||||
|
color: this._color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Tag;
|
@ -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();
|
@ -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;
|
@ -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;
|
@ -0,0 +1,124 @@
|
|||||||
|
const NotificationProvider = require("./notification-provider");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
class Teams extends NotificationProvider {
|
||||||
|
name = "teams";
|
||||||
|
|
||||||
|
_statusMessageFactory = (status, monitorName) => {
|
||||||
|
if (status === DOWN) {
|
||||||
|
return `🔴 Application [${monitorName}] went down`;
|
||||||
|
} else if (status === UP) {
|
||||||
|
return `✅ Application [${monitorName}] is back online`;
|
||||||
|
}
|
||||||
|
return "Notification";
|
||||||
|
};
|
||||||
|
|
||||||
|
_getThemeColor = (status) => {
|
||||||
|
if (status === DOWN) {
|
||||||
|
return "ff0000";
|
||||||
|
}
|
||||||
|
if (status === UP) {
|
||||||
|
return "00e804";
|
||||||
|
}
|
||||||
|
return "008cff";
|
||||||
|
};
|
||||||
|
|
||||||
|
_notificationPayloadFactory = ({
|
||||||
|
status,
|
||||||
|
monitorMessage,
|
||||||
|
monitorName,
|
||||||
|
monitorUrl,
|
||||||
|
}) => {
|
||||||
|
const notificationMessage = this._statusMessageFactory(
|
||||||
|
status,
|
||||||
|
monitorName
|
||||||
|
);
|
||||||
|
|
||||||
|
const facts = [];
|
||||||
|
|
||||||
|
if (monitorName) {
|
||||||
|
facts.push({
|
||||||
|
name: "Monitor",
|
||||||
|
value: monitorName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitorUrl) {
|
||||||
|
facts.push({
|
||||||
|
name: "URL",
|
||||||
|
value: monitorUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"@context": "https://schema.org/extensions",
|
||||||
|
"@type": "MessageCard",
|
||||||
|
themeColor: this._getThemeColor(status),
|
||||||
|
summary: notificationMessage,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
activityImage:
|
||||||
|
"https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
|
||||||
|
activityTitle: "**Uptime Kuma**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activityTitle: notificationMessage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activityTitle: "**Description**",
|
||||||
|
text: monitorMessage,
|
||||||
|
facts,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
_sendNotification = async (webhookUrl, payload) => {
|
||||||
|
await axios.post(webhookUrl, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleGeneralNotification = (webhookUrl, msg) => {
|
||||||
|
const payload = this._notificationPayloadFactory({
|
||||||
|
monitorMessage: msg
|
||||||
|
});
|
||||||
|
|
||||||
|
return this._sendNotification(webhookUrl, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
|
let okMsg = "Sent Successfully. ";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
await this._handleGeneralNotification(notification.webhookUrl, msg);
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url;
|
||||||
|
|
||||||
|
if (monitorJSON["type"] === "port") {
|
||||||
|
url = monitorJSON["hostname"];
|
||||||
|
if (monitorJSON["port"]) {
|
||||||
|
url += ":" + monitorJSON["port"];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
url = monitorJSON["url"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = this._notificationPayloadFactory({
|
||||||
|
monitorMessage: heartbeatJSON.msg,
|
||||||
|
monitorName: monitorJSON.name,
|
||||||
|
monitorUrl: url,
|
||||||
|
status: heartbeatJSON.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._sendNotification(notification.webhookUrl, payload);
|
||||||
|
return okMsg;
|
||||||
|
} catch (error) {
|
||||||
|
this.throwGeneralAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Teams;
|
@ -0,0 +1,191 @@
|
|||||||
|
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");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const { UP } = require("../../src/util");
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
let cache = apicache.middleware;
|
||||||
|
let io = server.io;
|
||||||
|
|
||||||
|
router.get("/api/entry-page", async (_, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
response.json(server.entryPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
|
try {
|
||||||
|
let pushToken = request.params.pushToken;
|
||||||
|
let msg = request.query.msg || "OK";
|
||||||
|
let ping = request.query.ping;
|
||||||
|
|
||||||
|
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
|
||||||
|
pushToken
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! monitor) {
|
||||||
|
throw new Error("Monitor not found or not active.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let bean = R.dispense("heartbeat");
|
||||||
|
bean.monitor_id = monitor.id;
|
||||||
|
bean.time = R.isoDateTime(dayjs.utc());
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = msg;
|
||||||
|
bean.ping = ping;
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
|
||||||
|
Monitor.sendStats(io, monitor.id, monitor.user_id);
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
response.json({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
@ -0,0 +1,5 @@
|
|||||||
|
html[lang='fa'] {
|
||||||
|
#app {
|
||||||
|
font-family: 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
@import "vars.scss";
|
||||||
|
@import "node_modules/vue-multiselect/dist/vue-multiselect";
|
||||||
|
|
||||||
|
.multiselect__tags {
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 6px 40px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect--active .multiselect__tags {
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--highlight {
|
||||||
|
background: $primary !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--highlight::after {
|
||||||
|
background: $primary !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tag {
|
||||||
|
border-radius: 50rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 6px 26px 6px 10px;
|
||||||
|
background: $primary !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__placeholder {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
opacity: 0.67;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__input,
|
||||||
|
.multiselect__single {
|
||||||
|
line-height: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.multiselect__tag {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tags {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__input,
|
||||||
|
.multiselect__single {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__content-wrapper {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect--above .multiselect__content-wrapper {
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--selected {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h4>{{ $t("Certificate Info") }}</h4>
|
||||||
|
{{ $t("Certificate Chain") }}:
|
||||||
|
<div
|
||||||
|
v-if="valid"
|
||||||
|
class="rounded d-inline-flex ms-2 text-white tag-valid"
|
||||||
|
>
|
||||||
|
{{ $t("Valid") }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!valid"
|
||||||
|
class="rounded d-inline-flex ms-2 text-white tag-invalid"
|
||||||
|
>
|
||||||
|
{{ $t("Invalid") }}
|
||||||
|
</div>
|
||||||
|
<certificate-info-row :cert="certInfo" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CertificateInfoRow from "./CertificateInfoRow.vue";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CertificateInfoRow,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
certInfo: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
valid: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.tag-valid {
|
||||||
|
padding: 2px 25px;
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-invalid {
|
||||||
|
padding: 2px 25px;
|
||||||
|
background-color: $danger;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="d-flex flex-row align-items-center p-1 overflow-hidden">
|
||||||
|
<div class="m-3 ps-3">
|
||||||
|
<div class="cert-icon">
|
||||||
|
<font-awesome-icon icon="file" />
|
||||||
|
<font-awesome-icon class="award-icon" icon="award" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-3">
|
||||||
|
<table class="text-start">
|
||||||
|
<tbody>
|
||||||
|
<tr class="my-3">
|
||||||
|
<td class="px-3">Subject:</td>
|
||||||
|
<td>{{ formatSubject(cert.subject) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="my-3">
|
||||||
|
<td class="px-3">Valid To:</td>
|
||||||
|
<td><Datetime :value="cert.validTo" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="my-3">
|
||||||
|
<td class="px-3">Days Remaining:</td>
|
||||||
|
<td>{{ cert.daysRemaining }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="my-3">
|
||||||
|
<td class="px-3">Issuer:</td>
|
||||||
|
<td>{{ formatSubject(cert.issuer) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="my-3">
|
||||||
|
<td class="px-3">Fingerprint:</td>
|
||||||
|
<td>{{ cert.fingerprint }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<font-awesome-icon
|
||||||
|
v-if="cert.issuerCertificate"
|
||||||
|
class="m-2 ps-6 link-icon"
|
||||||
|
icon="link"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<certificate-info-row
|
||||||
|
v-if="cert.issuerCertificate"
|
||||||
|
:cert="cert.issuerCertificate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Datetime from "../components/Datetime.vue";
|
||||||
|
export default {
|
||||||
|
name: "CertificateInfoRow",
|
||||||
|
components: {
|
||||||
|
Datetime,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
cert: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatSubject(subject) {
|
||||||
|
if (subject.O && subject.CN && subject.C) {
|
||||||
|
return `${subject.CN} - ${subject.O} (${subject.C})`;
|
||||||
|
} else if (subject.O && subject.CN) {
|
||||||
|
return `${subject.CN} - ${subject.O}`;
|
||||||
|
} else if (subject.CN) {
|
||||||
|
return subject.CN;
|
||||||
|
} else {
|
||||||
|
return "no info";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
table {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-icon {
|
||||||
|
position: relative;
|
||||||
|
font-size: 70px;
|
||||||
|
color: $link-color;
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: $dark-font-color;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.award-icon {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 0.5em;
|
||||||
|
bottom: 20%;
|
||||||
|
left: 12%;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: $dark-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-left: 50px !important;
|
||||||
|
color: $link-color;
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: $dark-font-color;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
:id="id"
|
||||||
|
ref="input"
|
||||||
|
v-model="model"
|
||||||
|
:type="type"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:autocomplete="autocomplete"
|
||||||
|
:required="required"
|
||||||
|
:readonly="readonly"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
|
||||||
|
<a class="btn btn-outline-primary" @click="copyToClipboard(model)">
|
||||||
|
<font-awesome-icon :icon="icon" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
let timeout;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "text"
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
autocomplete: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visibility: "password",
|
||||||
|
icon: "copy",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
showInput() {
|
||||||
|
this.visibility = "text";
|
||||||
|
},
|
||||||
|
|
||||||
|
hideInput() {
|
||||||
|
this.visibility = "password";
|
||||||
|
},
|
||||||
|
|
||||||
|
copyToClipboard(textToCopy) {
|
||||||
|
this.icon = "check";
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
this.icon = "copy";
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// navigator clipboard api needs a secure context (https)
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
// navigator clipboard api method'
|
||||||
|
return navigator.clipboard.writeText(textToCopy);
|
||||||
|
} else {
|
||||||
|
// text area method
|
||||||
|
let textArea = document.createElement("textarea");
|
||||||
|
textArea.value = textToCopy;
|
||||||
|
// make the textarea out of viewport
|
||||||
|
textArea.style.position = "fixed";
|
||||||
|
textArea.style.left = "-999999px";
|
||||||
|
textArea.style.top = "-999999px";
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
// here the magic happens
|
||||||
|
document.execCommand("copy") ? res() : rej();
|
||||||
|
textArea.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -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>
|
@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tag-wrapper rounded d-inline-flex"
|
||||||
|
:class="{ 'px-3': size == 'normal',
|
||||||
|
'py-1': size == 'normal',
|
||||||
|
'm-2': size == 'normal',
|
||||||
|
'px-2': size == 'sm',
|
||||||
|
'py-0': size == 'sm',
|
||||||
|
'm-1': size == 'sm',
|
||||||
|
}"
|
||||||
|
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
|
||||||
|
>
|
||||||
|
<span class="tag-text">{{ displayText }}</span>
|
||||||
|
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
|
||||||
|
<font-awesome-icon icon="times" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "normal",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayText() {
|
||||||
|
if (this.item.value == "") {
|
||||||
|
return this.item.name;
|
||||||
|
} else {
|
||||||
|
return `${this.item.name}: ${this.item.value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tag-wrapper {
|
||||||
|
color: white;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-text {
|
||||||
|
padding-bottom: 1px !important;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove {
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 24px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,405 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-3">{{ $t("Tags") }}</h4>
|
||||||
|
<div class="mb-3 p-1">
|
||||||
|
<tag
|
||||||
|
v-for="item in selectedTags"
|
||||||
|
:key="item.id"
|
||||||
|
:item="item"
|
||||||
|
:remove="deleteTag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="p-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary btn-add"
|
||||||
|
:disabled="processing"
|
||||||
|
@click.stop="showAddDialog"
|
||||||
|
>
|
||||||
|
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body">
|
||||||
|
<vue-multiselect
|
||||||
|
v-model="newDraftTag.select"
|
||||||
|
class="mb-2"
|
||||||
|
:options="tagOptions"
|
||||||
|
:multiple="false"
|
||||||
|
:searchable="true"
|
||||||
|
:placeholder="$t('Add New below or Select...')"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
|
||||||
|
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #singleLabel="{ option }">
|
||||||
|
<div class="py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="height: 24px;"
|
||||||
|
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</vue-multiselect>
|
||||||
|
<div v-if="newDraftTag.select?.name == null" class="d-flex mb-2">
|
||||||
|
<div class="w-50 pe-2">
|
||||||
|
<input v-model="newDraftTag.name" class="form-control"
|
||||||
|
:class="{'is-invalid': validateDraftTag.nameInvalid}"
|
||||||
|
:placeholder="$t('Name')"
|
||||||
|
@keydown.enter.prevent="onEnter"
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $t("Tag with this name already exist.") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-50 ps-2">
|
||||||
|
<vue-multiselect
|
||||||
|
v-model="newDraftTag.color"
|
||||||
|
:options="colorOptions"
|
||||||
|
:multiple="false"
|
||||||
|
:searchable="true"
|
||||||
|
:placeholder="$t('color')"
|
||||||
|
track-by="color"
|
||||||
|
label="name"
|
||||||
|
select-label=""
|
||||||
|
deselect-label=""
|
||||||
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="height: 24px; color: white;"
|
||||||
|
:style="{ backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #singleLabel="{ option }">
|
||||||
|
<div class="py-1 px-3 rounded d-inline-flex"
|
||||||
|
style="height: 24px; color: white;"
|
||||||
|
:style="{ backgroundColor: option.color + ' !important' }"
|
||||||
|
>
|
||||||
|
<span>{{ option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</vue-multiselect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<input v-model="newDraftTag.value" class="form-control"
|
||||||
|
:class="{'is-invalid': validateDraftTag.valueInvalid}"
|
||||||
|
:placeholder="$t('value (optional)')"
|
||||||
|
@keydown.enter.prevent="onEnter"
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $t("Tag with this value already exist.") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary float-end"
|
||||||
|
:disabled="processing || validateDraftTag.invalid"
|
||||||
|
@click.stop="addDraftTag"
|
||||||
|
>
|
||||||
|
{{ $t("Add") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
import VueMultiselect from "vue-multiselect";
|
||||||
|
import Tag from "../components/Tag.vue";
|
||||||
|
import { useToast } from "vue-toastification"
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Tag,
|
||||||
|
VueMultiselect,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
preSelectedTags: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modal: null,
|
||||||
|
existingTags: [],
|
||||||
|
processing: false,
|
||||||
|
newTags: [],
|
||||||
|
deleteTags: [],
|
||||||
|
newDraftTag: {
|
||||||
|
name: null,
|
||||||
|
select: null,
|
||||||
|
color: null,
|
||||||
|
value: "",
|
||||||
|
invalid: true,
|
||||||
|
nameInvalid: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tagOptions() {
|
||||||
|
const tagOptions = this.existingTags;
|
||||||
|
for (const tag of this.newTags) {
|
||||||
|
if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) {
|
||||||
|
tagOptions.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tagOptions;
|
||||||
|
},
|
||||||
|
selectedTags() {
|
||||||
|
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id));
|
||||||
|
},
|
||||||
|
colorOptions() {
|
||||||
|
return [
|
||||||
|
{ name: this.$t("Gray"),
|
||||||
|
color: "#4B5563" },
|
||||||
|
{ name: this.$t("Red"),
|
||||||
|
color: "#DC2626" },
|
||||||
|
{ name: this.$t("Orange"),
|
||||||
|
color: "#D97706" },
|
||||||
|
{ name: this.$t("Green"),
|
||||||
|
color: "#059669" },
|
||||||
|
{ name: this.$t("Blue"),
|
||||||
|
color: "#2563EB" },
|
||||||
|
{ name: this.$t("Indigo"),
|
||||||
|
color: "#4F46E5" },
|
||||||
|
{ name: this.$t("Purple"),
|
||||||
|
color: "#7C3AED" },
|
||||||
|
{ name: this.$t("Pink"),
|
||||||
|
color: "#DB2777" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
validateDraftTag() {
|
||||||
|
let nameInvalid = false;
|
||||||
|
let valueInvalid = false;
|
||||||
|
let invalid = true;
|
||||||
|
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) {
|
||||||
|
// Undo removing a Tag
|
||||||
|
nameInvalid = false;
|
||||||
|
valueInvalid = false;
|
||||||
|
invalid = false;
|
||||||
|
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) {
|
||||||
|
// Try to create new tag with existing name
|
||||||
|
nameInvalid = true;
|
||||||
|
invalid = true;
|
||||||
|
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
|
||||||
|
tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value
|
||||||
|
) || (
|
||||||
|
tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value
|
||||||
|
)).length > 0) {
|
||||||
|
// Try to add a tag with existing name and value
|
||||||
|
valueInvalid = true;
|
||||||
|
invalid = true;
|
||||||
|
} else if (this.newDraftTag.select != null) {
|
||||||
|
// Select an existing tag, no need to validate
|
||||||
|
invalid = false;
|
||||||
|
valueInvalid = false;
|
||||||
|
} else if (this.newDraftTag.color == null || this.newDraftTag.name === "") {
|
||||||
|
// Missing form inputs
|
||||||
|
nameInvalid = false;
|
||||||
|
invalid = true;
|
||||||
|
} else {
|
||||||
|
// Looks valid
|
||||||
|
invalid = false;
|
||||||
|
nameInvalid = false;
|
||||||
|
valueInvalid = false;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
invalid,
|
||||||
|
nameInvalid,
|
||||||
|
valueInvalid,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
this.getExistingTags();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showAddDialog() {
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
getExistingTags() {
|
||||||
|
this.$root.getSocket().emit("getTags", (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.existingTags = res.tags;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteTag(item) {
|
||||||
|
if (item.new) {
|
||||||
|
// Undo Adding a new Tag
|
||||||
|
this.newTags = this.newTags.filter(tag => !(tag.name == item.name && tag.value == item.value));
|
||||||
|
} else {
|
||||||
|
// Remove an Existing Tag
|
||||||
|
this.deleteTags.push(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textColor(option) {
|
||||||
|
if (option.color) {
|
||||||
|
return "white";
|
||||||
|
} else {
|
||||||
|
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addDraftTag() {
|
||||||
|
console.log("Adding Draft Tag: ", this.newDraftTag);
|
||||||
|
if (this.newDraftTag.select != null) {
|
||||||
|
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) {
|
||||||
|
// Undo removing a tag
|
||||||
|
this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value));
|
||||||
|
} else {
|
||||||
|
// Add an existing Tag
|
||||||
|
this.newTags.push({
|
||||||
|
id: this.newDraftTag.select.id,
|
||||||
|
color: this.newDraftTag.select.color,
|
||||||
|
name: this.newDraftTag.select.name,
|
||||||
|
value: this.newDraftTag.value,
|
||||||
|
new: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add new Tag
|
||||||
|
this.newTags.push({
|
||||||
|
color: this.newDraftTag.color.color,
|
||||||
|
name: this.newDraftTag.name.trim(),
|
||||||
|
value: this.newDraftTag.value,
|
||||||
|
new: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.clearDraftTag();
|
||||||
|
},
|
||||||
|
clearDraftTag() {
|
||||||
|
this.newDraftTag = {
|
||||||
|
name: null,
|
||||||
|
select: null,
|
||||||
|
color: null,
|
||||||
|
value: "",
|
||||||
|
invalid: true,
|
||||||
|
nameInvalid: false,
|
||||||
|
};
|
||||||
|
this.modal.hide();
|
||||||
|
},
|
||||||
|
addTagAsync(newTag) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("addTag", newTag, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addMonitorTagAsync(tagId, monitorId, value) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteMonitorTagAsync(tagId, monitorId, value) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onEnter() {
|
||||||
|
if (!this.validateDraftTag.invalid) {
|
||||||
|
this.addDraftTag();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submit(monitorId) {
|
||||||
|
console.log(`Submitting tag changes for monitor ${monitorId}...`);
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
for (const newTag of this.newTags) {
|
||||||
|
let tagId;
|
||||||
|
if (newTag.id == null) {
|
||||||
|
// Create a New Tag
|
||||||
|
let newTagResult;
|
||||||
|
await this.addTagAsync(newTag).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
newTagResult = false;
|
||||||
|
}
|
||||||
|
newTagResult = res.tag;
|
||||||
|
});
|
||||||
|
if (!newTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tagId = newTagResult.id;
|
||||||
|
// Assign the new ID to the tags of the same name & color
|
||||||
|
this.newTags.map(tag => {
|
||||||
|
if (tag.name == newTag.name && tag.color == newTag.color) {
|
||||||
|
tag.id = newTagResult.id;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
tagId = newTag.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newMonitorTagResult;
|
||||||
|
// Assign tag to monitor
|
||||||
|
await this.addMonitorTagAsync(tagId, monitorId, newTag.value).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
newMonitorTagResult = false;
|
||||||
|
}
|
||||||
|
newMonitorTagResult = true;
|
||||||
|
});
|
||||||
|
if (!newMonitorTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const deleteTag of this.deleteTags) {
|
||||||
|
let deleteMonitorTagResult;
|
||||||
|
await this.deleteMonitorTagAsync(deleteTag.tag_id, deleteTag.monitor_id, deleteTag.value).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(res.msg);
|
||||||
|
deleteMonitorTagResult = false;
|
||||||
|
}
|
||||||
|
deleteMonitorTagResult = true;
|
||||||
|
});
|
||||||
|
if (!deleteMonitorTagResult) {
|
||||||
|
// abort
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getExistingTags();
|
||||||
|
this.newTags = [];
|
||||||
|
this.deleteTags = [];
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btn-add {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="apprise-url" class="form-label">Apprise URL</label>
|
||||||
|
<input id="apprise-url" v-model="$parent.notification.appriseURL" type="text" class="form-control" required>
|
||||||
|
<div class="form-text">
|
||||||
|
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
|
||||||
|
<p>
|
||||||
|
Read more: <a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<p>
|
||||||
|
Status:
|
||||||
|
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
|
||||||
|
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise" target="_blank">Read more</a></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
appriseInstalled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||||
|
this.appriseInstalled = installed;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
|
||||||
|
<input id="discord-webhook-url" v-model="$parent.notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
|
||||||
|
<div class="form-text">
|
||||||
|
You can get this by going to Server Settings -> Integrations -> Create Webhook
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="discord-username" class="form-label">Bot Display Name</label>
|
||||||
|
<input id="discord-username" v-model="$parent.notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="discord-prefix-message" class="form-label">Prefix Custom Message</label>
|
||||||
|
<input id="discord-prefix-message" v-model="$parent.notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" placeholder="Hello @everyone is...">
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gotify-application-token" class="form-label">Application Token</label>
|
||||||
|
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gotify-server-url" class="form-label">Server URL</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="gotify-priority" class="form-label">Priority</label>
|
||||||
|
<input id="gotify-priority" v-model="$parent.notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (typeof this.$parent.notification.gotifyPriority === "undefined") {
|
||||||
|
this.$parent.notification.gotifyPriority = 8;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="line-channel-access-token" class="form-label">Channel access token</label>
|
||||||
|
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
Line Developers Console - <b>Basic Settings</b>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" style="margin-top: 12px;">
|
||||||
|
<label for="line-user-id" class="form-label">User ID</label>
|
||||||
|
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
Line Developers Console - <b>Messaging API</b>
|
||||||
|
</div>
|
||||||
|
<div class="form-text" style="margin-top: 8px;">
|
||||||
|
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="lunasea-device" v-model="$parent.notification.lunaseaDevice" type="text" class="form-control" required>
|
||||||
|
<div class="form-text">
|
||||||
|
<p><span style="color: red;"><sup>*</sup></span>Required</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="mattermost-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
||||||
|
<input id="mattermost-webhook-url" v-model="$parent.notification.mattermostWebhookUrl" type="text" class="form-control" required>
|
||||||
|
<label for="mattermost-username" class="form-label">Username</label>
|
||||||
|
<input id="mattermost-username" v-model="$parent.notification.mattermostusername" type="text" class="form-control">
|
||||||
|
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
|
||||||
|
<input id="mattermost-iconurl" v-model="$parent.notification.mattermosticonurl" type="text" class="form-control">
|
||||||
|
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
|
||||||
|
<input id="mattermost-iconemo" v-model="$parent.notification.mattermosticonemo" type="text" class="form-control">
|
||||||
|
<label for="mattermost-channel" class="form-label">Channel Name</label>
|
||||||
|
<input id="mattermost-channel-name" v-model="$parent.notification.mattermostchannel" type="text" class="form-control">
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color:red;"><sup>*</sup></span>Required
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info about webhooks on: <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
You can override the default channel that webhook posts to by entering the channel name into "Channel Name" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
You can provide a link to a picture in "Icon URL" to override the default profile picture. Will not be used if Icon Emoji is set.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a> Note: emoji takes preference over Icon URL.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="octopush-key" class="form-label">API KEY</label>
|
||||||
|
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
<label for="octopush-login" class="form-label">API LOGIN</label>
|
||||||
|
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="octopush-type-sms" class="form-label">SMS Type</label>
|
||||||
|
<select id="octopush-type-sms" v-model="$parent.notification.octopushSMSType" class="form-select">
|
||||||
|
<option value="sms_premium">Premium (Fast - recommended for alerting)</option>
|
||||||
|
<option value="sms_low_cost">Low Cost (Slow, sometimes blocked by operator)</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
Check octopush prices <a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="octopush-phone-number" class="form-label">Phone number (intl format, eg : +33612345678) </label>
|
||||||
|
<input id="octopush-phone-number" v-model="$parent.notification.octopushPhoneNumber" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="octopush-sender-name" class="form-label">SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)</label>
|
||||||
|
<input id="octopush-sender-name" v-model="$parent.notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info on: <a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pushbullet-access-token" class="form-label">Access Token</label>
|
||||||
|
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
<label for="pushover-app-token" class="form-label">Application Token<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
<label for="pushover-device" class="form-label">Device</label>
|
||||||
|
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
|
||||||
|
<label for="pushover-device" class="form-label">Message Title</label>
|
||||||
|
<input id="pushover-title" v-model="$parent.notification.pushovertitle" type="text" class="form-control">
|
||||||
|
<label for="pushover-priority" class="form-label">Priority</label>
|
||||||
|
<select id="pushover-priority" v-model="$parent.notification.pushoverpriority" class="form-select">
|
||||||
|
<option>-2</option>
|
||||||
|
<option>-1</option>
|
||||||
|
<option>0</option>
|
||||||
|
<option>1</option>
|
||||||
|
<option>2</option>
|
||||||
|
</select>
|
||||||
|
<label for="pushover-sound" class="form-label">Notification Sound</label>
|
||||||
|
<select id="pushover-sound" v-model="$parent.notification.pushoversounds" class="form-select">
|
||||||
|
<option>pushover</option>
|
||||||
|
<option>bike</option>
|
||||||
|
<option>bugle</option>
|
||||||
|
<option>cashregister</option>
|
||||||
|
<option>classical</option>
|
||||||
|
<option>cosmic</option>
|
||||||
|
<option>falling</option>
|
||||||
|
<option>gamelan</option>
|
||||||
|
<option>incoming</option>
|
||||||
|
<option>intermission</option>
|
||||||
|
<option>mechanical</option>
|
||||||
|
<option>pianobar</option>
|
||||||
|
<option>siren</option>
|
||||||
|
<option>spacealarm</option>
|
||||||
|
<option>tugboat</option>
|
||||||
|
<option>alien</option>
|
||||||
|
<option>climb</option>
|
||||||
|
<option>persistent</option>
|
||||||
|
<option>echo</option>
|
||||||
|
<option>updown</option>
|
||||||
|
<option>vibrate</option>
|
||||||
|
<option>none</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>Required
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
If you want to send notifications to different devices, fill out Device field.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||||
|
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HiddenInput from "../HiddenInput.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HiddenInput,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="rocket-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="rocket-webhook-url" v-model="$parent.notification.rocketwebhookURL" type="text" class="form-control" required>
|
||||||
|
<label for="rocket-username" class="form-label">Username</label>
|
||||||
|
<input id="rocket-username" v-model="$parent.notification.rocketusername" type="text" class="form-control">
|
||||||
|
<label for="rocket-iconemo" class="form-label">Icon Emoji</label>
|
||||||
|
<input id="rocket-iconemo" v-model="$parent.notification.rocketiconemo" type="text" class="form-control">
|
||||||
|
<label for="rocket-channel" class="form-label">Channel Name</label>
|
||||||
|
<input id="rocket-channel-name" v-model="$parent.notification.rocketchannel" type="text" class="form-control">
|
||||||
|
<label for="rocket-button-url" class="form-label">Uptime Kuma URL</label>
|
||||||
|
<input id="rocket-button" v-model="$parent.notification.rocketbutton" type="text" class="form-control">
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>Required
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info about webhooks on: <a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
Enter the channel name on Rocket.chat Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="signal-url" class="form-label">Post URL</label>
|
||||||
|
<input id="signal-url" v-model="$parent.notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="signal-number" class="form-label">Number</label>
|
||||||
|
<input id="signal-number" v-model="$parent.notification.signalNumber" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="signal-recipients" class="form-label">Recipients</label>
|
||||||
|
<input id="signal-recipients" v-model="$parent.notification.signalRecipients" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
You need to have a signal client with REST API.
|
||||||
|
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
You can check this url to view how to setup one:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
IMPORTANT: You cannot mix groups and numbers in recipients!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
|
||||||
|
<input id="slack-webhook-url" v-model="$parent.notification.slackwebhookURL" type="text" class="form-control" required>
|
||||||
|
<label for="slack-username" class="form-label">Username</label>
|
||||||
|
<input id="slack-username" v-model="$parent.notification.slackusername" type="text" class="form-control">
|
||||||
|
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
||||||
|
<input id="slack-iconemo" v-model="$parent.notification.slackiconemo" type="text" class="form-control">
|
||||||
|
<label for="slack-channel" class="form-label">Channel Name</label>
|
||||||
|
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
|
||||||
|
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
||||||
|
<input id="slack-button" v-model="$parent.notification.slackbutton" type="text" class="form-control">
|
||||||
|
<div class="form-text">
|
||||||
|
<span style="color: red;"><sup>*</sup></span>Required
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="teams-webhookurl" class="form-label">Webhook URL</label>
|
||||||
|
<input
|
||||||
|
id="teams-webhookurl"
|
||||||
|
v-model="$parent.notification.webhookUrl"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
You can learn how to create a webhook url
|
||||||
|
<a
|
||||||
|
href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook"
|
||||||
|
target="_blank"
|
||||||
|
>here</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="webhook-url" class="form-label">Post URL</label>
|
||||||
|
<input id="webhook-url" v-model="$parent.notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="webhook-content-type" class="form-label">Content Type</label>
|
||||||
|
<select id="webhook-content-type" v-model="$parent.notification.webhookContentType" class="form-select" required>
|
||||||
|
<option value="json">
|
||||||
|
application/json
|
||||||
|
</option>
|
||||||
|
<option value="form-data">
|
||||||
|
multipart/form-data
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
<p>"application/json" is good for any modern http servers such as express.js</p>
|
||||||
|
<p>"multipart/form-data" is good for PHP, you just need to parse the json by <strong>json_decode($_POST['data'])</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,44 @@
|
|||||||
|
import STMP from "./SMTP.vue"
|
||||||
|
import Telegram from "./Telegram.vue";
|
||||||
|
import Discord from "./Discord.vue";
|
||||||
|
import Webhook from "./Webhook.vue";
|
||||||
|
import Signal from "./Signal.vue";
|
||||||
|
import Gotify from "./Gotify.vue";
|
||||||
|
import Slack from "./Slack.vue";
|
||||||
|
import RocketChat from "./RocketChat.vue";
|
||||||
|
import Teams from "./Teams.vue";
|
||||||
|
import Pushover from "./Pushover.vue";
|
||||||
|
import Pushy from "./Pushy.vue";
|
||||||
|
import Octopush from "./Octopush.vue";
|
||||||
|
import LunaSea from "./LunaSea.vue";
|
||||||
|
import Apprise from "./Apprise.vue";
|
||||||
|
import Pushbullet from "./Pushbullet.vue";
|
||||||
|
import Line from "./Line.vue";
|
||||||
|
import Mattermost from "./Mattermost.vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage all notification form.
|
||||||
|
*
|
||||||
|
* @type { Record<string, any> }
|
||||||
|
*/
|
||||||
|
const NotificationFormList = {
|
||||||
|
"telegram": Telegram,
|
||||||
|
"webhook": Webhook,
|
||||||
|
"smtp": STMP,
|
||||||
|
"discord": Discord,
|
||||||
|
"teams": Teams,
|
||||||
|
"signal": Signal,
|
||||||
|
"gotify": Gotify,
|
||||||
|
"slack": Slack,
|
||||||
|
"rocket.chat": RocketChat,
|
||||||
|
"pushover": Pushover,
|
||||||
|
"pushy": Pushy,
|
||||||
|
"octopush": Octopush,
|
||||||
|
"lunasea": LunaSea,
|
||||||
|
"apprise": Apprise,
|
||||||
|
"pushbullet": Pushbullet,
|
||||||
|
"line": Line,
|
||||||
|
"mattermost": Mattermost
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotificationFormList
|
@ -0,0 +1,63 @@
|
|||||||
|
import { createI18n } from "vue-i18n";
|
||||||
|
import bgBG from "./languages/bg-BG";
|
||||||
|
import daDK from "./languages/da-DK";
|
||||||
|
import deDE from "./languages/de-DE";
|
||||||
|
import en from "./languages/en";
|
||||||
|
import fa from "./languages/fa";
|
||||||
|
import esEs from "./languages/es-ES";
|
||||||
|
import ptBR from "./languages/pt-BR";
|
||||||
|
import etEE from "./languages/et-EE";
|
||||||
|
import frFR from "./languages/fr-FR";
|
||||||
|
import hu from "./languages/hu";
|
||||||
|
import itIT from "./languages/it-IT";
|
||||||
|
import ja from "./languages/ja";
|
||||||
|
import koKR from "./languages/ko-KR";
|
||||||
|
import nlNL from "./languages/nl-NL";
|
||||||
|
import pl from "./languages/pl";
|
||||||
|
import ruRU from "./languages/ru-RU";
|
||||||
|
import sr from "./languages/sr";
|
||||||
|
import srLatn from "./languages/sr-latn";
|
||||||
|
import trTR from "./languages/tr-TR";
|
||||||
|
import svSE from "./languages/sv-SE";
|
||||||
|
import zhCN from "./languages/zh-CN";
|
||||||
|
import zhHK from "./languages/zh-HK";
|
||||||
|
|
||||||
|
const languageList = {
|
||||||
|
en,
|
||||||
|
"zh-HK": zhHK,
|
||||||
|
"bg-BG": bgBG,
|
||||||
|
"de-DE": deDE,
|
||||||
|
"nl-NL": nlNL,
|
||||||
|
"es-ES": esEs,
|
||||||
|
"fa": fa,
|
||||||
|
"pt-BR": ptBR,
|
||||||
|
"fr-FR": frFR,
|
||||||
|
"hu": hu,
|
||||||
|
"it-IT": itIT,
|
||||||
|
"ja": ja,
|
||||||
|
"da-DK": daDK,
|
||||||
|
"sr": sr,
|
||||||
|
"sr-latn": srLatn,
|
||||||
|
"sv-SE": svSE,
|
||||||
|
"tr-TR": trTR,
|
||||||
|
"ko-KR": koKR,
|
||||||
|
"ru-RU": ruRU,
|
||||||
|
"zh-CN": zhCN,
|
||||||
|
"pl": pl,
|
||||||
|
"et-EE": etEE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rtlLangs = ["fa"];
|
||||||
|
|
||||||
|
export const currentLocale = () => localStorage.locale || "en";
|
||||||
|
|
||||||
|
export const localeDirection = () => {
|
||||||
|
return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr"
|
||||||
|
}
|
||||||
|
export const i18n = createI18n({
|
||||||
|
locale: currentLocale(),
|
||||||
|
fallbackLocale: "en",
|
||||||
|
silentFallbackWarn: true,
|
||||||
|
silentTranslationWarn: true,
|
||||||
|
messages: languageList,
|
||||||
|
});
|
@ -0,0 +1,181 @@
|
|||||||
|
export default {
|
||||||
|
languageName: "Magyar",
|
||||||
|
checkEverySecond: "Ellenőrzés {0} másodpercenként",
|
||||||
|
retryCheckEverySecond: "Újrapróbál {0} másodpercenként.",
|
||||||
|
retriesDescription: "Maximális próbálkozás mielőtt a szolgáltatás leállt jelőlést kap és értesítés kerül kiküldésre",
|
||||||
|
ignoreTLSError: "TLS/SSL hibák figyelnen kívül hagyása HTTPS weboldalaknál",
|
||||||
|
upsideDownModeDescription: "Az állapot megfordítása. Ha a szolgáltatás elérhető, akkor lesz leállt állapotú.",
|
||||||
|
maxRedirectDescription: "Az átirányítások maximális száma. állítsa 0-ra az átirányítás tiltásához.",
|
||||||
|
acceptedStatusCodesDescription: "Válassza ki az állapot kódokat amelyek sikeres válasznak fognak számítani.",
|
||||||
|
passwordNotMatchMsg: "A megismételt jelszó nem egyezik.",
|
||||||
|
notificationDescription: "Kérem, rendeljen egy értesítést a figyeléshez, hogy működjön.",
|
||||||
|
keywordDescription: "Kulcsszó keresése a html-ben vagy a JSON válaszban. (kis-nagybetű érzékeny)",
|
||||||
|
pauseDashboardHome: "Szünetel",
|
||||||
|
deleteMonitorMsg: "Biztos, hogy törölni akarja ezt a figyelőt?",
|
||||||
|
deleteNotificationMsg: "Biztos, hogy törölni akarja ezt az értesítést az összes figyelőnél?",
|
||||||
|
resoverserverDescription: "A Cloudflare az alapértelmezett szerver, bármikor meg tudja változtatni a resolver server-t.",
|
||||||
|
rrtypeDescription: "Válassza ki az RR-Típust a figyelőhöz",
|
||||||
|
pauseMonitorMsg: "Biztos, hogy szüneteltetni akarja?",
|
||||||
|
enableDefaultNotificationDescription: "Minden új figyelőhöz ez az értesítés engedélyezett lesz alapértelmezetten. Kikapcsolhatja az értesítést külön minden figyelőnél.",
|
||||||
|
clearEventsMsg: "Biztos, hogy törölni akar miden eseményt ennél a figyelnél?",
|
||||||
|
clearHeartbeatsMsg: "Biztos, hogy törölni akar minden heartbeat-et ennél a figyelőnél?",
|
||||||
|
confirmClearStatisticsMsg: "Biztos, hogy törölni akat MINDEN statisztikát?",
|
||||||
|
importHandleDescription: "Válassza a 'Meglévő kihagyását', ha ki szeretné hagyni az azonos nevő figyelőket vagy értesítésket. A 'Felülírás' törölni fog minden meglévő figyelőt és értesítést.",
|
||||||
|
confirmImportMsg: "Biztos, hogy importálja a mentést? Győzödjön meg róla, hogy jól választotta ki az importálás opciót.",
|
||||||
|
twoFAVerifyLabel: "Kérem, adja meg a token-t, hogy a 2FA működését ellenőrizzük",
|
||||||
|
tokenValidSettingsMsg: "A token érvényes! El tudja menteni a 2FA beállításait.",
|
||||||
|
confirmEnableTwoFAMsg: "Biztosan engedélyezi a 2FA-t?",
|
||||||
|
confirmDisableTwoFAMsg: "Biztosan letiltja a 2FA-t?",
|
||||||
|
Settings: "Beállítások",
|
||||||
|
Dashboard: "Irányítópult",
|
||||||
|
"New Update": "Új frissítés",
|
||||||
|
Language: "Nyelv",
|
||||||
|
Appearance: "Megjelenés",
|
||||||
|
Theme: "Téma",
|
||||||
|
General: "Általános",
|
||||||
|
Version: "Verzió",
|
||||||
|
"Check Update On GitHub": "Frissítések keresése a GitHub-on",
|
||||||
|
List: "Lista",
|
||||||
|
Add: "Hozzáadás",
|
||||||
|
"Add New Monitor": "Új figyelő hozzáadása",
|
||||||
|
"Quick Stats": "Gyors statisztikák",
|
||||||
|
Up: "Működik",
|
||||||
|
Down: "Leállt",
|
||||||
|
Pending: "Függőben",
|
||||||
|
Unknown: "Ismeretlen",
|
||||||
|
Pause: "Szünet",
|
||||||
|
Name: "Név",
|
||||||
|
Status: "Állapot",
|
||||||
|
DateTime: "Időpont",
|
||||||
|
Message: "Üzenet",
|
||||||
|
"No important events": "Nincs fontos esemény",
|
||||||
|
Resume: "Folytatás",
|
||||||
|
Edit: "Szerkesztés",
|
||||||
|
Delete: "Törlés",
|
||||||
|
Current: "Aktuális",
|
||||||
|
Uptime: "Uptime",
|
||||||
|
"Cert Exp.": "Tanúsítvány lejár",
|
||||||
|
days: "napok",
|
||||||
|
day: "nap",
|
||||||
|
"-day": "-nap",
|
||||||
|
hour: "óra",
|
||||||
|
"-hour": "-óra",
|
||||||
|
Response: "Válasz",
|
||||||
|
Ping: "Ping",
|
||||||
|
"Monitor Type": "Figyelő típusa",
|
||||||
|
Keyword: "Kulcsszó",
|
||||||
|
"Friendly Name": "Rövid név",
|
||||||
|
URL: "URL",
|
||||||
|
Hostname: "Hostnév",
|
||||||
|
Port: "Port",
|
||||||
|
"Heartbeat Interval": "Heartbeat időköz",
|
||||||
|
Retries: "Újrapróbálkozás",
|
||||||
|
"Heartbeat Retry Interval": "Heartbeat újrapróbálkozások időköze",
|
||||||
|
Advanced: "Haladó",
|
||||||
|
"Upside Down Mode": "Fordított mód",
|
||||||
|
"Max. Redirects": "Max. átirányítás",
|
||||||
|
"Accepted Status Codes": "Elfogadott állapot kódok",
|
||||||
|
Save: "Mentés",
|
||||||
|
Notifications: "Értesítések",
|
||||||
|
"Not available, please setup.": "Nem elérhető, állítsa be.",
|
||||||
|
"Setup Notification": "Értesítés beállítása",
|
||||||
|
Light: "Világos",
|
||||||
|
Dark: "Sötét",
|
||||||
|
Auto: "Auto",
|
||||||
|
"Theme - Heartbeat Bar": "Téma - Heartbeat Bar",
|
||||||
|
Normal: "Normal",
|
||||||
|
Bottom: "Nyomógomb",
|
||||||
|
None: "Nincs",
|
||||||
|
Timezone: "Időzóna",
|
||||||
|
"Search Engine Visibility": "Látható a keresőmotoroknak",
|
||||||
|
"Allow indexing": "Indexelés engedélyezése",
|
||||||
|
"Discourage search engines from indexing site": "Keresőmotorok elriasztása az oldal indexelésétől",
|
||||||
|
"Change Password": "Jelszó változtatása",
|
||||||
|
"Current Password": "Jelenlegi jelszó",
|
||||||
|
"New Password": "Új jelszó",
|
||||||
|
"Repeat New Password": "Ismételje meg az új jelszót",
|
||||||
|
"Update Password": "Jelszó módosítása",
|
||||||
|
"Disable Auth": "Hitelesítés tiltása",
|
||||||
|
"Enable Auth": "Hitelesítés engedélyezése",
|
||||||
|
Logout: "Kijelenetkezés",
|
||||||
|
Leave: "Elhagy",
|
||||||
|
"I understand, please disable": "Megértettem, kérem tilsa le",
|
||||||
|
Confirm: "Megerősítés",
|
||||||
|
Yes: "Igen",
|
||||||
|
No: "Nem",
|
||||||
|
Username: "Felhasználónév",
|
||||||
|
Password: "Jelszó",
|
||||||
|
"Remember me": "Emlékezzen rám",
|
||||||
|
Login: "Bejelentkezés",
|
||||||
|
"No Monitors, please": "Nincs figyelő, kérem",
|
||||||
|
"add one": "adjon hozzá egyet",
|
||||||
|
"Notification Type": "Értesítés típusa",
|
||||||
|
Email: "Email",
|
||||||
|
Test: "Teszt",
|
||||||
|
"Certificate Info": "Tanúsítvány információk",
|
||||||
|
"Resolver Server": "Resolver szerver",
|
||||||
|
"Resource Record Type": "Resource Record típusa",
|
||||||
|
"Last Result": "Utolsó eredmény",
|
||||||
|
"Create your admin account": "Hozza létre az adminisztrátor felhasználót",
|
||||||
|
"Repeat Password": "Jelszó ismétlése",
|
||||||
|
"Import Backup": "Mentés importálása",
|
||||||
|
"Export Backup": "Mentés exportálása",
|
||||||
|
Export: "Exportálás",
|
||||||
|
Import: "Importálás",
|
||||||
|
respTime: "Válaszidő (ms)",
|
||||||
|
notAvailableShort: "N/A",
|
||||||
|
"Default enabled": "Alapértelmezetten engedélyezett",
|
||||||
|
"Apply on all existing monitors": "Alkalmazza az összes figyelőre",
|
||||||
|
Create: "Létrehozás",
|
||||||
|
"Clear Data": "Adatok törlése",
|
||||||
|
Events: "Események",
|
||||||
|
Heartbeats: "Heartbeats",
|
||||||
|
"Auto Get": "Auto Get",
|
||||||
|
backupDescription: "Ki tudja menteni az összes figyelőt és értesítést egy JSON fájlba.",
|
||||||
|
backupDescription2: "Ui.: Történeti és esemény adatokat nem tartalmaz.",
|
||||||
|
backupDescription3: "Érzékeny adatok, pl. szolgáltatás kulcsok is vannak az export fájlban. Figyelmesen őrizze!",
|
||||||
|
alertNoFile: "Válaszzon ki egy fájlt az importáláshoz.",
|
||||||
|
alertWrongFileType: "Válasszon egy JSON fájlt.",
|
||||||
|
"Clear all statistics": "Összes statisztika törlése",
|
||||||
|
"Skip existing": "Meglévő kihagyása",
|
||||||
|
Overwrite: "Felülírás",
|
||||||
|
Options: "Opciók",
|
||||||
|
"Keep both": "Mindegyiket tartsa meg",
|
||||||
|
"Verify Token": "Token ellenőrzése",
|
||||||
|
"Setup 2FA": "2FA beállítása",
|
||||||
|
"Enable 2FA": "2FA engedélyezése",
|
||||||
|
"Disable 2FA": "2FA toltása",
|
||||||
|
"2FA Settings": "2FA beállítások",
|
||||||
|
"Two Factor Authentication": "Two Factor Authentication",
|
||||||
|
Active: "Aktív",
|
||||||
|
Inactive: "Inaktív",
|
||||||
|
Token: "Token",
|
||||||
|
"Show URI": "URI megmutatása",
|
||||||
|
Tags: "Cimkék",
|
||||||
|
"Add New below or Select...": "Adjon hozzá lentre vagy válasszon...",
|
||||||
|
"Tag with this name already exist.": "Ilyen nevű cimke már létezik.",
|
||||||
|
"Tag with this value already exist.": "Ilyen értékű cimke már létezik.",
|
||||||
|
color: "szín",
|
||||||
|
"value (optional)": "érték (opcionális)",
|
||||||
|
Gray: "Szürke",
|
||||||
|
Red: "Piros",
|
||||||
|
Orange: "Narancs",
|
||||||
|
Green: "Zöld",
|
||||||
|
Blue: "Kék",
|
||||||
|
Indigo: "Indigó",
|
||||||
|
Purple: "Lila",
|
||||||
|
Pink: "Rózsaszín",
|
||||||
|
"Search...": "Keres...",
|
||||||
|
"Avg. Ping": "Átl. ping",
|
||||||
|
"Avg. Response": "Átl. válasz",
|
||||||
|
"Entry Page": "Nyitólap",
|
||||||
|
statusPageNothing: "Semmi nincs itt, kérem, adjon hozzá egy figyelőt.",
|
||||||
|
"No Services": "Nincs szolgáltatás",
|
||||||
|
"All Systems Operational": "Minden rendszer működik",
|
||||||
|
"Partially Degraded Service": "Részlegesen leállt szolgáltatás",
|
||||||
|
"Degraded Service": "Leállt szolgáltatás",
|
||||||
|
"Add Group": "Csoport hozzáadása",
|
||||||
|
"Add a monitor": "Figyelő hozzáadása",
|
||||||
|
"Edit Status Page": "Sátusz oldal szerkesztése",
|
||||||
|
"Go to Dashboard": "Menj az irányítópulthoz",
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue