Browse Source

Merge remote-tracking branch 'louislam/master' into feature/monitor-checks

bertyhell/feature/monitor-checks
Bert Verhelst 3 years ago
parent
commit
fc261825a9
  1. 2
      .dockerignore
  2. 1
      .eslintrc.js
  3. 1
      .github/ISSUE_TEMPLATE/ask-for-help.md
  4. 8
      .github/ISSUE_TEMPLATE/bug_report.md
  5. 32
      CONTRIBUTING.md
  6. 15
      README.md
  7. 3
      SECURITY.md
  8. 7
      db/patch-add-retry-interval-monitor.sql
  9. 19
      db/patch10.sql
  10. 14
      dockerfile
  11. 10
      dockerfile-alpine
  12. 21
      extra/entrypoint.sh
  13. 26
      extra/update-language-files/index.js
  14. 1
      index.html
  15. 23
      kubernetes/README.md
  16. 8337
      package-lock.json
  17. 33
      package.json
  18. BIN
      public/icon-192x192.png
  19. BIN
      public/icon-512x512.png
  20. 19
      public/manifest.json
  21. 3
      server/database.js
  22. 17
      server/model/monitor.js
  23. 13
      server/model/tag.js
  24. 10
      server/notification-providers/discord.js
  25. 124
      server/notification-providers/teams.js
  26. 2
      server/notification.js
  27. 224
      server/server.js
  28. 85
      src/components/MonitorList.vue
  29. 10
      src/components/NotificationDialog.vue
  30. 73
      src/components/Tag.vue
  31. 405
      src/components/TagsManager.vue
  32. 2
      src/components/notifications/SMTP.vue
  33. 29
      src/components/notifications/Teams.vue
  34. 48
      src/i18n.js
  35. 39
      src/icon.js
  36. 16
      src/languages/README.md
  37. 29
      src/languages/da-DK.js
  38. 31
      src/languages/de-DE.js
  39. 31
      src/languages/en.js
  40. 31
      src/languages/es-ES.js
  41. 29
      src/languages/et-EE.js
  42. 29
      src/languages/fr-FR.js
  43. 78
      src/languages/it-IT.js
  44. 29
      src/languages/ja.js
  45. 29
      src/languages/ko-KR.js
  46. 89
      src/languages/nl-NL.js
  47. 93
      src/languages/pl.js
  48. 101
      src/languages/ru-RU.js
  49. 29
      src/languages/sr-latn.js
  50. 29
      src/languages/sr.js
  51. 29
      src/languages/sv-SE.js
  52. 171
      src/languages/tr-TR.js
  53. 30
      src/languages/zh-CN.js
  54. 30
      src/languages/zh-HK.js
  55. 125
      src/main.js
  56. 8
      src/mixins/socket.js
  57. 38
      src/pages/Details.vue
  58. 59
      src/pages/EditMonitor.vue
  59. 70
      src/pages/Settings.vue
  60. 71
      src/router.js
  61. 368
      src/util-frontend.js
  62. 10
      test/ubuntu-nodejs16.dockerfile

2
.dockerignore

@ -2,6 +2,8 @@
/dist /dist
/node_modules /node_modules
/data /data
/test
/kubernetes
/.do /.do
**/.dockerignore **/.dockerignore
**/.git **/.git

1
.eslintrc.js

