133 changed files with 18369 additions and 7785 deletions
@ -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/* |
@ -1,37 +1,49 @@ |
|||
# DON'T UPDATE TO node:14-bullseye-slim, see #372. |
|||
FROM node:14-buster-slim AS build |
|||
FROM louislam/uptime-kuma:base-debian AS build |
|||
WORKDIR /app |
|||
|
|||
# split the sqlite install here, so that it can caches the arm prebuilt |
|||
# do not modify it, since we don't want to re-compile the arm prebuilt again |
|||
RUN apt update && \ |
|||
apt --yes install python3 python3-pip python3-dev git g++ make && \ |
|||
ln -s /usr/bin/python3 /usr/bin/python && \ |
|||
npm install mapbox/node-sqlite3#593c9d --build-from-source |
|||
|
|||
COPY . . |
|||
RUN npm install --legacy-peer-deps && npm run build && npm prune --production |
|||
RUN npm install --legacy-peer-deps && \ |
|||
npm run build && \ |
|||
npm prune --production && \ |
|||
chmod +x /app/extra/entrypoint.sh |
|||
|
|||
FROM node:14-bullseye-slim AS release |
|||
WORKDIR /app |
|||
|
|||
# Install Apprise, |
|||
# add sqlite3 cli for debugging in the future |
|||
# iputils-ping for ping |
|||
RUN apt update && \ |
|||
apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ |
|||
sqlite3 \ |
|||
iputils-ping && \ |
|||
pip3 --no-cache-dir install apprise && \ |
|||
rm -rf /var/lib/apt/lists/* |
|||
FROM louislam/uptime-kuma:base-debian AS release |
|||
WORKDIR /app |
|||
|
|||
# Copy app files from build layer |
|||
COPY --from=build /app /app |
|||
COPY --from=build /app /app |
|||
|
|||
EXPOSE 3001 |
|||
VOLUME ["/app/data"] |
|||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js |
|||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"] |
|||
CMD ["node", "server/server.js"] |
|||
|
|||
FROM release AS nightly |
|||
RUN npm run mark-as-nightly |
|||
|
|||
# Upload the artifact to Github |
|||
FROM louislam/uptime-kuma:base-debian AS upload-artifact |
|||
WORKDIR / |
|||
RUN apt update && \ |
|||
apt --yes install curl file |
|||
|
|||
ARG GITHUB_TOKEN |
|||
ARG TARGETARCH |
|||
ARG PLATFORM=debian |
|||
ARG VERSION |
|||
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz |
|||
ARG DIST=dist.tar.gz |
|||
|
|||
COPY --from=build /app /app |
|||
RUN chmod +x /app/extra/upload-github-release-asset.sh |
|||
|
|||
# Full Build |
|||
# RUN tar -zcvf $FILE app |
|||
# RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$FILE |
|||
|
|||
# Dist only |
|||
RUN cd /app && tar -zcvf $DIST dist |
|||
RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$DIST |
|||
|
|||
|
@ -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
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
@ -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, |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
}; |
@ -1,7 +1,12 @@ |
|||
<template> |
|||
<router-view /> |
|||
<router-view /> |
|||
</template> |
|||
|
|||
<script> |
|||
export default {} |
|||
import { setPageLocale } from "./util-frontend"; |
|||
export default { |
|||
created() { |
|||
setPageLocale(); |
|||
}, |
|||
}; |
|||
</script> |
|||
|
@ -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: "Български", |
|||
checkEverySecond: "Проверявай на всеки {0} секунди.", |
|||
retryCheckEverySecond: "Повторен опит на всеки {0} секунди.", |
|||
retriesDescription: "Максимакен брой опити преди услугата да бъде маркирана като недостъпна и да бъде изпратено известие", |
|||
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове", |
|||
upsideDownModeDescription: "Обърни статуса от достъпен на недостъпен. Ако услугата е достъпна се вижда НЕДОСТЪПНА.", |
|||
maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.", |
|||
acceptedStatusCodesDescription: "Изберете статус кодове, които се считат за успешен отговор.", |
|||
passwordNotMatchMsg: "Повторената парола не съвпада.", |
|||
notificationDescription: "Моля, задайте известието към монитор(и), за да функционира.", |
|||
keywordDescription: "Търсете ключова дума в обикновен html или JSON отговор - чувствителна е към регистъра", |
|||
pauseDashboardHome: "Пауза", |
|||
deleteMonitorMsg: "Наистина ли желаете да изтриете този монитор?", |
|||
deleteNotificationMsg: "Наистина ли желаете да изтриете известието за всички монитори?", |
|||
resoverserverDescription: "Cloudflare е сървърът по подразбиране, можете да промените сървъра по всяко време.", |
|||
rrtypeDescription: "Изберете ресурсния запис, който желаете да наблюдавате", |
|||
pauseMonitorMsg: "Наистина ли желаете да поставите в режим пауза?", |
|||
enableDefaultNotificationDescription: "За всеки нов монитор това известие ще бъде активирано по подразбиране. Можете да изключите известието за всеки отделен монитор.", |
|||
clearEventsMsg: "Наистина ли желаете да изтриете всички събития за този монитор?", |
|||
clearHeartbeatsMsg: "Наистина ли желаете да изтриете всички записи за честотни проверки на този монитор?", |
|||
confirmClearStatisticsMsg: "Наистина ли желаете да изтриете всички статистически данни?", |
|||
importHandleDescription: "Изберете 'Пропусни съществуващите', ако искате да пропуснете всеки монитор или известие със същото име. 'Презапис' ще изтрие всеки съществуващ монитор и известие.", |
|||
confirmImportMsg: "Сигурни ли сте за импортирането на архива? Моля, уверете се, че сте избрали правилната опция за импортиране.", |
|||
twoFAVerifyLabel: "Моля, въведете вашия токен код, за да проверите дали 2FA работи", |
|||
tokenValidSettingsMsg: "Токен кодът е валиден! Вече можете да запазите настройките за 2FA.", |
|||
confirmEnableTwoFAMsg: "Сигурни ли сте, че желаете да активирате 2FA?", |
|||
confirmDisableTwoFAMsg: "Сигурни ли сте, че желаете да изключите 2FA?", |
|||
Settings: "Настройки", |
|||
Dashboard: "Табло", |
|||
"New Update": "Нова актуализация", |
|||
Language: "Език", |
|||
Appearance: "Изглед", |
|||
Theme: "Тема", |
|||
General: "Общи", |
|||
Version: "Версия", |
|||
"Check Update On GitHub": "Провери за актуализация в GitHub", |
|||
List: "Списък", |
|||
Add: "Добави", |
|||
"Add New Monitor": "Добави монитор", |
|||
"Quick Stats": "Кратка статистика", |
|||
Up: "Достъпни", |
|||
Down: "Недостъпни", |
|||
Pending: "В изчакване", |
|||
Unknown: "Неизвестни", |
|||
Pause: "В пауза", |
|||
Name: "Име", |
|||
Status: "Статус", |
|||
DateTime: "Дата и час", |
|||
Message: "Съобщение", |
|||
"No important events": "Няма важни събития", |
|||
Resume: "Възобнови", |
|||
Edit: "Редактирай", |
|||
Delete: "Изтрий", |
|||
Current: "Текущ", |
|||
Uptime: "Време на работа", |
|||
"Cert Exp.": "Вал. сертификат", |
|||
days: "дни", |
|||
day: "ден", |
|||
"-day": "-ден", |
|||
hour: "час", |
|||
"-hour": "-час", |
|||
Response: "Отговор", |
|||
Ping: "Пинг", |
|||
"Monitor Type": "Монитор тип", |
|||
Keyword: "Ключова дума", |
|||
"Friendly Name": "Псевдоним", |
|||
URL: "URL Адрес", |
|||
Hostname: "Име на хост", |
|||
Port: "Порт", |
|||
"Heartbeat Interval": "Честота на проверка", |
|||
Retries: "Повторни опити", |
|||
"Heartbeat Retry Interval": "Честота на повторните опити", |
|||
Advanced: "Разширени", |
|||
"Upside Down Mode": "Обърнат режим", |
|||
"Max. Redirects": "Макс. брой пренасочвания", |
|||
"Accepted Status Codes": "Допустими статус кодове", |
|||
Save: "Запази", |
|||
Notifications: "Известявания", |
|||
"Not available, please setup.": "Не е налично. Моля, настройте.", |
|||
"Setup Notification": "Настройка за известяване", |
|||
Light: "Светла", |
|||
Dark: "Тъмна", |
|||
Auto: "Автоматично", |
|||
"Theme - Heartbeat Bar": "Тема - поле проверки", |
|||
Normal: "Нормално", |
|||
Bottom: "Долу", |
|||
None: "Без", |
|||
Timezone: "Часова зона", |
|||
"Search Engine Visibility": "Видимост за търсачки", |
|||
"Allow indexing": "Разреши индексиране", |
|||
"Discourage search engines from indexing site": "Обезкуражи индексирането на сайта от търсачките", |
|||
"Change Password": "Промени парола", |
|||
"Current Password": "Текуща парола", |
|||
"New Password": "Нова парола", |
|||
"Repeat New Password": "Повторете новата парола", |
|||
"Update Password": "Актуализирай парола", |
|||
"Disable Auth": "Изключи удостоверяване", |
|||
"Enable Auth": "Включи удостоверяване", |
|||
Logout: "Изход от профила", |
|||
Leave: "Напускам", |
|||
"I understand, please disable": "Разбирам. Моля, изключи", |
|||
Confirm: "Потвърди", |
|||
Yes: "Да", |
|||
No: "Не", |
|||
Username: "Потребител", |
|||
Password: "Парола", |
|||
"Remember me": "Запомни ме", |
|||
Login: "Вход", |
|||
"No Monitors, please": "Моля, без монитори", |
|||
"add one": "добави един", |
|||
"Notification Type": "Тип известяване", |
|||
Email: "Имейл", |
|||
Test: "Тест", |
|||
"Certificate Info": "Информация за сертификат", |
|||
"Resolver Server": "Преобразуващ (DNS) сървър", |
|||
"Resource Record Type": "Тип запис", |
|||
"Last Result": "Последен резултат", |
|||
"Create your admin account": "Създаване на администриращ акаунт", |
|||
"Repeat Password": "Повторете паролата", |
|||
"Import Backup": "Импорт на архив", |
|||
"Export Backup": "Експорт на архив", |
|||
Export: "Експорт", |
|||
Import: "Импорт", |
|||
respTime: "Време за отговор (ms)", |
|||
notAvailableShort: "Няма", |
|||
"Default enabled": "Включен по подразбиране", |
|||
"Apply on all existing monitors": "Приложи върху всички съществуващи монитори", |
|||
Create: "Създай", |
|||
"Clear Data": "Изчисти данни", |
|||
Events: "Събития", |
|||
Heartbeats: "Проверки", |
|||
"Auto Get": "Автоматияно получаване", |
|||
backupDescription: "Можете да архивирате всички монитори и всички известия в JSON файл.", |
|||
backupDescription2: "PS: Данни за история и събития не са включени.", |
|||
backupDescription3: "Чувствителни данни, като токен кодове за известяване, се съдържат в експортирания файл. Моля, бъдете внимателни с неговото съхранение.", |
|||
alertNoFile: "Моля, изберете файл за импортиране.", |
|||
alertWrongFileType: "Моля, изберете JSON файл.", |
|||
"Clear all statistics": "Изчисти всички статистики", |
|||
"Skip existing": "Пропусни съществуващите", |
|||
Overwrite: "Презапиши", |
|||
Options: "Опции", |
|||
"Keep both": "Запази двете", |
|||
"Verify Token": "Проверка на токен код", |
|||
"Setup 2FA": "Настройка 2FA", |
|||
"Enable 2FA": "Включи 2FA", |
|||
"Disable 2FA": "Изключи 2FA", |
|||
"2FA Settings": "Настройки 2FA", |
|||
"Two Factor Authentication": "Двуфакторно удостоверяване", |
|||
Active: "Активно", |
|||
Inactive: "Неактивно", |
|||
Token: "Токен код", |
|||
"Show URI": "Покажи URI", |
|||
Tags: "Етикети", |
|||
"Add New below or Select...": "Добави нов по-долу или избери...", |
|||
"Tag with this name already exist.": "Етикет с това име вече съществува.", |
|||
"Tag with this value already exist.": "Етикет с тази стойност вече съществува.", |
|||
color: "цвят", |
|||
"value (optional)": "стойност (по желание)", |
|||
Gray: "Сиво", |
|||
Red: "Червено", |
|||
Orange: "Оранжево", |
|||
Green: "Зелено", |
|||
Blue: "Синьо", |
|||
Indigo: "Индиго", |
|||
Purple: "Лилаво", |
|||
Pink: "Розово", |
|||
"Search...": "Търси...", |
|||
"Avg. Ping": "Ср. пинг", |
|||
"Avg. Response": "Ср. отговор", |
|||
"Entry Page": "Основна страница", |
|||
statusPageNothing: "Все още няма нищо тук. Моля, добавете група или монитор.", |
|||
"No Services": "Няма Услуги", |
|||
"All Systems Operational": "Всички системи функционират", |
|||
"Partially Degraded Service": "Частично влошена услуга", |
|||
"Degraded Service": "Влошена услуга", |
|||
"Add Group": "Добави група", |
|||
"Add a monitor": "Добави монитор", |
|||
"Edit Status Page": "Редактирай статус страница", |
|||
"Go to Dashboard": "Към Таблото", |
|||
}; |
@ -0,0 +1,190 @@ |
|||
export default { |
|||
languageName: "Farsi", |
|||
checkEverySecond: "بررسی هر {0} ثانیه.", |
|||
retryCheckEverySecond: "تکرار مجدد هر {0} ثانیه.", |
|||
retriesDescription: "حداکثر تعداد تکرار پیش از علامت گذاری وبسایت بعنوان خارج از دسترس و ارسال اطلاعرسانی.", |
|||
ignoreTLSError: "بیخیال ارور TLS/SSL برای سایتهای HTTPS", |
|||
upsideDownModeDescription: "نتیجه وضعیت را برعکس کن، مثلا اگر سرویس در دسترس بود فرض کن که سرویس پایین است!", |
|||
maxRedirectDescription: "حداکثر تعداد ریدایرکتی که سرویس پشتیبانی کند. برای اینکه ریدایرکتها پشتیبانی نشوند، عدد 0 را وارد کنید.", |
|||
acceptedStatusCodesDescription: "لطفا HTTP Status Code هایی که میخواهید به عنوان پاسخ موفقیت آمیز در نظر گرفته شود را انتخاب کنید.", |
|||
passwordNotMatchMsg: "تکرار رمز عبور مطابقت ندارد!", |
|||
notificationDescription: "برای اینکه سرویس اطلاعرسانی کار کند، آنرا به یکی از مانیتورها متصل کنید.", |
|||
keywordDescription: "در نتیجه درخواست (اهمیتی ندارد پاسخ JSON است یا HTML) بدنبال این کلمه بگرد (حساس به کوچک/بزرگ بودن حروف).", |
|||
pauseDashboardHome: "متوقف شده", |
|||
deleteMonitorMsg: "آیا از حذف این مانیتور مطمئن هستید؟", |
|||
deleteNotificationMsg: "آیا مطمئن هستید که میخواهید این سرویس اطلاعرسانی را برای تمامی مانیتورها حذف کنید؟", |
|||
resoverserverDescription: "سرویس CloudFlare به عنوان سرور پیشفرض استفاده میشود، شما میتوانید آنرا به هر سرور دیگری بعدا تغییر دهید.", |
|||
rrtypeDescription: "لطفا نوع Resource Record را انتخاب کنید.", |
|||
pauseMonitorMsg: "آیا مطمئن هستید که میخواهید این مانیتور را متوقف کنید ؟", |
|||
enableDefaultNotificationDescription: "برای هر مانیتور جدید، این سرویس اطلاعرسانی به صورت پیشفرض فعال خواهد شد. البته که شما میتوانید به صورت دستی آنرا برای هر مانیتور به صورت جداگانه غیر فعال کنید.", |
|||
clearEventsMsg: "آیا از اینکه تمامی تاریخچه رویدادهای این مانیتور حذف شود مطمئن هستید؟", |
|||
clearHeartbeatsMsg: "آیا از اینکه تاریخچه تمامی Heartbeat های این مانیتور حذف شود مطمئن هستید؟ ", |
|||
confirmClearStatisticsMsg: "آیا از حذف تمامی آمار و ارقام مطمئن هستید؟", |
|||
importHandleDescription: " اگر که میخواهید بیخیال مانیتورها و یا سرویسهای اطلاعرسانی که با نام مشابه از قبل موجود هستند شوید، گزینه 'بیخیال موارد ..' را انتخاب کنید. توجه کنید که گزینه 'بازنویسی' تمامی موارد موجود با نام مشابه را از بین خواهد برد.", |
|||
confirmImportMsg: "آیا از بازگردانی بک آپ مطمئن هستید؟ لطفا از اینکه نوع بازگردانی درستی را انتخاب کردهاید اطمینان حاصل کنید!", |
|||
twoFAVerifyLabel: "لطفا جهت اطمینان از عملکرد احراز هویت دو مرحلهای توکن خود را وارد کنید!", |
|||
tokenValidSettingsMsg: "توکن شما معتبر است، هم اکنون میتوانید احراز هویت دو مرحلهای را فعال کنید!", |
|||
confirmEnableTwoFAMsg: " آیا از فعال سازی احراز هویت دو مرحلهای مطمئن هستید؟", |
|||
confirmDisableTwoFAMsg: "آیا از غیرفعال سازی احراز هویت دومرحلهای مطمئن هستید؟", |
|||
Settings: "تنظیمات", |
|||
Dashboard: "پیشخوان", |
|||
"New Update": "بروزرسانی جدید!", |
|||
Language: "زبان", |
|||
Appearance: "ظاهر", |
|||
Theme: "پوسته", |
|||
General: "عمومی", |
|||
Version: "نسخه", |
|||
"Check Update On GitHub": "بررسی بروزرسانی بر روی گیتهاب", |
|||
List: "لیست", |
|||
Add: "اضافه", |
|||
"Add New Monitor": "اضافه کردن مانیتور جدید", |
|||
"Quick Stats": "خلاصه وضعیت", |
|||
Up: "فعال", |
|||
Down: "غیرفعال", |
|||
Pending: "در انتظار تایید", |
|||
Unknown: "نامشخص", |
|||
Pause: "توقف", |
|||
Name: "نام", |
|||
Status: "وضعیت", |
|||
DateTime: "تاریخ و زمان", |
|||
Message: "پیام", |
|||
"No important events": "رخداد جدیدی موجود نیست.", |
|||
Resume: "ادامه", |
|||
Edit: "ویرایش", |
|||
Delete: "حذف", |
|||
Current: "فعلی", |
|||
Uptime: "آپتایم", |
|||
"Cert Exp.": "تاریخ انقضای SSL", |
|||
days: "روز", |
|||
day: "روز", |
|||
"-day": "-روز", |
|||
hour: "ساعت", |
|||
"-hour": "-ساعت", |
|||
Response: "پاسخ", |
|||
Ping: "Ping", |
|||
"Monitor Type": "نوع مانیتور", |
|||
Keyword: "کلمه کلیدی", |
|||
"Friendly Name": "عنوان", |
|||
URL: "آدرس (URL)", |
|||
Hostname: "نام میزبان (Hostname)", |
|||
Port: "پورت", |
|||
"Heartbeat Interval": "فاصله هر Heartbeat", |
|||
Retries: "تلاش مجدد", |
|||
"Heartbeat Retry Interval": "فاصله تلاش مجدد برایHeartbeat", |
|||
Advanced: "پیشرفته", |
|||
"Upside Down Mode": "حالت بر عکس", |
|||
"Max. Redirects": "حداکثر تعداد ریدایرکت", |
|||
"Accepted Status Codes": "وضعیتهای (Status Code) های قابل قبول", |
|||
Save: "ذخیره", |
|||
Notifications: "اطلاعرسانیها", |
|||
"Not available, please setup.": "هیچ موردی موجود نیست، اولین مورد را راه اندازی کنید!", |
|||
"Setup Notification": "راه اندازی اطلاعرسانی", |
|||
Light: "روشن", |
|||
Dark: "تاریک", |
|||
Auto: "اتوماتیک", |
|||
"Theme - Heartbeat Bar": "ظاهر نوار Heartbeat", |
|||
Normal: "معمولی", |
|||
Bottom: "پایین", |
|||
None: "هیچ کدام", |
|||
Timezone: "موقعیت زمانی", |
|||
"Search Engine Visibility": "قابلیت دسترسی برای موتورهای جستجو", |
|||
"Allow indexing": "اجازه ایندکس شدن را بده.", |
|||
"Discourage search engines from indexing site": "به موتورهای جستجو اجازه ایندکس کردن این سامانه را نده.", |
|||
"Change Password": "تغییر رمزعبور", |
|||
"Current Password": "رمزعبور فعلی", |
|||
"New Password": "رمزعبور جدید", |
|||
"Repeat New Password": "تکرار رمزعبور جدید", |
|||
"Update Password": "بروز رسانی رمز عبور", |
|||
"Disable Auth": "غیر فعال سازی تایید هویت", |
|||
"Enable Auth": "فعال سازی تایید هویت", |
|||
Logout: "خروج", |
|||
Leave: "منصرف شدم", |
|||
"I understand, please disable": "متوجه هستم، لطفا غیرفعال کنید!", |
|||
Confirm: "تایید", |
|||
Yes: "بلی", |
|||
No: "خیر", |
|||
Username: "نام کاربری", |
|||
Password: "کلمه عبور", |
|||
"Remember me": "مراب هب خاطر بسپار", |
|||
Login: "ورود", |
|||
"No Monitors, please": "هیچ مانیتوری موجود نیست، لطفا", |
|||
"add one": "یک مورد اضافه کنید", |
|||
"Notification Type": "نوع اطلاعرسانی", |
|||
Email: "ایمیل", |
|||
Test: "تست", |
|||
"Certificate Info": "اطلاعات سرتیفیکت", |
|||
"Resolver Server": "سرور Resolver", |
|||
"Resource Record Type": "نوع رکورد (Resource Record Type)", |
|||
"Last Result": "آخرین نتیجه", |
|||
"Create your admin account": "ایجاد حساب کاربری مدیر", |
|||
"Repeat Password": "تکرار رمز عبور", |
|||
"Import Backup": "بازگردانی فایل پشتیبان", |
|||
"Export Backup": "ذخیره فایل پشتیبان", |
|||
Export: "استخراج اطلاعات", |
|||
Import: "ورود اطلاعات", |
|||
respTime: "زمان پاسخگویی (میلیثانیه)", |
|||
notAvailableShort: "ناموجود", |
|||
"Default enabled": "به صورت پیشفرض فعال باشد.", |
|||
"Apply on all existing monitors": "بر روی تمامی مانیتورهای فعلی اعمال شود.", |
|||
Create: "ایجاد", |
|||
"Clear Data": "پاکسازی دادهها", |
|||
Events: "رخدادها", |
|||
Heartbeats: "Heartbeats", |
|||
"Auto Get": "Auto Get", |
|||
backupDescription: "شما میتوانید تمامی مانیتورها و تنظیمات اطلاعرسانیها را در قالب یه فایل JSON دریافت کنید.", |
|||
backupDescription2: "البته تاریخچه رخدادها دراین فایل قرار نخواهند داشت.", |
|||
backupDescription3: "توجه داشته باشید که تمامی اطلاعات حساس شما مانند توکنها نیز در این فایل وجود خواهد داشت ، پس از این فایل به خوبی مراقبت کنید.", |
|||
alertNoFile: "لطفا یک فایل برای «ورود اطلاعات» انتخاب کنید..", |
|||
alertWrongFileType: "یک فایل JSON انتخاب کنید.", |
|||
"Clear all statistics": "پاکسازی تمامی آمار و ارقام", |
|||
"Skip existing": "بیخیال مواردی که از قبل موجود است", |
|||
Overwrite: "بازنویسی", |
|||
Options: "تنظیمات", |
|||
"Keep both": "هر دو را نگه دار", |
|||
"Verify Token": "تایید توکن", |
|||
"Setup 2FA": "تنظیمات احراز دو مرحلهای", |
|||
"Enable 2FA": "فعال سازی احراز 2 مرحلهای", |
|||
"Disable 2FA": "غیر فعال کردن احراز 2 مرحلهای", |
|||
"2FA Settings": "تنظیمات احراز 2 مرحلهای", |
|||
"Two Factor Authentication": "احراز هویت دومرحلهای", |
|||
Active: "فعال", |
|||
Inactive: "غیرفعال", |
|||
Token: "توکن", |
|||
"Show URI": "نمایش آدرس (URI) ", |
|||
Tags: "برچسبها", |
|||
"Add New below or Select...": "یک مورد جدید اضافه کنید و یا از لیست انتخاب کنید...", |
|||
"Tag with this name already exist.": "یک برچسب با این «نام» از قبل وجود دارد", |
|||
"Tag with this value already exist.": "یک برچسب با این «مقدار» از قبل وجود دارد.", |
|||
color: "رنگ", |
|||
"value (optional)": "مقدار (اختیاری)", |
|||
Gray: "خاکستری", |
|||
Red: "قرمز", |
|||
Orange: "نارنجی", |
|||
Green: "سبز", |
|||
Blue: "آبی", |
|||
Indigo: "نیلی", |
|||
Purple: "بنفش", |
|||
Pink: "صورتی", |
|||
"Search...": "جستجو...", |
|||
"Avg. Ping": "متوسط پینگ", |
|||
"Avg. Response": "متوسط زمان پاسخ", |
|||
"Entry Page": "صفحه ورودی", |
|||
statusPageNothing: "چیزی اینجا نیست، لطفا یک گروه و یا یک مانیتور اضافه کنید!", |
|||
"No Services": "هیچ سرویسی موجود نیست", |
|||
"All Systems Operational": "تمامی سیستمها عملیاتی هستند!", |
|||
"Partially Degraded Service": "افت نسبی کیفیت سرویس", |
|||
"Degraded Service": "افت کامل کیفیت سرویس", |
|||
"Add Group": "اضافه کردن گروه", |
|||
"Add a monitor": "اضافه کردن مانیتور", |
|||
"Edit Status Page": "ویرایش صفحه وضعیت", |
|||
"Status Page": "صفحه وضعیت", |
|||
"Go to Dashboard": "رفتن به پیشخوان", |
|||
"Uptime Kuma": "آپتایم کوما", |
|||
records: "مورد", |
|||
"One record": "یک مورد", |
|||
"Showing {from} to {to} of {count} records": "نمایش از {from} تا {to} از {count} مورد", |
|||
First: "اولین", |
|||
Last: "آخرین", |
|||
Info: "اطلاعات", |
|||
"Powered By": "نیرو گرفته از", |
|||
}; |
@ -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 changed in this diff
Loading…
Reference in new issue