Merge branch 'louislam:master' into master

This commit is contained in:
Phuong Nguyen Minh 2021-10-22 08:08:22 +07:00 committed by GitHub
commit 060dde9827
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 2444 additions and 693 deletions

View file

@ -6,7 +6,7 @@ labels: help
assignees: '' assignees: ''
--- ---
**Is it a duplicate question?** **Is it a duplicated question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Describe your problem** **Describe your problem**

View file

@ -7,7 +7,7 @@ assignees: ''
--- ---
**Is it a duplicate question?** **Is it a duplicated question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Describe the bug** **Describe the bug**

View file

@ -6,7 +6,7 @@ labels: enhancement
assignees: '' assignees: ''
--- ---
**Is it a duplicate question?** **Is it a duplicated question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q= Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**

22
.github/workflows/stale-bot vendored Normal file
View file

@ -0,0 +1,22 @@
name: 'Automatically close stale issues and PRs'
on:
schedule:
- cron: '0 0 * * *'
#Run once a day at midnight
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.'
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.'
days-before-stale: 180
days-before-close: 7
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,'
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,'
exempt-issue-assignees: 'louislam'
exempt-pr-assignees: 'louislam'

View file

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
louis@uptimekuma.louislam.net. uptime@kuma.pet.
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View file

@ -178,3 +178,24 @@ Patch release = the third digit ([Semantic Versioning](https://semver.org/))
## Translations ## Translations
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
## Maintainer
Check the latest issues and pull requests:
https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
### Release Procedures
1. Draft a release note
1. Make sure the repo is cleared
1. `npm run update-version 1.X.X`
1. `npm run build-docker`
1. git push
1. Publish the release note as 1.X.X
1. npm run upload-artifacts
1. SSH to demo site server and update to 1.X.X
Checking:
- Check all tags is fine on https://hub.docker.com/r/louislam/uptime-kuma/tags
- Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 / armv7)
- Try clean install with Node.js

View file

@ -16,7 +16,7 @@ Try it!
https://demo.uptime.kuma.pet https://demo.uptime.kuma.pet
It is a 10 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it. It is a temporary live demo, all data will be deleted after 10 minutes. The server is located at Tokyo, so if you live far from there it may affect your experience. I suggest that you should install and try it out for the best demo experience.
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much! VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
@ -25,7 +25,7 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push. * Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record / Push.
* Fancy, Reactive, Fast UI/UX. * Fancy, Reactive, Fast UI/UX.
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications). * Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
* 20 seconds interval. * 20 second intervals.
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages) * [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
* Simple Status Page * Simple Status Page
* Ping Chart * Ping Chart
@ -40,7 +40,7 @@ docker volume create uptime-kuma
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1 docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
``` ```
Browse to http://localhost:3001 after started. Browse to http://localhost:3001 after starting.
### 💪🏻 Without Docker ### 💪🏻 Without Docker
@ -58,11 +58,11 @@ npm run setup
node server/server.js node server/server.js
# (Recommended) Option 2. Run in background using PM2 # (Recommended) Option 2. Run in background using PM2
# Install PM2 if you don't have: npm install pm2 -g # Install PM2 if you don't have it: npm install pm2 -g
pm2 start server/server.js --name uptime-kuma pm2 start server/server.js --name uptime-kuma
``` ```
Browse to http://localhost:3001 after started. Browse to http://localhost:3001 after starting.
### Advanced Installation ### Advanced Installation
@ -124,7 +124,7 @@ You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-k
### Subreddit ### Subreddit
My Reddit account: louislamlam My Reddit account: louislamlam
You can mention me if you ask question on Reddit. You can mention me if you ask a question on Reddit.
https://www.reddit.com/r/UptimeKuma/ https://www.reddit.com/r/UptimeKuma/
## Contribute ## Contribute

View file

@ -9,8 +9,8 @@ currently being supported with security updates.
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 1.7.X | :white_check_mark: | | 1.8.X | :white_check_mark: |
| < 1.7 | | | <= 1.7.X | ❌ |
### Upgradable Docker Tags ### Upgradable Docker Tags

View file

@ -0,0 +1,13 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD method TEXT default 'GET' not null;
ALTER TABLE monitor
ADD body TEXT default null;
ALTER TABLE monitor
ADD headers TEXT default null;
COMMIT;

View file

@ -31,14 +31,15 @@ WORKDIR /
RUN apt update && \ RUN apt update && \
apt --yes install curl file apt --yes install curl file
COPY --from=build /app /app
ARG VERSION=1.9.1
ARG GITHUB_TOKEN ARG GITHUB_TOKEN
ARG TARGETARCH ARG TARGETARCH
ARG PLATFORM=debian ARG PLATFORM=debian
ARG VERSION
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz
ARG DIST=dist.tar.gz ARG DIST=dist.tar.gz
COPY --from=build /app /app
RUN chmod +x /app/extra/upload-github-release-asset.sh RUN chmod +x /app/extra/upload-github-release-asset.sh
# Full Build # Full Build

View file

@ -12,50 +12,59 @@ const rl = readline.createInterface({
output: process.stdout output: process.stdout
}); });
(async () => { const main = async () => {
Database.init(args); Database.init(args);
await Database.connect(); await Database.connect();
try { try {
const user = await R.findOne("user"); // No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
if (!process.env.TEST_BACKEND) {
if (! user) { const user = await R.findOne("user");
throw new Error("user not found, have you installed?"); if (! user) {
} throw new Error("user not found, have you installed?");
console.log("Found user: " + user.username);
while (true) {
let password = await question("New Password: ");
let confirmPassword = await question("Confirm New Password: ");
if (password === confirmPassword) {
await user.resetPassword(password);
// Reset all sessions by reset jwt secret
await initJWTSecret();
rl.close();
break;
} else {
console.log("Passwords do not match, please try again.");
} }
}
console.log("Password reset successfully."); console.log("Found user: " + user.username);
while (true) {
let password = await question("New Password: ");
let confirmPassword = await question("Confirm New Password: ");
if (password === confirmPassword) {
await user.resetPassword(password);
// Reset all sessions by reset jwt secret
await initJWTSecret();
break;
} else {
console.log("Passwords do not match, please try again.");
}
}
console.log("Password reset successfully.");
}
} catch (e) { } catch (e) {
console.error("Error: " + e.message); console.error("Error: " + e.message);
} }
await Database.close(); await Database.close();
rl.close();
console.log("Finished. You should restart the Uptime Kuma server.") console.log("Finished.");
})(); };
function question(question) { function question(question) {
return new Promise((resolve) => { return new Promise((resolve) => {
rl.question(question, (answer) => { rl.question(question, (answer) => {
resolve(answer); resolve(answer);
}) });
}); });
} }
if (!process.env.TEST_BACKEND) {
main();
}
module.exports = {
main,
};

View file

@ -26,10 +26,12 @@ const copyRecursiveSync = function (src, dest) {
} }
}; };
console.log("Arguments:", process.argv) console.log("Arguments:", process.argv);
const baseLangCode = process.argv[2] || "en"; const baseLangCode = process.argv[2] || "en";
console.log("Base Lang: " + baseLangCode); console.log("Base Lang: " + baseLangCode);
fs.rmdirSync("./languages", { recursive: true }); if (fs.existsSync("./languages")) {
fs.rmdirSync("./languages", { recursive: true });
}
copyRecursiveSync("../../src/languages", "./languages"); copyRecursiveSync("../../src/languages", "./languages");
const en = (await import("./languages/en.js")).default; const en = (await import("./languages/en.js")).default;
@ -39,7 +41,7 @@ console.log("Files:", files);
for (const file of files) { for (const file of files) {
if (!file.endsWith(".js")) { if (!file.endsWith(".js")) {
console.log("Skipping " + file) console.log("Skipping " + file);
continue; continue;
} }

View file

@ -1,32 +0,0 @@
# Uptime-Kuma K8s Deployment
⚠ Warning: K8s deployment is provided by contributors. I have no experience with K8s and I can't fix error in the future. I only test Docker and Node.js. Use at your own risk.
## How does it work?
Kustomize is a tool which builds a complete deployment file for all config elements.
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service.
## What do I have to edit?
You have to edit the ```ingressroute.yml``` to your needs.
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
- Host
- Secrets and secret names
- (Cluster)Issuer (optional)
- The Version in the Deployment-File
- Update:
- Change to newer version and run the above commands, it will update the pods one after another
## How To use
- Install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
- Edit files mentioned above to your needs
- Run ```kustomize build > apply.yml```
- Run ```kubectl apply -f apply.yml```
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.

View file

@ -1,10 +0,0 @@
namespace: uptime-kuma
namePrefix: uptime-kuma-
commonLabels:
app: uptime-kuma
bases:
- uptime-kuma

View file

@ -1,45 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
component: uptime-kuma
name: deployment
spec:
selector:
matchLabels:
component: uptime-kuma
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
component: uptime-kuma
spec:
containers:
- name: app
image: louislam/uptime-kuma:1
ports:
- containerPort: 3001
volumeMounts:
- mountPath: /app/data
name: storage
livenessProbe:
exec:
command:
- node
- extra/healthcheck.js
initialDelaySeconds: 180
periodSeconds: 60
timeoutSeconds: 30
readinessProbe:
httpGet:
path: /
port: 3001
scheme: HTTP
volumes:
- name: storage
persistentVolumeClaim:
claimName: pvc

View file

@ -1,39 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/server-snippets: |
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_cache_bypass $http_upgrade;
}
name: ingress
spec:
tls:
- hosts:
- example.com
secretName: example-com-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service
port:
number: 3001

View file

@ -1,5 +0,0 @@
resources:
- deployment.yml
- service.yml
- ingressroute.yml
- pvc.yml

View file

@ -1,10 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 4Gi

View file

@ -1,13 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: service
spec:
selector:
component: uptime-kuma
type: ClusterIP
ports:
- name: http
port: 3001
targetPort: 3001
protocol: TCP

653
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.8.0", "version": "1.9.1",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,13 +30,13 @@
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine", "build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push", "build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push", "build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
"build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.8.0-alpine --target release . --push", "build-docker-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.9.1-alpine --target release . --push",
"build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.8.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.8.0-debian --target release . --push", "build-docker-debian": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.9.1 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.9.1-debian --target release . --push",
"build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-nightly": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push", "build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"upload-artifacts": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.8.0 && npm ci --production && npm run download-dist", "setup": "git checkout 1.9.1 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js", "download-dist": "node extra/download-dist.js",
"update-version": "node extra/update-version.js", "update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js", "mark-as-nightly": "node extra/mark-as-nightly.js",
@ -60,11 +60,13 @@
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0", "args-parser": "~1.3.0",
"axios": "~0.21.4", "axios": "~0.21.4",
"babel-plugin-rewire": "~1.2.0",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"bootstrap": "~5.1.1", "bootstrap": "~5.1.1",
"bree": "~6.3.1",
"chardet": "^1.3.0",
"chart.js": "~3.5.1", "chart.js": "~3.5.1",
"chartjs-adapter-dayjs": "~1.0.0", "chartjs-adapter-dayjs": "~1.0.0",
"check-password-strength": "^2.0.3",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~3.6.0", "compare-versions": "~3.6.0",
"dayjs": "~1.10.7", "dayjs": "~1.10.7",
@ -72,7 +74,9 @@
"express-basic-auth": "~1.2.0", "express-basic-auth": "~1.2.0",
"form-data": "~4.0.0", "form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.4", "http-graceful-shutdown": "~3.1.4",
"iconv-lite": "^0.6.3",
"jsonwebtoken": "~8.5.1", "jsonwebtoken": "~8.5.1",
"limiter": "^2.1.0",
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
"password-hash": "~1.2.2", "password-hash": "~1.2.2",
@ -108,6 +112,7 @@
"@vitejs/plugin-legacy": "~1.6.1", "@vitejs/plugin-legacy": "~1.6.1",
"@vitejs/plugin-vue": "~1.9.2", "@vitejs/plugin-vue": "~1.9.2",
"@vue/compiler-sfc": "~3.2.19", "@vue/compiler-sfc": "~3.2.19",
"babel-plugin-rewire": "~1.2.0",
"core-js": "~3.18.1", "core-js": "~3.18.1",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"dns2": "~2.0.1", "dns2": "~2.0.1",

7
server/config.js Normal file
View file

@ -0,0 +1,7 @@
const args = require("args-parser")(process.argv);
const demoMode = args["demo"] || false;
module.exports = {
args,
demoMode
};

View file

@ -49,10 +49,11 @@ class Database {
"patch-incident-table.sql": true, "patch-incident-table.sql": true,
"patch-group-table.sql": true, "patch-group-table.sql": true,
"patch-monitor-push_token.sql": true, "patch-monitor-push_token.sql": true,
"patch-http-monitor-method-body-and-headers.sql": true,
} }
/** /**
* The finally version should be 10 after merged tag feature * The final version should be 10 after merged tag feature
* @deprecated Use patchList for any new feature * @deprecated Use patchList for any new feature
*/ */
static latestVersion = 10; static latestVersion = 10;
@ -130,7 +131,7 @@ class Database {
console.info("Latest database version: " + this.latestVersion); console.info("Latest database version: " + this.latestVersion);
if (version === this.latestVersion) { if (version === this.latestVersion) {
console.info("Database no need to patch"); console.info("Database patch not needed");
} else if (version > this.latestVersion) { } else if (version > this.latestVersion) {
console.info("Warning: Database version is newer than expected"); console.info("Warning: Database version is newer than expected");
} else { } else {
@ -151,8 +152,8 @@ class Database {
await Database.close(); await Database.close();
console.error(ex); console.error(ex);
console.error("Start Uptime-Kuma failed due to patch db failed"); console.error("Start Uptime-Kuma failed due to issue patching the database");
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues"); console.error("Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore(); this.restore();
process.exit(1); process.exit(1);
@ -190,7 +191,7 @@ class Database {
await Database.close(); await Database.close();
console.error(ex); console.error(ex);
console.error("Start Uptime-Kuma failed due to patch db failed"); console.error("Start Uptime-Kuma failed due to issue patching the database");
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues"); console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore(); this.restore();
@ -231,7 +232,7 @@ class Database {
this.patched = true; this.patched = true;
await this.importSQLFile("./db/" + sqlFilename); await this.importSQLFile("./db/" + sqlFilename);
databasePatchedFiles[sqlFilename] = true; databasePatchedFiles[sqlFilename] = true;
console.log(sqlFilename + " is patched successfully"); console.log(sqlFilename + " was patched successfully");
} else { } else {
debug(sqlFilename + " is already patched, skip"); debug(sqlFilename + " is already patched, skip");
@ -286,7 +287,7 @@ class Database {
}; };
process.addListener("unhandledRejection", listener); process.addListener("unhandledRejection", listener);
console.log("Closing DB"); console.log("Closing the database");
while (true) { while (true) {
Database.noReject = true; Database.noReject = true;
@ -296,7 +297,7 @@ class Database {
if (Database.noReject) { if (Database.noReject) {
break; break;
} else { } else {
console.log("Waiting to close the db"); console.log("Waiting to close the database");
} }
} }
console.log("SQLite closed"); console.log("SQLite closed");
@ -311,7 +312,7 @@ class Database {
*/ */
static backup(version) { static backup(version) {
if (! this.backupPath) { if (! this.backupPath) {
console.info("Backup the db"); console.info("Backing up the database");
this.backupPath = this.dataDir + "kuma.db.bak" + version; this.backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, this.backupPath); fs.copyFileSync(Database.path, this.backupPath);
@ -334,7 +335,7 @@ class Database {
*/ */
static restore() { static restore() {
if (this.backupPath) { if (this.backupPath) {
console.error("Patch db failed!!! Restoring the backup"); console.error("Patching the database failed!!! Restoring the backup");
const shmPath = Database.path + "-shm"; const shmPath = Database.path + "-shm";
const walPath = Database.path + "-wal"; const walPath = Database.path + "-wal";
@ -353,7 +354,7 @@ class Database {
fs.unlinkSync(walPath); fs.unlinkSync(walPath);
} }
} catch (e) { } catch (e) {
console.log("Restore failed, you may need to restore the backup manually"); console.log("Restore failed; you may need to restore the backup manually");
process.exit(1); process.exit(1);
} }

31
server/jobs.js Normal file
View file

@ -0,0 +1,31 @@
const path = require("path");
const Bree = require("bree");
const { SHARE_ENV } = require("worker_threads");
const jobs = [
{
name: "clear-old-data",
interval: "at 03:14",
}
];
const initBackgroundJobs = function (args) {
const bree = new Bree({
root: path.resolve("server", "jobs"),
jobs,
worker: {
env: SHARE_ENV,
workerData: args,
},
workerMessageHandler: (message) => {
console.log("[Background Job]:", message);
}
});
bree.start();
return bree;
};
module.exports = {
initBackgroundJobs
};

View file

@ -0,0 +1,40 @@
const { log, exit, connectDb } = require("./util-worker");
const { R } = require("redbean-node");
const { setSetting, setting } = require("../util-server");
const DEFAULT_KEEP_PERIOD = 180;
(async () => {
await connectDb();
let period = await setting("keepDataPeriodDays");
// Set Default Period
if (period == null) {
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
period = DEFAULT_KEEP_PERIOD;
}
// Try parse setting
let parsedPeriod;
try {
parsedPeriod = parseInt(period);
} catch (_) {
log("Failed to parse setting, resetting to default..");
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
parsedPeriod = DEFAULT_KEEP_PERIOD;
}
log(`Clearing Data older than ${parsedPeriod} days...`);
try {
await R.exec(
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
[parsedPeriod]
);
} catch (e) {
log(`Failed to clear old data: ${e.message}`);
}
exit();
})();

View file

@ -0,0 +1,39 @@
const { parentPort, workerData } = require("worker_threads");
const Database = require("../database");
const path = require("path");
const log = function (any) {
if (parentPort) {
parentPort.postMessage(any);
}
};
const exit = function (error) {
if (error && error != 0) {
process.exit(error);
} else {
if (parentPort) {
parentPort.postMessage("done");
} else {
process.exit(0);
}
}
};
const connectDb = async function () {
const dbPath = path.join(
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
);
Database.init({
"data-dir": dbPath,
});
await Database.connect();
};
module.exports = {
log,
exit,
connectDb,
};

View file

@ -7,11 +7,11 @@ dayjs.extend(timezone);
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification"); const { Notification } = require("../notification");
const { demoMode } = require("../server"); const { demoMode } = require("../config");
const version = require("../../package.json").version; const version = require("../../package.json").version;
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
@ -55,6 +55,9 @@ class Monitor extends BeanModel {
id: this.id, id: this.id,
name: this.name, name: this.name,
url: this.url, url: this.url,
method: this.method,
body: this.body,
headers: this.headers,
hostname: this.hostname, hostname: this.hostname,
port: this.port, port: this.port,
maxretries: this.maxretries, maxretries: this.maxretries,
@ -138,11 +141,15 @@ class Monitor extends BeanModel {
// Do not do any queries/high loading things before the "bean.ping" // Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
let res = await axios.get(this.url, { const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
...(this.body ? { data: JSON.parse(this.body) } : {}),
timeout: this.interval * 1000 * 0.8, timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
...(this.headers ? JSON.parse(this.headers) : {}),
}, },
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
@ -152,7 +159,8 @@ class Monitor extends BeanModel {
validateStatus: (status) => { validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes()); return checkStatusCode(status, this.getAcceptedStatuscodes());
}, },
}); };
let res = await axios.request(options);
bean.msg = `${res.status} - ${res.statusText}`; bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
@ -172,6 +180,10 @@ class Monitor extends BeanModel {
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms"); debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
} }
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
console.log(res.data);
}
if (this.type === "http") { if (this.type === "http") {
bean.status = UP; bean.status = UP;
} else { } else {
@ -260,6 +272,46 @@ class Monitor extends BeanModel {
return; return;
} }
} else if (this.type === "steam") {
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
const steamAPIKey = await setting("steamAPIKey");
const filter = `addr\\${this.hostname}:${this.port}`;
if (!steamAPIKey) {
throw new Error("Steam API Key not found");
}
let res = await axios.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(),
}),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
params: {
filter: filter,
key: steamAPIKey,
}
});
if (res.data.response && res.data.response.servers && res.data.response.servers.length > 0) {
bean.status = UP;
bean.msg = res.data.response.servers[0].name;
try {
bean.ping = await ping(this.hostname);
} catch (_) { }
} else {
throw new Error("Server not found on Steam");
}
} else { } else {
bean.msg = "Unknown Monitor Type"; bean.msg = "Unknown Monitor Type";
bean.status = PENDING; bean.status = PENDING;
@ -292,54 +344,13 @@ class Monitor extends BeanModel {
let beatInterval = this.interval; let beatInterval = this.interval;
// * ? -> ANY STATUS = important [isFirstBeat] let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
let isImportant = isFirstBeat ||
(previousBeat.status === UP && bean.status === DOWN) ||
(previousBeat.status === DOWN && bean.status === UP) ||
(previousBeat.status === PENDING && bean.status === DOWN);
// Mark as important if status changed, ignore pending pings, // Mark as important if status changed, ignore pending pings,
// Don't notify if disrupted changes to up // Don't notify if disrupted changes to up
if (isImportant) { if (isImportant) {
bean.important = true; bean.important = true;
await Monitor.sendNotification(isFirstBeat, this, bean);
// Send only if the first beat is DOWN
if (!isFirstBeat || bean.status === DOWN) {
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
this.id,
]);
let text;
if (bean.status === UP) {
text = "✅ Up";
} else {
text = "🔴 Down";
}
let msg = `[${this.name}] [${text}] ${bean.msg}`;
for (let notification of notificationList) {
try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
} catch (e) {
console.error("Cannot send notification to " + notification.name);
console.log(e);
}
}
// Clear Status Page Cache
apicache.clear();
}
} else { } else {
bean.important = false; bean.important = false;
} }
@ -546,6 +557,53 @@ class Monitor extends BeanModel {
io.to(userID).emit("uptime", monitorID, duration, uptime); io.to(userID).emit("uptime", monitorID, duration, uptime);
} }
static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
let isImportant = isFirstBeat ||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
return isImportant;
}
static async sendNotification(isFirstBeat, monitor, bean) {
if (!isFirstBeat || bean.status === DOWN) {
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
monitor.id,
]);
let text;
if (bean.status === UP) {
text = "✅ Up";
} else {
text = "🔴 Down";
}
let msg = `[${monitor.name}] [${text}] ${bean.msg}`;
for (let notification of notificationList) {
try {
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
} catch (e) {
console.error("Cannot send notification to " + notification.name);
console.log(e);
}
}
// Clear Status Page Cache
apicache.clear();
}
}
} }
module.exports = Monitor; module.exports = Monitor;