@ -1,4 +1,5 @@
module.exports = { module.exports = {
root: true,
env: { env: {
browser: true, browser: true,
commonjs: true, commonjs: true,

1
.github/ISSUE_TEMPLATE/ask-for-help.md

@ -16,4 +16,3 @@ Docker Version:
Node.js Version (Without Docker only): Node.js Version (Without Docker only):
OS: OS:
Browser: Browser:

8
.github/ISSUE_TEMPLATE/bug_report.md

@ -15,6 +15,7 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@ -23,7 +24,6 @@ Steps to reproduce the behavior:
**Expected behavior** **Expected behavior**
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Info** **Info**
Uptime Kuma Version: Uptime Kuma Version:
Using Docker?: Yes/No Using Docker?: Yes/No
@ -32,13 +32,11 @@ Node.js Version (Without Docker only):
OS: OS:
Browser: Browser:
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Error Log** **Error Log**
It is easier for us to find out the problem. It is easier for us to find out the problem.
Docker: "docker logs <container id>" Docker: `docker logs <container id>`
PM2: "~/.pm2/logs/" (e.g. /home/ubuntu/.pm2/logs) PM2: `~/.pm2/logs/` (e.g. `/home/ubuntu/.pm2/logs`)

32
CONTRIBUTING.md

@ -52,8 +52,8 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
# Coding Styles # Coding Styles
- Follow .editorconfig - Follow `.editorconfig`
- Follow eslint - Follow ESLint
## Name convention ## Name convention
@ -62,9 +62,10 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
- CSS/SCSS: dash-type - CSS/SCSS: dash-type
# Tools # Tools
- Node.js >= 14 - Node.js >= 14
- Git - Git
- IDE that supports .editorconfig and eslint (I am using Intellji Idea) - IDE that supports EditorConfig and ESLint (I am using Intellji Idea)
- A SQLite tool (I am using SQLite Expert Personal) - A SQLite tool (I am using SQLite Expert Personal)
# Install dependencies # Install dependencies
@ -75,7 +76,7 @@ npm install --dev
For npm@7, you need --legacy-peer-deps For npm@7, you need --legacy-peer-deps
``` ```bash
npm install --legacy-peer-deps --dev npm install --legacy-peer-deps --dev
``` ```
@ -89,8 +90,7 @@ npm run start-server
node server/server.js node server/server.js
``` ```
It binds to 0.0.0.0:3001 by default. It binds to `0.0.0.0:3001` by default.
## Backend Details ## Backend Details
@ -100,7 +100,7 @@ express.js is just used for serving the frontend built files (index.html, .js an
# Frontend Dev # Frontend Dev
Start frontend dev server. Hot-reload enabled in this way. It binds to 0.0.0.0:3000. Start frontend dev server. Hot-reload enabled in this way. It binds to `0.0.0.0:3000` by default.
```bash ```bash
npm run dev npm run dev
@ -108,7 +108,7 @@ npm run dev
PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix. PS: You can ignore those scss warnings, those warnings are from Bootstrap that I cannot fix.
You can use Vue Devtool Chrome extension for debugging. You can use Vue.js devtools Chrome extension for debugging.
After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh: After the frontend server started. It cannot connect to the websocket server even you have started the server. You need to tell the frontend that is a dev env by running this in DevTool console and refresh:
@ -118,8 +118,7 @@ localStorage.dev = "dev";
So that the frontend will try to connect websocket server in 3001. So that the frontend will try to connect websocket server in 3001.
Alternately, you can specific NODE_ENV to "development". Alternately, you can specific `NODE_ENV` to "development".
## Build the frontend ## Build the frontend
@ -131,22 +130,17 @@ npm run build
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router. Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
The router in "src/main.js" The router is in `src/router.js`
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages. As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
The data and socket logic in "src/mixins/socket.js" The data and socket logic are in `src/mixins/socket.js`.
# Database Migration # Database Migration
1. create `patch{num}.sql` in `./db/` 1. Create `patch{num}.sql` in `./db/`
1. update `latestVersion` in `./server/database.js` 2. Update `latestVersion` in `./server/database.js`
# Unit Test # Unit Test
Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points. Yes, no unit test for now. I know it is very important, but at the same time my spare time is very limited. I want to implement my ideas first. I will go back to this in some points.

15
README.md

@ -20,7 +20,6 @@ It is a 5 minutes live demo, all data will be deleted after that. The server is
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!
## ⭐ Features ## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record. * Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record.
@ -65,7 +64,6 @@ If you need more options or need to browse via a reserve proxy, please read:
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
## 🆙 How to Update ## 🆙 How to Update
Please read: Please read:
@ -107,15 +105,15 @@ Telegram Notification Sample:
If you love this project, please consider giving me a ⭐. If you love this project, please consider giving me a ⭐.
## 🗣️ Discussion ## 🗣️ Discussion
You can also discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues). ### Issues Page
You can discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
Alternatively, you can discuss in my original post on reddit: https://www.reddit.com/r/selfhosted/comments/oi7dc7/uptime_kuma_a_fancy_selfhosted_monitoring_tool_an/
I think the real "Discussion" tab is hard to use, as it is reddit-like flow, I always missed new comments.
### Subreddit
My Reddit account: louislamlam
You can mention me if you ask question on Reddit.
https://www.reddit.com/r/UptimeKuma/
## Contribute ## Contribute
@ -126,4 +124,3 @@ If you want to translate Uptime Kuma into your langauge, please read: https://gi
If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md If you want to modify Uptime Kuma, this guideline may be useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki. English proofreading is needed too because my grammar is not that great sadly. Feel free to correct my grammar in this readme, source code, or wiki.

3
SECURITY.md

@ -10,5 +10,6 @@ currently being supported with security updates.
| 1.x.x | :white_check_mark: | | 1.x.x | :white_check_mark: |
## Reporting a Vulnerability ## Reporting a Vulnerability
Please report security issues to uptime@kuma.pet.
https://github.com/louislam/uptime-kuma/issues Do not use the issue tracker or discuss it in the public as it will cause more damage.

7
db/patch-add-retry-interval-monitor.sql

@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD retry_interval INTEGER default 0 not null;
COMMIT;

19
db/patch10.sql

@ -0,0 +1,19 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
CREATE TABLE tag (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
color VARCHAR(255) NOT NULL,
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
);
CREATE TABLE monitor_tag (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
monitor_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
value TEXT,
CONSTRAINT FK_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor(id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX monitor_tag_monitor_id_index ON monitor_tag (monitor_id);
CREATE INDEX monitor_tag_tag_id_index ON monitor_tag (tag_id);

14
dockerfile

@ -10,18 +10,19 @@ RUN apt update && \
npm install mapbox/node-sqlite3#593c9d --build-from-source npm install mapbox/node-sqlite3#593c9d --build-from-source
COPY . . COPY . .
RUN npm install --legacy-peer-deps && npm run build && npm prune --production RUN npm install --legacy-peer-deps && \
npm run build && \
npm prune --production && \
chmod +x /app/extra/entrypoint.sh
FROM node:14-bullseye-slim AS release FROM node:14-bullseye-slim AS release
WORKDIR /app WORKDIR /app
# Install Apprise, # Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
# add sqlite3 cli for debugging in the future
# iputils-ping for ping
RUN apt update && \ RUN apt update && \
apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 \ sqlite3 iputils-ping util-linux && \
iputils-ping && \
pip3 --no-cache-dir install apprise && \ pip3 --no-cache-dir install apprise && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
@ -31,6 +32,7 @@ COPY --from=build /app /app
EXPOSE 3001 EXPOSE 3001
VOLUME ["/app/data"] VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["extra/entrypoint.sh"]
CMD ["node", "server/server.js"] CMD ["node", "server/server.js"]
FROM release AS nightly FROM release AS nightly

10
dockerfile-alpine

@ -10,14 +10,17 @@ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git &&
rm -f /usr/bin/python rm -f /usr/bin/python
COPY . . COPY . .
RUN npm install --legacy-peer-deps && npm run build && npm prune --production RUN npm install --legacy-peer-deps && \
npm run build && \
npm prune --production && \
chmod +x /app/extra/entrypoint.sh
FROM node:14-alpine3.12 AS release FROM node:14-alpine3.12 AS release
WORKDIR /app WORKDIR /app
# Install apprise # Install apprise, iputils for non-root ping, setpriv
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \ RUN apk add --no-cache iputils setpriv python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
pip3 --no-cache-dir install apprise && \ pip3 --no-cache-dir install apprise && \
rm -rf /root/.cache rm -rf /root/.cache
@ -27,6 +30,7 @@ COPY --from=build /app /app
EXPOSE 3001 EXPOSE 3001
VOLUME ["/app/data"] VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["extra/entrypoint.sh"]
CMD ["node", "server/server.js"] CMD ["node", "server/server.js"]
FROM release AS nightly FROM release AS nightly

21
extra/entrypoint.sh

@ -0,0 +1,21 @@
#!/usr/bin/env sh
# set -e Exit the script if an error happens
set -e
PUID=${PUID=1000}
PGID=${PGID=1000}
files_ownership () {
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
# -R Recursively descends the specified directories
# -c Like verbose but report only when a change is made
chown -hRc "$PUID":"$PGID" /app/data
}
echo "==> Performing startup jobs and maintenance tasks"
files_ownership
echo "==> Starting application with user $PUID group $PGID"
# --clear-groups Clear supplementary groups.
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@"

26
extra/update-language-files/index.js

@ -1,4 +1,4 @@
// Need to use es6 to read language files // Need to use ES6 to read language files
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
@ -14,6 +14,7 @@ const copyRecursiveSync = function (src, dest) {
let exists = fs.existsSync(src); let exists = fs.existsSync(src);
let stats = exists && fs.statSync(src); let stats = exists && fs.statSync(src);
let isDirectory = exists && stats.isDirectory(); let isDirectory = exists && stats.isDirectory();
if (isDirectory) { if (isDirectory) {
fs.mkdirSync(dest); fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function (childItemName) { fs.readdirSync(src).forEach(function (childItemName) {
@ -24,8 +25,9 @@ const copyRecursiveSync = function (src, dest) {
fs.copyFileSync(src, dest); fs.copyFileSync(src, dest);
} }
}; };
console.log(process.argv)
const baseLangCode = process.argv[2] || "zh-HK"; console.log("Arguments:", process.argv)
const baseLangCode = process.argv[2] || "en";
console.log("Base Lang: " + baseLangCode); console.log("Base Lang: " + baseLangCode);
fs.rmdirSync("./languages", { recursive: true }); fs.rmdirSync("./languages", { recursive: true });
copyRecursiveSync("../../src/languages", "./languages"); copyRecursiveSync("../../src/languages", "./languages");
@ -33,19 +35,23 @@ copyRecursiveSync("../../src/languages", "./languages");
const en = (await import("./languages/en.js")).default; const en = (await import("./languages/en.js")).default;
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default; const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
const files = fs.readdirSync("./languages"); const files = fs.readdirSync("./languages");
console.log(files); 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)
continue;
}
console.log("Processing " + file); console.log("Processing " + file);
const lang = await import("./languages/" + file); const lang = await import("./languages/" + file);
let obj; let obj;
if (lang.default) { if (lang.default) {
console.log("is js module");
obj = lang.default; obj = lang.default;
} else { } else {
console.log("empty file"); console.log("Empty file");
obj = { obj = {
languageName: "<Your Language name in your language (not in English)>" languageName: "<Your Language name in your language (not in English)>"
}; };
@ -58,21 +64,21 @@ for (const file of files) {
} }
} }
if (baseLang !== en) {
// Base second // Base second
for (const key in baseLang) { for (const key in baseLang) {
if (! obj[key]) { if (! obj[key]) {
obj[key] = key; obj[key] = key;
} }
} }
}
const code = "export default " + util.inspect(obj, { const code = "export default " + util.inspect(obj, {
depth: null, depth: null,
}); });
fs.writeFileSync(`../../src/languages/${file}`, code); fs.writeFileSync(`../../src/languages/${file}`, code);
}
} }
fs.rmdirSync("./languages", { recursive: true }); fs.rmdirSync("./languages", { recursive: true });
console.log("Done, fix the format by eslint now"); console.log("Done. Fixing formatting by ESLint...");

1
index.html

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="/icon.svg" /> <link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="manifest" href="manifest.json" />
<meta name="theme-color" id="theme-color" content="" /> <meta name="theme-color" id="theme-color" content="" />
<meta name="description" content="Uptime Kuma monitoring tool" /> <meta name="description" content="Uptime Kuma monitoring tool" />
<title>Uptime Kuma</title> <title>Uptime Kuma</title>

23
kubernetes/README.md

@ -8,24 +8,25 @@ Kustomize is a tool which builds a complete deployment file for all config eleme
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing. 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. 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 It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service.
## What do I have to edit?
## What do i have to edit?
You have to edit the ```ingressroute.yml``` to your needs. 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/). 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 - Host
- secrets and secret names - Secrets and secret names
- (Cluster)Issuer (optional) - (Cluster)Issuer (optional)
- the Version in the Deployment-File - The Version in the Deployment-File
- update: - Update:
- change to newer version and run the above commands, it will update the pods one after another - Change to newer version and run the above commands, it will update the pods one after another
## How To use: ## How To use
- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) - Install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
- Edit files mentioned above to your needs - Edit files mentioned above to your needs
- run ```kustomize build > apply.yml``` - Run ```kustomize build > apply.yml```
- run ```kubectl apply -f 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. Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.

8337
package-lock.json

File diff suppressed because it is too large

33
package.json

@ -34,24 +34,26 @@
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .", "test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .", "test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .", "test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js", "simple-dns-server": "node extra/simple-dns-server.js",
"update-language-files": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix" "update-language-files_old": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-4", "@fortawesome/vue-fontawesome": "^3.0.0-4",
"@popperjs/core": "^2.9.3", "@popperjs/core": "^2.10.1",
"args-parser": "^1.3.0", "args-parser": "^1.3.0",
"axios": "^0.21.1", "axios": "^0.21.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bootstrap": "^5.1.0", "bootstrap": "^5.1.1",
"chart.js": "^3.5.1", "chart.js": "^3.5.1",
"chartjs-adapter-dayjs": "^1.0.0", "chartjs-adapter-dayjs": "^1.0.0",
"command-exists": "^1.2.9", "command-exists": "^1.2.9",
"compare-versions": "^3.6.0", "compare-versions": "^3.6.0",
"dayjs": "^1.10.6", "dayjs": "^1.10.7",
"express": "^4.17.1", "express": "^4.17.1",
"express-basic-auth": "^1.2.0", "express-basic-auth": "^1.2.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@ -69,10 +71,11 @@
"socket.io-client": "^4.2.0", "socket.io-client": "^4.2.0",
"sqlite3": "github:mapbox/node-sqlite3#593c9d", "sqlite3": "github:mapbox/node-sqlite3#593c9d",
"tcp-ping": "^0.1.1", "tcp-ping": "^0.1.1",
"timezones-list": "^3.0.1",
"thirty-two": "^1.0.2", "thirty-two": "^1.0.2",
"v-pagination-3": "^0.1.6", "v-pagination-3": "^0.1.6",
"vue": "^3.2.8", "vue": "^3.2.8",
"vue-chart-3": "^0.5.7", "vue-chart-3": "^0.5.8",
"vue-confirm-dialog": "^1.0.2", "vue-confirm-dialog": "^1.0.2",
"vue-i18n": "^9.1.7", "vue-i18n": "^9.1.7",
"vue-multiselect": "^3.0.0-alpha.2", "vue-multiselect": "^3.0.0-alpha.2",
@ -81,19 +84,19 @@
"vue-toastification": "^2.0.0-rc.1" "vue-toastification": "^2.0.0-rc.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.15.0", "@babel/eslint-parser": "^7.15.4",
"@types/bootstrap": "^5.1.2", "@types/bootstrap": "^5.1.4",
"@vitejs/plugin-legacy": "^1.5.2", "@vitejs/plugin-legacy": "^1.5.3",
"@vitejs/plugin-vue": "^1.6.0", "@vitejs/plugin-vue": "^1.6.2",
"@vue/compiler-sfc": "^3.2.6", "@vue/compiler-sfc": "^3.2.11",
"core-js": "^3.17.0", "core-js": "^3.17.3",
"dns2": "^2.0.1", "dns2": "^2.0.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-vue": "^7.17.0", "eslint-plugin-vue": "^7.17.0",
"sass": "^1.38.2", "sass": "^1.39.2",
"stylelint": "^13.13.1", "stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0", "stylelint-config-standard": "^22.0.0",
"typescript": "^4.4.2", "typescript": "^4.4.3",
"vite": "^2.5.3" "vite": "^2.5.7"
} }
} }

BIN
public/icon-192x192.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/icon-512x512.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

19
public/manifest.json

@ -0,0 +1,19 @@
{
"name": "Uptime Kuma",
"short_name": "Uptime Kuma",
"start_url": "/",
"background_color": "#fff",
"display": "standalone",
"icons": [
{
"src": "icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

3
server/database.js

@ -31,6 +31,7 @@ class Database {
"patch-setting-value-type.sql": true, "patch-setting-value-type.sql": true,
"patch-improve-performance.sql": true, "patch-improve-performance.sql": true,
"patch-2fa.sql": true, "patch-2fa.sql": true,
"patch-add-retry-interval-monitor.sql": true,
"patch-add-monitor-checks-table.sql": true, "patch-add-monitor-checks-table.sql": true,
} }
@ -38,7 +39,7 @@ class Database {
* The finally version should be 10 after merged tag feature * The finally version should be 10 after merged tag feature
* @deprecated Use patchList for any new feature * @deprecated Use patchList for any new feature
*/ */
static latestVersion = 9; static latestVersion = 10;
static noReject = true; static noReject = true;

17
server/model/monitor.js

@ -33,6 +33,8 @@ class Monitor extends BeanModel {
notificationIDList[bean.notification_id] = true; notificationIDList[bean.notification_id] = true;
} }
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
return { return {
id: this.id, id: this.id,
name: this.name, name: this.name,
@ -44,6 +46,7 @@ class Monitor extends BeanModel {
active: this.active, active: this.active,
type: this.type, type: this.type,
interval: this.interval, interval: this.interval,
retryInterval: this.retryInterval,
ignoreTls: this.getIgnoreTls(), ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(), upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects, maxredirects: this.maxredirects,
@ -52,6 +55,7 @@ class Monitor extends BeanModel {
dns_last_result: this.dns_last_result, dns_last_result: this.dns_last_result,
notificationIDList, notificationIDList,
checks: this.checks, checks: this.checks,
tags: tags,
}; };
} }
@ -288,12 +292,17 @@ class Monitor extends BeanModel {
bean.important = false; bean.important = false;
} }
let beatInterval = this.interval;
if (bean.status === UP) { if (bean.status === UP) {
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`) console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`)
} else if (bean.status === PENDING) { } else if (bean.status === PENDING) {
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Type: ${this.type}`) if (this.retryInterval !== this.interval) {
beatInterval = this.retryInterval;
}
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`)
} else { } else {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`) console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`)
} }
io.to(this.user_id).emit("heartbeat", bean.toJSON()); io.to(this.user_id).emit("heartbeat", bean.toJSON());
@ -305,7 +314,7 @@ class Monitor extends BeanModel {
previousBeat = bean; previousBeat = bean;
if (! this.isStop) { if (! this.isStop) {
this.heartbeatInterval = setTimeout(beat, this.interval * 1000); this.heartbeatInterval = setTimeout(beat, beatInterval * 1000);
} }
} }

13
server/model/tag.js

@ -0,0 +1,13 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
class Tag extends BeanModel {
toJSON() {
return {
id: this._id,
name: this._name,
color: this._color,
};
}
}
module.exports = Tag;

10
server/notification-providers/discord.js

@ -62,6 +62,11 @@ class Discord extends NotificationProvider {
], ],
}], }],
} }
if (notification.discordPrefixMessage) {
discorddowndata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discorddowndata) await axios.post(notification.discordWebhookUrl, discorddowndata)
return okMsg; return okMsg;
@ -92,6 +97,11 @@ class Discord extends NotificationProvider {
], ],
}], }],
} }
if (notification.discordPrefixMessage) {
discordupdata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discordupdata) await axios.post(notification.discordWebhookUrl, discordupdata)
return okMsg; return okMsg;
} }

124
server/notification-providers/teams.js

@ -0,0 +1,124 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Teams extends NotificationProvider {
name = "teams";
_statusMessageFactory = (status, monitorName) => {
if (status === DOWN) {
return `🔴 Application [${monitorName}] went down`;
} else if (status === UP) {
return `✅ Application [${monitorName}] is back online`;
}
return "Notification";
};
_getThemeColor = (status) => {
if (status === DOWN) {
return "ff0000";
}
if (status === UP) {
return "00e804";
}
return "008cff";
};
_notificationPayloadFactory = ({
status,
monitorMessage,
monitorName,
monitorUrl,
}) => {
const notificationMessage = this._statusMessageFactory(
status,
monitorName
);
const facts = [];
if (monitorName) {
facts.push({
name: "Monitor",
value: monitorName,
});
}
if (monitorUrl) {
facts.push({
name: "URL",
value: monitorUrl,
});
}
return {
"@context": "https://schema.org/extensions",
"@type": "MessageCard",
themeColor: this._getThemeColor(status),
summary: notificationMessage,
sections: [
{
activityImage:
"https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
activityTitle: "**Uptime Kuma**",
},
{
activityTitle: notificationMessage,
},
{
activityTitle: "**Description**",
text: monitorMessage,
facts,
},
],
};
};
_sendNotification = async (webhookUrl, payload) => {
await axios.post(webhookUrl, payload);
};
_handleGeneralNotification = (webhookUrl, msg) => {
const payload = this._notificationPayloadFactory({
monitorMessage: msg
});
return this._sendNotification(webhookUrl, payload);
};
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
if (heartbeatJSON == null) {
await this._handleGeneralNotification(notification.webhookUrl, msg);
return okMsg;
}
let url;
if (monitorJSON["type"] === "port") {
url = monitorJSON["hostname"];
if (monitorJSON["port"]) {
url += ":" + monitorJSON["port"];
}
} else {
url = monitorJSON["url"];
}
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: url,
status: heartbeatJSON.status,
});
await this._sendNotification(notification.webhookUrl, payload);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Teams;

2
server/notification.js

@ -13,6 +13,7 @@ const RocketChat = require("./notification-providers/rocket-chat");
const Signal = require("./notification-providers/signal"); const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack"); const Slack = require("./notification-providers/slack");
const SMTP = require("./notification-providers/smtp"); const SMTP = require("./notification-providers/smtp");
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");
@ -28,6 +29,7 @@ class Notification {
const list = [ const list = [
new Apprise(), new Apprise(),
new Discord(), new Discord(),
new Teams(),
new Gotify(), new Gotify(),
new Line(), new Line(),
new LunaSea(), new LunaSea(),

224
server/server.js

@ -1,4 +1,9 @@
console.log("Welcome to Uptime Kuma"); console.log("Welcome to Uptime Kuma");
if (! process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
}
console.log("Node Env: " + process.env.NODE_ENV); console.log("Node Env: " + process.env.NODE_ENV);
const { sleep, debug, getRandomInt } = require("../src/util"); const { sleep, debug, getRandomInt } = require("../src/util");
@ -151,6 +156,10 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
app.use("/", express.static("dist")); app.use("/", express.static("dist"));
app.get("/.well-known/change-password", async (_, response) => {
response.redirect("https://github.com/louislam/uptime-kuma/wiki/Reset-Password-via-CLI");
});
// Universal Route Handler, must be at the end // Universal Route Handler, must be at the end
app.get("*", async (_request, response) => { app.get("*", async (_request, response) => {
response.send(indexHTML); response.send(indexHTML);
@ -478,6 +487,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
bean.type = monitor.type bean.type = monitor.type
bean.url = monitor.url bean.url = monitor.url
bean.interval = monitor.interval bean.interval = monitor.interval
bean.retryInterval = monitor.retryInterval;
bean.hostname = monitor.hostname; bean.hostname = monitor.hostname;
bean.maxretries = monitor.maxretries; bean.maxretries = monitor.maxretries;
bean.port = monitor.port; bean.port = monitor.port;
@ -513,6 +523,22 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
} }
}); });
socket.on("getMonitorList", async (callback) => {
try {
checkLogin(socket)
await sendMonitorList(socket);
callback({
ok: true,
});
} catch (e) {
console.error(e)
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMonitor", async (monitorID, callback) => { socket.on("getMonitor", async (monitorID, callback) => {
try { try {
checkLogin(socket) checkLogin(socket)
@ -607,6 +633,160 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
} }
}); });
socket.on("getTags", async (callback) => {
try {
checkLogin(socket)
const list = await R.findAll("tag")
callback({
ok: true,
tags: list.map(bean => bean.toJSON()),
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("addTag", async (tag, callback) => {
try {
checkLogin(socket)
let bean = R.dispense("tag")
bean.name = tag.name
bean.color = tag.color
await R.store(bean)
callback({
ok: true,
tag: await bean.toJSON(),
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("editTag", async (tag, callback) => {
try {
checkLogin(socket)
let bean = await R.findOne("monitor", " id = ? ", [ tag.id ])
bean.name = tag.name
bean.color = tag.color
await R.store(bean)
callback({
ok: true,
tag: await bean.toJSON(),
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("deleteTag", async (tagID, callback) => {
try {
checkLogin(socket)
await R.exec("DELETE FROM tag WHERE id = ? ", [ tagID ])
callback({
ok: true,
msg: "Deleted Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("addMonitorTag", async (tagID, monitorID, value, callback) => {
try {
checkLogin(socket)
await R.exec("INSERT INTO monitor_tag (tag_id, monitor_id, value) VALUES (?, ?, ?)", [
tagID,
monitorID,
value,
])
callback({
ok: true,
msg: "Added Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("editMonitorTag", async (tagID, monitorID, value, callback) => {
try {
checkLogin(socket)
await R.exec("UPDATE monitor_tag SET value = ? WHERE tag_id = ? AND monitor_id = ?", [
value,
tagID,
monitorID,
])
callback({
ok: true,
msg: "Edited Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("deleteMonitorTag", async (tagID, monitorID, value, callback) => {
try {
checkLogin(socket)
await R.exec("DELETE FROM monitor_tag WHERE tag_id = ? AND monitor_id = ? AND value = ?", [
tagID,
monitorID,
value,
])
// Cleanup unused Tags
await R.exec("delete from tag where ( select count(*) from monitor_tag mt where tag.id = mt.tag_id ) = 0");
callback({
ok: true,
msg: "Deleted Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("changePassword", async (password, callback) => { socket.on("changePassword", async (password, callback) => {
try { try {
checkLogin(socket) checkLogin(socket)
@ -747,7 +927,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
} }
}); });
socket.on("uploadBackup", async (uploadedJSON, callback) => { socket.on("uploadBackup", async (uploadedJSON, importHandle, callback) => {
try { try {
checkLogin(socket) checkLogin(socket)
@ -755,18 +935,42 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`) console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`)
let notificationList = backupData.notificationList; let notificationListData = backupData.notificationList;
let monitorList = backupData.monitorList; let monitorListData = backupData.monitorList;
if (notificationList.length >= 1) { if (importHandle == "overwrite") {
for (let i = 0; i < notificationList.length; i++) { for (let id in monitorList) {
let notification = JSON.parse(notificationList[i].config); let monitor = monitorList[id]
await monitor.stop()
}
await R.exec("DELETE FROM heartbeat");
await R.exec("DELETE FROM monitor_notification");
await R.exec("DELETE FROM monitor_tls_info");
await R.exec("DELETE FROM notification");
await R.exec("DELETE FROM monitor");
}
if (notificationListData.length >= 1) {
let notificationNameList = await R.getAll("SELECT name FROM notification");
let notificationNameListString = JSON.stringify(notificationNameList);
for (let i = 0; i < notificationListData.length; i++) {
if ((importHandle == "skip" && notificationNameListString.includes(notificationListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") {
let notification = JSON.parse(notificationListData[i].config);
await Notification.save(notification, null, socket.userID) await Notification.save(notification, null, socket.userID)
}
} }
} }
if (monitorList.length >= 1) { if (monitorListData.length >= 1) {
for (let i = 0; i < monitorList.length; i++) { let monitorNameList = await R.getAll("SELECT name FROM monitor");
let monitorNameListString = JSON.stringify(monitorNameList);
for (let i = 0; i < monitorListData.length; i++) {
if ((importHandle == "skip" && monitorNameListString.includes(monitorListData[i].name) == false) || importHandle == "keep" || importHandle == "overwrite") {
let monitor = { let monitor = {
name: monitorList[i].name, name: monitorList[i].name,
type: monitorList[i].type, type: monitorList[i].type,
@ -798,11 +1002,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
await updateMonitorNotification(bean.id, notificationIDList) await updateMonitorNotification(bean.id, notificationIDList)
if (monitorList[i].active == 1) { if (monitorListData[i].active == 1) {
await startMonitor(socket.userID, bean.id); await startMonitor(socket.userID, bean.id);
} else { } else {
await pauseMonitor(socket.userID, bean.id); await pauseMonitor(socket.userID, bean.id);
} }
}
} }
await sendNotificationList(socket) await sendNotificationList(socket)

85
src/components/MonitorList.vue

@ -1,5 +1,18 @@
<template> <template>
<div class="shadow-box list mb-3" :class="{ scrollbar: scrollbar }"> <div class="shadow-box mb-3">
<div class="list-header">
<div class="placeholder"></div>
<div class="search-wrapper">
<a v-if="searchText == ''" class="search-icon">
<font-awesome-icon icon="search" />
</a>
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
<font-awesome-icon icon="times" />
</a>
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
</div>
</div>
<div class="list" :class="{ scrollbar: scrollbar }">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3"> <div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link> {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div> </div>
@ -11,6 +24,9 @@
<Uptime :monitor="item" type="24" :pill="true" /> <Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }} {{ item.name }}
</div> </div>
<div class="tags">
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div> </div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4"> <div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" /> <HeartbeatBar size="small" :monitor-id="item.id" />
@ -24,21 +40,30 @@
</div> </div>
</router-link> </router-link>
</div> </div>
</div>
</template> </template>
<script> <script>
import HeartbeatBar from "../components/HeartbeatBar.vue"; import HeartbeatBar from "../components/HeartbeatBar.vue";
import Uptime from "../components/Uptime.vue"; import Uptime from "../components/Uptime.vue";
import Tag from "../components/Tag.vue";
export default { export default {
components: { components: {
Uptime, Uptime,
HeartbeatBar, HeartbeatBar,
Tag,
}, },
props: { props: {
scrollbar: { scrollbar: {
type: Boolean, type: Boolean,
}, },
}, },
data() {
return {
searchText: "",
}
},
computed: { computed: {
sortedMonitorList() { sortedMonitorList() {
let result = Object.values(this.$root.monitorList); let result = Object.values(this.$root.monitorList);
@ -68,6 +93,17 @@ export default {
return m1.name.localeCompare(m2.name); return m1.name.localeCompare(m2.name);
}) })
// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText != "") {
const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText))
})
}
return result; return result;
}, },
}, },
@ -75,6 +111,9 @@ export default {
monitorURL(id) { monitorURL(id) {
return "/dashboard/" + id; return "/dashboard/" + id;
}, },
clearSearchText() {
this.searchText = "";
}
}, },
} }
</script> </script>
@ -87,6 +126,43 @@ export default {
padding-right: 5px !important; padding-right: 5px !important;
} }
.list-header {
border-bottom: 1px solid #dee2e6;
border-radius: 10px 10px 0 0;
margin: -10px;
margin-bottom: 10px;
padding: 10px;
display: flex;
justify-content: space-between;
.dark & {
background-color: #161b22;
border-bottom: 0;
}
}
@media (max-width: 770px) {
.list-header {
margin: -20px;
margin-bottom: 10px;
padding: 5px;
}
}
.search-wrapper {
display: flex;
align-items: center;
}
.search-icon {
padding: 10px;
color: #c0c0c0;
}
.search-input {
max-width: 15em;
}
.list { .list {
&.scrollbar { &.scrollbar {
min-height: calc(100vh - 240px); min-height: calc(100vh - 240px);
@ -140,4 +216,11 @@ export default {
.monitorItem { .monitorItem {
width: 100%; width: 100%;
} }
.tags {
padding-left: 62px;
display: flex;
flex-wrap: wrap;
gap: 0;
}
</style> </style>

10
src/components/NotificationDialog.vue

@ -17,6 +17,7 @@
<option value="webhook">Webhook</option> <option value="webhook">Webhook</option>
<option value="smtp">{{ $t("Email") }} (SMTP)</option> <option value="smtp">{{ $t("Email") }} (SMTP)</option>
<option value="discord">Discord</option> <option value="discord">Discord</option>
<option value="teams">Microsoft Teams</option>
<option value="signal">Signal</option> <option value="signal">Signal</option>
<option value="gotify">Gotify</option> <option value="gotify">Gotify</option>
<option value="slack">Slack</option> <option value="slack">Slack</option>
@ -80,6 +81,11 @@
<label for="discord-username" class="form-label">Bot Display Name</label> <label for="discord-username" class="form-label">Bot Display Name</label>
<input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName"> <input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
</div> </div>
<div class="mb-3">
<label for="discord-prefix-message" class="form-label">Prefix Custom Message</label>
<input id="discord-prefix-message" v-model="notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" placeholder="Hello @everyone is...">
</div>
</template> </template>
<template v-if="notification.type === 'signal'"> <template v-if="notification.type === 'signal'">
@ -395,6 +401,8 @@
<!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" --> <!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" -->
<Teams v-if="notification.type === 'teams'" />
<div class="mb-3 mt-4"> <div class="mb-3 mt-4">
<hr class="dropdown-divider mb-4"> <hr class="dropdown-divider mb-4">
@ -444,6 +452,7 @@ import { ucfirst } from "../util.ts"
import Confirm from "./Confirm.vue"; import Confirm from "./Confirm.vue";
import HiddenInput from "./HiddenInput.vue"; import HiddenInput from "./HiddenInput.vue";
import Telegram from "./notifications/Telegram.vue"; import Telegram from "./notifications/Telegram.vue";
import Teams from "./notifications/Teams.vue";
import SMTP from "./notifications/SMTP.vue"; import SMTP from "./notifications/SMTP.vue";
export default { export default {
@ -451,6 +460,7 @@ export default {
Confirm, Confirm,
HiddenInput, HiddenInput,
Telegram, Telegram,
Teams,
SMTP, SMTP,
}, },
props: {}, props: {},

73
src/components/Tag.vue

@ -0,0 +1,73 @@
<template>
<div class="tag-wrapper rounded d-inline-flex"
:class="{ 'px-3': size == 'normal',
'py-1': size == 'normal',
'm-2': size == 'normal',
'px-2': size == 'sm',
'py-0': size == 'sm',
'm-1': size == 'sm',
}"
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
>
<span class="tag-text">{{ displayText }}</span>
<span v-if="remove != null" class="ps-1 btn-remove" @click="remove(item)">
<font-awesome-icon icon="times" />
</span>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true,
},
remove: {
type: Function,
default: null,
},
size: {
type: String,
default: "normal",
}
},
computed: {
displayText() {
if (this.item.value == "") {
return this.item.name;
} else {
return `${this.item.name}: ${this.item.value}`;
}
}
}
}
</script>
<style lang="scss" scoped>
.tag-wrapper {
color: white;
opacity: 0.85;
.dark & {
opacity: 1;
}
}
.tag-text {
padding-bottom: 1px !important;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.btn-remove {
font-size: 0.9em;
line-height: 24px;
opacity: 0.3;
}
.btn-remove:hover {
opacity: 1;
}
</style>

405
src/components/TagsManager.vue

@ -0,0 +1,405 @@
<template>
<div>
<h4 class="mb-3">{{ $t("Tags") }}</h4>
<div class="mb-3 p-1">
<tag
v-for="item in selectedTags"
:key="item.id"
:item="item"
:remove="deleteTag"
/>
</div>
<div class="p-1">
<button
type="button"
class="btn btn-outline-secondary btn-add"
:disabled="processing"
@click.stop="showAddDialog"
>
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
</button>
</div>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<vue-multiselect
v-model="newDraftTag.select"
class="mb-2"
:options="tagOptions"
:multiple="false"
:searchable="true"
:placeholder="$t('Add New below or Select...')"
track-by="id"
label="name"
>
<template #option="{ option }">
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
style="margin-top: -5px; margin-bottom: -5px; height: 24px;"
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
>
<span>
{{ option.name }}</span>
</div>
</template>
<template #singleLabel="{ option }">
<div class="py-1 px-3 rounded d-inline-flex"
style="height: 24px;"
:style="{ color: textColor(option), backgroundColor: option.color + ' !important' }"
>
<span>{{ option.name }}</span>
</div>
</template>
</vue-multiselect>
<div v-if="newDraftTag.select?.name == null" class="d-flex mb-2">
<div class="w-50 pe-2">
<input v-model="newDraftTag.name" class="form-control"
:class="{'is-invalid': validateDraftTag.nameInvalid}"
:placeholder="$t('Name')"
@keydown.enter.prevent="onEnter"
/>
<div class="invalid-feedback">
{{ $t("Tag with this name already exist.") }}
</div>
</div>
<div class="w-50 ps-2">
<vue-multiselect
v-model="newDraftTag.color"
:options="colorOptions"
:multiple="false"
:searchable="true"
:placeholder="$t('color')"
track-by="color"
label="name"
select-label=""
deselect-label=""
>
<template #option="{ option }">
<div class="mx-2 py-1 px-3 rounded d-inline-flex"
style="height: 24px; color: white;"
:style="{ backgroundColor: option.color + ' !important' }"
>
<span>{{ option.name }}</span>
</div>
</template>
<template #singleLabel="{ option }">
<div class="py-1 px-3 rounded d-inline-flex"
style="height: 24px; color: white;"
:style="{ backgroundColor: option.color + ' !important' }"
>
<span>{{ option.name }}</span>
</div>
</template>
</vue-multiselect>
</div>
</div>
<div class="mb-2">
<input v-model="newDraftTag.value" class="form-control"
:class="{'is-invalid': validateDraftTag.valueInvalid}"
:placeholder="$t('value (optional)')"
@keydown.enter.prevent="onEnter"
/>
<div class="invalid-feedback">
{{ $t("Tag with this value already exist.") }}
</div>
</div>
<div class="mb-2">
<button
type="button"
class="btn btn-secondary float-end"
:disabled="processing || validateDraftTag.invalid"
@click.stop="addDraftTag"
>
{{ $t("Add") }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect";
import Tag from "../components/Tag.vue";
import { useToast } from "vue-toastification"
const toast = useToast()
export default {
components: {
Tag,
VueMultiselect,
},
props: {
preSelectedTags: {
type: Array,
default: () => [],
},
},
data() {
return {
modal: null,
existingTags: [],
processing: false,
newTags: [],
deleteTags: [],
newDraftTag: {
name: null,
select: null,
color: null,
value: "",
invalid: true,
nameInvalid: false,
},
};
},
computed: {
tagOptions() {
const tagOptions = this.existingTags;
for (const tag of this.newTags) {
if (!tagOptions.find(t => t.name == tag.name && t.color == tag.color)) {
tagOptions.push(tag);
}
}
return tagOptions;
},
selectedTags() {
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id == tag.id));
},
colorOptions() {
return [
{ name: this.$t("Gray"),
color: "#4B5563" },
{ name: this.$t("Red"),
color: "#DC2626" },
{ name: this.$t("Orange"),
color: "#D97706" },
{ name: this.$t("Green"),
color: "#059669" },
{ name: this.$t("Blue"),
color: "#2563EB" },
{ name: this.$t("Indigo"),
color: "#4F46E5" },
{ name: this.$t("Purple"),
color: "#7C3AED" },
{ name: this.$t("Pink"),
color: "#DB2777" },
]
},
validateDraftTag() {
let nameInvalid = false;
let valueInvalid = false;
let invalid = true;
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value)) {
// Undo removing a Tag
nameInvalid = false;
valueInvalid = false;
invalid = false;
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) {
// Try to create new tag with existing name
nameInvalid = true;
invalid = true;
} else if (this.newTags.concat(this.preSelectedTags).filter(tag => (
tag.name == this.newDraftTag.select?.name && tag.value == this.newDraftTag.value
) || (
tag.name == this.newDraftTag.name && tag.value == this.newDraftTag.value
)).length > 0) {
// Try to add a tag with existing name and value
valueInvalid = true;
invalid = true;
} else if (this.newDraftTag.select != null) {
// Select an existing tag, no need to validate
invalid = false;
valueInvalid = false;
} else if (this.newDraftTag.color == null || this.newDraftTag.name === "") {
// Missing form inputs
nameInvalid = false;
invalid = true;
} else {
// Looks valid
invalid = false;
nameInvalid = false;
valueInvalid = false;
}
return {
invalid,
nameInvalid,
valueInvalid,
}
},
},
mounted() {
this.modal = new Modal(this.$refs.modal);
this.getExistingTags();
},
methods: {
showAddDialog() {
this.modal.show();
},
getExistingTags() {
this.$root.getSocket().emit("getTags", (res) => {
if (res.ok) {
this.existingTags = res.tags;
} else {
toast.error(res.msg)
}
});
},
deleteTag(item) {
if (item.new) {
// Undo Adding a new Tag
this.newTags = this.newTags.filter(tag => !(tag.name == item.name && tag.value == item.value));
} else {
// Remove an Existing Tag
this.deleteTags.push(item);
}
},
textColor(option) {
if (option.color) {
return "white";
} else {
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
}
},
addDraftTag() {
console.log("Adding Draft Tag: ", this.newDraftTag);
if (this.newDraftTag.select != null) {
if (this.deleteTags.find(tag => tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value)) {
// Undo removing a tag
this.deleteTags = this.deleteTags.filter(tag => !(tag.name == this.newDraftTag.select.name && tag.value == this.newDraftTag.value));
} else {
// Add an existing Tag
this.newTags.push({
id: this.newDraftTag.select.id,
color: this.newDraftTag.select.color,
name: this.newDraftTag.select.name,
value: this.newDraftTag.value,
new: true,
})
}
} else {
// Add new Tag
this.newTags.push({
color: this.newDraftTag.color.color,
name: this.newDraftTag.name.trim(),
value: this.newDraftTag.value,
new: true,
})
}
this.clearDraftTag();
},
clearDraftTag() {
this.newDraftTag = {
name: null,
select: null,
color: null,
value: "",
invalid: true,
nameInvalid: false,
};
this.modal.hide();
},
addTagAsync(newTag) {
return new Promise((resolve) => {
this.$root.getSocket().emit("addTag", newTag, resolve);
});
},
addMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
});
},
deleteMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
});
},
onEnter() {
if (!this.validateDraftTag.invalid) {
this.addDraftTag();
}
},
async submit(monitorId) {
console.log(`Submitting tag changes for monitor ${monitorId}...`);
this.processing = true;
for (const newTag of this.newTags) {
let tagId;
if (newTag.id == null) {
// Create a New Tag
let newTagResult;
await this.addTagAsync(newTag).then((res) => {
if (!res.ok) {
toast.error(res.msg);
newTagResult = false;
}
newTagResult = res.tag;
});
if (!newTagResult) {
// abort
this.processing = false;
return;
}
tagId = newTagResult.id;
// Assign the new ID to the tags of the same name & color
this.newTags.map(tag => {
if (tag.name == newTag.name && tag.color == newTag.color) {
tag.id = newTagResult.id;
}
})
} else {
tagId = newTag.id;
}
let newMonitorTagResult;
// Assign tag to monitor
await this.addMonitorTagAsync(tagId, monitorId, newTag.value).then((res) => {
if (!res.ok) {
toast.error(res.msg);
newMonitorTagResult = false;
}
newMonitorTagResult = true;
});
if (!newMonitorTagResult) {
// abort
this.processing = false;
return;
}
}
for (const deleteTag of this.deleteTags) {
let deleteMonitorTagResult;
await this.deleteMonitorTagAsync(deleteTag.tag_id, deleteTag.monitor_id, deleteTag.value).then((res) => {
if (!res.ok) {
toast.error(res.msg);
deleteMonitorTagResult = false;
}
deleteMonitorTagResult = true;
});
if (!deleteMonitorTagResult) {
// abort
this.processing = false;
return;
}
}
this.getExistingTags();
this.newTags = [];
this.deleteTags = [];
this.processing = false;
}
},
};
</script>
<style scoped>
.btn-add {
width: 100%;
}
.modal-body {
padding: 1.5rem;
}
</style>

2
src/components/notifications/SMTP.vue

@ -33,7 +33,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">{{ $t("Password") }}</label> <label for="password" class="form-label">{{ $t("Password") }}</label>
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="true" autocomplete="one-time-code"></HiddenInput> <HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="false" autocomplete="one-time-code"></HiddenInput>
</div> </div>
<div class="mb-3"> <div class="mb-3">

29
src/components/notifications/Teams.vue

@ -0,0 +1,29 @@
<template>
<div class="mb-3">
<label for="teams-webhookurl" class="form-label">Webhook URL</label>
<input
id="teams-webhookurl"
v-model="$parent.notification.webhookUrl"
type="text"
class="form-control"
required
/>
<div class="form-text">
You can learn how to create a webhook url
<a
href="https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook"
target="_blank"
>here</a>.
</div>
</div>
</template>
<script>
export default {
data() {
return {
name: "teams",
};
},
};
</script>

48
src/i18n.js

@ -0,0 +1,48 @@
import { createI18n } from "vue-i18n";
import daDK from "./languages/da-DK";
import deDE from "./languages/de-DE";
import en from "./languages/en";
import esEs from "./languages/es-ES";
import etEE from "./languages/et-EE";
import frFR from "./languages/fr-FR";
import itIT from "./languages/it-IT";
import ja from "./languages/ja";
import koKR from "./languages/ko-KR";
import nlNL from "./languages/nl-NL";
import pl from "./languages/pl";
import ruRU from "./languages/ru-RU";
import sr from "./languages/sr";
import srLatn from "./languages/sr-latn";
import trTR from "./languages/tr-TR";
import svSE from "./languages/sv-SE";
import zhCN from "./languages/zh-CN";
import zhHK from "./languages/zh-HK";
const languageList = {
en,
"zh-HK": zhHK,
"de-DE": deDE,
"nl-NL": nlNL,
"es-ES": esEs,
"fr-FR": frFR,
"it-IT": itIT,
"ja": ja,
"da-DK": daDK,
"sr": sr,
"sr-latn": srLatn,
"sv-SE": svSE,
"tr-TR": trTR,
"ko-KR": koKR,
"ru-RU": ruRU,
"zh-CN": zhCN,
"pl": pl,
"et-EE": etEE,
};
export const i18n = createI18n({
locale: localStorage.locale || "en",
fallbackLocale: "en",
silentFallbackWarn: true,
silentTranslationWarn: false,
messages: languageList,
});

39
src/icon.js

@ -1,10 +1,39 @@
import { library } from "@fortawesome/fontawesome-svg-core" import { library } from "@fortawesome/fontawesome-svg-core";
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash, faTimes } from "@fortawesome/free-solid-svg-icons" import {
faArrowAltCircleUp,
faCog,
faEdit,
faEye,
faEyeSlash,
faList,
faPause,
faPlay,
faPlus,
faSearch,
faTachometerAlt,
faTimes,
faTrash
} from "@fortawesome/free-solid-svg-icons";
//import { fa } from '@fortawesome/free-regular-svg-icons' //import { fa } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome" import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
// Add Free Font Awesome Icons here // Add Free Font Awesome Icons here
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free // https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash, faTimes); library.add(
faArrowAltCircleUp,
faCog,
faEdit,
faEye,
faEyeSlash,
faList,
faPause,
faPlay,
faPlus,
faSearch,
faTachometerAlt,
faTimes,
faTrash,
);
export { FontAwesomeIcon };
export { FontAwesomeIcon }

16
src/languages/README.md

@ -1,18 +1,14 @@
# How to translate # How to translate
1. Fork this repo. 1. Fork this repo.
2. Create a language file. (e.g. `zh-TW.js`) The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm 2. Create a language file (e.g. `zh-TW.js`). The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
3. `npm run update-language-files --base-lang=de-DE` 3. Run `npm run update-language-files`. You can also use this command to check if there are new strings to translate for your language.
6. Your language file should be filled in. You can translate now. 4. Your language file should be filled in. You can translate now.
7. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`). 5. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
8. Import your language file in `src/main.js` and add it to `languageList` constant. 6. Import your language file in `src/i18n.js` and add it to `languageList` constant.
9. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done. 7. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
One of good examples: One of good examples:
https://github.com/louislam/uptime-kuma/pull/316/files https://github.com/louislam/uptime-kuma/pull/316/files
If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏 If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏

29
src/languages/da-DK.js

@ -36,7 +36,6 @@ export default {
hour: "Timer", hour: "Timer",
"-hour": "-Timer", "-hour": "-Timer",
checkEverySecond: "Tjek hvert {0} sekund", checkEverySecond: "Tjek hvert {0} sekund",
"Avg.": "Gns.",
Response: "Respons", Response: "Respons",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Overvåger Type", "Monitor Type": "Overvåger Type",
@ -120,7 +119,6 @@ export default {
enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.", enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.",
"Default enabled": "Standard aktiveret", "Default enabled": "Standard aktiveret",
"Also apply to existing monitors": "Anvend også på eksisterende overvågere", "Also apply to existing monitors": "Anvend også på eksisterende overvågere",
"Import/Export Backup": " Importér/Eksportér sikkerhedskopi",
Export: "Eksport", Export: "Eksport",
Import: "Import", Import: "Import",
backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.", backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.",
@ -144,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

31
src/languages/de-DE.js

@ -36,7 +36,6 @@ export default {
hour: "Stunde", hour: "Stunde",
"-hour": "-Stunden", "-hour": "-Stunden",
checkEverySecond: "Überprüfe alle {0} Sekunden", checkEverySecond: "Überprüfe alle {0} Sekunden",
"Avg.": "Durchschn.",
Response: "Antwortzeit", Response: "Antwortzeit",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Monitor Typ", "Monitor Type": "Monitor Typ",
@ -113,7 +112,6 @@ export default {
"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",
"Import/Export Backup": "Import/Export Backup",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
respTime: "Antw. Zeit (ms)", respTime: "Antw. Zeit (ms)",
@ -128,6 +126,13 @@ export default {
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",
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.",
"Skip existing": "Vorhandene überspringen",
Overwrite: "Überschreiben",
Options: "Optionen",
confirmImportMsg: "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import Option ausgewählt ist.",
"Keep both": "Beide behalten",
twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert", twoFAVerifyLabel: "Bitte trage deinen Token ein um zu verifizieren das 2FA funktioniert",
"Verify Token": "Token verifizieren", "Verify Token": "Token verifizieren",
"Setup 2FA": "2FA Einrichten", "Setup 2FA": "2FA Einrichten",
@ -142,5 +147,25 @@ export default {
Inactive: "Inaktiv", Inactive: "Inaktiv",
Token: "Token", Token: "Token",
"Show URI": "URI Anzeigen", "Show URI": "URI Anzeigen",
"Clear all statistics": "Lösche alle Statistiken", Tags: "Tags",
"Add New below or Select...": "Füge neuen hinzu oder wähle aus...",
"Tag with this name already exist.": "Ein Tag mit dem Namen existiert bereits.",
"Tag with this value already exist.": "Ein Tag mit dem Wert existiert bereits.",
color: "Farbe",
"value (optional)": "Wert (Optional)",
Gray: "Grau",
Red: "Rot",
Orange: "Orange",
Green: "Grün",
Blue: "Blau",
Indigo: "Indigo",
Purple: "Lila",
Pink: "Pink",
"Search...": "Suchen...",
"Heartbeat Retry Interval": "Takt-Wiederholungsintervall",
retryCheckEverySecond: "Versuche alle {0} Sekunden",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

31
src/languages/en.js

@ -1,7 +1,7 @@
export default { export default {
languageName: "English", languageName: "English",
checkEverySecond: "Check every {0} seconds.", checkEverySecond: "Check every {0} seconds.",
"Avg.": "Avg.", 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.",
@ -20,6 +20,8 @@ export default {
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 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.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
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?",
@ -68,6 +70,7 @@ export default {
Port: "Port", Port: "Port",
"Heartbeat Interval": "Heartbeat Interval", "Heartbeat Interval": "Heartbeat Interval",
Retries: "Retries", Retries: "Retries",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
Advanced: "Advanced", Advanced: "Advanced",
"Upside Down Mode": "Upside Down Mode", "Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "Max. Redirects", "Max. Redirects": "Max. Redirects",
@ -115,7 +118,8 @@ export default {
"Last Result": "Last Result", "Last Result": "Last Result",
"Create your admin account": "Create your admin account", "Create your admin account": "Create your admin account",
"Repeat Password": "Repeat Password", "Repeat Password": "Repeat Password",
"Import/Export Backup": "Import/Export Backup", "Import Backup": "Import Backup",
"Export Backup": "Export Backup",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
respTime: "Resp. Time (ms)", respTime: "Resp. Time (ms)",
@ -132,6 +136,11 @@ export default {
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
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",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
"Verify Token": "Verify Token", "Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA", "Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA", "Enable 2FA": "Enable 2FA",
@ -142,5 +151,21 @@ export default {
Inactive: "Inactive", Inactive: "Inactive",
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

31
src/languages/es-ES.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "Español", languageName: "Español",
checkEverySecond: "Comprobar cada {0} segundos.", checkEverySecond: "Comprobar cada {0} segundos.",
"Avg.": "Media.",
retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.", retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.",
ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS", ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS",
upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.", upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.",
@ -32,7 +31,7 @@ export default {
Up: "Funcional", Up: "Funcional",
Down: "Caído", Down: "Caído",
Pending: "Pendiente", Pending: "Pendiente",
Unknown: "Desconociso", Unknown: "Desconocido",
Pause: "Pausa", Pause: "Pausa",
Name: "Nombre", Name: "Nombre",
Status: "Estado", Status: "Estado",
@ -120,7 +119,6 @@ export default {
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled", "Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "You can backup all monitors and all notifications into a JSON file.",
@ -144,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

29
src/languages/et-EE.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "eesti", languageName: "eesti",
checkEverySecond: "Kontrolli {0} sekundilise vahega.", checkEverySecond: "Kontrolli {0} sekundilise vahega.",
"Avg.": "≈",
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.", retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.", ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.", upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
@ -113,7 +112,6 @@ export default {
clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?", clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?",
clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?", clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?", confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?",
"Import/Export Backup": "Impordi/Ekspordi varukoopia",
Export: "Eksport", Export: "Eksport",
Import: "Import", Import: "Import",
"Default enabled": "Kasuta vaikimisi", "Default enabled": "Kasuta vaikimisi",
@ -144,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

29
src/languages/fr-FR.js

@ -36,7 +36,6 @@ export default {
hour: "Heure", hour: "Heure",
"-hour": "Heures", "-hour": "Heures",
checkEverySecond: "Vérifier toutes les {0} secondes", checkEverySecond: "Vérifier toutes les {0} secondes",
"Avg.": "Moyen",
Response: "Temps de réponse", Response: "Temps de réponse",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Type de Sonde", "Monitor Type": "Type de Sonde",
@ -120,7 +119,6 @@ export default {
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled", "Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "You can backup all monitors and all notifications into a JSON file.",
@ -144,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

78
src/languages/it-IT.js

@ -1,7 +1,7 @@
export default { export default {
languageName: "Italiano (Italian)", languageName: "Italiano (Italian)",
checkEverySecond: "controlla ogni {0} secondi", checkEverySecond: "controlla ogni {0} secondi",
"Avg.": "Media", retryCheckEverySecond: "Riprova ogni {0} secondi.",
retriesDescription: "Tentativi da fare prima che il servizio venga marcato come \"giù\" e che una notifica venga inviata.", retriesDescription: "Tentativi da fare prima che il servizio venga marcato come \"giù\" e che una notifica venga inviata.",
ignoreTLSError: "Ignora gli errori TLS/SSL per i siti in HTTPS.", ignoreTLSError: "Ignora gli errori TLS/SSL per i siti in HTTPS.",
upsideDownModeDescription: "Capovolgi lo stato. Se il servizio è raggiungibile viene marcato come \"GIÙ\".", upsideDownModeDescription: "Capovolgi lo stato. Se il servizio è raggiungibile viene marcato come \"GIÙ\".",
@ -16,9 +16,16 @@ export default {
resoverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.", resoverserverDescription: "Cloudflare è il server predefinito, è possibile cambiare il server DNS.",
rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare", rrtypeDescription: "Scegliere il tipo di RR che si vuole monitorare",
pauseMonitorMsg: "Si è certi di voler mettere in pausa?", pauseMonitorMsg: "Si è certi di voler mettere in pausa?",
enableDefaultNotificationDescription: "Per ogni nuovo monitoraggio questa notifica sarà abilitata di default. È comunque possibile disabilitare la notifica separatamente per ogni monitoraggio.",
clearEventsMsg: "Si è certi di voler eliminare tutti gli eventi per questo servizio?", clearEventsMsg: "Si è certi di voler eliminare tutti gli eventi per questo servizio?",
clearHeartbeatsMsg: "Si è certi di voler eliminare tutti gli intervalli di controllo per questo servizio?", clearHeartbeatsMsg: "Si è certi di voler eliminare tutti gli intervalli di controllo per questo servizio?",
confirmClearStatisticsMsg: "Si è certi di voler eliminare TUTTE le statistiche?", confirmClearStatisticsMsg: "Si è certi di voler eliminare TUTTE le statistiche?",
importHandleDescription: "Selezionare 'Ignora gli esistenti' si vuole ignorare l'importazione dei monitoraggi o delle notifiche con lo stesso nome. 'Sovrascrivi' eliminerà ogni monitoraggio e notifica esistente.",
confirmImportMsg: "Si è certi di voler importare il backup? Essere certi di aver selezionato l'opzione corretta di importazione.",
twoFAVerifyLabel: "Scrivi il token per verificare che l'autenticazione a due fattori funzioni",
tokenValidSettingsMsg: "Il token è valido! È ora possibile salvare le impostazioni.",
confirmEnableTwoFAMsg: "Si è certi di voler abilitare l'autenticazione a due fattori?",
confirmDisableTwoFAMsg: "Si è certi di voler disabilitare l'autenticazione a due fattori?",
Settings: "Impostazioni", Settings: "Impostazioni",
Dashboard: "Cruscotto", Dashboard: "Cruscotto",
"New Update": "Nuovo Aggiornamento Disponibile", "New Update": "Nuovo Aggiornamento Disponibile",
@ -63,6 +70,7 @@ export default {
Port: "Porta", Port: "Porta",
"Heartbeat Interval": "Intervallo di controllo", "Heartbeat Interval": "Intervallo di controllo",
Retries: "Tentativi", Retries: "Tentativi",
"Heartbeat Retry Interval": "Intervallo tra un tentativo di controllo e l'altro",
Advanced: "Avanzate", Advanced: "Avanzate",
"Upside Down Mode": "Modalità capovolta", "Upside Down Mode": "Modalità capovolta",
"Max. Redirects": "Redirezionamenti massimi", "Max. Redirects": "Redirezionamenti massimi",
@ -110,38 +118,54 @@ export default {
"Last Result": "Ultimo risultato", "Last Result": "Ultimo risultato",
"Create your admin account": "Crea l'account amministratore", "Create your admin account": "Crea l'account amministratore",
"Repeat Password": "Ripeti Password", "Repeat Password": "Ripeti Password",
"Import Backup": "Importa Backup",
"Export Backup": "Esporta Backup",
Export: "Esporta",
Import: "Importa",
respTime: "Tempo di Risposta (ms)", respTime: "Tempo di Risposta (ms)",
notAvailableShort: "N/D", notAvailableShort: "N/D",
"Default enabled": "Abilitato di default",
"Apply on all existing monitors": "Applica su tutti i monitoraggi",
Create: "Crea", Create: "Crea",
"Clear Data": "Cancella dati", "Clear Data": "Cancella dati",
Events: "Eventi", Events: "Eventi",
Heartbeats: "Controlli", Heartbeats: "Controlli",
"Auto Get": "Auto Get", "Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", backupDescription: "È possibile fare il backup di tutti i monitoraggi e di tutte le notifiche in un file JSON.",
"Import/Export Backup": "Import/Export Backup", backupDescription2: "P.S.: lo storico e i dati relativi agli eventi non saranno inclusi.",
Export: "Export", backupDescription3: "Dati sensibili come i token di autenticazione saranno inclusi nel backup, tenere quindi in un luogo sicuro.",
Import: "Import", alertNoFile: "Selezionare il file da importare.",
"Default enabled": "Default enabled", alertWrongFileType: "Selezionare un file JSON.",
"Also apply to existing monitors": "Also apply to existing monitors", "Clear all statistics": "Pulisci tutte le statistiche",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", "Skip existing": "Ignora gli esistenti",
backupDescription2: "PS: History and event data is not included.", Overwrite: "Sovrascrivi",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", Options: "Opzioni",
alertNoFile: "Please select a file to import.", "Keep both": "Mantieni entrambi",
alertWrongFileType: "Please select a JSON file.", "Verify Token": "Verifica Token",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", "Setup 2FA": "Imposta l'autenticazione a due fattori",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.", "Enable 2FA": "Abilita l'autenticazione a due fattori",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?", "Disable 2FA": "Disabilita l'autenticazione a due fattori",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?", "2FA Settings": "Impostazioni autenticazione a due fattori",
"Apply on all existing monitors": "Apply on all existing monitors", "Two Factor Authentication": "Autenticazione a due fattori",
"Verify Token": "Verify Token", Active: "Attivata",
"Setup 2FA": "Setup 2FA", Inactive: "Disattivata",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Mostra URI",
"Clear all statistics": "Clear all Statistics", Tags: "Etichette",
"Add New below or Select...": "Aggiungine una oppure scegli...",
"Tag with this name already exist.": "Un'etichetta con questo nome già esiste.",
"Tag with this value already exist.": "Un'etichetta con questo valore già esiste.",
color: "colori",
"value (optional)": "valore (opzionale)",
Gray: "Grigio",
Red: "Rosso",
Orange: "Arancione",
Green: "Verde",
Blue: "Blu",
Indigo: "Indigo",
Purple: "Viola",
Pink: "Rosa",
"Search...": "Cerca...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

29
src/languages/ja.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "日本語", languageName: "日本語",
checkEverySecond: "{0}秒ごとにチェックします。", checkEverySecond: "{0}秒ごとにチェックします。",
"Avg.": "平均",
retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数", retriesDescription: "サービスがダウンとしてマークされ、通知が送信されるまでの最大リトライ数",
ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する", ignoreTLSError: "HTTPS ウェブサイトの TLS/SSL エラーを無視する",
upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。", upsideDownModeDescription: "ステータスの扱いを逆にします。サービスに到達可能な場合は、DOWNとなる。",
@ -120,7 +119,6 @@ export default {
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled", "Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "You can backup all monitors and all notifications into a JSON file.",
@ -144,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

29
src/languages/ko-KR.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "한국어", languageName: "한국어",
checkEverySecond: "{0} 초마다 체크해요.", checkEverySecond: "{0} 초마다 체크해요.",
"Avg.": "평균",
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수", retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기", ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기",
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.", upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거에요.",
@ -120,7 +119,6 @@ export default {
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled", "Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "You can backup all monitors and all notifications into a JSON file.",
@ -144,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

89
src/languages/nl-NL.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "Nederlands", languageName: "Nederlands",
checkEverySecond: "Controleer elke {0} seconden.", checkEverySecond: "Controleer elke {0} seconden.",
"Avg.": "Gem.",
retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden", retriesDescription: "Maximum aantal nieuwe pogingen voordat de service wordt gemarkeerd als niet beschikbaar en er een melding wordt verzonden",
ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites", ignoreTLSError: "Negeer TLS/SSL-fout voor HTTPS-websites",
upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.", upsideDownModeDescription: "Draai de status om. Als de service bereikbaar is, is deze OFFLINE.",
@ -16,6 +15,14 @@ export default {
resoverserverDescription: "Cloudflare is de standaardserver, u kunt de resolver server op elk moment wijzigen.", resoverserverDescription: "Cloudflare is de standaardserver, u kunt de resolver server op elk moment wijzigen.",
rrtypeDescription: "Selecteer het RR-type dat u wilt monitoren", rrtypeDescription: "Selecteer het RR-type dat u wilt monitoren",
pauseMonitorMsg: "Weet je zeker dat je wilt pauzeren?", pauseMonitorMsg: "Weet je zeker dat je wilt pauzeren?",
enableDefaultNotificationDescription: "Voor elke nieuwe monitor wordt deze melding standaard ingeschakeld. U kunt de melding nog steeds afzonderlijk uitschakelen voor elke monitor.",
clearEventsMsg: "Weet je zeker dat je alle evenementen voor deze monitor wilt verwijderen?",
clearHeartbeatsMsg: "Weet je zeker dat je alle heartbeats voor deze monitor wilt verwijderen?",
confirmClearStatisticsMsg: "Weet u zeker dat u alle statistieken wilt verwijderen?",
twoFAVerifyLabel: "Voer uw 2FA controle token in voor verificatie",
tokenValidSettingsMsg: "Token is geldig! U kunt nu de 2FA-instellingen opslaan.",
confirmEnableTwoFAMsg: "Weet je zeker dat je 2FA wilt inschakelen?",
confirmDisableTwoFAMsg: "Weet je zeker dat je 2FA wilt uitschakelen?",
Settings: "Instellingen", Settings: "Instellingen",
Dashboard: "Dashboard", Dashboard: "Dashboard",
"New Update": "Nieuwe update", "New Update": "Nieuwe update",
@ -107,41 +114,59 @@ export default {
"Last Result": "Laatste resultaat", "Last Result": "Laatste resultaat",
"Create your admin account": "Maak uw beheerdersaccount aan", "Create your admin account": "Maak uw beheerdersaccount aan",
"Repeat Password": "Herhaal wachtwoord", "Repeat Password": "Herhaal wachtwoord",
Export: "Exporteren",
Import: "Importeren",
respTime: "resp. tijd (ms)", respTime: "resp. tijd (ms)",
notAvailableShort: "N.v.t.", notAvailableShort: "N.v.t.",
Create: "Create", "Default enabled": "Default enabled",
clearEventsMsg: "Are you sure want to delete all events for this monitor?", "Apply on all existing monitors": "Pas toe op alle bestaande monitors",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", Create: "Aanmaken",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", "Clear Data": "Data wissen",
"Clear Data": "Clear Data", Events: "Gebeurtenissen",
Events: "Events",
Heartbeats: "Heartbeats", Heartbeats: "Heartbeats",
"Auto Get": "Auto Get", "Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", backupDescription: "U kunt een back-up maken van alle monitoren en alle meldingen in een JSON-bestand.",
"Default enabled": "Default enabled", backupDescription2: "PS: Geschiedenis- en gebeurtenisgegevens zijn niet inbegrepen.",
backupDescription3: "Gevoelige gegevens zoals melding tokens zijn opgenomen in het exportbestand, houd het veilig opgeslagen.",
alertNoFile: "Selecteer een bestand om te importeren.",
alertWrongFileType: "Selecteer een JSON-bestand.",
"Verify Token": "Controleer token",
"Setup 2FA": "2FA instellingen",
"Enable 2FA": "Schakel 2FA in",
"Disable 2FA": "Schakel 2FA uit",
"2FA Settings": "2FA-instellingen",
"Two Factor Authentication": "Two Factor Authenticatie",
Active: "Actief",
Inactive: "Inactief",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Toon URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Wis alle statistieken",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

93
src/languages/pl.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "Polski", languageName: "Polski",
checkEverySecond: "Sprawdzaj co {0} sekund.", checkEverySecond: "Sprawdzaj co {0} sekund.",
"Avg.": "Średnia",
retriesDescription: "Maksymalna liczba powtórzeń, zanim usługa zostanie oznaczona jako wyłączona i zostanie wysłane powiadomienie", retriesDescription: "Maksymalna liczba powtórzeń, zanim usługa zostanie oznaczona jako wyłączona i zostanie wysłane powiadomienie",
ignoreTLSError: "Ignoruj błąd TLS/SSL dla stron HTTPS", ignoreTLSError: "Ignoruj błąd TLS/SSL dla stron HTTPS",
upsideDownModeDescription: "Odwróć status do góry nogami. Jeśli usługa jest osiągalna, to jest oznaczona jako niedostępna.", upsideDownModeDescription: "Odwróć status do góry nogami. Jeśli usługa jest osiągalna, to jest oznaczona jako niedostępna.",
@ -110,38 +109,64 @@ export default {
respTime: "Czas odp. (ms)", respTime: "Czas odp. (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
Create: "Stwórz", Create: "Stwórz",
clearEventsMsg: "Are you sure want to delete all events for this monitor?", clearEventsMsg: "Jesteś pewien, że chcesz usunąć wszystkie monitory dla tej strony?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", clearHeartbeatsMsg: "Jesteś pewien, że chcesz usunąć wszystkie bicia serca dla tego monitora?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", confirmClearStatisticsMsg: "Jesteś pewien, że chcesz usunąć WSZYSTKIE statystyki?",
"Clear Data": "Clear Data", "Clear Data": "Usuń dane",
Events: "Events", Events: "Wydarzenia",
Heartbeats: "Heartbeats", Heartbeats: "Bicia serca",
"Auto Get": "Auto Get", "Auto Get": "Pobierz automatycznie",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "Dla każdego nowego monitora to powiadomienie będzie domyślnie włączone. Nadal możesz wyłączyć powiadomienia osobno dla każdego monitora.",
"Default enabled": "Default enabled", "Default enabled": "Domyślnie włączone",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Również zastosuj do obecnych monitorów",
"Import/Export Backup": "Import/Export Backup", Export: "Eksportuj",
Export: "Export", Import: "Importuj",
Import: "Import", backupDescription: "Możesz wykonać kopię zapasową wszystkich monitorów i wszystkich powiadomień do pliku JSON.",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription2: "PS: Historia i dane zdarzeń nie są uwzględniane.",
backupDescription2: "PS: History and event data is not included.", backupDescription3: "Poufne dane, takie jak tokeny powiadomień, są zawarte w pliku eksportu, prosimy o ostrożne przechowywanie.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", alertNoFile: "Proszę wybrać plik do importu.",
alertNoFile: "Please select a file to import.", alertWrongFileType: "Proszę wybrać plik JSON.",
alertWrongFileType: "Please select a JSON file.", twoFAVerifyLabel: "Proszę podaj swój token 2FA, aby sprawdzić czy 2FA działa",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", tokenValidSettingsMsg: "Token jest poprawny! Możesz teraz zapisać ustawienia 2FA.",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.", confirmEnableTwoFAMsg: "Jesteś pewien że chcesz włączyć 2FA?",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?", confirmDisableTwoFAMsg: "Jesteś pewien że chcesz wyłączyć 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?", "Apply on all existing monitors": "Zastosuj do wszystki obecnych monitorów",
"Apply on all existing monitors": "Apply on all existing monitors", "Verify Token": "Weryfikuj token",
"Verify Token": "Verify Token", "Setup 2FA": "Konfiguracja 2FA",
"Setup 2FA": "Setup 2FA", "Enable 2FA": "Włącz 2FA",
"Enable 2FA": "Enable 2FA", "Disable 2FA": "Wyłącz 2FA",
"Disable 2FA": "Disable 2FA", "2FA Settings": "Ustawienia 2FA",
"2FA Settings": "2FA Settings", "Two Factor Authentication": "Uwierzytelnienie dwuskładnikowe",
"Two Factor Authentication": "Two Factor Authentication", Active: "Włączone",
Active: "Active", Inactive: "Wyłączone",
Inactive: "Inactive",
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Pokaż URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Wyczyść wszystkie statystyki",
retryCheckEverySecond: "Ponawiaj co {0} sekund.",
importHandleDescription: "Wybierz 'Pomiń istniejące', jeśli chcesz pominąć każdy monitor lub powiadomienie o tej samej nazwie. 'Nadpisz' spowoduje usunięcie każdego istniejącego monitora i powiadomienia.",
confirmImportMsg: "Czy na pewno chcesz zaimportować kopię zapasową? Upewnij się, że wybrałeś właściwą opcję importu.",
"Heartbeat Retry Interval": "Częstotliwość ponawiania bicia serca",
"Import Backup": "Importuj kopię zapasową",
"Export Backup": "Eksportuj kopię zapasową",
"Skip existing": "Pomiń istniejące",
Overwrite: "Nadpisz",
Options: "Opcje",
"Keep both": "Zachowaj oba",
Tags: "Tagi",
"Add New below or Select...": "Dodaj nowy poniżej lub wybierz...",
"Tag with this name already exist.": "Tag o tej nazwie już istnieje.",
"Tag with this value already exist.": "Tag o tej wartości już istnieje.",
color: "kolor",
"value (optional)": "wartość (opcjonalnie)",
Gray: "Szary",
Red: "Czerwony",
Orange: "Pomarańczowy",
Green: "Zielony",
Blue: "Niebieski",
Indigo: "Indygo",
Purple: "Fioletowy",
Pink: "Różowy",
"Search...": "Szukaj...",
"Avg. Ping": "Średni ping",
"Avg. Response": "Średnia odpowiedź",
} }

101
src/languages/ru-RU.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "Русский", languageName: "Русский",
checkEverySecond: "Проверять каждые {0} секунд.", checkEverySecond: "Проверять каждые {0} секунд.",
"Avg.": "Средн.",
retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления", retriesDescription: "Максимальное количество попыток перед пометкой сервиса как недоступного и отправкой уведомления",
ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов", ignoreTLSError: "Игнорировать ошибку TLS/SSL для HTTPS сайтов",
upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.", upsideDownModeDescription: "Реверс статуса сервиса. Если сервис доступен, то он помечается как НЕДОСТУПНЫЙ.",
@ -107,41 +106,67 @@ export default {
"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: "Н/Д",
Create: "Create", Create: "Создать",
clearEventsMsg: "Are you sure want to delete all events for this monitor?", clearEventsMsg: "Вы действительно хотите удалить всю статистику событий данного монитора?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?", clearHeartbeatsMsg: "Вы действительно хотите удалить всю статистику опросов данного монитора?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?", confirmClearStatisticsMsg: "Вы действительно хотите удалить ВСЮ статистику?",
"Clear Data": "Clear Data", "Clear Data": "Очистить статистику",
Events: "Events", Events: "События",
Heartbeats: "Heartbeats", Heartbeats: "Опросы",
"Auto Get": "Auto Get", "Auto Get": "Авто-получение",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "Для каждого нового монитора это уведомление будет включено по умолчанию. Вы всё ещё можете отключить уведомления в каждом мониторе отдельно.",
"Default enabled": "Default enabled", "Default enabled": "Использовать по умолчанию",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Применить к существующим мониторам",
"Import/Export Backup": "Import/Export Backup", Export: "Экспорт",
Export: "Export", Import: "Импорт",
Import: "Import", backupDescription: "Вы можете сохранить резервную копию всех мониторов и уведомлений в виде JSON-файла",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription2: "P.S.: История и события сохранены не будут.",
backupDescription2: "PS: History and event data is not included.", backupDescription3: "Важные данные, такие как токены уведомлений, добавляются при экспорте, поэтому храните файлы в безопасном месте.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", alertNoFile: "Выберите файл для импорта.",
alertNoFile: "Please select a file to import.", alertWrongFileType: "Выберите JSON-файл.",
alertWrongFileType: "Please select a JSON file.", twoFAVerifyLabel: "Пожалуйста, введите свой токен, чтобы проверить работу 2FA",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working", tokenValidSettingsMsg: "Токен действителен! Теперь вы можете сохранить настройки 2FA.",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.", confirmEnableTwoFAMsg: "Вы действительно хотите включить 2FA?",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?", confirmDisableTwoFAMsg: "Вы действительно хотите выключить 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?", "Apply on all existing monitors": "Применить ко всем существующим мониторам",
"Apply on all existing monitors": "Apply on all existing monitors", "Verify Token": "Проверить токен",
"Verify Token": "Verify Token", "Setup 2FA": "Настройка 2FA",
"Setup 2FA": "Setup 2FA", "Enable 2FA": "Включить 2FA",
"Enable 2FA": "Enable 2FA", "Disable 2FA": "Выключить 2FA",
"Disable 2FA": "Disable 2FA", "2FA Settings": "Настройки 2FA",
"2FA Settings": "2FA Settings", "Two Factor Authentication": "Двухфакторная аутентификация",
"Two Factor Authentication": "Two Factor Authentication", Active: "Активно",
Active: "Active", Inactive: "Неактивно",
Inactive: "Inactive", Token: "Токен",
Token: "Token", "Show URI": "Показать URI",
"Show URI": "Show URI", "Clear all statistics": "Очистить всю статистику",
"Clear all statistics": "Clear all Statistics", retryCheckEverySecond: "Повторять каждые {0} секунд.",
importHandleDescription: "Выберите 'Пропустить существующие' если вы хотите пропустить каждый монитор или уведомление с таким же именем. 'Перезаписать' удалит каждый существующий монитор или уведомление.",
confirmImportMsg: "Вы действительно хотите восстановить резервную копию? Убедитесь, что вы выбрали подходящий вариант импорта.",
"Heartbeat Retry Interval": "Интервал повтора опроса",
"Import Backup": "Импорт резервной копии",
"Export Backup": "Экспорт резервной копии",
"Skip existing": "Пропустить существующие",
Overwrite: "Перезаписать",
Options: "Опции",
"Keep both": "Оставить оба",
Tags: "Теги",
"Add New below or Select...": "Добавить новое ниже или выбрать...",
"Tag with this name already exist.": "Такой тег уже существует.",
"Tag with this value already exist.": "Тег с таким значением уже существует.",
color: "цвет",
"value (optional)": "значение (опционально)",
Gray: "Серый",
Red: "Красный",
Orange: "Оранжевый",
Green: "Зелёный",
Blue: "Синий",
Indigo: "Индиго",
Purple: "Пурпурный",
Pink: "Розовый",
"Search...": "Поиск...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

29
src/languages/sr-latn.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "Srpski", languageName: "Srpski",
checkEverySecond: "Proveri svakih {0} sekundi.", checkEverySecond: "Proveri svakih {0} sekundi.",
"Avg.": "Prosečni",
retriesDescription: "Maksimum pokušaja pre nego što se servis obeleži kao neaktivan i pošalje se obaveštenje.", retriesDescription: "Maksimum pokušaja pre nego što se servis obeleži kao neaktivan i pošalje se obaveštenje.",
ignoreTLSError: "Ignoriši TLS/SSL greške za HTTPS veb stranice.", ignoreTLSError: "Ignoriši TLS/SSL greške za HTTPS veb stranice.",
upsideDownModeDescription: "Obrnite status. Ako je servis dostupan, onda je obeležen kao neaktivan.", upsideDownModeDescription: "Obrnite status. Ako je servis dostupan, onda je obeležen kao neaktivan.",
@ -120,7 +119,6 @@ export default {
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled", "Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "You can backup all monitors and all notifications into a JSON file.",
@ -144,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

29
src/languages/sr.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "Српски", languageName: "Српски",
checkEverySecond: "Провери сваких {0} секунди.", checkEverySecond: "Провери сваких {0} секунди.",
"Avg.": "Просечни",
retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.", retriesDescription: "Максимум покушаја пре него што се сервис обележи као неактиван и пошаље се обавештење.",
ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.", ignoreTLSError: "Игнориши TLS/SSL грешке за HTTPS веб странице.",
upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.", upsideDownModeDescription: "Обрните статус. Ако је сервис доступан, онда је обележен као неактиван.",
@ -120,7 +119,6 @@ export default {
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled", "Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "You can backup all monitors and all notifications into a JSON file.",
@ -144,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

29
src/languages/sv-SE.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "Svenska", languageName: "Svenska",
checkEverySecond: "Uppdatera var {0} sekund.", checkEverySecond: "Uppdatera var {0} sekund.",
"Avg.": "Genomsnittligt",
retriesDescription: "Max antal försök innan tjänsten markeras som nere och en notis skickas", retriesDescription: "Max antal försök innan tjänsten markeras som nere och en notis skickas",
ignoreTLSError: "Ignorera TLS/SSL-fel för webbsidor med HTTPS", ignoreTLSError: "Ignorera TLS/SSL-fel för webbsidor med HTTPS",
upsideDownModeDescription: "Vänd upp och ner på statusen. Om tjänsten är nåbar visas den som NERE.", upsideDownModeDescription: "Vänd upp och ner på statusen. Om tjänsten är nåbar visas den som NERE.",
@ -120,7 +119,6 @@ export default {
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.", enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled", "Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors", "Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export", Export: "Export",
Import: "Import", Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.", backupDescription: "You can backup all monitors and all notifications into a JSON file.",
@ -144,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

171
src/languages/tr-TR.js

@ -0,0 +1,171 @@
export default {
languageName: "Türkçe",
checkEverySecond: "{0} Saniyede bir kontrol et.",
retriesDescription: "Servisin kapalı olarak işaretlenmeden ve bir bildirim gönderilmeden önce maksimum yeniden deneme sayısı",
ignoreTLSError: "HTTPS web siteleri için TLS/SSL hatasını yoksay",
upsideDownModeDescription: "Servisin durumunu tersine çevirir. Servis çalışıyorsa kapalı olarak işaretler.",
maxRedirectDescription: "İzlenecek maksimum yönlendirme sayısı. Yönlendirmeleri devre dışı bırakmak için 0'a ayarlayın.",
acceptedStatusCodesDescription: "Servisin çalıştığını hangi durum kodları belirlesin?",
passwordNotMatchMsg: "Şifre eşleşmiyor.",
notificationDescription: "Servislerin bildirim gönderebilmesi için bir bildirim yöntemi belirleyin.",
keywordDescription: "Anahtar kelimeyi düz html veya JSON yanıtında arayın ve büyük/küçük harfe duyarlıdır",
pauseDashboardHome: "Durdur",
deleteMonitorMsg: "Servisi silmek istediğinden emin misin?",
deleteNotificationMsg: "Bu bildirimi tüm servisler için silmek istediğinden emin misin?",
resoverserverDescription: "Cloudflare varsayılan sunucudur, çözümleyici sunucusunu istediğiniz zaman değiştirebilirsiniz.",
rrtypeDescription: "İzlemek istediğiniz servisin RR-Tipini seçin",
pauseMonitorMsg: "Durdurmak istediğinden emin misin?",
clearEventsMsg: "Bu servisin bütün kayıtlarını silmek istediğinden emin misin?",
clearHeartbeatsMsg: "Bu servis için tüm sağlık durumunu silmek istediğinden emin misin?",
confirmClearStatisticsMsg: "Tüm istatistikleri silmek istediğinden emin misin?",
Settings: "Ayarlar",
Dashboard: "Panel",
"New Update": "Yeni Güncelleme",
Language: "Dil",
Appearance: "Görünüm",
Theme: "Tema",
General: "Genel",
Version: "Versiyon",
"Check Update On GitHub": "GitHub'da Güncellemeyi Kontrol Edin",
List: "Liste",
Add: "Ekle",
"Add New Monitor": "Yeni Servis Ekle",
"Quick Stats": "Servis istatistikleri",
Up: "Normal",
Down: "Hatalı",
Pending: "Bekliyor",
Unknown: "Bilinmeyen",
Pause: "Durdur",
Name: "Servis ismi",
Status: "Durum",
DateTime: "Zaman",
Message: "Mesaj",
"No important events": "Önemli olay yok",
Resume: "Devam et",
Edit: "Düzenle",
Delete: "Sil",
Current: "Şu anda",
Uptime: "Çalışma zamanı",
"Cert Exp.": "Sertifika Süresi",
days: "günler",
day: "gün",
"-day": "-gün",
hour: "saat",
"-hour": "-saat",
Response: "Cevap Süresi",
Ping: "Ping",
"Monitor Type": "Servis Tipi",
Keyword: "Anahtar Kelime",
"Friendly Name": "Panelde görünecek isim",
URL: "URL",
Hostname: "IP Adresi",
Port: "Port",
"Heartbeat Interval": "Servis Test Aralığı",
Retries: "Yeniden deneme",
Advanced: "Gelişmiş",
"Upside Down Mode": "Ters/Düz Modu",
"Max. Redirects": "Maksimum Yönlendirme",
"Accepted Status Codes": "Kabul Edilen Durum Kodları",
Save: "Kaydet",
Notifications: "Bildirimler",
"Not available, please setup.": "Atanmış bildirim yöntemi yok. Ayarlardan belirleyebilirsiniz.",
"Setup Notification": "Bildirim yöntemi kur",
Light: "Açık",
Dark: "Koyu",
Auto: "Oto",
"Theme - Heartbeat Bar": "Servis Bar Konumu",
Normal: "Normal",
Bottom: "Aşağıda",
None: "Gösterme",
Timezone: "Zaman Dilimi",
"Search Engine Visibility": "Arama Motoru Görünürlüğü",
"Allow indexing": "İndekslemeye izin ver",
"Discourage search engines from indexing site": "İndekslemeyi reddet",
"Change Password": "Şifre Değiştir",
"Current Password": "Şuan ki Şifre",
"New Password": "Yeni Şifre",
"Repeat New Password": "Yeni Şifreyi Tekrar Girin",
"Update Password": "Şifreyi Değiştir",
"Disable Auth": "Şifreli girişi iptal et.",
"Enable Auth": "Şifreli girişi aktif et.",
Logout: "Çıkış yap",
Leave: "Ayrıl",
"I understand, please disable": "Evet farkındayım, iptal et",
Confirm: "Onayla",
Yes: "Evet",
No: "Hayır",
Username: "Kullanıcı Adı",
Password: "Şifre",
"Remember me": "Beni Hatırla",
Login: "Giriş yap",
"No Monitors, please": "Servis yok, lütfen",
"add one": "bir servis ekleyin",
"Notification Type": "Bildirim Yöntemi",
Email: "E-mail",
Test: "Test",
"Certificate Info": "Sertifika Bilgisi",
"Resolver Server": "Çözümleyici Sunucu",
"Resource Record Type": "Kaynak Kayıt Türü",
"Last Result": "En son sonuçlar",
"Create your admin account": "Yönetici hesabınızı oluşturun",
"Repeat Password": "Şifrenizi tekrar girin",
respTime: "Cevap Süresi (ms)",
notAvailableShort: "N/A",
Create: "Yarat",
"Clear Data": "Verileri Temizle",
Events: "Olaylar",
Heartbeats: "Sağlık Durumları",
"Auto Get": "Otomatik Al",
retryCheckEverySecond: "Retry every {0} seconds.",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
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.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
Export: "Export",
Import: "Import",
"Default enabled": "Default enabled",
"Apply on all existing monitors": "Apply on all existing monitors",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
"Clear all statistics": "Clear all Statistics",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
}

30
src/languages/zh-CN.js

@ -1,7 +1,6 @@
export default { export default {
languageName: "简体中文", languageName: "简体中文",
checkEverySecond: "检测频率 {0} 秒", checkEverySecond: "检测频率 {0} 秒",
"Avg.": "平均",
retriesDescription: "最大重试失败次数", retriesDescription: "最大重试失败次数",
ignoreTLSError: "忽略HTTPS站点的证书错误", ignoreTLSError: "忽略HTTPS站点的证书错误",
upsideDownModeDescription: "反向状态监控(状态码范围外为有效状态,反之为无效)", upsideDownModeDescription: "反向状态监控(状态码范围外为有效状态,反之为无效)",
@ -119,7 +118,7 @@ export default {
"Auto Get": "自动获取", "Auto Get": "自动获取",
enableDefaultNotificationDescription: "新的监控项将默认启用,你也可以在每个监控项中分别设置", enableDefaultNotificationDescription: "新的监控项将默认启用,你也可以在每个监控项中分别设置",
"Default enabled": "默认开启", "Default enabled": "默认开启",
"Import/Export Backup": "导入/导出备份", "Also apply to existing monitors": "应用到所有监控项",
Export: "导出", Export: "导出",
Import: "导入", Import: "导入",
backupDescription: "你可以将所有的监控项和消息通知备份到一个 JSON 文件中", backupDescription: "你可以将所有的监控项和消息通知备份到一个 JSON 文件中",
@ -143,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "Show URI", "Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics", "Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

30
src/languages/zh-HK.js

@ -36,7 +36,6 @@ export default {
hour: "小時", hour: "小時",
"-hour": "小時", "-hour": "小時",
checkEverySecond: "每 {0} 秒檢查一次", checkEverySecond: "每 {0} 秒檢查一次",
"Avg.": "平均",
Response: "反應時間", Response: "反應時間",
Ping: "反應時間", Ping: "反應時間",
"Monitor Type": "監測器類型", "Monitor Type": "監測器類型",
@ -119,7 +118,7 @@ export default {
"Auto Get": "自動獲取", "Auto Get": "自動獲取",
enableDefaultNotificationDescription: "新增監測器時這個通知會預設啟用,當然每個監測器亦可分別控制開關。", enableDefaultNotificationDescription: "新增監測器時這個通知會預設啟用,當然每個監測器亦可分別控制開關。",
"Default enabled": "預設通知", "Default enabled": "預設通知",
"Import/Export Backup": "匯入/匯出 備份", "Also apply to existing monitors": "同時取用至目前所有監測器",
Export: "匯出", Export: "匯出",
Import: "匯入", Import: "匯入",
backupDescription: "您可以備份所有監測器及所有通知。", backupDescription: "您可以備份所有監測器及所有通知。",
@ -143,4 +142,31 @@ export default {
Token: "Token", Token: "Token",
"Show URI": "顯示 URI", "Show URI": "顯示 URI",
"Clear all statistics": "清除所有歷史記錄", "Clear all statistics": "清除所有歷史記錄",
retryCheckEverySecond: "Retry every {0} seconds.",
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.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"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 value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
} }

125
src/main.js

@ -1,135 +1,18 @@
import "bootstrap"; import "bootstrap";
import { createApp, h } from "vue"; import { createApp, h } from "vue";
import { createI18n } from "vue-i18n"
import { createRouter, createWebHistory } from "vue-router";
import Toast from "vue-toastification"; import Toast from "vue-toastification";
import "vue-toastification/dist/index.css"; import "vue-toastification/dist/index.css";
import App from "./App.vue"; import App from "./App.vue";
import "./assets/app.scss"; import "./assets/app.scss";
import { i18n } from "./i18n";
import { FontAwesomeIcon } from "./icon.js"; import { FontAwesomeIcon } from "./icon.js";
import EmptyLayout from "./layouts/EmptyLayout.vue"; import datetime from "./mixins/datetime";
import Layout from "./layouts/Layout.vue"; import mobile from "./mixins/mobile";
import socket from "./mixins/socket"; import socket from "./mixins/socket";
import theme from "./mixins/theme"; import theme from "./mixins/theme";
import mobile from "./mixins/mobile"; import { router } from "./router";
import datetime from "./mixins/datetime";
import Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue";
import Details from "./pages/Details.vue";
import EditMonitor from "./pages/EditMonitor.vue";
import Settings from "./pages/Settings.vue";
import Setup from "./pages/Setup.vue";
import List from "./pages/List.vue";
import { appName } from "./util.ts"; import { appName } from "./util.ts";
import en from "./languages/en";
import zhHK from "./languages/zh-HK";
import deDE from "./languages/de-DE";
import nlNL from "./languages/nl-NL";
import esEs from "./languages/es-ES";
import frFR from "./languages/fr-FR";
import itIT from "./languages/it-IT";
import ja from "./languages/ja";
import daDK from "./languages/da-DK";
import sr from "./languages/sr";
import srLatn from "./languages/sr-latn";
import svSE from "./languages/sv-SE";
import koKR from "./languages/ko-KR";
import ruRU from "./languages/ru-RU";
import zhCN from "./languages/zh-CN";
import pl from "./languages/pl"
import etEE from "./languages/et-EE"
const routes = [
{
path: "/",
component: Layout,
children: [
{
name: "root",
path: "",
component: Dashboard,
children: [
{
name: "DashboardHome",
path: "/dashboard",
component: DashboardHome,
children: [
{
path: "/dashboard/:id",
component: EmptyLayout,
children: [
{
path: "",
component: Details,
},
{
path: "/edit/:id",
component: EditMonitor,
},
],
},
{
path: "/add",
component: EditMonitor,
},
{
path: "/list",
component: List,
},
],
},
{
path: "/settings",
component: Settings,
},
],
},
],
},
{
path: "/setup",
component: Setup,
},
]
const router = createRouter({
linkActiveClass: "active",
history: createWebHistory(),
routes,
})
const languageList = {
en,
"zh-HK": zhHK,
"de-DE": deDE,
"nl-NL": nlNL,
"es-ES": esEs,
"fr-FR": frFR,
"it-IT": itIT,
"ja": ja,
"da-DK": daDK,
"sr": sr,
"sr-latn": srLatn,
"sv-SE": svSE,
"ko-KR": koKR,
"ru-RU": ruRU,
"zh-CN": zhCN,
"pl": pl,
"et-EE": etEE,
};
const i18n = createI18n({
locale: localStorage.locale || "en",
fallbackLocale: "en",
silentFallbackWarn: true,
silentTranslationWarn: true,
messages: languageList
});
const app = createApp({ const app = createApp({
mixins: [ mixins: [
socket, socket,

8
src/mixins/socket.js

@ -266,6 +266,10 @@ export default {
socket.emit("twoFAStatus", callback) socket.emit("twoFAStatus", callback)
}, },
getMonitorList(callback) {
socket.emit("getMonitorList", callback)
},
add(monitor, callback) { add(monitor, callback) {
socket.emit("add", monitor, callback) socket.emit("add", monitor, callback)
}, },
@ -280,8 +284,8 @@ export default {
this.importantHeartbeatList = {} this.importantHeartbeatList = {}
}, },
uploadBackup(uploadedJSON, callback) { uploadBackup(uploadedJSON, importHandle, callback) {
socket.emit("uploadBackup", uploadedJSON, callback) socket.emit("uploadBackup", uploadedJSON, importHandle, callback)
}, },
clearEvents(monitorID, callback) { clearEvents(monitorID, callback) {

38
src/pages/Details.vue

@ -2,6 +2,9 @@
<transition name="slide-fade" appear> <transition name="slide-fade" appear>
<div v-if="monitor"> <div v-if="monitor">
<h1> {{ monitor.name }}</h1> <h1> {{ monitor.name }}</h1>
<div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
</div>
<p class="url"> <p class="url">
<a v-if="monitor.type === 'http'" :href="monitor.url" target="_blank">{{ monitor.url }}</a> <a v-if="monitor.type === 'http'" :href="monitor.url" target="_blank">{{ monitor.url }}</a>
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span> <span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
@ -42,7 +45,7 @@
<div class="shadow-box big-padding text-center stats"> <div class="shadow-box big-padding text-center stats">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h4>{{ pingTitle }}</h4> <h4>{{ pingTitle() }}</h4>
<p>({{ $t("Current") }})</p> <p>({{ $t("Current") }})</p>
<span class="num"> <span class="num">
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox"> <a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
@ -51,7 +54,7 @@
</span> </span>
</div> </div>
<div class="col"> <div class="col">
<h4>{{ $t("Avg.") }} {{ pingTitle }}</h4> <h4>{{ pingTitle(true) }}</h4>
<p>(24{{ $t("-hour") }})</p> <p>(24{{ $t("-hour") }})</p>
<span class="num"><CountUp :value="avgPing" /></span> <span class="num"><CountUp :value="avgPing" /></span>
</div> </div>
@ -209,6 +212,7 @@ import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue"; import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3"; import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue")); const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
import Tag from "../components/Tag.vue";
export default { export default {
components: { components: {
@ -220,6 +224,7 @@ export default {
Status, Status,
Pagination, Pagination,
PingChart, PingChart,
Tag,
}, },
data() { data() {
return { return {
@ -231,14 +236,6 @@ export default {
} }
}, },
computed: { computed: {
pingTitle() {
if (this.monitor.type === "http") {
return this.$t("Response");
}
return this.$t("Ping");
},
monitor() { monitor() {
let id = this.$route.params.id let id = this.$route.params.id
return this.$root.monitorList[id]; return this.$root.monitorList[id];
@ -369,6 +366,19 @@ export default {
} }
}) })
}, },
pingTitle(average = false) {
let translationPrefix = ""
if (average) {
translationPrefix = "Avg. "
}
if (this.monitor.type === "http") {
return this.$t(translationPrefix + "Response");
}
return this.$t(translationPrefix + "Ping");
},
}, },
} }
</script> </script>
@ -499,4 +509,12 @@ table {
} }
} }
.tags {
margin-bottom: 0.5rem;
}
.tags > div:first-child {
margin-left: 0 !important;
}
</style> </style>

59
src/pages/EditMonitor.vue

@ -39,7 +39,7 @@
<!-- TCP Port / Ping / DNS only --> <!-- TCP Port / Ping / DNS only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' " class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' " 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" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="ipRegexPattern || hostnameRegexPattern" required>
</div> </div>
<!-- For TCP Port Type --> <!-- For TCP Port Type -->
@ -95,6 +95,14 @@
</div> </div>
</div> </div>
<div class="my-3">
<label for="retry-interval" class="form-label">
{{ $t("Heartbeat Retry Interval") }}
<span>({{ $t("retryCheckEverySecond", [ monitor.retryInterval ]) }})</span>
</label>
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required min="20" step="1">
</div>
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2> <h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http'" class="my-3 form-check"> <div v-if="monitor.type === 'http'" class="my-3 form-check">
@ -134,6 +142,10 @@
</div> </div>
</template> </template>
<div class="my-3">
<tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager>
</div>
<div class="mt-5 mb-1"> <div class="mt-5 mb-1">
<button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button> <button class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
</div> </div>
@ -173,6 +185,7 @@
<script> <script>
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import TagsManager from "../components/TagsManager.vue";
import MonitorCheckEditor from "../components/MonitorCheckEditor.vue"; import MonitorCheckEditor from "../components/MonitorCheckEditor.vue";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification"
import VueMultiselect from "vue-multiselect" import VueMultiselect from "vue-multiselect"
@ -182,6 +195,7 @@ const toast = useToast()
export default { export default {
components: { components: {
NotificationDialog, NotificationDialog,
TagsManager,
MonitorCheckEditor, MonitorCheckEditor,
VueMultiselect, VueMultiselect,
}, },
@ -199,6 +213,9 @@ export default {
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/ // Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
// eslint-disable-next-line // eslint-disable-next-line
ipRegexPattern: "((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))", ipRegexPattern: "((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))",
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
// eslint-disable-next-line
hostnameRegexPattern: "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$"
} }
}, },
@ -227,6 +244,12 @@ export default {
"$route.fullPath"() { "$route.fullPath"() {
this.init(); this.init();
}, },
"monitor.interval"(value, oldValue) {
// Link interval and retryInerval if they are the same value.
if (this.monitor.retryInterval === oldValue) {
this.monitor.retryInterval = value;
}
}
}, },
mounted() { mounted() {
this.init(); this.init();
@ -268,6 +291,7 @@ export default {
name: "", name: "",
url: "https://", url: "https://",
interval: 60, interval: 60,
retryInterval: this.interval,
maxretries: 0, maxretries: 0,
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,
@ -305,25 +329,32 @@ export default {
this.monitor.checks = this.monitor.checks.splice(index, 1); this.monitor.checks = this.monitor.checks.splice(index, 1);
}, },
submit() { async submit() {
this.processing = true; this.processing = true;
if (this.isAdd) { if (this.isAdd) {
this.$root.add(this.monitor, (res) => { this.$root.add(this.monitor, async (res) => {
this.processing = false;
if (res.ok) { if (res.ok) {
await this.$refs.tagsManager.submit(res.monitorID);
toast.success(res.msg); toast.success(res.msg);
this.processing = false;
this.$root.getMonitorList();
this.$router.push("/dashboard/" + res.monitorID) this.$router.push("/dashboard/" + res.monitorID)
} else { } else {
toast.error(res.msg); toast.error(res.msg);
this.processing = false;
} }
}) })
} else { } else {
await this.$refs.tagsManager.submit(this.monitor.id);
this.$root.getSocket().emit("editMonitor", this.monitor, (res) => { this.$root.getSocket().emit("editMonitor", this.monitor, (res) => {
this.processing = false; this.processing = false;
this.$root.toastRes(res) this.$root.toastRes(res);
this.init();
}) })
} }
}, },
@ -345,6 +376,8 @@ export default {
.multiselect__tags { .multiselect__tags {
border-radius: 1.5rem; border-radius: 1.5rem;
border: 1px solid #ced4da; border: 1px solid #ced4da;
min-height: 38px;
padding: 6px 40px 0 8px;
} }
.multiselect--active .multiselect__tags { .multiselect--active .multiselect__tags {
@ -361,9 +394,25 @@ export default {
.multiselect__tag { .multiselect__tag {
border-radius: 50rem; border-radius: 50rem;
margin-bottom: 0;
padding: 6px 26px 6px 10px;
background: $primary !important; background: $primary !important;
} }
.multiselect__placeholder {
font-size: 1rem;
padding-left: 6px;
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
opacity: 0.67;
}
.multiselect__input, .multiselect__single {
line-height: 14px;
margin-bottom: 0;
}
.dark { .dark {
.multiselect__tag { .multiselect__tag {
color: $dark-font-color2; color: $dark-font-color2;

70
src/pages/Settings.vue

@ -120,35 +120,61 @@
</form> </form>
</template> </template>
<h2 class="mt-5 mb-2"> <div v-if="! settings.disableAuth" class="mt-5 mb-3">
<h2 class="mb-2">
{{ $t("Two Factor Authentication") }} {{ $t("Two Factor Authentication") }}
</h2> </h2>
<div class="mb-3">
<button class="btn btn-primary me-2" type="button" @click="$refs.TwoFADialog.show()">{{ $t("2FA Settings") }}</button> <button class="btn btn-primary me-2" type="button" @click="$refs.TwoFADialog.show()">{{ $t("2FA Settings") }}</button>
</div> </div>
<h2 class="mt-5 mb-2">{{ $t("Import/Export Backup") }}</h2> <h2 class="mt-5 mb-2">{{ $t("Export Backup") }}</h2>
<p> <p>
{{ $t("backupDescription") }} <br /> {{ $t("backupDescription") }} <br />
({{ $t("backupDescription2") }}) <br /> ({{ $t("backupDescription2") }}) <br />
</p> </p>
<div class="input-group mb-3"> <div class="mb-2">
<button class="btn btn-outline-primary" @click="downloadBackup">{{ $t("Export") }}</button> <button class="btn btn-primary" @click="downloadBackup">{{ $t("Export") }}</button>
<button type="button" class="btn btn-outline-primary" :disabled="processing" @click="importBackup"> </div>
<p><strong>{{ $t("backupDescription3") }}</strong></p>
<h2 class="mt-5 mb-2">{{ $t("Import Backup") }}</h2>
<label class="form-label">{{ $t("Options") }}:</label>
<br>
<div class="form-check form-check-inline">
<input id="radioKeep" v-model="importHandle" class="form-check-input" type="radio" name="radioImportHandle" value="keep">
<label class="form-check-label" for="radioKeep">{{ $t("Keep both") }}</label>
</div>
<div class="form-check form-check-inline">
<input id="radioSkip" v-model="importHandle" class="form-check-input" type="radio" name="radioImportHandle" value="skip">
<label class="form-check-label" for="radioSkip">{{ $t("Skip existing") }}</label>
</div>
<div class="form-check form-check-inline">
<input id="radioOverwrite" v-model="importHandle" class="form-check-input" type="radio" name="radioImportHandle" value="overwrite">
<label class="form-check-label" for="radioOverwrite">{{ $t("Overwrite") }}</label>
</div>
<div class="form-text mb-2">
{{ $t("importHandleDescription") }}
</div>
<div class="mb-2">
<input id="importBackup" type="file" class="form-control" accept="application/json">
</div>
<div class="input-group mb-2 justify-content-end">
<button type="button" class="btn btn-outline-primary" :disabled="processing" @click="confirmImport">
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div> <div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Import") }} {{ $t("Import") }}
</button> </button>
<input id="importBackup" type="file" class="form-control" accept="application/json">
</div> </div>
<div v-if="importAlert" class="alert alert-danger mt-3" style="padding: 6px 16px;"> <div v-if="importAlert" class="alert alert-danger mt-3" style="padding: 6px 16px;">
{{ importAlert }} {{ importAlert }}
</div> </div>
<p><strong>{{ $t("backupDescription3") }}</strong></p>
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2> <h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div class="mb-3"> <div class="mb-3">
@ -233,6 +259,12 @@
<p>Molim Vas koristite ovo sa pažnjom.</p> <p>Molim Vas koristite ovo sa pažnjom.</p>
</template> </template>
<template v-else-if="$i18n.locale === 'tr-TR' ">
<p><strong>Şifreli girişi devre dışı bırakmak istediğinizden</strong>emin misiniz?</p>
<p>Bu, Uptime Kuma'nın önünde Cloudflare Access gibi <strong>üçüncü taraf yetkilendirmesi olan</strong> kişiler içindir.</p>
<p>Lütfen dikkatli kullanın.</p>
</template>
<template v-else-if="$i18n.locale === 'ko-KR' "> <template v-else-if="$i18n.locale === 'ko-KR' ">
<p>정말로 <strong>인증 기능을 끌까요</strong>?</p> <p>정말로 <strong>인증 기능을 끌까요</strong>?</p>
<p> 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong> Uptime Kuma 앞에 사용자를 위한 기능이에요.</p> <p> 기능은 <strong>Cloudflare Access와 같은 서드파티 인증</strong> Uptime Kuma 앞에 사용자를 위한 기능이에요.</p>
@ -257,6 +289,12 @@
<p>Utilizzare con attenzione.</p> <p>Utilizzare con attenzione.</p>
</template> </template>
<template v-else-if="$i18n.locale === 'ru-RU' ">
<p>Вы уверены, что хотите <strong>отключить авторизацию</strong>?</p>
<p>Это подходит для <strong>тех, у кого стоит другая авторизация</strong> перед открытием Uptime Kuma, например Cloudflare Access.</p>
<p>Пожалуйста, используйте с осторожностью.</p>
</template>
<!-- English (en) --> <!-- English (en) -->
<template v-else> <template v-else>
<p>Are you sure want to <strong>disable auth</strong>?</p> <p>Are you sure want to <strong>disable auth</strong>?</p>
@ -268,6 +306,9 @@
<Confirm ref="confirmClearStatistics" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearStatistics"> <Confirm ref="confirmClearStatistics" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="clearStatistics">
{{ $t("confirmClearStatisticsMsg") }} {{ $t("confirmClearStatisticsMsg") }}
</Confirm> </Confirm>
<Confirm ref="confirmImport" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="importBackup">
{{ $t("confirmImportMsg") }}
</Confirm>
</div> </div>
</transition> </transition>
</template> </template>
@ -308,6 +349,7 @@ export default {
}, },
loaded: false, loaded: false,
importAlert: null, importAlert: null,
importHandle: "skip",
processing: false, processing: false,
} }
}, },
@ -374,6 +416,10 @@ export default {
this.$refs.confirmClearStatistics.show(); this.$refs.confirmClearStatistics.show();
}, },
confirmImport() {
this.$refs.confirmImport.show();
},
disableAuth() { disableAuth() {
this.settings.disableAuth = true; this.settings.disableAuth = true;
this.saveSettings(); this.saveSettings();
@ -396,7 +442,7 @@ export default {
} }
exportData = JSON.stringify(exportData, null, 4); exportData = JSON.stringify(exportData, null, 4);
let downloadItem = document.createElement("a"); let downloadItem = document.createElement("a");
downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURI(exportData)); downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURIComponent(exportData));
downloadItem.setAttribute("download", fileName); downloadItem.setAttribute("download", fileName);
downloadItem.click(); downloadItem.click();
}, },
@ -419,7 +465,7 @@ export default {
fileReader.readAsText(uploadItem.item(0)); fileReader.readAsText(uploadItem.item(0));
fileReader.onload = item => { fileReader.onload = item => {
this.$root.uploadBackup(item.target.result, (res) => { this.$root.uploadBackup(item.target.result, this.importHandle, (res) => {
this.processing = false; this.processing = false;
if (res.ok) { if (res.ok) {

71
src/router.js

@ -0,0 +1,71 @@
import { createRouter, createWebHistory } from "vue-router";
import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue";
import Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue";
import Details from "./pages/Details.vue";
import EditMonitor from "./pages/EditMonitor.vue";
import List from "./pages/List.vue";
import Settings from "./pages/Settings.vue";
import Setup from "./pages/Setup.vue";
const routes = [
{
path: "/",
component: Layout,
children: [
{
name: "root",
path: "",
component: Dashboard,
children: [
{
name: "DashboardHome",
path: "/dashboard",
component: DashboardHome,
children: [
{
path: "/dashboard/:id",
component: EmptyLayout,
children: [
{
path: "",
component: Details,
},
{
path: "/edit/:id",
component: EditMonitor,
},
],
},
{
path: "/add",
component: EditMonitor,
},
{
path: "/list",
component: List,
},
],
},
{
path: "/settings",
component: Settings,
},
],
},
],
},
{
path: "/setup",
component: Setup,
},
]
export const router = createRouter({
linkActiveClass: "active",
history: createWebHistory(),
routes,
});

368
src/util-frontend.js

@ -1,6 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone"; import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import timezones from "timezones-list";
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@ -16,376 +17,21 @@ function getTimezoneOffset(timeZone) {
return -offset; return -offset;
} }
// From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
// TODO: Move to separate file
const aryIannaTimeZones = [
"Europe/Andorra",
"Asia/Dubai",
"Asia/Kabul",
"Europe/Tirane",
"Asia/Yerevan",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/Mawson",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"America/Argentina/Buenos_Aires",
"America/Argentina/Cordoba",
"America/Argentina/Salta",
"America/Argentina/Jujuy",
"America/Argentina/Tucuman",
"America/Argentina/Catamarca",
"America/Argentina/La_Rioja",
"America/Argentina/San_Juan",
"America/Argentina/Mendoza",
"America/Argentina/San_Luis",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Ushuaia",
"Pacific/Pago_Pago",
"Europe/Vienna",
"Australia/Lord_Howe",
"Antarctica/Macquarie",
"Australia/Hobart",
"Australia/Currie",
"Australia/Melbourne",
"Australia/Sydney",
"Australia/Broken_Hill",
"Australia/Brisbane",
"Australia/Lindeman",
"Australia/Adelaide",
"Australia/Darwin",
"Australia/Perth",
"Australia/Eucla",
"Asia/Baku",
"America/Barbados",
"Asia/Dhaka",
"Europe/Brussels",
"Europe/Sofia",
"Atlantic/Bermuda",
"Asia/Brunei",
"America/La_Paz",
"America/Noronha",
"America/Belem",
"America/Fortaleza",
"America/Recife",
"America/Araguaina",
"America/Maceio",
"America/Bahia",
"America/Sao_Paulo",
"America/Campo_Grande",
"America/Cuiaba",
"America/Santarem",
"America/Porto_Velho",
"America/Boa_Vista",
"America/Manaus",
"America/Eirunepe",
"America/Rio_Branco",
"America/Nassau",
"Asia/Thimphu",
"Europe/Minsk",
"America/Belize",
"America/St_Johns",
"America/Halifax",
"America/Glace_Bay",
"America/Moncton",
"America/Goose_Bay",
"America/Blanc-Sablon",
"America/Toronto",
"America/Nipigon",
"America/Thunder_Bay",
"America/Iqaluit",
"America/Pangnirtung",
"America/Atikokan",
"America/Winnipeg",
"America/Rainy_River",
"America/Resolute",
"America/Rankin_Inlet",
"America/Regina",
"America/Swift_Current",
"America/Edmonton",
"America/Cambridge_Bay",
"America/Yellowknife",
"America/Inuvik",
"America/Creston",
"America/Dawson_Creek",
"America/Fort_Nelson",
"America/Vancouver",
"America/Whitehorse",
"America/Dawson",
"Indian/Cocos",
"Europe/Zurich",
"Africa/Abidjan",
"Pacific/Rarotonga",
"America/Santiago",
"America/Punta_Arenas",
"Pacific/Easter",
"Asia/Shanghai",
"Asia/Urumqi",
"America/Bogota",
"America/Costa_Rica",
"America/Havana",
"Atlantic/Cape_Verde",
"America/Curacao",
"Indian/Christmas",
"Asia/Nicosia",
"Asia/Famagusta",
"Europe/Prague",
"Europe/Berlin",
"Europe/Copenhagen",
"America/Santo_Domingo",
"Africa/Algiers",
"America/Guayaquil",
"Pacific/Galapagos",
"Europe/Tallinn",
"Africa/Cairo",
"Africa/El_Aaiun",
"Europe/Madrid",
"Africa/Ceuta",
"Atlantic/Canary",
"Europe/Helsinki",
"Pacific/Fiji",
"Atlantic/Stanley",
"Pacific/Chuuk",
"Pacific/Pohnpei",
"Pacific/Kosrae",
"Atlantic/Faroe",
"Europe/Paris",
"Europe/London",
"Asia/Tbilisi",
"America/Cayenne",
"Africa/Accra",
"Europe/Gibraltar",
"America/Godthab",
"America/Danmarkshavn",
"America/Scoresbysund",
"America/Thule",
"Europe/Athens",
"Atlantic/South_Georgia",
"America/Guatemala",
"Pacific/Guam",
"Africa/Bissau",
"America/Guyana",
"Asia/Hong_Kong",
"America/Tegucigalpa",
"America/Port-au-Prince",
"Europe/Budapest",
"Asia/Jakarta",
"Asia/Pontianak",
"Asia/Makassar",
"Asia/Jayapura",
"Europe/Dublin",
"Asia/Jerusalem",
"Asia/Kolkata",
"Indian/Chagos",
"Asia/Baghdad",
"Asia/Tehran",
"Atlantic/Reykjavik",
"Europe/Rome",
"America/Jamaica",
"Asia/Amman",
"Asia/Tokyo",
"Africa/Nairobi",
"Asia/Bishkek",
"Pacific/Tarawa",
"Pacific/Enderbury",
"Pacific/Kiritimati",
"Asia/Pyongyang",
"Asia/Seoul",
"Asia/Almaty",
"Asia/Qyzylorda",
"Asia/Aqtobe",
"Asia/Aqtau",
"Asia/Atyrau",
"Asia/Oral",
"Asia/Beirut",
"Asia/Colombo",
"Africa/Monrovia",
"Europe/Vilnius",
"Europe/Luxembourg",
"Europe/Riga",
"Africa/Tripoli",
"Africa/Casablanca",
"Europe/Monaco",
"Europe/Chisinau",
"Pacific/Majuro",
"Pacific/Kwajalein",
"Asia/Yangon",
"Asia/Ulaanbaatar",
"Asia/Hovd",
"Asia/Choibalsan",
"Asia/Macau",
"America/Martinique",
"Europe/Malta",
"Indian/Mauritius",
"Indian/Maldives",
"America/Mexico_City",
"America/Cancun",
"America/Merida",
"America/Monterrey",
"America/Matamoros",
"America/Mazatlan",
"America/Chihuahua",
"America/Ojinaga",
"America/Hermosillo",
"America/Tijuana",
"America/Bahia_Banderas",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Africa/Maputo",
"Africa/Windhoek",
"Pacific/Noumea",
"Pacific/Norfolk",
"Africa/Lagos",
"America/Managua",
"Europe/Amsterdam",
"Europe/Oslo",
"Asia/Kathmandu",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Auckland",
"Pacific/Chatham",
"America/Panama",
"America/Lima",
"Pacific/Tahiti",
"Pacific/Marquesas",
"Pacific/Gambier",
"Pacific/Port_Moresby",
"Pacific/Bougainville",
"Asia/Manila",
"Asia/Karachi",
"Europe/Warsaw",
"America/Miquelon",
"Pacific/Pitcairn",
"America/Puerto_Rico",
"Asia/Gaza",
"Asia/Hebron",
"Europe/Lisbon",
"Atlantic/Madeira",
"Atlantic/Azores",
"Pacific/Palau",
"America/Asuncion",
"Asia/Qatar",
"Indian/Reunion",
"Europe/Bucharest",
"Europe/Belgrade",
"Europe/Kaliningrad",
"Europe/Moscow",
"Europe/Simferopol",
"Europe/Kirov",
"Europe/Astrakhan",
"Europe/Volgograd",
"Europe/Saratov",
"Europe/Ulyanovsk",
"Europe/Samara",
"Asia/Yekaterinburg",
"Asia/Omsk",
"Asia/Novosibirsk",
"Asia/Barnaul",
"Asia/Tomsk",
"Asia/Novokuznetsk",
"Asia/Krasnoyarsk",
"Asia/Irkutsk",
"Asia/Chita",
"Asia/Yakutsk",
"Asia/Khandyga",
"Asia/Vladivostok",
"Asia/Ust-Nera",
"Asia/Magadan",
"Asia/Sakhalin",
"Asia/Srednekolymsk",
"Asia/Kamchatka",
"Asia/Anadyr",
"Asia/Riyadh",
"Pacific/Guadalcanal",
"Indian/Mahe",
"Africa/Khartoum",
"Europe/Stockholm",
"Asia/Singapore",
"America/Paramaribo",
"Africa/Juba",
"Africa/Sao_Tome",
"America/El_Salvador",
"Asia/Damascus",
"America/Grand_Turk",
"Africa/Ndjamena",
"Indian/Kerguelen",
"Asia/Bangkok",
"Asia/Dushanbe",
"Pacific/Fakaofo",
"Asia/Dili",
"Asia/Ashgabat",
"Africa/Tunis",
"Pacific/Tongatapu",
"Europe/Istanbul",
"America/Port_of_Spain",
"Pacific/Funafuti",
"Asia/Taipei",
"Europe/Kiev",
"Europe/Uzhgorod",
"Europe/Zaporozhye",
"Pacific/Wake",
"America/New_York",
"America/Detroit",
"America/Kentucky/Louisville",
"America/Kentucky/Monticello",
"America/Indiana/Indianapolis",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Vevay",
"America/Chicago",
"America/Indiana/Tell_City",
"America/Indiana/Knox",
"America/Menominee",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/North_Dakota/Beulah",
"America/Denver",
"America/Boise",
"America/Phoenix",
"America/Los_Angeles",
"America/Anchorage",
"America/Juneau",
"America/Sitka",
"America/Metlakatla",
"America/Yakutat",
"America/Nome",
"America/Adak",
"Pacific/Honolulu",
"America/Montevideo",
"Asia/Samarkand",
"Asia/Tashkent",
"America/Caracas",
"Asia/Ho_Chi_Minh",
"Pacific/Efate",
"Pacific/Wallis",
"Pacific/Apia",
"Africa/Johannesburg",
];
export function timezoneList() { export function timezoneList() {
let result = []; let result = [];
for (let timezone of aryIannaTimeZones) { for (let timezone of timezones) {
try { try {
let display = dayjs().tz(timezone).format("Z"); let display = dayjs().tz(timezone.tzCode).format("Z");
result.push({ result.push({
name: `(UTC${display}) ${timezone}`, name: `(UTC${display}) ${timezone.tzCode}`,
value: timezone, value: timezone.tzCode,
time: getTimezoneOffset(timezone), time: getTimezoneOffset(timezone.tzCode),
}) })
} catch (e) { } catch (e) {
console.error(e.message); console.log("Skip Timezone: " + timezone.tzCode);
console.log("Skip this timezone")
} }
} }
result.sort((a, b) => { result.sort((a, b) => {

10
test/ubuntu-nodejs16.dockerfile

@ -0,0 +1,10 @@
FROM ubuntu
WORKDIR /app
RUN apt update && apt --yes install git curl
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash -
RUN apt --yes install nodejs
RUN git clone https://github.com/louislam/uptime-kuma.git .
RUN npm run setup
# Option 1. Try it
RUN node server/server.js
Loading…
Cancel
Save