View file

@ -0,0 +1,108 @@
const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
const { default: axios } = require("axios");
const Crypto = require("crypto");
const qs = require("qs");
class AliyunSMS extends NotificationProvider {
name = "AliyunSMS";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
if (heartbeatJSON != null) {
let msgBody = JSON.stringify({
name: monitorJSON["name"],
time: heartbeatJSON["time"],
status: this.statusToString(heartbeatJSON["status"]),
msg: heartbeatJSON["msg"],
});
if (this.sendSms(notification, msgBody)) {
return okMsg;
}
} else {
let msgBody = JSON.stringify({
name: "",
time: "",
status: "",
msg: msg,
});
if (this.sendSms(notification, msgBody)) {
return okMsg;
}
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
async sendSms(notification, msgbody) {
let params = {
PhoneNumbers: notification.phonenumber,
TemplateCode: notification.templateCode,
SignName: notification.signName,
TemplateParam: msgbody,
AccessKeyId: notification.accessKeyId,
Format: "JSON",
SignatureMethod: "HMAC-SHA1",
SignatureVersion: "1.0",
SignatureNonce: Math.random().toString(),
Timestamp: new Date().toISOString(),
Action: "SendSms",
Version: "2017-05-25",
};
params.Signature = this.sign(params, notification.secretAccessKey);
let config = {
method: "POST",
url: "http://dysmsapi.aliyuncs.com/",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: qs.stringify(params),
};
let result = await axios(config);
if (result.data.Message == "OK") {
return true;
}
return false;
}
/** Aliyun request sign */
sign(param, AccessKeySecret) {
let param2 = {};
let data = [];
let oa = Object.keys(param).sort();
for (let i = 0; i < oa.length; i++) {
let key = oa[i];
param2[key] = param[key];
}
for (let key in param2) {
data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
}
let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
return Crypto
.createHmac("sha1", `${AccessKeySecret}&`)
.update(Buffer.from(StringToSign))
.digest("base64");
}
statusToString(status) {
switch (status) {
case DOWN:
return "DOWN";
case UP:
return "UP";
default:
return status;
}
}
}
module.exports = AliyunSMS;

View file

@ -0,0 +1,79 @@
const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
const { default: axios } = require("axios");
const Crypto = require("crypto");
class DingDing extends NotificationProvider {
name = "DingDing";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
if (heartbeatJSON != null) {
let params = {
msgtype: "markdown",
markdown: {
title: monitorJSON["name"],
text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
}
};
if (this.sendToDingDing(notification, params)) {
return okMsg;
}
} else {
let params = {
msgtype: "text",
text: {
content: msg
}
};
if (this.sendToDingDing(notification, params)) {
return okMsg;
}
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
async sendToDingDing(notification, params) {
let timestamp = Date.now();
let config = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
url: `${notification.webHookUrl}&timestamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
data: JSON.stringify(params),
};
let result = await axios(config);
if (result.data.errmsg == "ok") {
return true;
}
return false;
}
/** DingDing sign */
sign(timestamp, secretKey) {
return Crypto
.createHmac("sha256", Buffer.from(secretKey, "utf8"))
.update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8"))
.digest("base64");
}
statusToString(status) {
switch (status) {
case DOWN:
return "DOWN";
case UP:
return "UP";
default:
return status;
}
}
}
module.exports = DingDing;

View file

@ -39,8 +39,9 @@ class Slack extends NotificationProvider {
} }
const time = heartbeatJSON["time"]; const time = heartbeatJSON["time"];
const textMsg = "Uptime Kuma Alert";
let data = { let data = {
"text": "Uptime Kuma Alert", "text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
"channel": notification.slackchannel, "channel": notification.slackchannel,
"username": notification.slackusername, "username": notification.slackusername,
"icon_emoji": notification.slackiconemo, "icon_emoji": notification.slackiconemo,

View file

@ -1,5 +1,6 @@
const nodemailer = require("nodemailer"); const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider"); const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
class SMTP extends NotificationProvider { class SMTP extends NotificationProvider {
@ -20,6 +21,56 @@ class SMTP extends NotificationProvider {
pass: notification.smtpPassword, pass: notification.smtpPassword,
}; };
} }
// Lets start with default subject and empty string for custom one
let subject = msg;
// Change the subject if:
// - The msg ends with "Testing" or
// - Actual Up/Down Notification
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
let customSubject = "";
// Our subject cannot end with whitespace it's often raise spam score
// Once I got "Cannot read property 'trim' of undefined", better be safe than sorry
if (notification.customSubject) {
customSubject = notification.customSubject.trim();
}
// If custom subject is not empty, change subject for notification
if (customSubject !== "") {
// Replace "MACROS" with corresponding variable
let replaceName = new RegExp("{{NAME}}", "g");
let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g");
let replaceStatus = new RegExp("{{STATUS}}", "g");
// Lets start with dummy values to simplify code
let monitorName = "Test";
let monitorHostnameOrURL = "testing.hostname";
let serviceStatus = "⚠️ Test";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
}
}
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
// Break replace to one by line for better readability
customSubject = customSubject.replace(replaceStatus, serviceStatus);
customSubject = customSubject.replace(replaceName, monitorName);
customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL);
subject = customSubject;
}
}
let transporter = nodemailer.createTransport(config); let transporter = nodemailer.createTransport(config);
@ -34,7 +85,7 @@ class SMTP extends NotificationProvider {
cc: notification.smtpCC, cc: notification.smtpCC,
bcc: notification.smtpBCC, bcc: notification.smtpBCC,
to: notification.smtpTo, to: notification.smtpTo,
subject: msg, subject: subject,
text: bodyTextContent, text: bodyTextContent,
tls: { tls: {
rejectUnauthorized: notification.smtpIgnoreTLSError || false, rejectUnauthorized: notification.smtpIgnoreTLSError || false,

View file

@ -19,6 +19,8 @@ const Teams = require("./notification-providers/teams");
const Telegram = require("./notification-providers/telegram"); const Telegram = require("./notification-providers/telegram");
const Webhook = require("./notification-providers/webhook"); const Webhook = require("./notification-providers/webhook");
const Feishu = require("./notification-providers/feishu"); const Feishu = require("./notification-providers/feishu");
const AliyunSms = require("./notification-providers/aliyun-sms");
const DingDing = require("./notification-providers/dingding");
class Notification { class Notification {
@ -31,6 +33,8 @@ class Notification {
const list = [ const list = [
new Apprise(), new Apprise(),
new AliyunSms(),
new DingDing(),
new Discord(), new Discord(),
new Teams(), new Teams(),
new Gotify(), new Gotify(),

View file

@ -4,10 +4,7 @@ const net = require("net");
const spawn = require("child_process").spawn; const spawn = require("child_process").spawn;
const events = require("events"); const events = require("events");
const fs = require("fs"); const fs = require("fs");
const WIN = /^win/.test(process.platform); const util = require("./util-server");
const LIN = /^linux/.test(process.platform);
const MAC = /^darwin/.test(process.platform);
const FBSD = /^freebsd/.test(process.platform);
module.exports = Ping; module.exports = Ping;
@ -23,12 +20,12 @@ function Ping(host, options) {
const timeout = 10; const timeout = 10;
if (WIN) { if (util.WIN) {
this._bin = "c:/windows/system32/ping.exe"; this._bin = "c:/windows/system32/ping.exe";
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ]; this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
this._regmatch = /[><=]([0-9.]+?)ms/; this._regmatch = /[><=]([0-9.]+?)ms/;
} else if (LIN) { } else if (util.LIN) {
this._bin = "/bin/ping"; this._bin = "/bin/ping";
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ]; const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
@ -40,7 +37,7 @@ function Ping(host, options) {
this._args = (options.args) ? options.args : defaultArgs; this._args = (options.args) ? options.args : defaultArgs;
this._regmatch = /=([0-9.]+?) ms/; this._regmatch = /=([0-9.]+?) ms/;
} else if (MAC) { } else if (util.MAC) {
if (net.isIPv6(host) || options.ipv6) { if (net.isIPv6(host) || options.ipv6) {
this._bin = "/sbin/ping6"; this._bin = "/sbin/ping6";
@ -51,7 +48,7 @@ function Ping(host, options) {
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ]; this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/; this._regmatch = /=([0-9.]+?) ms/;
} else if (FBSD) { } else if (util.FBSD) {
this._bin = "/sbin/ping"; this._bin = "/sbin/ping";
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ]; const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
@ -101,6 +98,9 @@ Ping.prototype.send = function (callback) {
}); });
this._ping.stdout.on("data", function (data) { // log stdout this._ping.stdout.on("data", function (data) { // log stdout
if (util.WIN) {
data = convertOutput(data);
}
this._stdout = (this._stdout || "") + data; this._stdout = (this._stdout || "") + data;
}); });
@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) {
}); });
this._ping.stderr.on("data", function (data) { // log stderr this._ping.stderr.on("data", function (data) { // log stderr
if (util.WIN) {
data = convertOutput(data);
}
this._stderr = (this._stderr || "") + data; this._stderr = (this._stderr || "") + data;
}); });
@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) {
Ping.prototype.stop = function () { Ping.prototype.stop = function () {
clearInterval(this._i); clearInterval(this._i);
}; };
/**
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
* Thank @pemassi
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
* @param data
* @returns {string}
*/
function convertOutput(data) {
if (util.WIN) {
if (data) {
return util.convertToUTF8(data);
}
}
return data;
}

View file

@ -5,7 +5,7 @@ const server = require("../server");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor"); const Monitor = require("../model/monitor");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { UP } = require("../../src/util"); const { UP, flipStatus, debug } = require("../../src/util");
let router = express.Router(); let router = express.Router();
let cache = apicache.middleware; let cache = apicache.middleware;
@ -18,9 +18,10 @@ router.get("/api/entry-page", async (_, response) => {
router.get("/api/push/:pushToken", async (request, response) => { router.get("/api/push/:pushToken", async (request, response) => {
try { try {
let pushToken = request.params.pushToken; let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK"; let msg = request.query.msg || "OK";
let ping = request.query.ping; let ping = request.query.ping || null;
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [ let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
pushToken pushToken
@ -30,12 +31,40 @@ router.get("/api/push/:pushToken", async (request, response) => {
throw new Error("Monitor not found or not active."); throw new Error("Monitor not found or not active.");
} }
const previousHeartbeat = await R.getRow(`
SELECT status, time FROM heartbeat
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
`, [
monitor.id
]);
let status = UP;
if (monitor.isUpsideDown()) {
status = flipStatus(status);
}
let isFirstBeat = true;
let previousStatus = status;
let duration = 0;
let bean = R.dispense("heartbeat"); let bean = R.dispense("heartbeat");
bean.monitor_id = monitor.id;
bean.time = R.isoDateTime(dayjs.utc()); bean.time = R.isoDateTime(dayjs.utc());
bean.status = UP;
if (previousHeartbeat) {
isFirstBeat = false;
previousStatus = previousHeartbeat.status;
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
}
debug("PreviousStatus: " + previousStatus);
debug("Current Status: " + status);
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
bean.monitor_id = monitor.id;
bean.status = status;
bean.msg = msg; bean.msg = msg;
bean.ping = ping; bean.ping = ping;
bean.duration = duration;
await R.store(bean); await R.store(bean);
@ -45,6 +74,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
response.json({ response.json({
ok: true, ok: true,
}); });
if (bean.important) {
await Monitor.sendNotification(isFirstBeat, monitor, bean);
}
} catch (e) { } catch (e) {
response.json({ response.json({
ok: false, ok: false,

View file

@ -1,6 +1,7 @@
console.log("Welcome to Uptime Kuma"); console.log("Welcome to Uptime Kuma");
const args = require("args-parser")(process.argv); const args = require("args-parser")(process.argv);
const { sleep, debug, getRandomInt, genSecret } = require("../src/util"); const { sleep, debug, getRandomInt, genSecret } = require("../src/util");
const config = require("./config");
debug(args); debug(args);
@ -8,10 +9,6 @@ if (! process.env.NODE_ENV) {
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
} }
// Demo Mode?
const demoMode = args["demo"] || false;
exports.demoMode = demoMode;
console.log("Node Env: " + process.env.NODE_ENV); console.log("Node Env: " + process.env.NODE_ENV);
console.log("Importing Node libraries"); console.log("Importing Node libraries");
@ -34,6 +31,7 @@ debug("Importing prometheus-api-metrics");
const prometheusAPIMetrics = require("prometheus-api-metrics"); const prometheusAPIMetrics = require("prometheus-api-metrics");
debug("Importing compare-versions"); debug("Importing compare-versions");
const compareVersions = require("compare-versions"); const compareVersions = require("compare-versions");
const { passwordStrength } = require("check-password-strength");
debug("Importing 2FA Modules"); debug("Importing 2FA Modules");
const notp = require("notp"); const notp = require("notp");
@ -43,7 +41,7 @@ console.log("Importing this project modules");
debug("Importing Monitor"); debug("Importing Monitor");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
debug("Importing Settings"); debug("Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest } = require("./util-server"); const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD } = require("./util-server");
debug("Importing Notification"); debug("Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
@ -52,6 +50,9 @@ Notification.init();
debug("Importing Database"); debug("Importing Database");
const Database = require("./database"); const Database = require("./database");
debug("Importing Background Jobs");
const { initBackgroundJobs } = require("./jobs");
const { basicAuth } = require("./auth"); const { basicAuth } = require("./auth");
const { login } = require("./auth"); const { login } = require("./auth");
const passwordHash = require("./password-hash"); const passwordHash = require("./password-hash");
@ -61,12 +62,29 @@ console.info("Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise. // If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::) // Dual-stack support for (::)
const hostname = process.env.HOST || args.host; let hostname = process.env.UPTIME_KUMA_HOST || args.host;
const port = parseInt(process.env.PORT || args.port || 3001);
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
if (!hostname && !FBSD) {
hostname = process.env.HOST;
}
if (hostname) {
console.log("Custom hostname: " + hostname);
}
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001);
// SSL // SSL
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined; const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined; const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
// 2FA / notp verification defaults
const twofa_verification_opts = {
"window": 1,
"time": 30
};
/** /**
* Run unit test after the server is ready * Run unit test after the server is ready
@ -74,7 +92,7 @@ const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
*/ */
const testMode = !!args["test"] || false; const testMode = !!args["test"] || false;
if (demoMode) { if (config.demoMode) {
console.log("==== Demo Mode ===="); console.log("==== Demo Mode ====");
} }
@ -103,6 +121,15 @@ const { statusPageSocketHandler } = require("./socket-handlers/status-page-socke
app.use(express.json()); app.use(express.json());
// Global Middleware
app.use(function (req, res, next) {
if (!disableFrameSameOrigin) {
res.setHeader("X-Frame-Options", "SAMEORIGIN");
}
res.removeHeader("X-Powered-By");
next();
});
/** /**
* Total WebSocket client connected to server currently, no actual use * Total WebSocket client connected to server currently, no actual use
* @type {number} * @type {number}
@ -131,7 +158,17 @@ let needSetup = false;
* Cache Index HTML * Cache Index HTML
* @type {string} * @type {string}
*/ */
let indexHTML = fs.readFileSync("./dist/index.html").toString(); let indexHTML = "";
try {
indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
console.error("Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
exports.entryPage = "dashboard"; exports.entryPage = "dashboard";
@ -176,7 +213,7 @@ exports.entryPage = "dashboard";
const apiRouter = require("./routers/api-router"); const apiRouter = require("./routers/api-router");
app.use(apiRouter); app.use(apiRouter);
// Universal Route Handler, must be at the end of all express route. // Universal Route Handler, must be at the end of all express routes.
app.get("*", async (_request, response) => { app.get("*", async (_request, response) => {
if (_request.originalUrl.startsWith("/upload/")) { if (_request.originalUrl.startsWith("/upload/")) {
response.status(404).send("File not found."); response.status(404).send("File not found.");
@ -265,7 +302,7 @@ exports.entryPage = "dashboard";
} }
if (data.token) { if (data.token) {
let verify = notp.totp.verify(data.token, user.twofa_secret); let verify = notp.totp.verify(data.token, user.twofa_secret, twofa_verification_opts);
if (verify && verify.delta == 0) { if (verify && verify.delta == 0) {
callback({ callback({
@ -305,7 +342,7 @@ exports.entryPage = "dashboard";
]); ]);
if (user.twofa_status == 0) { if (user.twofa_status == 0) {
let newSecret = await genSecret(); let newSecret = genSecret();
let encodedSecret = base32.encode(newSecret); let encodedSecret = base32.encode(newSecret);
// Google authenticator doesn't like equal signs // Google authenticator doesn't like equal signs
@ -383,7 +420,7 @@ exports.entryPage = "dashboard";
socket.userID, socket.userID,
]); ]);
let verify = notp.totp.verify(token, user.twofa_secret); let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts);
if (verify && verify.delta == 0) { if (verify && verify.delta == 0) {
callback({ callback({
@ -432,8 +469,12 @@ exports.entryPage = "dashboard";
socket.on("setup", async (username, password, callback) => { socket.on("setup", async (username, password, callback) => {
try { try {
if (passwordStrength(password).value === "Too weak") {
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
}
if ((await R.count("user")) !== 0) { if ((await R.count("user")) !== 0) {
throw new Error("Uptime Kuma has been setup. If you want to setup again, please delete the database."); throw new Error("Uptime Kuma has been initialized. If you want to run setup again, please delete the database.");
} }
let user = R.dispense("user"); let user = R.dispense("user");
@ -509,6 +550,9 @@ exports.entryPage = "dashboard";
bean.name = monitor.name; bean.name = monitor.name;
bean.type = monitor.type; bean.type = monitor.type;
bean.url = monitor.url; bean.url = monitor.url;
bean.method = monitor.method;
bean.body = monitor.body;
bean.headers = monitor.headers;
bean.interval = monitor.interval; bean.interval = monitor.interval;
bean.retryInterval = monitor.retryInterval; bean.retryInterval = monitor.retryInterval;
bean.hostname = monitor.hostname; bean.hostname = monitor.hostname;
@ -818,10 +862,14 @@ exports.entryPage = "dashboard";
try { try {
checkLogin(socket); checkLogin(socket);
if (! password.currentPassword) { if (! password.newPassword) {
throw new Error("Invalid new password"); throw new Error("Invalid new password");
} }
if (passwordStrength(password.newPassword).value === "Too weak") {
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
}
let user = await R.findOne("user", " id = ? AND active = 1 ", [ let user = await R.findOne("user", " id = ? AND active = 1 ", [
socket.userID, socket.userID,
]); ]);
@ -1034,6 +1082,9 @@ exports.entryPage = "dashboard";
name: monitorListData[i].name, name: monitorListData[i].name,
type: monitorListData[i].type, type: monitorListData[i].type,
url: monitorListData[i].url, url: monitorListData[i].url,
method: monitorListData[i].method || "GET",
body: monitorListData[i].body,
headers: monitorListData[i].headers,
interval: monitorListData[i].interval, interval: monitorListData[i].interval,
retryInterval: retryInterval, retryInterval: retryInterval,
hostname: monitorListData[i].hostname, hostname: monitorListData[i].hostname,
@ -1239,6 +1290,8 @@ exports.entryPage = "dashboard";
} }
}); });
initBackgroundJobs(args);
})(); })();
async function updateMonitorNotification(monitorID, notificationIDList) { async function updateMonitorNotification(monitorID, notificationIDList) {
@ -1315,7 +1368,7 @@ async function initDatabase() {
fs.copyFileSync(Database.templatePath, Database.path); fs.copyFileSync(Database.templatePath, Database.path);
} }
console.log("Connecting to Database"); console.log("Connecting to the Database");
await Database.connect(); await Database.connect();
console.log("Connected"); console.log("Connected");
@ -1415,7 +1468,7 @@ async function shutdownFunction(signal) {
} }
function finalFunction() { function finalFunction() {
console.log("Graceful shutdown successfully!"); console.log("Graceful shutdown successful!");
} }
gracefulShutdown(server, { gracefulShutdown(server, {

View file

@ -6,6 +6,14 @@ const passwordHash = require("./password-hash");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { Resolver } = require("dns"); const { Resolver } = require("dns");
const child_process = require("child_process"); const child_process = require("child_process");
const iconv = require("iconv-lite");
const chardet = require("chardet");
// From ping-lite
exports.WIN = /^win/.test(process.platform);
exports.LIN = /^linux/.test(process.platform);
exports.MAC = /^darwin/.test(process.platform);
exports.FBSD = /^freebsd/.test(process.platform);
/** /**
* Init or reset JWT secret * Init or reset JWT secret
@ -116,7 +124,7 @@ exports.setting = async function (key) {
} }
}; };
exports.setSetting = async function (key, value) { exports.setSetting = async function (key, value, type = null) {
let bean = await R.findOne("setting", " `key` = ? ", [ let bean = await R.findOne("setting", " `key` = ? ", [
key, key,
]); ]);
@ -124,6 +132,7 @@ exports.setSetting = async function (key, value) {
bean = R.dispense("setting"); bean = R.dispense("setting");
bean.key = key; bean.key = key;
} }
bean.type = type;
bean.value = JSON.stringify(value); bean.value = JSON.stringify(value);
await R.store(bean); await R.store(bean);
}; };
@ -312,3 +321,14 @@ exports.startUnitTest = async () => {
process.exit(code); process.exit(code);
}); });
}; };
/**
* @param body : Buffer
* @returns {string}
*/
exports.convertToUTF8 = (body) => {
const guessEncoding = chardet.detect(body);
//debug("Guess Encoding: " + guessEncoding);
const str = iconv.decode(body, guessEncoding);
return str.toString();
};

View file

@ -14,6 +14,10 @@ h2 {
font-size: 26px; font-size: 26px;
} }
textarea.form-control {
border-radius: 19px;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;
} }

View file

@ -21,7 +21,7 @@
} }
.multiselect__tag { .multiselect__tag {
border-radius: 50rem; border-radius: $border-radius;
margin-bottom: 0; margin-bottom: 0;
padding: 6px 26px 6px 10px; padding: 6px 26px 6px 10px;
background: $primary !important; background: $primary !important;

View file

@ -186,7 +186,7 @@ export default {
.beat { .beat {
display: inline-block; display: inline-block;
background-color: $primary; background-color: $primary;
border-radius: 50rem; border-radius: $border-radius;
&.empty { &.empty {
background-color: aliceblue; background-color: aliceblue;

View file

@ -0,0 +1,25 @@
<template>
<div class="mb-3">
<label for="accessKeyId" class="form-label">{{ $t("AccessKeyId") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="accessKeyId" v-model="$parent.notification.accessKeyId" type="text" class="form-control" required>
<label for="secretAccessKey" class="form-label">{{ $t("SecretAccessKey") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="secretAccessKey" v-model="$parent.notification.secretAccessKey" type="text" class="form-control" required>
<label for="phonenumber" class="form-label">{{ $t("Phonenumber") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="phonenumber" v-model="$parent.notification.phonenumber" type="text" class="form-control" required>
<label for="templateCode" class="form-label">{{ $t("TemplateCode") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="templateCode" v-model="$parent.notification.templateCode" type="text" class="form-control" required>
<label for="signName" class="form-label">{{ $t("SignName") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="signName" v-model="$parent.notification.signName" type="text" class="form-control" required>
<div class="form-text">
<p>Sms template must contain parameters: <br> <code>${name} ${time} ${status} ${msg}</code></p>
<i18n-t tag="p" keypath="Read more:">
<a href="https://help.aliyun.com/document_detail/101414.html" target="_blank">https://help.aliyun.com/document_detail/101414.html</a>
</i18n-t>
</div>
</div>
</template>

View file

@ -0,0 +1,16 @@
<template>
<div class="mb-3">
<label for="WebHookUrl" class="form-label">{{ $t("WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="WebHookUrl" v-model="$parent.notification.webHookUrl" type="text" class="form-control" required>
<label for="secretKey" class="form-label">{{ $t("SecretKey") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
<div class="form-text">
<p>For safety, must use secret key</p>
<i18n-t tag="p" keypath="Read more:">
<a href="https://developers.dingtalk.com/document/robots/custom-robot-access" target="_blank">https://developers.dingtalk.com/document/robots/custom-robot-access</a>
</i18n-t>
</div>
</div>
</template>

View file

@ -5,7 +5,7 @@
<div class="form-text"> <div class="form-text">
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p> <p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
</div> </div>
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text"> <i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
<a <a
href="https://www.feishu.cn/hc/zh-CN/articles/360024984973" href="https://www.feishu.cn/hc/zh-CN/articles/360024984973"
target="_blank" target="_blank"

View file

@ -1,25 +1,25 @@
<template> <template>
<div class="mb-3"> <div class="mb-3">
<label for="homeserver-url" class="form-label">Homeserver URL (with http(s):// and optionally port)</label><span style="color: red;"><sup>*</sup></span> <label for="homeserver-url" class="form-label">{{ $t("matrixHomeserverURL") }}</label><span style="color: red;"><sup>*</sup></span>
<input id="homeserver-url" v-model="$parent.notification.homeserverUrl" type="text" class="form-control" :required="true"> <input id="homeserver-url" v-model="$parent.notification.homeserverUrl" type="text" class="form-control" :required="true">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="internal-room-id" class="form-label">Internal Room Id</label><span style="color: red;"><sup>*</sup></span> <label for="internal-room-id" class="form-label">{{ $t("Internal Room Id") }}</label><span style="color: red;"><sup>*</sup></span>
<input id="internal-room-id" v-model="$parent.notification.internalRoomId" type="text" class="form-control" required="true"> <input id="internal-room-id" v-model="$parent.notification.internalRoomId" type="text" class="form-control" required="true">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="access-token" class="form-label">Access Token</label><span style="color: red;"><sup>*</sup></span> <label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput> <HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
</div> </div>
<div class="form-text"> <div class="form-text">
<span style="color: red;"><sup>*</sup></span>Required <span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server. {{ $t("matrixDesc1") }}
</p>
<p style="margin-top: 8px;">
It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running <code>curl -XPOST -d '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/r0/login"</code>.
</p> </p>
<i18n-t tag="p" keypath="matrixDesc2" style="margin-top: 8px;">
<code>curl -XPOST -d '{"type": "m.login.password", "identifier": {"user": "botusername", "type": "m.id.user"}, "password": "passwordforuser"}' "https://home.server/_matrix/client/r0/login"</code>.
</i18n-t>
</div> </div>
</template> </template>
@ -30,5 +30,5 @@ export default {
components: { components: {
HiddenInput, HiddenInput,
}, },
} };
</script> </script>

View file

@ -6,7 +6,7 @@
<option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option> <option value="1">Legacy Octopush-DM (endpoint: www.octopush-dm.com)</option>
</select> </select>
<div class="form-text"> <div class="form-text">
Do you use the legacy version of Octopush (2011-2020) or the new version? {{ $t("octopushLegacyHint") }}
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View file

@ -10,12 +10,13 @@
<select id="promosms-type-sms" v-model="$parent.notification.promosmsSMSType" class="form-select"> <select id="promosms-type-sms" v-model="$parent.notification.promosmsSMSType" class="form-select">
<option value="0">{{ $t("promosmsTypeFlash") }}</option> <option value="0">{{ $t("promosmsTypeFlash") }}</option>
<option value="1">{{ $t("promosmsTypeEco") }}</option> <option value="1">{{ $t("promosmsTypeEco") }}</option>
<option value="2">{{ $t("promosmsTypeFull") }}</option> <option value="3">{{ $t("promosmsTypeFull") }}</option>
<option value="3">{{ $t("promosmsTypeSpeed") }}</option> <option value="4">{{ $t("promosmsTypeSpeed") }}</option>
</select> </select>
<i18n-t tag="div" keypath="Check PromoSMS prices" class="form-text"> <div class="form-text">
{{ $t("checkPrice", [$t("promosms")]) }}
<a href="https://promosms.com/cennik/" target="_blank">https://promosms.com/cennik/</a> <a href="https://promosms.com/cennik/" target="_blank">https://promosms.com/cennik/</a>
</i18n-t> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="promosms-phone-number" class="form-label">{{ $t("promosmsPhoneNumber") }}</label> <label for="promosms-phone-number" class="form-label">{{ $t("promosmsPhoneNumber") }}</label>
@ -25,7 +26,6 @@
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label> <label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control"> <input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div> </div>
</template> </template>
<script> <script>

View file

@ -11,7 +11,7 @@
<div class="form-text"> <div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }} <span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;"> <i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
<a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a> <a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://docs.rocket.chat/guides/administration/administration/integrations</a>
</i18n-t> </i18n-t>
<p style="margin-top: 8px;"> <p style="margin-top: 8px;">
{{ $t("aboutChannelName", [$t("rocket.chat")]) }} {{ $t("aboutChannelName", [$t("rocket.chat")]) }}

View file

@ -57,6 +57,18 @@
<label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label> <label for="to-bcc" class="form-label">{{ $t("smtpBCC") }}</label>
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient"> <input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
</div> </div>
<div class="mb-3">
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
<div v-pre class="form-text">
(leave blank for default one)<br />
{{NAME}}: Service Name<br />
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
{{URL}}: URL<br />
{{STATUS}}: Status<br />
</div>
</div>
</template> </template>
<script> <script>

View file

@ -2,9 +2,9 @@
<div class="mb-3"> <div class="mb-3">
<label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label> <label for="telegram-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<div class="form-text"> <i18n-t tag="div" keypath="wayToGetTelegramToken" class="form-text">
{{ $t("You can get a token from") }} <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>. <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>
</div> </i18n-t>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View file

@ -16,7 +16,7 @@
</select> </select>
<div class="form-text"> <div class="form-text">
<p>"application/json" is good for any modern http servers such as express.js</p> <p>{{ $t("webhookJsonDesc", ["\"application/json\""]) }}</p>
<i18n-t tag="p" keypath="webhookFormDataDesc"> <i18n-t tag="p" keypath="webhookFormDataDesc">
<template #multipart>"multipart/form-data"</template> <template #multipart>"multipart/form-data"</template>
<template #decodeFunction> <template #decodeFunction>

View file

@ -18,6 +18,8 @@ import Pushbullet from "./Pushbullet.vue";
import Line from "./Line.vue"; import Line from "./Line.vue";
import Mattermost from "./Mattermost.vue"; import Mattermost from "./Mattermost.vue";
import Matrix from "./Matrix.vue"; import Matrix from "./Matrix.vue";
import AliyunSMS from "./AliyunSms.vue";
import DingDing from "./DingDing.vue";
/** /**
* Manage all notification form. * Manage all notification form.
@ -40,11 +42,13 @@ const NotificationFormList = {
"promosms": PromoSMS, "promosms": PromoSMS,
"lunasea": LunaSea, "lunasea": LunaSea,
"Feishu": Feishu, "Feishu": Feishu,
"AliyunSMS": AliyunSMS,
"apprise": Apprise, "apprise": Apprise,
"pushbullet": Pushbullet, "pushbullet": Pushbullet,
"line": Line, "line": Line,
"mattermost": Mattermost, "mattermost": Mattermost,
"matrix": Matrix, "matrix": Matrix,
"DingDing": DingDing
} }
export default NotificationFormList export default NotificationFormList

View file

@ -2,9 +2,9 @@ export default {
languageName: "Български", languageName: "Български",
checkEverySecond: "Ще се извършва на всеки {0} секунди", checkEverySecond: "Ще се извършва на всеки {0} секунди",
retryCheckEverySecond: "Ще се извършва на всеки {0} секунди", retryCheckEverySecond: "Ще се извършва на всеки {0} секунди",
retriesDescription: "Максимакен брой опити преди услугата да бъде маркирана като недостъпна и да бъде изпратено известие", retriesDescription: "Максимакен брой опити преди маркиране на услугата като недостъпна и изпращане на известие",
ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове", ignoreTLSError: "Игнорирай TLS/SSL грешки за HTTPS уебсайтове",
upsideDownModeDescription: "Обърни статуса от достъпен на недостъпен. Ако услугата е достъпна се вижда НЕДОСТЪПНА.", upsideDownModeDescription: "Обръща статуса от достъпен на недостъпен. Ако услугата е достъпна, ще се вижда като НЕДОСТЪПНА.",
maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.", maxRedirectDescription: "Максимален брой пренасочвания, които да бъдат следвани. Въведете 0 за да изключите пренасочване.",
acceptedStatusCodesDescription: "Изберете статус кодове, които се считат за успешен отговор.", acceptedStatusCodesDescription: "Изберете статус кодове, които се считат за успешен отговор.",
passwordNotMatchMsg: "Повторената парола не съвпада.", passwordNotMatchMsg: "Повторената парола не съвпада.",
@ -48,7 +48,7 @@ export default {
Status: "Статус", Status: "Статус",
DateTime: "Дата и час", DateTime: "Дата и час",
Message: "Отговор", Message: "Отговор",
"No important events": "Няма важни събития", "No important events": "Все още няма събития",
Resume: "Възобнови", Resume: "Възобнови",
Edit: "Редактирай", Edit: "Редактирай",
Delete: "Изтрий", Delete: "Изтрий",
@ -107,8 +107,8 @@ export default {
Password: "Парола", Password: "Парола",
"Remember me": "Запомни ме", "Remember me": "Запомни ме",
Login: "Вход", Login: "Вход",
"No Monitors, please": "Моля, без монитори", "No Monitors, please": "Все още няма монитори. Моля, добавете поне ",
"add one": "добави един", "add one": "един.",
"Notification Type": "Тип известяване", "Notification Type": "Тип известяване",
Email: "Имейл", Email: "Имейл",
Test: "Тест", Test: "Тест",
@ -179,8 +179,8 @@ export default {
"Edit Status Page": "Редактиране Статус страница", "Edit Status Page": "Редактиране Статус страница",
"Go to Dashboard": "Към Таблото", "Go to Dashboard": "Към Таблото",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Уеб кука",
smtp: "Email (SMTP)", smtp: "Имейл (SMTP)",
discord: "Discord", discord: "Discord",
teams: "Microsoft Teams", teams: "Microsoft Teams",
signal: "Signal", signal: "Signal",
@ -197,4 +197,110 @@ export default {
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"Status Page": "Статус страница", "Status Page": "Статус страница",
"Primary Base URL": "Основен базов URL адрес",
"Push URL": "Генериран Push URL адрес",
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди.",
pushOptionalParams: "Допълнителни, но незадължителни параметри: {0}",
defaultNotificationName: "Моето {notification} известяване ({number})",
here: "тук",
Required: "Задължително поле",
"Bot Token": "Бот токен",
wayToGetTelegramToken: "Можете да получите токен от {0}.",
"Chat ID": "Чат ID",
supportTelegramChatID: "Поддържа Direct Chat / Group / Channel's Chat ID",
wayToGetTelegramChatID: "Можете да получите вашето чат ID, като изпратите съобщение на бота, след което е нужно да посетите този URL адрес за да го видите:",
"YOUR BOT TOKEN HERE": "ВАШИЯТ БОТ ТОКЕН ТУК",
chatIDNotFound: "Чат ID не е намерено. Моля, първо изпратете съобщение до този бот",
"Post URL": "Post URL адрес",
"Content Type": "Тип съдържание",
webhookJsonDesc: "{0} е подходящ за всички съвременни http сървъри, като например express.js",
webhookFormDataDesc: "{multipart} е подходящ за PHP, нужно е да анализирате json чрез {decodeFunction}",
secureOptionNone: "Няма (25) / STARTTLS (587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Игнорирай TLS грешките",
"From Email": "От имейл адрес",
emailCustomSubject: "Модифициране на тема",
"To Email": "Получател имейл адрес",
smtpCC: "Явно копие до имейл адрес:",
smtpBCC: "Скрито копие до имейл адрес:",
"Discord Webhook URL": "Discord URL адрес на уеб кука",
wayToGetDiscordURL: "Може да създадете, от меню \"Настройки на сървъра\" -> \"Интеграции\" -> \"Уеб куки\" -> \"Нова уеб кука\"",
"Bot Display Name": "Име на бота, което да се показва",
"Prefix Custom Message": "Модифицирано обръщение",
"Hello @everyone is...": "Здравейте, {'@'}everyone е...",
"Webhook URL": "Уеб кука URL адрес",
wayToGetTeamsURL: "Можете да научите как се създава URL адрес за уеб кука {0}.",
Number: "Номер",
Recipients: "Получатели",
needSignalAPI: "Необходимо е да разполагате със Signal клиент с REST API.",
wayToCheckSignalURL: "Може да посетите този URL адрес, ако се нуждаете от помощ при настройването:",
signalImportant: "ВАЖНО: Не може да смесвате \"Групи\" и \"Номера\" в поле \"Получатели\"!",
"Application Token": "Токен код за приложението",
"Server URL": "URL адрес на сървъра",
Priority: "Приоритет",
"Icon Emoji": "Иконка Емотикон",
"Channel Name": "Канал име",
"Uptime Kuma URL": "Uptime Kuma URL адрес",
aboutWebhooks: "Повече информация относно уеб куки на: {0}",
aboutChannelName: "Въведете името на канала в поле {0} \"Канал име\", ако желаете да заобиколите канала от уеб куката. Например: #other-channel",
aboutKumaURL: "Ако оставите празно полето \"Uptime Kuma URL адрес\", по подразбиране ще се използва GitHub страницата на проекта.",
emojiCheatSheet: "Подсказки за емотикони: {0}",
"User Key": "Потребителски ключ",
Device: "Устройство",
"Message Title": "Заглавие на съобщението",
"Notification Sound": "Звуков сигнал",
"More info on:": "Повече информация на: {0}",
pushoverDesc1: "Приоритет Спешно (2) по подразбиране изчаква 30 секунди между повторните опити и изтича след 1 час.",
pushoverDesc2: "Ако желаете да изпратите известявания до различни устройства, попълнете полето Устройство.",
"SMS Type": "СМС тип",
octopushTypePremium: "Премиум (Бърз - препоръчителен в случай на тревога)",
octopushTypeLowCost: "Евтин (Бавен - понякога бива блокиран от оператора)",
checkPrice: "Тарифни планове на {0}:",
octopushLegacyHint: "Дали използвате съвместима версия на Octopush (2011-2020) или нова версия?",
"Check octopush prices": "Тарифни планове на octopush {0}.",
octopushPhoneNumber: "Телефонен номер (в международен формат, например: +33612345678) ",
octopushSMSSender: "СМС подател Име: 3-11 знака - букви, цифри и интервал (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea ID на устройство",
"Apprise URL": "Apprise URL адрес",
"Example:": "Пример: {0}",
"Read more:": "Научете повече: {0}",
"Status:": "Статус: {0}",
"Read more": "Научете повече",
appriseInstalled: "Apprise е инсталиран.",
appriseNotInstalled: "Apprise не е инсталиран. {0}",
"Access Token": "Токен код за достъп",
"Channel access token": "Канал токен код",
"Line Developers Console": "Line - Конзола за разработчици",
lineDevConsoleTo: "Line - Конзола за разработчици - {0}",
"Basic Settings": "Основни настройки",
"User ID": "Потребител ID",
"Messaging API": "API за известяване",
wayToGetLineChannelToken: "Необходимо е първо да посетите {0}, за да създадете (Messaging API) за доставчик и канал, след което може да вземете токен кода за канал и потребителско ID от споменатите по-горе елементи на менюто.",
"Icon URL": "URL адрес за иконка",
aboutIconURL: "Може да предоставите линк към картинка в поле \"URL Адрес за иконка\" за да отмените картинката на профила по подразбиране. Няма да се използва, ако вече сте настроили емотикон.",
aboutMattermostChannelName: "Може да замените канала по подразбиране, към който публикува уеб куката, като въведете името на канала в полето \"Канал име\". Tрябва да бъде активирано в настройките за уеб кука на Mattermost. Например: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "СМС ECO - евтин, но бавен. Често е претоварен. Само за получатели от Полша.",
promosmsTypeFlash: "СМС FLASH - Съобщението автоматично се показва на устройството на получателя. Само за получатели от Полша.",
promosmsTypeFull: "СМС FULL - Високо ниво на СМС услуга. Може да използвате Вашето име като подател (Необходимо е първо да регистрирате името). Надежден метод за съобщения тип тревога.",
promosmsTypeSpeed: "СМС SPEED - Най-висок приоритет в системата. Много бърза и надеждна, но същвременно скъпа услуга. (Около два пъти по-висока цена в сравнение с SMS FULL).",
promosmsPhoneNumber: "Телефонен номер (за получатели от Полша, може да пропуснете въвеждането на код за населено място)",
promosmsSMSSender: "СМС Подател име: Предварително регистрирано име или някое от имената по подразбиране: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu URL адрес за уеб кука",
matrixHomeserverURL: "Сървър URL адрес (започва с http(s):// и порт по желание)",
"Internal Room Id": "ID на вътрешна стая",
matrixDesc1: "Може да намерите \"ID на вътрешна стая\" в разширените настройки на стаята във вашия Matrix клиент. Примерен изглед: !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребирел, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известяванията. Токен код за достъп ще получите изпълнявайки {0}",
Method: "Метод",
Body: "Съобщение",
Headers: "Хедъри",
PushUrl: "Push URL адрес",
HeadersInvalidFormat: "Заявените хедъри не са валидни JSON: ",
BodyInvalidFormat: "Заявеното съобщение не е валиден JSON: ",
"Monitor History": "История на мониторите:",
clearDataOlderThan: "Ще се съхранява за {0} дни.",
records: "записа",
"One record": "Един запис",
"Showing {from} to {to} of {count} records": "Показване на {from} до {to} от {count} записа",
steamApiKeyDescription: "За да мониторирате Steam Gameserver се нуждаете от Steam Web-API ключ. Може да регистрирате Вашия API ключ тук: ",
}; };

View file

@ -8,7 +8,7 @@ export default {
Theme: "Thema", Theme: "Thema",
General: "Allgemein", General: "Allgemein",
Version: "Version", Version: "Version",
"Check Update On GitHub": "Auf Github nach Updates suchen", "Check Update On GitHub": "Auf GitHub nach Updates suchen",
List: "Liste", List: "Liste",
Add: "Hinzufügen", Add: "Hinzufügen",
"Add New Monitor": "Neuer Monitor", "Add New Monitor": "Neuer Monitor",
@ -38,7 +38,7 @@ export default {
checkEverySecond: "Überprüfe alle {0} Sekunden", checkEverySecond: "Überprüfe alle {0} Sekunden",
Response: "Antwortzeit", Response: "Antwortzeit",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Monitor Typ", "Monitor Type": "Monitor-Typ",
Keyword: "Suchwort", Keyword: "Suchwort",
"Friendly Name": "Anzeigename", "Friendly Name": "Anzeigename",
URL: "URL", URL: "URL",
@ -48,11 +48,11 @@ export default {
Retries: "Wiederholungen", Retries: "Wiederholungen",
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.", retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
Advanced: "Erweitert", Advanced: "Erweitert",
ignoreTLSError: "Ignoriere TLS/SSL Fehler von Webseiten", ignoreTLSError: "Ignoriere TLS-/SSL-Fehler von Webseiten",
"Upside Down Mode": "Invertierter Modus", "Upside Down Mode": "Invertierter Modus",
upsideDownModeDescription: "Im invertierten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.", upsideDownModeDescription: "Im invertierten Modus wird der Dienst als inaktiv angezeigt, wenn er erreichbar ist.",
"Max. Redirects": "Max. Weiterleitungen", "Max. Redirects": "Max. Weiterleitungen",
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen denen gefolgt werden soll. Auf 0 setzen um Weiterleitungen zu deaktivieren.", maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Auf 0 setzen, um Weiterleitungen zu deaktivieren.",
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes", "Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.", acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.",
Save: "Speichern", Save: "Speichern",
@ -82,7 +82,7 @@ export default {
notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.", notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.",
Leave: "Verlassen", Leave: "Verlassen",
"I understand, please disable": "Ich verstehe, bitte deaktivieren", "I understand, please disable": "Ich verstehe, bitte deaktivieren",
Confirm: "Bestätige", Confirm: "Bestätigen",
Yes: "Ja", Yes: "Ja",
No: "Nein", No: "Nein",
Username: "Benutzername", Username: "Benutzername",
@ -95,7 +95,7 @@ export default {
Email: "E-Mail", Email: "E-Mail",
Test: "Test", Test: "Test",
"Certificate Info": "Zertifikatsinfo", "Certificate Info": "Zertifikatsinfo",
keywordDescription: "Ein Suchwort in der HTML oder JSON Ausgabe finden. Bitte beachte: es wird zwischen Groß-/Kleinschreibung unterschieden.", keywordDescription: "Ein Suchwort in der HTML- oder JSON-Ausgabe finden. Bitte beachte: es wird zwischen Groß-/Kleinschreibung unterschieden.",
deleteMonitorMsg: "Bist du sicher, dass du den Monitor löschen möchtest?", deleteMonitorMsg: "Bist du sicher, dass du den Monitor löschen möchtest?",
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?", deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.", resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.",
@ -108,13 +108,13 @@ export default {
"Clear Data": "Lösche Daten", "Clear Data": "Lösche Daten",
Events: "Ereignisse", Events: "Ereignisse",
Heartbeats: "Statistiken", Heartbeats: "Statistiken",
confirmClearStatisticsMsg: "Bist du dir wirklich sicher, dass du ALLE Statistiken löschen möchtest?", confirmClearStatisticsMsg: "Bist du dir sicher, dass du ALLE Statistiken löschen möchtest?",
"Create your admin account": "Erstelle dein Admin Konto", "Create your admin account": "Erstelle dein Admin-Konto",
"Repeat Password": "Wiederhole das Passwort", "Repeat Password": "Wiederhole das Passwort",
"Resource Record Type": "Resource Record Type", "Resource Record Type": "Resource Record Type",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
respTime: "Antw. Zeit (ms)", respTime: "Antw.-Zeit (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
"Default enabled": "Standardmäßig aktiviert", "Default enabled": "Standardmäßig aktiviert",
"Apply on all existing monitors": "Auf alle existierenden Monitore anwenden", "Apply on all existing monitors": "Auf alle existierenden Monitore anwenden",
@ -125,34 +125,34 @@ export default {
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.", backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.", backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
alertNoFile: "Bitte wähle eine Datei zum Importieren aus.", alertNoFile: "Bitte wähle eine Datei zum Importieren aus.",
alertWrongFileType: "Bitte wähle eine JSON Datei aus.", alertWrongFileType: "Bitte wähle eine JSON-Datei aus.",
"Clear all statistics": "Lösche alle Statistiken", "Clear all statistics": "Lösche alle Statistiken",
importHandleDescription: "Wähle 'Vorhandene überspringen' aus, wenn jeder Monitor oder Benachrichtigung mit demselben Namen übersprungen werden soll. 'Überschreiben' löscht jeden vorhandenen Monitor sowie Benachrichtigungen.", importHandleDescription: "Wähle 'Vorhandene überspringen' aus, wenn jeder Monitor oder jede Benachrichtigung mit demselben Namen übersprungen werden soll. 'Überschreiben' löscht jeden vorhandenen Monitor sowie Benachrichtigungen.",
"Skip existing": "Vorhandene überspringen", "Skip existing": "Vorhandene überspringen",
Overwrite: "Überschreiben", Overwrite: "Überschreiben",
Options: "Optionen", Options: "Optionen",
confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import Option ausgewählt ist.", confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import-Option ausgewählt ist.",
"Keep both": "Beide behalten", "Keep both": "Beide behalten",
twoFAVerifyLabel: "Bitte trage deinen Token ein, um zu verifizieren, dass 2FA funktioniert", twoFAVerifyLabel: "Bitte trage deinen Token ein, um zu verifizieren, dass 2FA funktioniert",
"Verify Token": "Token verifizieren", "Verify Token": "Token verifizieren",
"Setup 2FA": "2FA einrichten", "Setup 2FA": "2FA einrichten",
"Enable 2FA": "2FA aktivieren", "Enable 2FA": "2FA aktivieren",
"Disable 2FA": "2FA deaktivieren", "Disable 2FA": "2FA deaktivieren",
"2FA Settings": "2FA Einstellungen", "2FA Settings": "2FA-Einstellungen",
confirmEnableTwoFAMsg: "Bist du sicher, dass du 2FA aktivieren möchtest?", confirmEnableTwoFAMsg: "Bist du sicher, dass du 2FA aktivieren möchtest?",
confirmDisableTwoFAMsg: "Bist du sicher, dass du 2FA deaktivieren möchtest?", confirmDisableTwoFAMsg: "Bist du sicher, dass du 2FA deaktivieren möchtest?",
tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA Einstellungen speichern.", tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA-Einstellungen speichern.",
"Two Factor Authentication": "Zwei Faktor Authentifizierung", "Two Factor Authentication": "Zwei-Faktor-Authentifizierung",
Active: "Aktiv", Active: "Aktiv",
Inactive: "Inaktiv", Inactive: "Inaktiv",
Token: "Token", Token: "Token",
"Show URI": "URI Anzeigen", "Show URI": "URI anzeigen",
Tags: "Tags", Tags: "Tags",
"Add New below or Select...": "Bestehenden Tag auswählen oder neuen hinzufügen...", "Add New below or Select...": "Bestehenden Tag auswählen oder neuen hinzufügen...",
"Tag with this name already exist.": "Ein Tag mit dem Namen existiert bereits.", "Tag with this name already exist.": "Ein Tag mit diesem Namen existiert bereits.",
"Tag with this value already exist.": "Ein Tag mit dem Wert existiert bereits.", "Tag with this value already exist.": "Ein Tag mit diesem Wert existiert bereits.",
color: "Farbe", color: "Farbe",
"value (optional)": "Wert (Optional)", "value (optional)": "Wert (optional)",
Gray: "Grau", Gray: "Grau",
Red: "Rot", Red: "Rot",
Orange: "Orange", Orange: "Orange",
@ -176,9 +176,9 @@ export default {
"Degraded Service": "Eingeschränkter Dienst", "Degraded Service": "Eingeschränkter Dienst",
"Add Group": "Gruppe hinzufügen", "Add Group": "Gruppe hinzufügen",
"Add a monitor": "Monitor hinzufügen", "Add a monitor": "Monitor hinzufügen",
"Edit Status Page": "Bearbeite Status Seite", "Edit Status Page": "Bearbeite Status-Seite",
"Go to Dashboard": "Gehe zum Dashboard", "Go to Dashboard": "Gehe zum Dashboard",
"Status Page": "Status Seite", "Status Page": "Status-Seite",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "E-Mail (SMTP)", smtp: "E-Mail (SMTP)",

View file

@ -1,28 +1,28 @@
export default { export default {
languageName: "English", languageName: "English",
checkEverySecond: "Check every {0} seconds.", checkEverySecond: "Check every {0} seconds",
retryCheckEverySecond: "Retry every {0} seconds.", retryCheckEverySecond: "Retry every {0} seconds",
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent", retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites", ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.", upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.", maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.", acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
passwordNotMatchMsg: "The repeat password does not match.", passwordNotMatchMsg: "The repeat password does not match.",
notificationDescription: "Please assign a notification to monitor(s) to get it to work.", notificationDescription: "Notifications must be assigned to a monitor to function.",
keywordDescription: "Search keyword in plain html or JSON response and it is case-sensitive", keywordDescription: "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
pauseDashboardHome: "Pause", pauseDashboardHome: "Pause",
deleteMonitorMsg: "Are you sure want to delete this monitor?", deleteMonitorMsg: "Are you sure want to delete this monitor?",
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?", deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
resoverserverDescription: "Cloudflare is the default server, you can change the resolver server anytime.", resoverserverDescription: "Cloudflare is the default server. You can change the resolver server anytime.",
rrtypeDescription: "Select the RR-Type you want to monitor", rrtypeDescription: "Select the RR type you want to monitor",
pauseMonitorMsg: "Are you sure want to pause?", pauseMonitorMsg: "Are you sure want to pause?",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "This notification will be enabled by default for new monitors. You can still disable the notification separately for each monitor.",
clearEventsMsg: "Are you sure want to delete all events for this monitor?", clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", confirmClearStatisticsMsg: "Are you sure you want to delete ALL statistics?",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.", importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.", confirmImportMsg: "Are you sure you want to import the backup? Please verify you've selected the correct import option.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", twoFAVerifyLabel: "Please enter your token to verify 2FA:",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.", tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?", confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?", confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
@ -33,6 +33,7 @@ export default {
Appearance: "Appearance", Appearance: "Appearance",
Theme: "Theme", Theme: "Theme",
General: "General", General: "General",
"Primary Base URL": "Primary Base URL",
Version: "Version", Version: "Version",
"Check Update On GitHub": "Check Update On GitHub", "Check Update On GitHub": "Check Update On GitHub",
List: "List", List: "List",
@ -75,6 +76,9 @@ export default {
"Upside Down Mode": "Upside Down Mode", "Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "Max. Redirects", "Max. Redirects": "Max. Redirects",
"Accepted Status Codes": "Accepted Status Codes", "Accepted Status Codes": "Accepted Status Codes",
"Push URL": "Push URL",
needPushEvery: "You should call this URL every {0} seconds.",
pushOptionalParams: "Optional parameters: {0}",
Save: "Save", Save: "Save",
Notifications: "Notifications", Notifications: "Notifications",
"Not available, please setup.": "Not available, please setup.", "Not available, please setup.": "Not available, please setup.",
@ -131,9 +135,9 @@ export default {
Events: "Events", Events: "Events",
Heartbeats: "Heartbeats", Heartbeats: "Heartbeats",
"Auto Get": "Auto Get", "Auto Get": "Auto Get",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "You can backup all monitors and notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.", backupDescription2: "Note: history and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", backupDescription3: "Sensitive data such as notification tokens are included in the export file; please store export securely.",
alertNoFile: "Please select a file to import.", alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.", alertWrongFileType: "Please select a JSON file.",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
@ -153,8 +157,8 @@ export default {
"Show URI": "Show URI", "Show URI": "Show URI",
Tags: "Tags", Tags: "Tags",
"Add New below or Select...": "Add New below or Select...", "Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.", "Tag with this name already exist.": "Tag with this name already exists.",
"Tag with this value already exist.": "Tag with this value already exist.", "Tag with this value already exist.": "Tag with this value already exists.",
color: "color", color: "color",
"value (optional)": "value (optional)", "value (optional)": "value (optional)",
Gray: "Gray", Gray: "Gray",
@ -185,22 +189,23 @@ export default {
"Required": "Required", "Required": "Required",
"telegram": "Telegram", "telegram": "Telegram",
"Bot Token": "Bot Token", "Bot Token": "Bot Token",
"You can get a token from": "You can get a token from", wayToGetTelegramToken: "You can get a token from {0}.",
"Chat ID": "Chat ID", "Chat ID": "Chat ID",
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID", supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
wayToGetTelegramChatID: "You can get your chat id by sending message to the bot and go to this url to view the chat_id:", wayToGetTelegramChatID: "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id:",
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE", "YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
chatIDNotFound: "Chat ID is not found, please send a message to this bot first", chatIDNotFound: "Chat ID is not found; please send a message to this bot first",
"webhook": "Webhook", "webhook": "Webhook",
"Post URL": "Post URL", "Post URL": "Post URL",
"Content Type": "Content Type", "Content Type": "Content Type",
webhookJsonDesc: "{0} is good for any modern http servers such as express.js", webhookJsonDesc: "{0} is good for any modern HTTP servers such as Express.js",
webhookFormDataDesc: "{multipart} is good for PHP, you just need to parse the json by {decodeFunction}", webhookFormDataDesc: "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}",
"smtp": "Email (SMTP)", "smtp": "Email (SMTP)",
secureOptionNone: "None / STARTTLS (25, 587)", secureOptionNone: "None / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)", secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Ignore TLS Error", "Ignore TLS Error": "Ignore TLS Error",
"From Email": "From Email", "From Email": "From Email",
emailCustomSubject: "Custom Subject",
"To Email": "To Email", "To Email": "To Email",
smtpCC: "CC", smtpCC: "CC",
smtpBCC: "BCC", smtpBCC: "BCC",
@ -212,12 +217,12 @@ export default {
"Hello @everyone is...": "Hello {'@'}everyone is...", "Hello @everyone is...": "Hello {'@'}everyone is...",
"teams": "Microsoft Teams", "teams": "Microsoft Teams",
"Webhook URL": "Webhook URL", "Webhook URL": "Webhook URL",
wayToGetTeamsURL: "You can learn how to create a webhook url {0}.", wayToGetTeamsURL: "You can learn how to create a webhook URL {0}.",
"signal": "Signal", "signal": "Signal",
"Number": "Number", "Number": "Number",
"Recipients": "Recipients", "Recipients": "Recipients",
needSignalAPI: "You need to have a signal client with REST API.", needSignalAPI: "You need to have a signal client with REST API.",
wayToCheckSignalURL: "You can check this url to view how to setup one:", wayToCheckSignalURL: "You can check this URL to view how to set one up:",
signalImportant: "IMPORTANT: You cannot mix groups and numbers in recipients!", signalImportant: "IMPORTANT: You cannot mix groups and numbers in recipients!",
"gotify": "Gotify", "gotify": "Gotify",
"Application Token": "Application Token", "Application Token": "Application Token",
@ -227,11 +232,11 @@ export default {
"Icon Emoji": "Icon Emoji", "Icon Emoji": "Icon Emoji",
"Channel Name": "Channel Name", "Channel Name": "Channel Name",
"Uptime Kuma URL": "Uptime Kuma URL", "Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "More info about webhooks on: {0}", aboutWebhooks: "More info about Webhooks on: {0}",
aboutChannelName: "Enter the channel name on {0} Channel Name field if you want to bypass the webhook channel. Ex: #other-channel", aboutChannelName: "Enter the channel name on {0} Channel Name field if you want to bypass the Webhook channel. Ex: #other-channel",
aboutKumaURL: "If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.", aboutKumaURL: "If you leave the Uptime Kuma URL field blank, it will default to the Project GitHub page.",
emojiCheatSheet: "Emoji cheat sheet: {0}", emojiCheatSheet: "Emoji cheat sheet: {0}",
"rocket.chat": "Rocket.chat", "rocket.chat": "Rocket.Chat",
pushover: "Pushover", pushover: "Pushover",
pushy: "Pushy", pushy: "Pushy",
octopush: "Octopush", octopush: "Octopush",
@ -250,7 +255,9 @@ export default {
pushoverDesc2: "If you want to send notifications to different devices, fill out Device field.", pushoverDesc2: "If you want to send notifications to different devices, fill out Device field.",
"SMS Type": "SMS Type", "SMS Type": "SMS Type",
octopushTypePremium: "Premium (Fast - recommended for alerting)", octopushTypePremium: "Premium (Fast - recommended for alerting)",
octopushTypeLowCost: "Low Cost (Slow, sometimes blocked by operator)", octopushTypeLowCost: "Low Cost (Slow - sometimes blocked by operator)",
checkPrice: "Check {0} prices:",
octopushLegacyHint: "Do you use the legacy version of Octopush (2011-2020) or the new version?",
"Check octopush prices": "Check octopush prices {0}.", "Check octopush prices": "Check octopush prices {0}.",
octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ", octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ",
octopushSMSSender: "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)", octopushSMSSender: "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
@ -269,17 +276,34 @@ export default {
"Basic Settings": "Basic Settings", "Basic Settings": "Basic Settings",
"User ID": "User ID", "User ID": "User ID",
"Messaging API": "Messaging API", "Messaging API": "Messaging API",
wayToGetLineChannelToken: "First access the {0}, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.", wayToGetLineChannelToken: "First access the {0}, create a provider and channel (Messaging API), then you can get the channel access token and user ID from the above mentioned menu items.",
"Icon URL": "Icon URL", "Icon URL": "Icon URL",
aboutIconURL: "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.", aboutIconURL: "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.",
aboutMattermostChannelName: "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", aboutMattermostChannelName: "You can override the default channel that the Webhook posts to by entering the channel name into \"Channel Name\" field. This needs to be enabled in the Mattermost Webhook settings. Ex: #other-channel",
"matrix": "Matrix", "matrix": "Matrix",
promosmsTypeEco: "SMS ECO - cheap but slow and often overloaded. Limited only to Polish recipients.", promosmsTypeEco: "SMS ECO - cheap but slow and often overloaded. Limited only to Polish recipients.",
promosmsTypeFlash: "SMS FLASH - Message will automatically show on recipient device. Limited only to Polish recipients.", promosmsTypeFlash: "SMS FLASH - Message will automatically show on recipient device. Limited only to Polish recipients.",
promosmsTypeFull: "SMS FULL - Premium tier of SMS, You can use Your Sender Name (You need to register name first). Reliable for alerts.", promosmsTypeFull: "SMS FULL - Premium tier of SMS, You can use your Sender Name (You need to register name first). Reliable for alerts.",
promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).", promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).",
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)", promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS", promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookUrl", "Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
"Internal Room Id": "Internal Room ID",
matrixDesc1: "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
// End notification form // End notification form
Method: "Method",
Body: "Body",
Headers: "Headers",
PushUrl: "Push URL",
HeadersInvalidFormat: "The request headers are not valid JSON: ",
BodyInvalidFormat: "The request body is not valid JSON: ",
"Monitor History": "Monitor History:",
clearDataOlderThan: "Keep monitor history data for {0} days.",
PasswordsDoNotMatch: "Passwords do not match.",
records: "records",
"One record": "One record",
"Showing {from} to {to} of {count} records": "Showing {from} to {to} of {count} records",
steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ",
}; };

View file

@ -9,7 +9,7 @@ export default {
passwordNotMatchMsg: "La contraseña repetida no coincide.", passwordNotMatchMsg: "La contraseña repetida no coincide.",
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).", notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).",
keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas", keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas",
pauseDashboardHome: "Pausar", pauseDashboardHome: "Pausado",
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?", deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?", deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.", resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
@ -32,7 +32,7 @@ export default {
Down: "Caído", Down: "Caído",
Pending: "Pendiente", Pending: "Pendiente",
Unknown: "Desconocido", Unknown: "Desconocido",
Pause: "Pausa", Pause: "Pausar",
Name: "Nombre", Name: "Nombre",
Status: "Estado", Status: "Estado",
DateTime: "Fecha y Hora", DateTime: "Fecha y Hora",
@ -198,4 +198,10 @@ export default {
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"Monitor History": "Historial de monitor:",
clearDataOlderThan: "Mantener los datos del historial del monitor durante {0} días.",
records: "registros",
"One record": "Un registro",
"Showing {from} to {to} of {count} records": "Mostrando desde {from} a {to} de {count} registros",
steamApiKeyDescription: "Para monitorear un servidor de juegos de Steam, necesita una clave Steam Web-API. Puede registrar su clave API aquí: ",
}; };

View file

@ -10,7 +10,7 @@ export default {
passwordNotMatchMsg: "Les mots de passe ne correspondent pas", passwordNotMatchMsg: "Les mots de passe ne correspondent pas",
notificationDescription: "Une fois ajoutée, vous devez l'activer manuellement dans les paramètres de vos hôtes.", notificationDescription: "Une fois ajoutée, vous devez l'activer manuellement dans les paramètres de vos hôtes.",
keywordDescription: "Le mot clé sera recherché dans la réponse HTML/JSON reçue du site internet.", keywordDescription: "Le mot clé sera recherché dans la réponse HTML/JSON reçue du site internet.",
pauseDashboardHome: "Éléments mis en pause", pauseDashboardHome: "En pause",
deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer cette sonde ?", deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer cette sonde ?",
deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.", deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.",
resoverserverDescription: "Le DNS de cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.", resoverserverDescription: "Le DNS de cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.",
@ -54,7 +54,7 @@ export default {
Delete: "Supprimer", Delete: "Supprimer",
Current: "Actuellement", Current: "Actuellement",
Uptime: "Uptime", Uptime: "Uptime",
"Cert Exp.": "Certificat expiré", "Cert Exp.": "Expiration SSL",
days: "jours", days: "jours",
day: "jour", day: "jour",
"-day": "-jours", "-day": "-jours",
@ -185,7 +185,7 @@ export default {
"Required": "Requis", "Required": "Requis",
"telegram": "Telegram", "telegram": "Telegram",
"Bot Token": "Bot Token", "Bot Token": "Bot Token",
"You can get a token from": "Vous pouvez obtenir un token depuis", wayToGetTelegramToken: "Vous pouvez obtenir un token depuis {0}.",
"Chat ID": "Chat ID", "Chat ID": "Chat ID",
supportTelegramChatID: "Supporte les messages privés / en groupe / l'ID du salon", supportTelegramChatID: "Supporte les messages privés / en groupe / l'ID du salon",
wayToGetTelegramChatID: "Vous pouvez obtenir l'ID du chat en envoyant un message avec le bot puis en récupérant l'URL pour voir l'ID du salon :", wayToGetTelegramChatID: "Vous pouvez obtenir l'ID du chat en envoyant un message avec le bot puis en récupérant l'URL pour voir l'ID du salon :",

View file

@ -1,44 +1,44 @@
export default { export default {
languageName: "Indonesia", languageName: "Bahasa Indonesia (Indonesian)",
checkEverySecond: "Cek Setiap {0} detik.", checkEverySecond: "Cek Setiap {0} detik.",
retryCheckEverySecond: "Coba lagi setiap {0} detik.", retryCheckEverySecond: "Coba lagi setiap {0} detik.",
retriesDescription: "Percobaan ulang maksimum sebelum layanan dinyatakan tidak aktif dan notifikasi dikirim", retriesDescription: "Percobaan ulang maksimum sebelum layanan dinyatakan tidak aktif dan notifikasi dikirim",
ignoreTLSError: "Abaikan kesalahan TLS/SSL untuk situs web HTTPS", ignoreTLSError: "Abaikan kesalahan TLS/SSL untuk situs web HTTPS",
upsideDownModeDescription: "Balikkan statusnya. Jika layanan dapat dijangkau, TIDAK AKTIF.", upsideDownModeDescription: "Balikkan statusnya. Jika layanan dapat dijangkau, TIDAK AKTIF.",
maxRedirectDescription: "Jumlah maksimum pengalihan untuk diikuti. Setel ke 0 untuk menonaktifkan pengalihan.", maxRedirectDescription: "Jumlah maksimum pengalihan untuk diikuti. Setel ke 0 untuk menonaktifkan pengalihan.",
acceptedStatusCodesDescription: "Pilih kode status yang dianggap sebagai respons yang berhasil.", acceptedStatusCodesDescription: "Pilih kode status yang dianggap sebagai tanggapan yang berhasil.",
passwordNotMatchMsg: "Sandi kedua tidak cocok.", passwordNotMatchMsg: "Sandi kedua tidak cocok.",
notificationDescription: "Harap atur notifikasi ke monitor agar berfungsi.", notificationDescription: "Harap atur notifikasi ke monitor agar berfungsi.",
keywordDescription: "Cari kata kunci dalam code html atau JSON huruf besar-kecil berpengaruh", keywordDescription: "Cari kata kunci dalam code html atau JSON huruf besar-kecil berpengaruh",
pauseDashboardHome: "Jeda", pauseDashboardHome: "Jeda",
deleteMonitorMsg: "Apakah anda mau menghapus monitor ini?", deleteMonitorMsg: "Apakah Anda mau menghapus monitor ini?",
deleteNotificationMsg: "Apakah anda mau menghapus notifikasi ini untuk semua monitor?", deleteNotificationMsg: "Apakah Anda mau menghapus notifikasi ini untuk semua monitor?",
resoverserverDescription: "Cloudflare adalah server default, Anda dapat mengubah server resolver kapan saja.", resoverserverDescription: "Cloudflare adalah server bawaan, Anda dapat mengubah server resolver kapan saja.",
rrtypeDescription: "Pilih RR-Type yang mau anda monitor", rrtypeDescription: "Pilih RR-Type yang mau Anda monitor",
pauseMonitorMsg: "Apakah anda yakin mau menjeda?", pauseMonitorMsg: "Apakah Anda yakin mau menjeda?",
enableDefaultNotificationDescription: "Untuk setiap monitor baru, notifikasi ini akan diaktifkan secara default. Anda masih dapat menonaktifkan notifikasi secara terpisah untuk setiap monitor.", enableDefaultNotificationDescription: "Untuk setiap monitor baru, notifikasi ini akan diaktifkan secara bawaan. Anda masih dapat menonaktifkan notifikasi secara terpisah untuk setiap monitor.",
clearEventsMsg: "Apakah anda yakin mau menghapus semua event di monitor ini?", clearEventsMsg: "Apakah Anda yakin mau menghapus semua event di monitor ini?",
clearHeartbeatsMsg: "Apakah anda yakin mau menghapus semua heartbeats di monitor ini?", clearHeartbeatsMsg: "Apakah Anda yakin mau menghapus semua heartbeats di monitor ini?",
confirmClearStatisticsMsg: "Apakah anda yakin mau menghapus semua statistik?", confirmClearStatisticsMsg: "Apakah Anda yakin mau menghapus semua statistik?",
importHandleDescription: "Pilih 'Lewati yang ada' jika Anda ingin melewati setiap monitor atau notifikasi dengan nama yang sama. 'Timpa' akan menghapus setiap monitor dan notifikasi yang ada.", importHandleDescription: "Pilih 'Lewati yang ada' jika Anda ingin melewati setiap monitor atau notifikasi dengan nama yang sama. 'Timpa' akan menghapus setiap monitor dan notifikasi yang ada.",
confirmImportMsg: "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.", confirmImportMsg: "Apakah Anda yakin untuk mengimpor cadangan? Pastikan Anda telah memilih opsi impor yang tepat.",
twoFAVerifyLabel: "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi", twoFAVerifyLabel: "Silakan ketik token Anda untuk memverifikasi bahwa 2FA berfungsi",
tokenValidSettingsMsg: "Tokennya valid! Anda sekarang dapat menyimpan pengaturan 2FA.", tokenValidSettingsMsg: "Tokennya benar! Anda sekarang dapat menyimpan pengaturan 2FA.",
confirmEnableTwoFAMsg: "Apakah Anda yakin ingin mengaktifkan 2FA?", confirmEnableTwoFAMsg: "Apakah Anda yakin ingin mengaktifkan 2FA?",
confirmDisableTwoFAMsg: "Apakah Anda yakin ingin menonaktifkan 2FA?", confirmDisableTwoFAMsg: "Apakah Anda yakin ingin menonaktifkan 2FA?",
Settings: "Pengaturan", Settings: "Pengaturan",
Dashboard: "Dashboard", Dashboard: "Dasbor",
"New Update": "Update Baru", "New Update": "Pembaruan Baru",
Language: "Bahasa", Language: "Bahasa",
Appearance: "Tampilan", Appearance: "Tampilan",
Theme: "Tema", Theme: "Tema",
General: "General", General: "Umum",
Version: "Versi", Version: "Versi",
"Check Update On GitHub": "Cek Update di GitHub", "Check Update On GitHub": "Cek Pembaruan di GitHub",
List: "List", List: "Daftar",
Add: "Tambah", Add: "Tambah",
"Add New Monitor": "Tambah Monitor Baru", "Add New Monitor": "Tambah Monitor Baru",
"Quick Stats": "Statistik Cepat", "Quick Stats": "Statistik",
Up: "Aktif", Up: "Aktif",
Down: "Tidak Aktif", Down: "Tidak Aktif",
Pending: "Tertunda", Pending: "Tertunda",
@ -48,9 +48,9 @@ export default {
Status: "Status", Status: "Status",
DateTime: "Tanggal Waktu", DateTime: "Tanggal Waktu",
Message: "Pesan", Message: "Pesan",
"No important events": "Tidak ada Event penting", "No important events": "Tidak ada peristiwa penting",
Resume: "Melanjutkan", Resume: "Lanjut",
Edit: "Rubah", Edit: "Ubah",
Delete: "Hapus", Delete: "Hapus",
Current: "Saat ini", Current: "Saat ini",
Uptime: "Waktu aktif", Uptime: "Waktu aktif",
@ -60,20 +60,20 @@ export default {
"-day": "-hari", "-day": "-hari",
hour: "Jam", hour: "Jam",
"-hour": "-Jam", "-hour": "-Jam",
Response: "Respon", Response: "Tanggapan",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Tipe Monitor", "Monitor Type": "Tipe Monitor",
Keyword: "Keyword", Keyword: "Keyword",
"Friendly Name": "Friendly Name", "Friendly Name": "Nama yang Ramah",
URL: "URL", URL: "URL",
Hostname: "Hostname", Hostname: "Hostname",
Port: "Port", Port: "Port",
"Heartbeat Interval": "Interval Heartbeat ", "Heartbeat Interval": "Jarak Waktu Heartbeat ",
Retries: "Retries", Retries: "Coba lagi",
"Heartbeat Retry Interval": "Interval Heartbeat Mencoba kembali ", "Heartbeat Retry Interval": "Jarak Waktu Heartbeat Mencoba kembali ",
Advanced: "Advanced", Advanced: "Tingkat Lanjut",
"Upside Down Mode": "Mode Terbalik", "Upside Down Mode": "Mode Terbalik",
"Max. Redirects": "Maksimal Redirect/Pengalihan", "Max. Redirects": "Maksimal Pengalihan",
"Accepted Status Codes": "Kode Status yang Diterima", "Accepted Status Codes": "Kode Status yang Diterima",
Save: "Simpan", Save: "Simpan",
Notifications: "Notifikasi", Notifications: "Notifikasi",
@ -81,25 +81,25 @@ export default {
"Setup Notification": "Setel Notifikasi", "Setup Notification": "Setel Notifikasi",
Light: "Terang", Light: "Terang",
Dark: "Gelap", Dark: "Gelap",
Auto: "Automatis", Auto: "Otomatis",
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar", "Theme - Heartbeat Bar": "Tema - Heartbeat Bar",
Normal: "Normal", Normal: "Normal",
Bottom: "Bawah", Bottom: "Bawah",
None: "Tidak ada", None: "Tidak ada",
Timezone: "Zona Waktu", Timezone: "Zona Waktu",
"Search Engine Visibility": "Visibilitas Mesin Pencari", "Search Engine Visibility": "Visibilitas Mesin Pencari",
"Allow indexing": "Mengizinkan untuk diindex", "Allow indexing": "Mengizinkan untuk diindex",
"Discourage search engines from indexing site": "Mencegah mesin pencari untuk mengindex site", "Discourage search engines from indexing site": "Mencegah mesin pencari untuk mengindex situs",
"Change Password": "Ganti Sandi", "Change Password": "Ganti Sandi",
"Current Password": "Sandi Lama", "Current Password": "Sandi Lama",
"New Password": "Sandi Baru", "New Password": "Sandi Baru",
"Repeat New Password": "Ulangi Sandi Baru", "Repeat New Password": "Ulangi Sandi Baru",
"Update Password": "Perbarui Kata Sandi", "Update Password": "Perbarui Kata Sandi",
"Disable Auth": "Nonaktifkan auth", "Disable Auth": "Nonaktifkan Autentikasi",
"Enable Auth": "Aktifkan Auth", "Enable Auth": "Aktifkan Autentikasi",
Logout: "Keluar", Logout: "Keluar",
Leave: "Pergi", Leave: "Pergi",
"I understand, please disable": "Saya mengerti, silahkan dinonaktifkan", "I understand, please disable": "Saya mengerti, silakan dinonaktifkan",
Confirm: "Konfirmasi", Confirm: "Konfirmasi",
Yes: "Ya", Yes: "Ya",
No: "Tidak", No: "Tidak",
@ -107,35 +107,35 @@ export default {
Password: "Sandi", Password: "Sandi",
"Remember me": "Ingat saya", "Remember me": "Ingat saya",
Login: "Masuk", Login: "Masuk",
"No Monitors, please": "Tidak ada monitor, silahkan", "No Monitors, please": "Tidak ada monitor, silakan",
"add one": "tambah baru", "add one": "tambahkan satu",
"Notification Type": "Tipe Notifikasi", "Notification Type": "Tipe Notifikasi",
Email: "Email", Email: "Surel",
Test: "Test", Test: "Tes",
"Certificate Info": "Info Sertifikasi ", "Certificate Info": "Info Sertifikasi",
"Resolver Server": "Resolver Server", "Resolver Server": "Resolver Server",
"Resource Record Type": "Resource Record Type", "Resource Record Type": "Resource Record Type",
"Last Result": "Hasil Terakhir", "Last Result": "Hasil Terakhir",
"Create your admin account": "Buat admin akun anda", "Create your admin account": "Buat admin akun Anda",
"Repeat Password": "Ulangi Sandi", "Repeat Password": "Ulangi Sandi",
"Import Backup": "Impor Backup", "Import Backup": "Impor Cadangan",
"Export Backup": "Expor Backup", "Export Backup": "Expor Cadangan",
Export: "Expor", Export: "Expor",
Import: "Impor", Import: "Impor",
respTime: "Tanggapan. Waktu (milidetik)", respTime: "Tanggapan. Waktu (milidetik)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
"Default enabled": "Default diaktifkan", "Default enabled": "Bawaan diaktifkan",
"Apply on all existing monitors": "Terapkan pada semua monitor yang ada", "Apply on all existing monitors": "Terapkan pada semua monitor yang ada",
Create: "Buat", Create: "Buat",
"Clear Data": "Bersihkan Data", "Clear Data": "Bersihkan Data",
Events: "Event", Events: "Peristiwa",
Heartbeats: "Heartbeats", Heartbeats: "Heartbeats",
"Auto Get": "Auto Get", "Auto Get": "Ambil Otomatis",
backupDescription: "Anda dapat mencadangkan semua monitor dan semua notifikasi ke dalam file JSON.", backupDescription: "Anda dapat mencadangkan semua monitor dan semua notifikasi ke dalam berkas JSON.",
backupDescription2: "Catatan: Data sejarah dan event tidak disertakan.", backupDescription2: "Catatan: Data sejarah dan peristiwa tidak disertakan.",
backupDescription3: "Data sensitif seperti notifikasi token disertakan dalam file ekspor, harap simpan dengan hati-hati.", backupDescription3: "Data sensitif seperti notifikasi token disertakan dalam berkas ekspor, harap simpan dengan hati-hati.",
alertNoFile: "Silakan pilih file untuk diimpor.", alertNoFile: "Silakan pilih berkas untuk diimpor.",
alertWrongFileType: "Silakan pilih file JSON .", alertWrongFileType: "Silakan pilih berkas JSON.",
"Clear all statistics": "Hapus semua statistik", "Clear all statistics": "Hapus semua statistik",
"Skip existing": "Lewati yang ada", "Skip existing": "Lewati yang ada",
Overwrite: "Timpa", Overwrite: "Timpa",
@ -145,29 +145,29 @@ export default {
"Setup 2FA": "Pengaturan 2FA", "Setup 2FA": "Pengaturan 2FA",
"Enable 2FA": "Aktifkan 2FA", "Enable 2FA": "Aktifkan 2FA",
"Disable 2FA": "Nonaktifkan 2FA", "Disable 2FA": "Nonaktifkan 2FA",
"2FA Settings": "Settings 2FA", "2FA Settings": "Pengaturan 2FA",
"Two Factor Authentication": "Otentikasi Dua Faktor", "Two Factor Authentication": "Autentikasi Dua Faktor",
Active: "Aktif", Active: "Aktif",
Inactive: "Tidak Aktif", Inactive: "Tidak Aktif",
Token: "Token", Token: "Token",
"Show URI": "Lihat URI", "Show URI": "Lihat URI",
Tags: "Tag", Tags: "Tanda",
"Add New below or Select...": "Tambahkan Baru di bawah atau Pilih...", "Add New below or Select...": "Tambahkan Baru di bawah atau Pilih...",
"Tag with this name already exist.": "Tag dengan nama ini sudah ada.", "Tag with this name already exist.": "Tanda dengan nama ini sudah ada.",
"Tag with this value already exist.": "Tag dengan nilai ini sudah ada.", "Tag with this value already exist.": "Tanda dengan nilai ini sudah ada.",
color: "warna", color: "warna",
"value (optional)": "nilai (harus diisi)", "value (optional)": "nilai (harus diisi)",
Gray: "Abu Abu", Gray: "Abu-abu",
Red: "Merah", Red: "Merah",
Orange: "Oranye", Orange: "Jingga",
Green: "Hijau", Green: "Hijau",
Blue: "Biru", Blue: "Biru",
Indigo: "Indigo", Indigo: "Biru Tua",
Purple: "Ungu", Purple: "Ungu",
Pink: "Merah Muda", Pink: "Merah Muda",
"Search...": "Cari...", "Search...": "Cari...",
"Avg. Ping": "Rata-rata. Ping", "Avg. Ping": "Rata-rata Ping",
"Avg. Response": "Rata-rata. Respon", "Avg. Response": "Rata-rata Tanggapan",
"Entry Page": "Halaman Masuk", "Entry Page": "Halaman Masuk",
statusPageNothing: "Tidak ada di sini, silakan tambahkan grup atau monitor.", statusPageNothing: "Tidak ada di sini, silakan tambahkan grup atau monitor.",
"No Services": "Tidak ada Layanan", "No Services": "Tidak ada Layanan",
@ -177,7 +177,7 @@ export default {
"Add Group": "Tambah Grup", "Add Group": "Tambah Grup",
"Add a monitor": "Tambah monitor", "Add a monitor": "Tambah monitor",
"Edit Status Page": "Edit Halaman Status", "Edit Status Page": "Edit Halaman Status",
"Go to Dashboard": "Lihat Dashboard", "Go to Dashboard": "Pergi ke Dasbor",
"Status Page": "Halaman Status", "Status Page": "Halaman Status",
// Start notification form // Start notification form
defaultNotificationName: "{notification} saya Peringatan ({number})", defaultNotificationName: "{notification} saya Peringatan ({number})",
@ -194,22 +194,22 @@ export default {
"webhook": "Webhook", "webhook": "Webhook",
"Post URL": "Post URL", "Post URL": "Post URL",
"Content Type": "Tipe konten", "Content Type": "Tipe konten",
webhookJsonDesc: "{0} bagus untuk server http modern seperti express.js", webhookJsonDesc: "{0} bagus untuk peladen http modern seperti express.js",
webhookFormDataDesc: "{multipart} bagus untuk PHP, Anda hanya perlu mengurai json dengan {decodeFunction}", webhookFormDataDesc: "{multipart} bagus untuk PHP, Anda hanya perlu mengurai json dengan {decodeFunction}",
"smtp": "Email (SMTP)", "smtp": "Surel (SMTP)",
secureOptionNone: "None / STARTTLS (25, 587)", secureOptionNone: "None / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)", secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Ignore TLS Error", "Ignore TLS Error": "Abaikan Kesalahan TLS",
"From Email": "From Email", "From Email": "Dari Surel",
"To Email": "To Email", "To Email": "Ke Surel",
smtpCC: "CC", smtpCC: "CC",
smtpBCC: "BCC", smtpBCC: "BCC",
"discord": "Discord", "discord": "Discord",
"Discord Webhook URL": "Discord Webhook URL", "Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "Anda bisa mendapatkan ini dengan pergi ke Server Settings -> Integrations -> Create Webhook", wayToGetDiscordURL: "Anda bisa mendapatkan ini dengan pergi ke Server Settings -> Integrations -> Create Webhook",
"Bot Display Name": "Nama Bot", "Bot Display Name": "Nama Bot",
"Prefix Custom Message": "Prefix Pesan", "Prefix Custom Message": "Awalan Pesan",
"Hello @everyone is...": "Hallo {'@'}everyone is...", "Hello @everyone is...": "Halo {'@'}everyone is...",
"teams": "Microsoft Teams", "teams": "Microsoft Teams",
"Webhook URL": "Webhook URL", "Webhook URL": "Webhook URL",
wayToGetTeamsURL: "Anda dapat mempelajari cara membuat url webhook {0}.", wayToGetTeamsURL: "Anda dapat mempelajari cara membuat url webhook {0}.",
@ -221,16 +221,16 @@ export default {
signalImportant: "PENTING: Anda tidak dapat mencampur grup dan nomor di penerima!", signalImportant: "PENTING: Anda tidak dapat mencampur grup dan nomor di penerima!",
"gotify": "Gotify", "gotify": "Gotify",
"Application Token": "Token Aplikasi", "Application Token": "Token Aplikasi",
"Server URL": "Server URL", "Server URL": "URL Peladen",
"Priority": "Prioritas", "Priority": "Prioritas",
"slack": "Slack", "slack": "Slack",
"Icon Emoji": "Icon Emoji", "Icon Emoji": "Ikon Emoji",
"Channel Name": "Nama Channel", "Channel Name": "Nama Saluran",
"Uptime Kuma URL": "Uptime Kuma URL", "Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Info lain tentang webhook: {0}", aboutWebhooks: "Info lain tentang webhook: {0}",
aboutChannelName: "Masukan nama channel di {0} Kolom Nama Channel jika Anda ingin melewati channel webhook. Contoh: #other-channel", aboutChannelName: "Masukan nama saluran di {0} Kolom Nama Saluran jika Anda ingin melewati saluran webhook. Contoh: #saluran-lain",
aboutKumaURL: "Jika Anda membiarkan bidang URL Uptime Kuma kosong, itu akan menjadi default ke halaman Project Github.", aboutKumaURL: "Jika Anda membiarkan bidang URL Uptime Kuma kosong, itu akan menjadi bawaan ke halaman Proyek Github.",
emojiCheatSheet: "Emoji cheat sheet: {0}", emojiCheatSheet: "Lembar contekan emoji: {0}",
"rocket.chat": "Rocket.chat", "rocket.chat": "Rocket.chat",
pushover: "Pushover", pushover: "Pushover",
pushy: "Pushy", pushy: "Pushy",
@ -246,7 +246,7 @@ export default {
"Message Title": "Judul Pesan", "Message Title": "Judul Pesan",
"Notification Sound": "Suara Nofifikasi", "Notification Sound": "Suara Nofifikasi",
"More info on:": "Info lebih lanjut tentang: {0}", "More info on:": "Info lebih lanjut tentang: {0}",
pushoverDesc1: "Prioritas darurat (2) memiliki batas waktu default 30 detik antara percobaan ulang dan akan kadaluwarsa setelah 1 jam.", pushoverDesc1: "Prioritas darurat (2) memiliki batas waktu bawaan 30 detik antara percobaan ulang dan akan kadaluwarsa setelah 1 jam.",
pushoverDesc2: "Jika Anda ingin mengirim pemberitahuan ke perangkat yang berbeda, isi kolom Perangkat.", pushoverDesc2: "Jika Anda ingin mengirim pemberitahuan ke perangkat yang berbeda, isi kolom Perangkat.",
"SMS Type": "Tipe SMS", "SMS Type": "Tipe SMS",
octopushTypePremium: "Premium (Cepat - direkomendasikan untuk mengingatkan)", octopushTypePremium: "Premium (Cepat - direkomendasikan untuk mengingatkan)",
@ -262,24 +262,24 @@ export default {
"Read more": "Baca lebih lajut", "Read more": "Baca lebih lajut",
appriseInstalled: "Apprise diinstall.", appriseInstalled: "Apprise diinstall.",
appriseNotInstalled: "Apprise tidak diinstall. {0}", appriseNotInstalled: "Apprise tidak diinstall. {0}",
"Access Token": "Access Token", "Access Token": "Token Akses",
"Channel access token": "Channel access token", "Channel access token": "Token akses saluran",
"Line Developers Console": "Line Developers Console", "Line Developers Console": "Konsol Pengembang Line",
lineDevConsoleTo: "Line Developers Console - {0}", lineDevConsoleTo: "Konsol Pengembang Line - {0}",
"Basic Settings": "Pengaturan dasar", "Basic Settings": "Pengaturan Dasar",
"User ID": "User ID", "User ID": "ID User",
"Messaging API": "Messaging API", "Messaging API": "Messaging API",
wayToGetLineChannelToken: "Pertama akses {0}, buat penyedia dan saluran (Messaging API), lalu Anda bisa mendapatkan token akses saluran dan id pengguna dari item menu yang disebutkan di atas.", wayToGetLineChannelToken: "Pertama akses {0}, buat penyedia dan saluran (Messaging API), lalu Anda bisa mendapatkan token akses saluran dan id pengguna dari item menu yang disebutkan di atas.",
"Icon URL": "Icon URL", "Icon URL": "Icon URL",
aboutIconURL: "Anda dapat memberikan tautan ke gambar di \"Icon URL\" untuk mengganti gambar profil default. Tidak akan digunakan jika Ikon Emoji diset.", aboutIconURL: "Anda dapat memberikan tautan ke gambar di \"Icon URL\" untuk mengganti gambar profil bawaan. Tidak akan digunakan jika Ikon Emoji diset.",
aboutMattermostChannelName: "Anda dapat mengganti channel default tujuan posting webhook dengan memasukkan nama channel ke dalam Kolom \"Channel Name\". Ini perlu diaktifkan di pengaturan webhook Mattermost. contoh: #other-channel", aboutMattermostChannelName: "Anda dapat mengganti saluran bawaan tujuan posting webhook dengan memasukkan nama saluran ke dalam Kolom \"Channel Name\". Ini perlu diaktifkan di pengaturan webhook Mattermost. contoh: #other-channel",
"matrix": "Matrix", "matrix": "Matrix",
promosmsTypeEco: "SMS ECO - murah tapi lambat dan sering kelebihan beban. Terbatas hanya untuk penerima Polandia.", promosmsTypeEco: "SMS ECO - murah tapi lambat dan sering kelebihan beban. Terbatas hanya untuk penerima Polandia.",
promosmsTypeFlash: "SMS FLASH - Pesan akan otomatis muncul di perangkat penerima. Terbatas hanya untuk penerima Polandia.", promosmsTypeFlash: "SMS FLASH - Pesan akan otomatis muncul di perangkat penerima. Terbatas hanya untuk penerima Polandia.",
promosmsTypeFull: "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). Dapat diandalkan untuk peringatan.", promosmsTypeFull: "SMS FULL - SMS tingkat premium, Anda dapat menggunakan Nama Pengirim Anda (Anda harus mendaftarkan nama terlebih dahulu). Dapat diAndalkan untuk peringatan.",
promosmsTypeSpeed: "SMS SPEED - Prioritas tertinggi dalam sistem. Sangat cepat dan dapat diandalkan tetapi mahal (sekitar dua kali lipat dari harga SMS FULL).", promosmsTypeSpeed: "SMS SPEED - Prioritas tertinggi dalam sistem. Sangat cepat dan dapat diAndalkan tetapi mahal (sekitar dua kali lipat dari harga SMS FULL).",
promosmsPhoneNumber: "Nomor telepon (untuk penerima Polandia Anda dapat melewati kode area)", promosmsPhoneNumber: "Nomor telepon (untuk penerima Polandia Anda dapat melewati kode area)",
promosmsSMSSender: "Nama Pengirim SMS : Nama pra-registrasi atau salah satu default: InfoSMS, Info SMS, MaxSMS, INFO, SMS", promosmsSMSSender: "Nama Pengirim SMS : Nama pra-registrasi atau salah satu bawaan: InfoSMS, Info SMS, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookUrl", "Feishu WebHookUrl": "Feishu WebHookUrl",
// End notification form // End notification form
}; };

View file

@ -184,7 +184,7 @@ export default {
Required: "필수", Required: "필수",
telegram: "Telegram", telegram: "Telegram",
"Bot Token": "봇 토큰", "Bot Token": "봇 토큰",
"You can get a token from": "토큰은 여기서 얻을 수 있어요:", wayToGetTelegramToken: "토큰은 여기서 얻을 수 있어요: {0}.",
"Chat ID": "채팅 ID", "Chat ID": "채팅 ID",
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.", supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.", wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",

View file

@ -185,7 +185,7 @@ export default {
"Required": "Obligatorisk", "Required": "Obligatorisk",
"telegram": "Telegram", "telegram": "Telegram",
"Bot Token": "Bot Token", "Bot Token": "Bot Token",
"You can get a token from": "Du kan få et token fra", wayToGetTelegramToken: "Du kan få et token fra {0}.",
"Chat ID": "Chat ID", "Chat ID": "Chat ID",
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID", supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
wayToGetTelegramChatID: "Du kan få chat-ID-en din ved å sende meldingen til boten og gå til denne nettadressen for å se chat_id:", wayToGetTelegramChatID: "Du kan få chat-ID-en din ved å sende meldingen til boten og gå til denne nettadressen for å se chat_id:",

View file

@ -198,4 +198,10 @@ export default {
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
Method: "Methode",
Body: "Body",
Headers: "Headers",
PushUrl: "Push URL",
HeadersInvalidFormat: "The request headers is geen geldige JSON: ",
BodyInvalidFormat: "De request body is geen geldige JSON: "
}; };

View file

@ -185,7 +185,7 @@ export default {
"Required": "Wymagane", "Required": "Wymagane",
"telegram": "Telegram", "telegram": "Telegram",
"Bot Token": "Token Bota", "Bot Token": "Token Bota",
"You can get a token from": "Token można uzyskać z", wayToGetTelegramToken: "Token można uzyskać z {0}.",
"Chat ID": "Identyfikator Czatu", "Chat ID": "Identyfikator Czatu",
supportTelegramChatID: "Czat wsprarcia technicznego / Bezpośrednia Rozmowa / Czat Grupowy", supportTelegramChatID: "Czat wsprarcia technicznego / Bezpośrednia Rozmowa / Czat Grupowy",
wayToGetTelegramChatID: "Możesz uzyskać swój identyfikator czatu, wysyłając wiadomość do bota i przechodząc pod ten adres URL, aby wyświetlić identyfikator czatu:", wayToGetTelegramChatID: "Możesz uzyskać swój identyfikator czatu, wysyłając wiadomość do bota i przechodząc pod ten adres URL, aby wyświetlić identyfikator czatu:",

View file

@ -1,6 +1,6 @@
export default { export default {
languageName: "Русский", languageName: "Русский",
checkEverySecond: "проверять каждые {0} секунд", checkEverySecond: "Проверка каждые {0} секунд",
retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления", retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления",
ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов", ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов",
upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.", upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
@ -29,7 +29,7 @@ export default {
"Add New Monitor": "Новый монитор", "Add New Monitor": "Новый монитор",
"Quick Stats": "Статистика", "Quick Stats": "Статистика",
Up: "Доступен", Up: "Доступен",
Down: "Н", Down: "Недоступен",
Pending: "Ожидание", Pending: "Ожидание",
Unknown: "Неизвестно", Unknown: "Неизвестно",
Pause: "Пауза", Pause: "Пауза",
@ -65,8 +65,8 @@ export default {
"Accepted Status Codes": "Допустимые коды статуса", "Accepted Status Codes": "Допустимые коды статуса",
Save: "Сохранить", Save: "Сохранить",
Notifications: "Уведомления", Notifications: "Уведомления",
"Not available, please setup.": "Доступных уведомлений нет, необходима настройка.", "Not available, please setup.": "Доступных уведомлений нет, необходимо создать.",
"Setup Notification": "Настроить уведомления", "Setup Notification": "Создать уведомление",
Light: "Светлая", Light: "Светлая",
Dark: "Тёмная", Dark: "Тёмная",
Auto: "Авто", Auto: "Авто",
@ -142,7 +142,7 @@ export default {
Token: "Токен", Token: "Токен",
"Show URI": "Показать URI", "Show URI": "Показать URI",
"Clear all statistics": "Удалить всю статистику", "Clear all statistics": "Удалить всю статистику",
retryCheckEverySecond: "повторять каждые {0} секунд", retryCheckEverySecond: "Повтор каждые {0} секунд",
importHandleDescription: "Выберите \"Пропустить существующие\", если вы хотите пропустить каждый монитор или уведомление с таким же именем. \"Перезаписать\" удалит каждый существующий монитор или уведомление и добавит заново. Вариант \"Не проверять\" принудительно восстанавливает все мониторы и уведомления, даже если они уже существуют.", importHandleDescription: "Выберите \"Пропустить существующие\", если вы хотите пропустить каждый монитор или уведомление с таким же именем. \"Перезаписать\" удалит каждый существующий монитор или уведомление и добавит заново. Вариант \"Не проверять\" принудительно восстанавливает все мониторы и уведомления, даже если они уже существуют.",
confirmImportMsg: "Вы действительно хотите восстановить резервную копию? Убедитесь, что вы выбрали подходящий вариант импорта.", confirmImportMsg: "Вы действительно хотите восстановить резервную копию? Убедитесь, что вы выбрали подходящий вариант импорта.",
"Heartbeat Retry Interval": "Интервал повтора опроса", "Heartbeat Retry Interval": "Интервал повтора опроса",
@ -202,4 +202,111 @@ export default {
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"Primary Base URL": "Primary Base URL",
"Push URL": "Push URL",
needPushEvery: "You should call this URL every {0} seconds.",
pushOptionalParams: "Optional parameters: {0}",
defaultNotificationName: "My {notification} Alert ({number})",
here: "here",
Required: "Required",
"Bot Token": "Bot Token",
wayToGetTelegramToken: "You can get a token from {0}.",
"Chat ID": "Chat ID",
supportTelegramChatID: "Support Direct Chat / Group / Channel's Chat ID",
wayToGetTelegramChatID: "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id:",
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
chatIDNotFound: "Chat ID is not found; please send a message to this bot first",
"Post URL": "Post URL",
"Content Type": "Content Type",
webhookJsonDesc: "{0} is good for any modern HTTP servers such as Express.js",
webhookFormDataDesc: "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}",
secureOptionNone: "None / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Ignore TLS Error",
"From Email": "From Email",
emailCustomSubject: "Custom Subject",
"To Email": "To Email",
smtpCC: "CC",
smtpBCC: "BCC",
"Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "You can get this by going to Server Settings -> Integrations -> Create Webhook",
"Bot Display Name": "Bot Display Name",
"Prefix Custom Message": "Prefix Custom Message",
"Hello @everyone is...": "Hello {'@'}everyone is...",
"Webhook URL": "Webhook URL",
wayToGetTeamsURL: "You can learn how to create a webhook URL {0}.",
Number: "Number",
Recipients: "Recipients",
needSignalAPI: "You need to have a signal client with REST API.",
wayToCheckSignalURL: "You can check this URL to view how to set one up:",
signalImportant: "IMPORTANT: You cannot mix groups and numbers in recipients!",
"Application Token": "Application Token",
"Server URL": "Server URL",
Priority: "Priority",
"Icon Emoji": "Icon Emoji",
"Channel Name": "Channel Name",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "More info about Webhooks on: {0}",
aboutChannelName: "Enter the channel name on {0} Channel Name field if you want to bypass the Webhook channel. Ex: #other-channel",
aboutKumaURL: "If you leave the Uptime Kuma URL field blank, it will default to the Project GitHub page.",
emojiCheatSheet: "Emoji cheat sheet: {0}",
"User Key": "User Key",
Device: "Device",
"Message Title": "Message Title",
"Notification Sound": "Notification Sound",
"More info on:": "More info on: {0}",
pushoverDesc1: "Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.",
pushoverDesc2: "If you want to send notifications to different devices, fill out Device field.",
"SMS Type": "SMS Type",
octopushTypePremium: "Premium (Fast - recommended for alerting)",
octopushTypeLowCost: "Low Cost (Slow - sometimes blocked by operator)",
checkPrice: "Check {0} prices:",
octopushLegacyHint: "Do you use the legacy version of Octopush (2011-2020) or the new version?",
"Check octopush prices": "Check octopush prices {0}.",
octopushPhoneNumber: "Phone number (intl format, eg : +33612345678) ",
octopushSMSSender: "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea Device ID",
"Apprise URL": "Apprise URL",
"Example:": "Example: {0}",
"Read more:": "Read more: {0}",
"Status:": "Status: {0}",
"Read more": "Read more",
appriseInstalled: "Apprise is installed.",
appriseNotInstalled: "Apprise is not installed. {0}",
"Access Token": "Access Token",
"Channel access token": "Channel access token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Basic Settings",
"User ID": "User ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "First access the {0}, create a provider and channel (Messaging API), then you can get the channel access token and user ID from the above mentioned menu items.",
"Icon URL": "Icon URL",
aboutIconURL: "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.",
aboutMattermostChannelName: "You can override the default channel that the Webhook posts to by entering the channel name into \"Channel Name\" field. This needs to be enabled in the Mattermost Webhook settings. Ex: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - cheap but slow and often overloaded. Limited only to Polish recipients.",
promosmsTypeFlash: "SMS FLASH - Message will automatically show on recipient device. Limited only to Polish recipients.",
promosmsTypeFull: "SMS FULL - Premium tier of SMS, You can use your Sender Name (You need to register name first). Reliable for alerts.",
promosmsTypeSpeed: "SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price).",
promosmsPhoneNumber: "Phone number (for Polish recipient You can skip area codes)",
promosmsSMSSender: "SMS Sender Name : Pre-registred name or one of defaults: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
"Internal Room Id": "Internal Room ID",
matrixDesc1: "You can find the internal room ID by looking in the advanced section of the room settings in your Matrix client. It should look like !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "It is highly recommended you create a new user and do not use your own Matrix user's access token as it will allow full access to your account and all the rooms you joined. Instead, create a new user and only invite it to the room that you want to receive the notification in. You can get the access token by running {0}",
Method: "Метод",
Body: "Тело",
Headers: "Заголовки",
PushUrl: "URL пуша",
HeadersInvalidFormat: "Заголовки запроса некорректны JSON: ",
BodyInvalidFormat: "Тело запроса некорректно JSON: ",
"Monitor History": "История мониторов",
clearDataOlderThan: "Сохранять историю мониторов в течение {0} дней.",
PasswordsDoNotMatch: "Пароли не совпадают.",
records: "записей",
"One record": "Одна запись",
"Showing {from} to {to} of {count} records": "Показывается от {from} до {to} из {count} записей",
steamApiKeyDescription: "Для мониторинга игрового сервера Steam вам необходим Web-API ключ Steam. Зарегистрировать его можно здесь: ",
}; };

View file

@ -116,39 +116,39 @@ export default {
Events: "Olaylar", Events: "Olaylar",
Heartbeats: "Sağlık Durumları", Heartbeats: "Sağlık Durumları",
"Auto Get": "Otomatik Al", "Auto Get": "Otomatik Al",
retryCheckEverySecond: "Retry every {0} seconds.", retryCheckEverySecond: "{0} Saniyede bir dene.",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "Bu bildirim her yeni serviste aktif olacaktır. Bildirimi servisler için ayrı ayrı deaktive edebilirsiniz. ",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.", importHandleDescription: "Aynı isimdeki bütün servisleri ve bildirimleri atlamak için 'Var olanı atla' seçiniz. 'Üzerine yaz' var olan bütün servisleri ve bildirimleri silecektir. ",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.", confirmImportMsg: "Yedeği içeri aktarmak istediğinize emin misiniz? Lütfen doğru içeri aktarma seçeneğini seçtiğinizden emin olunuz. ",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", twoFAVerifyLabel: "Lütfen tokeni yazarak 2FA doğrulamanın çalıştığından emin olunuz.",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.", tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?", confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?", confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
"Heartbeat Retry Interval": "Heartbeat Retry Interval", "Heartbeat Retry Interval": "Sağlık Dırımları Tekrar Deneme Sıklığı",
"Import Backup": "Import Backup", "Import Backup": "Yedeği içe aktar",
"Export Backup": "Export Backup", "Export Backup": "Yedeği dışa aktar",
Export: "Export", Export: "Dışa aktar",
Import: "Import", Import: "İçe aktar",
"Default enabled": "Default enabled", "Default enabled": "Varsayılan etkinleştirilmiş",
"Apply on all existing monitors": "Apply on all existing monitors", "Apply on all existing monitors": "Var olan bütün servislere uygula",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "Bütün servisleri ve bildirimleri JSON dosyasına yedekleyebilirsiniz.",
backupDescription2: "PS: History and event data is not included.", backupDescription2: "Not: Geçmiş ve etkinlik verileri içinde değildir.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", backupDescription3: "Dışa aktarma dosyasında bildirim tokeni gibi hassas veriler bulunur, dikkatli bir şekilde saklayınız.",
alertNoFile: "Please select a file to import.", alertNoFile: "İçeri aktarmak için bir dosya seçiniz.",
alertWrongFileType: "Please select a JSON file.", alertWrongFileType: "Lütfen bir JSON dosyası seçiniz.",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Bütün istatistikleri temizle",
"Skip existing": "Skip existing", "Skip existing": "Var olanı atla",
Overwrite: "Overwrite", Overwrite: "Üzerine yaz",
Options: "Options", Options: "Seçenekler",
"Keep both": "Keep both", "Keep both": "İkisini sakla",
"Verify Token": "Verify Token", "Verify Token": "Tokeni doğrula",
"Setup 2FA": "Setup 2FA", "Setup 2FA": "2FA Kur",
"Enable 2FA": "Enable 2FA", "Enable 2FA": "2FA Etkinleştir",
"Disable 2FA": "Disable 2FA", "Disable 2FA": "2FA Devre dışı bırak",
"2FA Settings": "2FA Settings", "2FA Settings": "2FA Ayarları",
"Two Factor Authentication": "Two Factor Authentication", "Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
Active: "Active", Active: "Aktif",
Inactive: "Inactive", Inactive: "İnaktif",
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
Tags: "Tags", Tags: "Tags",

View file

@ -22,7 +22,8 @@ export default {
Appearance: "外观设置", Appearance: "外观设置",
Theme: "主题", Theme: "主题",
General: "基本设置", General: "基本设置",
Version: "Version", "Primary Base URL": "站点地址URL",
Version: "版本",
"Check Update On GitHub": "检查更新", "Check Update On GitHub": "检查更新",
List: "列表", List: "列表",
Add: "添加", Add: "添加",
@ -43,7 +44,7 @@ export default {
Delete: "删除", Delete: "删除",
Current: "当前", Current: "当前",
Uptime: "可用率", Uptime: "可用率",
"Cert Exp.": "证书期", "Cert Exp.": "证书有效期",
days: "天", days: "天",
day: "天", day: "天",
"-day": " 天", "-day": " 天",
@ -63,6 +64,9 @@ export default {
"Upside Down Mode": "反向监控", "Upside Down Mode": "反向监控",
"Max. Redirects": "重定向次数", "Max. Redirects": "重定向次数",
"Accepted Status Codes": "有效状态码", "Accepted Status Codes": "有效状态码",
"Push URL": "推送链接",
needPushEvery: "你需要每 {0} 秒调用一次。",
pushOptionalParams: "可选参数:{0}",
Save: "保存", Save: "保存",
Notifications: "消息通知", Notifications: "消息通知",
"Not available, please setup.": "无可用通道,请先设置", "Not available, please setup.": "无可用通道,请先设置",
@ -103,10 +107,10 @@ export default {
"Certificate Info": "证书信息", "Certificate Info": "证书信息",
"Resolver Server": "解析服务器", "Resolver Server": "解析服务器",
"Resource Record Type": "资源记录类型", "Resource Record Type": "资源记录类型",
"Last Result": "Last Result", "Last Result": "最后结果",
"Create your admin account": "创建管理员账号", "Create your admin account": "创建管理员账号",
"Repeat Password": "重复密码", "Repeat Password": "重复密码",
respTime: "Resp. Time (ms)", respTime: "响应时间(毫秒)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
Create: "创建", Create: "创建",
clearEventsMsg: "确定要删除此监控项的所有事件吗?", clearEventsMsg: "确定要删除此监控项的所有事件吗?",
@ -126,21 +130,21 @@ export default {
backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!", backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!",
alertNoFile: "请选择一个文件导入", alertNoFile: "请选择一个文件导入",
alertWrongFileType: "请选择一个 JSON 格式的文件", alertWrongFileType: "请选择一个 JSON 格式的文件",
twoFAVerifyLabel: "请输入Token以验证2FA(二次验证)是否正常工作", twoFAVerifyLabel: "请输入Token以验证 2FA二次验证是否正常工作",
tokenValidSettingsMsg: "Token有效您现在可以保存2FA(二次验证)设置", tokenValidSettingsMsg: "Token有效您现在可以保存 2FA二次验证设置",
confirmEnableTwoFAMsg: "确定要启用2FA(二次验证)吗?", confirmEnableTwoFAMsg: "确定要启用 2FA二次验证吗?",
confirmDisableTwoFAMsg: "确定要禁用2FA(二次验证)吗?", confirmDisableTwoFAMsg: "确定要禁用 2FA二次验证吗?",
"Apply on all existing monitors": "应用到所有监控项", "Apply on all existing monitors": "应用到所有监控项",
"Verify Token": "验证Token", "Verify Token": "验证 Token",
"Setup 2FA": "设置2FA", "Setup 2FA": "设置 2FA",
"Enable 2FA": "启用2FA", "Enable 2FA": "启用 2FA",
"Disable 2FA": "禁用2FA", "Disable 2FA": "禁用 2FA",
"2FA Settings": "2FA设置", "2FA Settings": "2FA 设置",
"Two Factor Authentication": "双因素认证", "Two Factor Authentication": "双因素认证",
Active: "效", Active: "效",
Inactive: "效", Inactive: "未生效",
Token: "Token", Token: "Token",
"Show URI": "显示URI", "Show URI": "显示链接",
"Clear all statistics": "清除所有统计数据", "Clear all statistics": "清除所有统计数据",
retryCheckEverySecond: "重试间隔 {0} 秒", retryCheckEverySecond: "重试间隔 {0} 秒",
importHandleDescription: "如果想跳过同名的监控项或通知,请选择“跳过”;“覆盖”将删除所有现有的监控项和通知。", importHandleDescription: "如果想跳过同名的监控项或通知,请选择“跳过”;“覆盖”将删除所有现有的监控项和通知。",
@ -167,7 +171,7 @@ export default {
Purple: "紫色", Purple: "紫色",
Pink: "粉色", Pink: "粉色",
"Search...": "搜索...", "Search...": "搜索...",
"Avg. Ping": "平均Ping", "Avg. Ping": "平均 Ping",
"Avg. Response": "平均响应", "Avg. Response": "平均响应",
"Entry Page": "入口页面", "Entry Page": "入口页面",
statusPageNothing: "这里什么也没有,请添加一个分组或一个监控项。", statusPageNothing: "这里什么也没有,请添加一个分组或一个监控项。",
@ -182,7 +186,7 @@ export default {
"Status Page": "状态页", "Status Page": "状态页",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "Email (SMTP)", smtp: "电子邮件SMTP",
discord: "Discord", discord: "Discord",
teams: "Microsoft Teams", teams: "Microsoft Teams",
signal: "Signal", signal: "Signal",
@ -194,9 +198,97 @@ export default {
octopush: "Octopush", octopush: "Octopush",
promosms: "PromoSMS", promosms: "PromoSMS",
lunasea: "LunaSea", lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)", apprise: "Apprise (支持50+种通知服务)",
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"Feishu WebHookUrl": "飞书 WebHook 地址", "Feishu WebHookUrl": "飞书 WebHook 地址",
defaultNotificationName: "{notification} 通知({number}",
here: "这里",
Required: "必填",
"Bot Token": "Bot Token",
wayToGetTelegramToken: "你可以从 {0} 获取 Token。",
"Chat ID": "Chat ID",
supportTelegramChatID: "支持对话/群组/频道的 ID",
wayToGetTelegramChatID: "你可以发送一条消息给你的机器人然后到下面的链接来查看你的 chat_id",
"YOUR BOT TOKEN HERE": "这里替换成你的 BOT TOKEN",
chatIDNotFound: "没有找到 Chat ID请先给你的机器人发送一条消息。",
"Post URL": "目标链接",
"Content Type": "Content Type",
webhookJsonDesc: "{0} 适合现代的服务,比如 express.js",
webhookFormDataDesc: "{multipart} 适合PHP解码使用 {decodeFunction}",
secureOptionNone: "无 / STARTTLS25587",
secureOptionTLS: "TLS465",
"Ignore TLS Error": "忽略 TLS 错误",
"From Email": "发信人",
"To Email": "收信人",
smtpCC: "抄送",
smtpBCC: "密送",
"Discord Webhook URL": "Discord Webhook 链接",
wayToGetDiscordURL: "获取方式:服务器设置 -> 整合 -> 创建 Webhook",
"Bot Display Name": "机器人显示名称",
"Prefix Custom Message": "自定义消息前缀",
"Hello @everyone is...": "{'@'}所有人,……",
"Webhook URL": "Webhook 链接",
wayToGetTeamsURL: "你可以在 {0} 获取 Webhook 链接。",
Number: "号码",
Recipients: "收件人",
needSignalAPI: "你需要有一个带 REST API 的 Signal 客户端。",
wayToCheckSignalURL: "你可以通过下面的链接来了解如何设置:",
signalImportant: "重要:你不能混合设定收件人的分组和号码!",
"Application Token": "Application Token",
"Server URL": "服务器链接",
Priority: "优先级",
"Icon Emoji": "Emoji 图标",
"Channel Name": "频道名称",
"Uptime Kuma URL": "Uptime Kuma 链接",
aboutWebhooks: "关于 Webhook 的更多信息:{0}",
aboutChannelName: "如果你想绕过 Webhook 设定的频道,请在设定 {0} 的频道名称字段为你想要的频道。例:#other-channel",
aboutKumaURL: "如果保留 Uptime Kuma 链接为空,将会默认指向项目的 Github 页面。",
emojiCheatSheet: "Emoji 参考表:{0}",
"User Key": "User Key",
Device: "设备",
"Message Title": "消息标题",
"Notification Sound": "通知铃声",
"More info on:": "更多信息:{0}",
pushoverDesc1: "紧急优先级2会在一小时内每30秒重试一次。",
pushoverDesc2: "如果你想发送通知给不同的设备,请填写“设备”字段。",
"SMS Type": "短信类型",
octopushTypePremium: "Premium快 - 推荐用于警报)",
octopushTypeLowCost: "Low Cost慢 - 有时会被运营商屏蔽)",
"Check octopush prices": "查看 Octopush 的价格 {0}。",
octopushPhoneNumber: "电话号码(国际格式,例:+33612345678",
octopushSMSSender: "短信发送名称3-11位大小写字母、数字和空格a-zA-Z0-9",
"LunaSea Device ID": "LunaSea 设备 ID",
"Apprise URL": "Apprise 链接",
"Example:": "例:{0}",
"Read more:": "了解更多:{0}",
"Status:": "状态:{0}",
"Read more": "了解更多",
appriseInstalled: "Apprise 已安装",
appriseNotInstalled: "Apprise 未安装。{0}",
"Access Token": "Access Token",
"Channel access token": "频道 access token",
"Line Developers Console": "Line Developers Console",
lineDevConsoleTo: "Line Developers Console - {0}",
"Basic Settings": "Basic Settings",
"User ID": "User ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "首先访问 {0}创建一个提供者和频道Messaging API然后你就可以从上面提到的地方获取频道的 access token 和用户 ID。",
"Icon URL": "图标链接",
aboutIconURL: "你可以在“Icon URL”中提供一个图片地址来覆盖默认的资料图片。如果设置了 Emoji 图标此字段会被忽略。",
aboutMattermostChannelName: "如果你想覆盖 Webhook 设定的频道,请在“频道名称”字段为你想要的频道。这需要在 Mattermost 的 Webhook 设定中启用。例:#other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - 便宜但是慢,并且容易超负荷。仅限波兰地区的收件人。",
promosmsTypeFlash: "SMS FLASH - 消息会自动显示在收件人设备上。仅限波兰地区的收件人。",
promosmsTypeFull: "SMS FULL - 高等级,你可以使用你自己的发件人名称(你需要先注册一个). 对于警报来说更可靠。",
promosmsTypeSpeed: "SMS SPEED - 最高优先级。非常快速可靠,但更贵(越两倍 SMS FULL 等级的价格)。",
promosmsPhoneNumber: "电话号码(波兰地区收件人可以不填区号)",
promosmsSMSSender: "短信发件人名称已注册的名称或以下默认值之一InfoSMSSMS InfoMaxSMSINFOSMS",
checkPrice: "查看 {0} 的价格:",
octopushLegacyHint: "你是否在使用旧版本的 Octopush2011-2020",
matrixHomeserverURL: "服务器链接(开头带 http(s):// 和可能的需要的端口号)",
"Internal Room Id": "Internal Room Id",
matrixDesc1: "你可以在 Matrix 客户端房间设置的高级选项找到 Internal Room Id。格式类似于 !QMdRCpUIfLwsfjxye6:home.server。",
matrixDesc2: "请不要使用你自己的 Access Token这将开放你所有的账户权限和你加入的房间权限。你可以创建一个新的用户并邀请它至你允许的的房间中。你可以运行以下命令来获取 Access Token{0}",
}; };

View file

@ -347,7 +347,7 @@ export default {
let result = {}; let result = {};
let unknown = { let unknown = {
text: "Unknown", text: this.$t("Unknown"),
color: "secondary", color: "secondary",
}; };
@ -358,17 +358,17 @@ export default {
result[monitorID] = unknown; result[monitorID] = unknown;
} else if (lastHeartBeat.status === 1) { } else if (lastHeartBeat.status === 1) {
result[monitorID] = { result[monitorID] = {
text: "Up", text: this.$t("Up"),
color: "primary", color: "primary",
}; };
} else if (lastHeartBeat.status === 0) { } else if (lastHeartBeat.status === 0) {
result[monitorID] = { result[monitorID] = {
text: "Down", text: this.$t("Down"),
color: "danger", color: "danger",
}; };
} else if (lastHeartBeat.status === 2) { } else if (lastHeartBeat.status === 2) {
result[monitorID] = { result[monitorID] = {
text: "Pending", text: this.$t("Pending"),
color: "warning", color: "warning",
}; };
} else { } else {

View file

@ -29,6 +29,9 @@
<option value="push"> <option value="push">
Push Push
</option> </option>
<option value="steam">
Steam Game Server
</option>
</select> </select>
</div> </div>
@ -46,11 +49,11 @@
<!-- Push URL --> <!-- Push URL -->
<div v-if="monitor.type === 'push' " class="my-3"> <div v-if="monitor.type === 'push' " class="my-3">
<label for="push-url" class="form-label">{{ $t("Push URL") }}</label> <label for="push-url" class="form-label">{{ $t("PushUrl") }}</label>
<CopyableInput id="push-url" v-model="pushURL" type="url" disabled="disabled" /> <CopyableInput id="push-url" v-model="pushURL" type="url" disabled="disabled" />
<div class="form-text"> <div class="form-text">
You should call this url every {{ monitor.interval }} seconds.<br /> {{ $t("needPushEvery", [monitor.interval]) }}<br />
Optional parameters: msg, ping {{ $t("pushOptionalParams", ["msg, ping"]) }}
</div> </div>
</div> </div>
@ -63,18 +66,21 @@
</div> </div>
</div> </div>
<!-- TCP Port / Ping / DNS only --> <!-- Hostname -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' " class="my-3"> <!-- TCP Port / Ping / DNS / Steam only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label> <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required>
</div> </div>
<!-- For TCP Port Type --> <!-- Port -->
<div v-if="monitor.type === 'port' " class="my-3"> <!-- For TCP Port / Steam Type -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam'" class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label> <label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1"> <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
<!-- DNS Resolver Server -->
<!-- For DNS Type --> <!-- For DNS Type -->
<template v-if="monitor.type === 'dns'"> <template v-if="monitor.type === 'dns'">
<div class="my-3"> <div class="my-3">
@ -140,7 +146,7 @@
</label> </label>
</div> </div>
<div v-if="monitor.type !== 'push'" class="my-3 form-check"> <div class="my-3 form-check">
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox"> <input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
<label class="form-check-label" for="upside-down"> <label class="form-check-label" for="upside-down">
{{ $t("Upside Down Mode") }} {{ $t("Upside Down Mode") }}
@ -195,6 +201,7 @@
<div class="col-md-6"> <div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" /> <div v-if="$root.isMobile" class="mt-3" />
<!-- Notifications -->
<h2 class="mb-2">{{ $t("Notifications") }}</h2> <h2 class="mb-2">{{ $t("Notifications") }}</h2>
<p v-if="$root.notificationList.length === 0"> <p v-if="$root.notificationList.length === 0">
{{ $t("Not available, please setup.") }} {{ $t("Not available, please setup.") }}
@ -214,6 +221,51 @@
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()"> <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
{{ $t("Setup Notification") }} {{ $t("Setup Notification") }}
</button> </button>
<!-- HTTP Options -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
<h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
<!-- Method -->
<div class="my-3">
<label for="method" class="form-label">{{ $t("Method") }}</label>
<select id="method" v-model="monitor.method" class="form-select">
<option value="GET">
GET
</option>
<option value="POST">
POST
</option>
<option value="PUT">
PUT
</option>
<option value="PATCH">
PATCH
</option>
<option value="DELETE">
DELETE
</option>
<option value="HEAD">
HEAD
</option>
<option value="OPTIONS">
OPTIONS
</option>
</select>
</div>
<!-- Body -->
<div class="my-3">
<label for="body" class="form-label">{{ $t("Body") }}</label>
<textarea id="body" v-model="monitor.body" class="form-control" :placeholder="bodyPlaceholder"></textarea>
</div>
<!-- Headers -->
<div class="my-3">
<label for="headers" class="form-label">{{ $t("Headers") }}</label>
<textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea>
</div>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -285,6 +337,14 @@ export default {
pushURL() { pushURL() {
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?msg=OK&ping="; return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?msg=OK&ping=";
},
bodyPlaceholder() {
return "{\n\t\"id\": 124357,\n\t\"username\": \"admin\",\n\t\"password\": \"myAdminPassword\"\n}";
},
headersPlaceholder() {
return "{\n\t\"Authorization\": \"Bearer abc123\",\n\t\"Content-Type\": \"application/json\"\n}";
} }
}, },
@ -295,7 +355,7 @@ export default {
}, },
"monitor.interval"(value, oldValue) { "monitor.interval"(value, oldValue) {
// Link interval and retryInerval if they are the same value. // Link interval and retryInterval if they are the same value.
if (this.monitor.retryInterval === oldValue) { if (this.monitor.retryInterval === oldValue) {
this.monitor.retryInterval = value; this.monitor.retryInterval = value;
} }
@ -349,6 +409,7 @@ export default {
type: "http", type: "http",
name: "", name: "",
url: "https://", url: "https://",
method: "GET",
interval: 60, interval: 60,
retryInterval: this.interval, retryInterval: this.interval,
maxretries: 0, maxretries: 0,
@ -383,9 +444,43 @@ export default {
}, },
isInputValid() {
if (this.monitor.body) {
try {
JSON.parse(this.monitor.body);
} catch (err) {
toast.error(this.$t("BodyInvalidFormat") + err.message);
return false;
}
}
if (this.monitor.headers) {
try {
JSON.parse(this.monitor.headers);
} catch (err) {
toast.error(this.$t("HeadersInvalidFormat") + err.message);
return false;
}
}
return true;
},
async submit() { async submit() {
this.processing = true; this.processing = true;
if (!this.isInputValid()) {
this.processing = false;
return;
}
// Beautify the JSON format
if (this.monitor.body) {
this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4);
}
if (this.monitor.headers) {
this.monitor.headers = JSON.stringify(JSON.parse(this.monitor.headers), null, 4);
}
if (this.isAdd) { if (this.isAdd) {
this.$root.add(this.monitor, async (res) => { this.$root.add(this.monitor, async (res) => {
@ -422,8 +517,12 @@ export default {
}; };
</script> </script>
<style scoped> <style lang="scss" scoped>
.shadow-box { .shadow-box {
padding: 20px; padding: 20px;
} }
textarea {
min-height: 200px;
}
</style> </style>

View file

@ -108,7 +108,7 @@
<!-- Primary Base URL --> <!-- Primary Base URL -->
<div class="mb-4"> <div class="mb-4">
<label class="form-label" for="primaryBaseURL">Primary Base URL</label> <label class="form-label" for="primaryBaseURL">{{ $t("Primary Base URL") }}</label>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input id="primaryBaseURL" v-model="settings.primaryBaseURL" class="form-control" name="primaryBaseURL" placeholder="https://" pattern="https?://.+"> <input id="primaryBaseURL" v-model="settings.primaryBaseURL" class="form-control" name="primaryBaseURL" placeholder="https://" pattern="https?://.+">
@ -119,6 +119,25 @@
</div> </div>
</div> </div>
<!-- Steam API Key -->
<div class="mb-4">
<label class="form-label" for="steamAPIKey">{{ $t("Steam API Key") }}</label>
<input id="steamAPIKey" v-model="settings.steamAPIKey" class="form-control" name="steamAPIKey">
<div class="form-text">
{{ $t("steamApiKeyDescription") }}<a href="https://steamcommunity.com/dev" target="_blank">https://steamcommunity.com/dev</a>
</div>
</div>
<!-- Monitor History -->
<div class="mb-4">
<h4 class="mt-4">{{ $t("Monitor History") }}</h4>
<div class="mt-2">
<label for="keepDataPeriodDays" class="form-label">{{ $t("clearDataOlderThan", [ settings.keepDataPeriodDays ]) }}</label>
<input id="keepDataPeriodDays" v-model="settings.keepDataPeriodDays" type="number" class="form-control" required min="1" step="1">
</div>
</div>
<!-- Save Button -->
<div> <div>
<button class="btn btn-primary" type="submit"> <button class="btn btn-primary" type="submit">
{{ $t("Save") }} {{ $t("Save") }}
@ -334,9 +353,9 @@
</template> </template>
<template v-else-if="$i18n.locale === 'id-ID' "> <template v-else-if="$i18n.locale === 'id-ID' ">
<p> Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>? </p> <p>Apakah Anda yakin ingin <strong>menonaktifkan autentikasi</strong>?</p>
<p> Ini untuk <strong>mereka yang memiliki otentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare. </p> <p>Ini untuk <strong>mereka yang memiliki autentikasi pihak ketiga</strong> diletakkan di depan Uptime Kuma, misalnya akses Cloudflare.</p>
<p> Gunakan dengan hati-hati. </p> <p>Gunakan dengan hati-hati.</p>
</template> </template>
<template v-else-if="$i18n.locale === 'ru-RU' "> <template v-else-if="$i18n.locale === 'ru-RU' ">
@ -477,6 +496,10 @@ export default {
this.settings.entryPage = "dashboard"; this.settings.entryPage = "dashboard";
} }
if (this.settings.keepDataPeriodDays === undefined) {
this.settings.keepDataPeriodDays = 180;
}
this.loaded = true; this.loaded = true;
}); });
}, },

View file

@ -75,7 +75,7 @@ export default {
this.processing = true; this.processing = true;
if (this.password !== this.repeatPassword) { if (this.password !== this.repeatPassword) {
toast.error("Repeat password do not match."); toast.error(this.$t("PasswordsDoNotMatch"));
this.processing = false; this.processing = false;
return; return;
} }

View file

@ -1,118 +1,167 @@
"use strict"; "use strict";
// Common Util for frontend and backend // Common Util for frontend and backend
// //
// DOT NOT MODIFY util.js! // DOT NOT MODIFY util.js!
// Need to run "tsc" to compile if there are any changes. // Need to run "tsc" to compile if there are any changes.
// //
// Backend uses the compiled file util.js // Backend uses the compiled file util.js
// Frontend uses util.ts // Frontend uses util.ts
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.getMonitorRelativeURL = exports.genSecret = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
const _dayjs = require("dayjs"); const _dayjs = require("dayjs");
const dayjs = _dayjs; const dayjs = _dayjs;
exports.isDev = process.env.NODE_ENV === "development"; exports.isDev = process.env.NODE_ENV === "development";
exports.appName = "Uptime Kuma"; exports.appName = "Uptime Kuma";
exports.DOWN = 0; exports.DOWN = 0;
exports.UP = 1; exports.UP = 1;
exports.PENDING = 2; exports.PENDING = 2;
exports.STATUS_PAGE_ALL_DOWN = 0; exports.STATUS_PAGE_ALL_DOWN = 0;
exports.STATUS_PAGE_ALL_UP = 1; exports.STATUS_PAGE_ALL_UP = 1;
exports.STATUS_PAGE_PARTIAL_DOWN = 2; exports.STATUS_PAGE_PARTIAL_DOWN = 2;
function flipStatus(s) { function flipStatus(s) {
if (s === exports.UP) { if (s === exports.UP) {
return exports.DOWN; return exports.DOWN;
} }
if (s === exports.DOWN) { if (s === exports.DOWN) {
return exports.UP; return exports.UP;
} }
return s; return s;
} }
exports.flipStatus = flipStatus; exports.flipStatus = flipStatus;
function sleep(ms) { function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
exports.sleep = sleep; exports.sleep = sleep;
/** /**
* PHP's ucfirst * PHP's ucfirst
* @param str * @param str
*/ */
function ucfirst(str) { function ucfirst(str) {
if (!str) { if (!str) {
return str; return str;
} }
const firstLetter = str.substr(0, 1); const firstLetter = str.substr(0, 1);
return firstLetter.toUpperCase() + str.substr(1); return firstLetter.toUpperCase() + str.substr(1);
} }
exports.ucfirst = ucfirst; exports.ucfirst = ucfirst;
function debug(msg) { function debug(msg) {
if (exports.isDev) { if (exports.isDev) {
console.log(msg); console.log(msg);
} }
} }
exports.debug = debug; exports.debug = debug;
function polyfill() { function polyfill() {
/** /**
* String.prototype.replaceAll() polyfill * String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/ * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi * @author Chris Ferdinandi
* @license MIT * @license MIT
*/ */
if (!String.prototype.replaceAll) { if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str, newStr) { String.prototype.replaceAll = function (str, newStr) {
// If a regex pattern // If a regex pattern
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") { if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
return this.replace(str, newStr); return this.replace(str, newStr);
} }
// If a string // If a string
return this.replace(new RegExp(str, "g"), newStr); return this.replace(new RegExp(str, "g"), newStr);
}; };
} }
} }
exports.polyfill = polyfill; exports.polyfill = polyfill;
class TimeLogger { class TimeLogger {
constructor() { constructor() {
this.startTime = dayjs().valueOf(); this.startTime = dayjs().valueOf();
} }
print(name) { print(name) {
if (exports.isDev && process.env.TIMELOGGER === "1") { if (exports.isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms"); console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
} }
} }
} }
exports.TimeLogger = TimeLogger; exports.TimeLogger = TimeLogger;
/** /**
* Returns a random number between min (inclusive) and max (exclusive) * Returns a random number between min (inclusive) and max (exclusive)
*/ */
function getRandomArbitrary(min, max) { function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min; return Math.random() * (max - min) + min;
} }
exports.getRandomArbitrary = getRandomArbitrary; exports.getRandomArbitrary = getRandomArbitrary;
/** /**
* From: https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range * From: https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range
* *
* Returns a random integer between min (inclusive) and max (inclusive). * Returns a random integer between min (inclusive) and max (inclusive).
* The value is no lower than min (or the next integer greater than min * The value is no lower than min (or the next integer greater than min
* if min isn't an integer) and no greater than max (or the next integer * if min isn't an integer) and no greater than max (or the next integer
* lower than max if max isn't an integer). * lower than max if max isn't an integer).
* Using Math.round() will give you a non-uniform distribution! * Using Math.round() will give you a non-uniform distribution!
*/ */
function getRandomInt(min, max) { function getRandomInt(min, max) {
min = Math.ceil(min); min = Math.ceil(min);
max = Math.floor(max); max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min; return Math.floor(Math.random() * (max - min + 1)) + min;
} }
exports.getRandomInt = getRandomInt; exports.getRandomInt = getRandomInt;
function genSecret(length = 64) { /**
let secret = ""; * Returns either the NodeJS crypto.randomBytes() function or its
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; * browser equivalent implemented via window.crypto.getRandomValues()
let charsLength = chars.length; */
for (let i = 0; i < length; i++) { let getRandomBytes = ((typeof window !== 'undefined' && window.crypto)
secret += chars.charAt(Math.floor(Math.random() * charsLength)); // Browsers
} ? function () {
return secret; return (numBytes) => {
} let randomBytes = new Uint8Array(numBytes);
exports.genSecret = genSecret; for (let i = 0; i < numBytes; i += 65536) {
function getMonitorRelativeURL(id) { window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
return "/dashboard/" + id; }
} return randomBytes;
exports.getMonitorRelativeURL = getMonitorRelativeURL; };
}
// Node
: function () {
return require("crypto").randomBytes;
})();
function getCryptoRandomInt(min, max) {
// synchronous version of: https://github.com/joepie91/node-random-number-csprng
const range = max - min;
if (range >= Math.pow(2, 32))
console.log("Warning! Range is too large.");
let tmpRange = range;
let bitsNeeded = 0;
let bytesNeeded = 0;
let mask = 1;
while (tmpRange > 0) {
if (bitsNeeded % 8 === 0)
bytesNeeded += 1;
bitsNeeded += 1;
mask = mask << 1 | 1;
tmpRange = tmpRange >>> 1;
}
const randomBytes = getRandomBytes(bytesNeeded);
let randomValue = 0;
for (let i = 0; i < bytesNeeded; i++) {
randomValue |= randomBytes[i] << 8 * i;
}
randomValue = randomValue & mask;
if (randomValue <= range) {
return min + randomValue;
}
else {
return getCryptoRandomInt(min, max);
}
}
exports.getCryptoRandomInt = getCryptoRandomInt;
function genSecret(length = 64) {
let secret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charsLength = chars.length;
for (let i = 0; i < length; i++) {
secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1));
}
return secret;
}
exports.genSecret = genSecret;
function getMonitorRelativeURL(id) {
return "/dashboard/" + id;
}
exports.getMonitorRelativeURL = getMonitorRelativeURL;

View file

@ -114,12 +114,72 @@ export function getRandomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min; return Math.floor(Math.random() * (max - min + 1)) + min;
} }
/**
* Returns either the NodeJS crypto.randomBytes() function or its
* browser equivalent implemented via window.crypto.getRandomValues()
*/
let getRandomBytes = (
(typeof window !== 'undefined' && window.crypto)
// Browsers
? function () {
return (numBytes: number) => {
let randomBytes = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i += 65536) {
window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
}
return randomBytes;
};
}
// Node
: function() {
return require("crypto").randomBytes;
}
)();
export function getCryptoRandomInt(min: number, max: number):number {
// synchronous version of: https://github.com/joepie91/node-random-number-csprng
const range = max - min
if (range >= Math.pow(2, 32))
console.log("Warning! Range is too large.")
let tmpRange = range
let bitsNeeded = 0
let bytesNeeded = 0
let mask = 1
while (tmpRange > 0) {
if (bitsNeeded % 8 === 0) bytesNeeded += 1
bitsNeeded += 1
mask = mask << 1 | 1
tmpRange = tmpRange >>> 1
}
const randomBytes = getRandomBytes(bytesNeeded)
let randomValue = 0
for (let i = 0; i < bytesNeeded; i++) {
randomValue |= randomBytes[i] << 8 * i
}
randomValue = randomValue & mask;
if (randomValue <= range) {
return min + randomValue
} else {
return getCryptoRandomInt(min, max)
}
}
export function genSecret(length = 64) { export function genSecret(length = 64) {
let secret = ""; let secret = "";
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let charsLength = chars.length; const charsLength = chars.length;
for ( let i = 0; i < length; i++ ) { for ( let i = 0; i < length; i++ ) {
secret += chars.charAt(Math.floor(Math.random() * charsLength)); secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1));
} }
return secret; return secret;
} }

View file

@ -1,10 +1,44 @@
beforeAll(() => { const { genSecret, sleep } = require("../src/util");
}); describe("Test genSecret", () => {
describe("", () => { beforeAll(() => {
it("should ", () => {
}); });
it("should be correct length", () => {
let secret = genSecret(-1);
expect(secret).toEqual("");
secret = genSecret(0);
expect(secret).toEqual("");
secret = genSecret(1);
expect(secret.length).toEqual(1);
secret = genSecret(2);
expect(secret.length).toEqual(2);
secret = genSecret(64);
expect(secret.length).toEqual(64);
secret = genSecret(9000);
expect(secret.length).toEqual(9000);
secret = genSecret(90000);
expect(secret.length).toEqual(90000);
});
it("should contain first and last possible chars", () => {
let secret = genSecret(90000);
expect(secret).toContain("A");
expect(secret).toContain("9");
});
});
describe("Test reset-password", () => {
it("should able to run", async () => {
await require("../extra/reset-password").main();
}, 120000);
}); });

View file

@ -1,6 +1,7 @@
{ {
"compileOnSave": true, "compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"newLine": "LF",
"target": "es2018", "target": "es2018",
"module": "commonjs", "module": "commonjs",
"lib": [ "lib": [