mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-10-01 01:25:45 -04:00
commit
ba4a4089eb
@ -2,8 +2,12 @@
|
|||||||
/dist
|
/dist
|
||||||
/node_modules
|
/node_modules
|
||||||
/data
|
/data
|
||||||
|
/out
|
||||||
|
/test
|
||||||
|
/kubernetes
|
||||||
/.do
|
/.do
|
||||||
**/.dockerignore
|
**/.dockerignore
|
||||||
|
/private
|
||||||
**/.git
|
**/.git
|
||||||
**/.gitignore
|
**/.gitignore
|
||||||
**/docker-compose*
|
**/docker-compose*
|
||||||
|
15
.eslintrc.js
15
.eslintrc.js
@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
commonjs: true,
|
commonjs: true,
|
||||||
@ -16,6 +17,7 @@ module.exports = {
|
|||||||
requireConfigFile: false,
|
requireConfigFile: false,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
"camelcase": ["warn", {
|
"camelcase": ["warn", {
|
||||||
"properties": "never",
|
"properties": "never",
|
||||||
"ignoreImports": true
|
"ignoreImports": true
|
||||||
@ -32,11 +34,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
quotes: ["warn", "double"],
|
quotes: ["warn", "double"],
|
||||||
//semi: ['off', 'never'],
|
semi: "warn",
|
||||||
"vue/html-indent": ["warn", 4], // default: 2
|
"vue/html-indent": ["warn", 4], // default: 2
|
||||||
"vue/max-attributes-per-line": "off",
|
"vue/max-attributes-per-line": "off",
|
||||||
"vue/singleline-html-element-content-newline": "off",
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
"vue/html-self-closing": "off",
|
"vue/html-self-closing": "off",
|
||||||
|
"vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly
|
||||||
"no-multi-spaces": ["error", {
|
"no-multi-spaces": ["error", {
|
||||||
ignoreEOLComments: true,
|
ignoreEOLComments: true,
|
||||||
}],
|
}],
|
||||||
@ -82,4 +85,12 @@ module.exports = {
|
|||||||
"one-var": ["error", "never"],
|
"one-var": ["error", "never"],
|
||||||
"max-statements-per-line": ["error", { "max": 1 }]
|
"max-statements-per-line": ["error", { "max": 1 }]
|
||||||
},
|
},
|
||||||
}
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [ "src/languages/*.js", "src/icon.js" ],
|
||||||
|
"rules": {
|
||||||
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
3
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
3
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
@ -12,6 +12,7 @@ Please search in Issues without filters: https://github.com/louislam/uptime-kuma
|
|||||||
**Info**
|
**Info**
|
||||||
Uptime Kuma Version:
|
Uptime Kuma Version:
|
||||||
Using Docker?: Yes/No
|
Using Docker?: Yes/No
|
||||||
|
Docker Version:
|
||||||
|
Node.js Version (Without Docker only):
|
||||||
OS:
|
OS:
|
||||||
Browser:
|
Browser:
|
||||||
|
|
||||||
|
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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,12 +24,13 @@ 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
|
||||||
- OS:
|
Docker Version:
|
||||||
- Browser:
|
Node.js Version (Without Docker only):
|
||||||
|
OS:
|
||||||
|
Browser:
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
@ -36,3 +38,5 @@ 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>`
|
||||||
|
PM2: `~/.pm2/logs/` (e.g. `/home/ubuntu/.pm2/logs`)
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -8,3 +8,6 @@ dist-ssr
|
|||||||
/data
|
/data
|
||||||
!/data/.gitkeep
|
!/data/.gitkeep
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
/private
|
||||||
|
/out
|
||||||
|
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
|||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
@ -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,22 +76,19 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
# Backend Dev
|
# Backend Dev
|
||||||
|
|
||||||
|
(2021-09-23 Update)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run start-server
|
npm run start-server-dev
|
||||||
|
|
||||||
# Or
|
|
||||||
|
|
||||||
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 +98,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 +106,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 +116,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 +128,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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
18
README.md
18
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.
|
||||||
@ -45,6 +44,9 @@ Browse to http://localhost:3001 after started.
|
|||||||
Required Tools: Node.js >= 14, git and pm2.
|
Required Tools: Node.js >= 14, git and pm2.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Update your npm to the latest version
|
||||||
|
npm install npm -g
|
||||||
|
|
||||||
git clone https://github.com/louislam/uptime-kuma.git
|
git clone https://github.com/louislam/uptime-kuma.git
|
||||||
cd uptime-kuma
|
cd uptime-kuma
|
||||||
npm run setup
|
npm run setup
|
||||||
@ -65,7 +67,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 +108,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 +127,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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
BIN
db/demo_kuma.db
BIN
db/demo_kuma.db
Binary file not shown.
10
db/patch-2fa.sql
Normal file
10
db/patch-2fa.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE user
|
||||||
|
ADD twofa_secret VARCHAR(64);
|
||||||
|
|
||||||
|
ALTER TABLE user
|
||||||
|
ADD twofa_status BOOLEAN default 0 NOT NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
7
db/patch-add-retry-interval-monitor.sql
Normal file
7
db/patch-add-retry-interval-monitor.sql
Normal file
@ -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;
|
30
db/patch-group-table.sql
Normal file
30
db/patch-group-table.sql
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table `group`
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
constraint group_pk
|
||||||
|
primary key autoincrement,
|
||||||
|
name VARCHAR(255) not null,
|
||||||
|
created_date DATETIME default (DATETIME('now')) not null,
|
||||||
|
public BOOLEAN default 0 not null,
|
||||||
|
active BOOLEAN default 1 not null,
|
||||||
|
weight BOOLEAN NOT NULL DEFAULT 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE [monitor_group]
|
||||||
|
(
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[monitor_id] INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
[group_id] INTEGER NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
weight BOOLEAN NOT NULL DEFAULT 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX [fk]
|
||||||
|
ON [monitor_group] (
|
||||||
|
[monitor_id],
|
||||||
|
[group_id]);
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
18
db/patch-incident-table.sql
Normal file
18
db/patch-incident-table.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table incident
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
constraint incident_pk
|
||||||
|
primary key autoincrement,
|
||||||
|
title VARCHAR(255) not null,
|
||||||
|
content TEXT not null,
|
||||||
|
style VARCHAR(30) default 'warning' not null,
|
||||||
|
created_date DATETIME default (DATETIME('now')) not null,
|
||||||
|
last_updated_date DATETIME,
|
||||||
|
pin BOOLEAN default 1 not null,
|
||||||
|
active BOOLEAN default 1 not null
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
22
db/patch-setting-value-type.sql
Normal file
22
db/patch-setting-value-type.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Generated by Intellij IDEA
|
||||||
|
create table setting_dg_tmp
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key autoincrement,
|
||||||
|
key VARCHAR(200) not null
|
||||||
|
unique,
|
||||||
|
value TEXT,
|
||||||
|
type VARCHAR(20)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into setting_dg_tmp(id, key, value, type) select id, key, value, type from setting;
|
||||||
|
|
||||||
|
drop table setting;
|
||||||
|
|
||||||
|
alter table setting_dg_tmp rename to setting;
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
19
db/patch10.sql
Normal file
19
db/patch10.sql
Normal file
@ -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);
|
42
dockerfile
42
dockerfile
@ -1,30 +1,32 @@
|
|||||||
FROM node:14-bullseye-slim AS release
|
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||||
|
# If the image changed, the second stage image should be changed too
|
||||||
|
FROM node:14-buster-slim AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# install dependencies
|
|
||||||
RUN apt update && apt --yes install python3 python3-pip python3-dev git g++ make iputils-ping
|
|
||||||
RUN ln -s /usr/bin/python3 /usr/bin/python
|
|
||||||
|
|
||||||
# split the sqlite install here, so that it can caches the arm prebuilt
|
|
||||||
RUN npm install mapbox/node-sqlite3#593c9d
|
|
||||||
|
|
||||||
# Install apprise
|
|
||||||
RUN apt --yes install python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib
|
|
||||||
RUN pip3 --no-cache-dir install apprise && \
|
|
||||||
rm -rf /root/.cache
|
|
||||||
|
|
||||||
# additional package should be added here, since we don't want to re-compile the arm prebuilt again
|
|
||||||
|
|
||||||
# add sqlite3 cli for debugging in the future
|
|
||||||
RUN apt --yes install sqlite3
|
|
||||||
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm install --legacy-peer-deps && npm run build && npm prune
|
RUN npm install --legacy-peer-deps && \
|
||||||
|
npm run build && \
|
||||||
|
npm prune --production && \
|
||||||
|
chmod +x /app/extra/entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
|
FROM node:14-buster-slim AS release
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||||
|
RUN apt update && \
|
||||||
|
apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||||
|
sqlite3 iputils-ping util-linux && \
|
||||||
|
pip3 --no-cache-dir install apprise && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy app files from build layer
|
||||||
|
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
|
||||||
|
@ -1,25 +1,29 @@
|
|||||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||||
|
FROM node:14-alpine3.12 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
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
|
||||||
|
|
||||||
# split the sqlite install here, so that it can caches the arm prebuilt
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \
|
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 && \
|
||||||
ln -s /usr/bin/python3 /usr/bin/python && \
|
pip3 --no-cache-dir install apprise && \
|
||||||
npm install mapbox/node-sqlite3#593c9d && \
|
rm -rf /root/.cache
|
||||||
apk del .build-deps && \
|
|
||||||
rm -f /usr/bin/python
|
|
||||||
|
|
||||||
# Install apprise
|
# Copy app files from build layer
|
||||||
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
|
COPY --from=build /app /app
|
||||||
RUN pip3 --no-cache-dir install apprise && \
|
|
||||||
rm -rf /root/.cache
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm install --legacy-peer-deps && npm run build && npm prune
|
|
||||||
|
|
||||||
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
Normal file
21
extra/entrypoint.sh
Normal file
@ -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 "$@"
|
@ -1,3 +1,6 @@
|
|||||||
|
/*
|
||||||
|
* This script should be run after a period of time (180s), because the server may need some time to prepare.
|
||||||
|
*/
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
|
||||||
let client;
|
let client;
|
||||||
|
@ -6,12 +6,14 @@ const Database = require("../server/database");
|
|||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const readline = require("readline");
|
const readline = require("readline");
|
||||||
const { initJWTSecret } = require("../server/util-server");
|
const { initJWTSecret } = require("../server/util-server");
|
||||||
|
const args = require("args-parser")(process.argv);
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout
|
output: process.stdout
|
||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
Database.init(args);
|
||||||
await Database.connect();
|
await Database.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -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,46 +35,50 @@ 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("Processing " + file);
|
console.log("Skipping " + file)
|
||||||
const lang = await import("./languages/" + file);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let obj;
|
console.log("Processing " + file);
|
||||||
|
const lang = await import("./languages/" + file);
|
||||||
|
|
||||||
if (lang.default) {
|
let obj;
|
||||||
console.log("is js module");
|
|
||||||
obj = lang.default;
|
if (lang.default) {
|
||||||
} else {
|
obj = lang.default;
|
||||||
console.log("empty file");
|
} else {
|
||||||
obj = {
|
console.log("Empty file");
|
||||||
languageName: "<Your Language name in your language (not in English)>"
|
obj = {
|
||||||
};
|
languageName: "<Your Language name in your language (not in English)>"
|
||||||
}
|
};
|
||||||
|
}
|
||||||
// En first
|
|
||||||
for (const key in en) {
|
// En first
|
||||||
if (! obj[key]) {
|
for (const key in en) {
|
||||||
obj[key] = en[key];
|
if (! obj[key]) {
|
||||||
}
|
obj[key] = en[key];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, {
|
|
||||||
depth: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.writeFileSync(`../../src/languages/${file}`, code);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const code = "export default " + util.inspect(obj, {
|
||||||
|
depth: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
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...");
|
||||||
|
@ -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>
|
||||||
|
@ -1,28 +1,32 @@
|
|||||||
# Uptime-Kuma K8s Deployment
|
# Uptime-Kuma K8s Deployment
|
||||||
|
|
||||||
|
⚠ Warning: K8s deployment is provided by contributors. I have no experience with K8s and I can't fix error in the future. I only test Docker and Node.js. Use at your own risk.
|
||||||
|
|
||||||
## How does it work?
|
## How does it work?
|
||||||
|
|
||||||
Kustomize is a tool which builds a complete deployment file for all config elements.
|
Kustomize is a tool which builds a complete deployment file for all config elements.
|
||||||
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
|
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.
|
@ -30,6 +30,9 @@ spec:
|
|||||||
command:
|
command:
|
||||||
- node
|
- node
|
||||||
- extra/healthcheck.js
|
- extra/healthcheck.js
|
||||||
|
initialDelaySeconds: 180
|
||||||
|
periodSeconds: 60
|
||||||
|
timeoutSeconds: 30
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
|
2293
package-lock.json
generated
2293
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
66
package.json
66
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.5.3",
|
"version": "1.7.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -10,20 +10,25 @@
|
|||||||
"node": "14.*"
|
"node": "14.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"install-legacy": "npm install --legacy-peer-deps",
|
||||||
|
"update-legacy": "npm update --legacy-peer-deps",
|
||||||
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
|
||||||
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
|
||||||
"lint": "npm run lint:js && npm run lint:style",
|
"lint": "npm run lint:js && npm run lint:style",
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"start": "npm run start-server",
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"tsc": "tsc",
|
||||||
"vite-preview-dist": "vite preview --host",
|
"vite-preview-dist": "vite preview --host",
|
||||||
"build-docker": "npm run build-docker-alpine && npm run build-docker-debian",
|
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
|
||||||
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.5.3-alpine --target release . --push",
|
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.7.0-alpine --target release . --push",
|
||||||
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.5.3 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.5.3-debian --target release . --push",
|
"build-docker-debian": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.7.0 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.7.0-debian --target release . --push",
|
||||||
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||||
|
"build-docker-nightly-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||||
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"setup": "git checkout 1.5.3 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
|
"setup": "git checkout 1.7.0 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
|
||||||
"update-version": "node extra/update-version.js",
|
"update-version": "node extra/update-version.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
@ -32,61 +37,72 @@
|
|||||||
"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-with-base-lang": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix",
|
||||||
|
"update-language-files": "cd extra/update-language-files && node index.js && eslint ../../src/languages/**.js --fix"
|
||||||
},
|
},
|
||||||
"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",
|
"@louislam/sqlite3": "^5.0.6",
|
||||||
|
"@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",
|
||||||
"http-graceful-shutdown": "^3.1.4",
|
"http-graceful-shutdown": "^3.1.4",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"nodemailer": "^6.6.3",
|
"nodemailer": "^6.6.5",
|
||||||
|
"notp": "^2.0.3",
|
||||||
"password-hash": "^1.2.2",
|
"password-hash": "^1.2.2",
|
||||||
"prom-client": "^13.2.0",
|
"prom-client": "^13.2.0",
|
||||||
"prometheus-api-metrics": "^3.2.0",
|
"prometheus-api-metrics": "^3.2.0",
|
||||||
|
"qrcode": "^1.4.4",
|
||||||
"redbean-node": "0.1.2",
|
"redbean-node": "0.1.2",
|
||||||
"socket.io": "^4.2.0",
|
"socket.io": "^4.2.0",
|
||||||
"socket.io-client": "^4.2.0",
|
"socket.io-client": "^4.2.0",
|
||||||
"sqlite3": "github:mapbox/node-sqlite3#593c9d",
|
|
||||||
"tcp-ping": "^0.1.1",
|
"tcp-ping": "^0.1.1",
|
||||||
|
"thirty-two": "^1.0.2",
|
||||||
|
"timezones-list": "^3.0.1",
|
||||||
"v-pagination-3": "^0.1.6",
|
"v-pagination-3": "^0.1.6",
|
||||||
"vue": "^3.2.8",
|
"vue": "next",
|
||||||
"vue-chart-3": "^0.5.7",
|
"vue-chart-3": "^0.5.8",
|
||||||
"vue-confirm-dialog": "^1.0.2",
|
"vue-confirm-dialog": "^1.0.2",
|
||||||
|
"vue-contenteditable": "^3.0.4",
|
||||||
"vue-i18n": "^9.1.7",
|
"vue-i18n": "^9.1.7",
|
||||||
|
"vue-image-crop-upload": "^3.0.3",
|
||||||
"vue-multiselect": "^3.0.0-alpha.2",
|
"vue-multiselect": "^3.0.0-alpha.2",
|
||||||
|
"vue-qrcode": "^1.0.0",
|
||||||
"vue-router": "^4.0.11",
|
"vue-router": "^4.0.11",
|
||||||
"vue-toastification": "^2.0.0-rc.1"
|
"vue-toastification": "^2.0.0-rc.1",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.15.0",
|
"@babel/eslint-parser": "^7.15.7",
|
||||||
"@types/bootstrap": "^5.1.2",
|
"@types/bootstrap": "^5.1.6",
|
||||||
"@vitejs/plugin-legacy": "^1.5.2",
|
"@vitejs/plugin-legacy": "^1.5.3",
|
||||||
"@vitejs/plugin-vue": "^1.6.0",
|
"@vitejs/plugin-vue": "^1.9.1",
|
||||||
"@vue/compiler-sfc": "^3.2.6",
|
"@vue/compiler-sfc": "^3.2.16",
|
||||||
"core-js": "^3.17.0",
|
"core-js": "^3.18.0",
|
||||||
|
"cross-env": "^7.0.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.18.0",
|
||||||
"sass": "^1.38.2",
|
"sass": "^1.42.1",
|
||||||
"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.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
public/icon-192x192.png
Normal file
BIN
public/icon-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icon-512x512.png
Normal file
BIN
public/icon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
19
public/manifest.json
Normal file
19
public/manifest.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -18,7 +18,7 @@ exports.startInterval = () => {
|
|||||||
|
|
||||||
// For debug
|
// For debug
|
||||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||||
res.data.version = "1000.0.0"
|
res.data.version = "1000.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.latestVersion = res.data.version;
|
exports.latestVersion = res.data.version;
|
||||||
|
@ -1,37 +1,110 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { setSetting, setting } = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
|
const { debug, sleep } = require("../src/util");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const knex = require("knex");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database & App Data Folder
|
||||||
|
*/
|
||||||
class Database {
|
class Database {
|
||||||
|
|
||||||
static templatePath = "./db/kuma.db"
|
static templatePath = "./db/kuma.db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Dir (Default: ./data)
|
||||||
|
*/
|
||||||
static dataDir;
|
static dataDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Upload Dir (Default: ./data/upload)
|
||||||
|
*/
|
||||||
|
static uploadDir;
|
||||||
|
|
||||||
static path;
|
static path;
|
||||||
static latestVersion = 9;
|
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
static patched = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For Backup only
|
||||||
|
*/
|
||||||
|
static backupPath = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add patch filename in key
|
||||||
|
* Values:
|
||||||
|
* true: Add it regardless of order
|
||||||
|
* false: Do nothing
|
||||||
|
* { parents: []}: Need parents before add it
|
||||||
|
*/
|
||||||
|
static patchList = {
|
||||||
|
"patch-setting-value-type.sql": true,
|
||||||
|
"patch-improve-performance.sql": true,
|
||||||
|
"patch-2fa.sql": true,
|
||||||
|
"patch-add-retry-interval-monitor.sql": true,
|
||||||
|
"patch-incident-table.sql": true,
|
||||||
|
"patch-group-table.sql": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The finally version should be 10 after merged tag feature
|
||||||
|
* @deprecated Use patchList for any new feature
|
||||||
|
*/
|
||||||
|
static latestVersion = 10;
|
||||||
|
|
||||||
static noReject = true;
|
static noReject = true;
|
||||||
static sqliteInstance = null;
|
|
||||||
|
static init(args) {
|
||||||
|
// Data Directory (must be end with "/")
|
||||||
|
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||||
|
Database.path = Database.dataDir + "kuma.db";
|
||||||
|
if (! fs.existsSync(Database.dataDir)) {
|
||||||
|
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
Database.uploadDir = Database.dataDir + "upload/";
|
||||||
|
|
||||||
|
if (! fs.existsSync(Database.uploadDir)) {
|
||||||
|
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Data Dir: ${Database.dataDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
static async connect() {
|
static async connect() {
|
||||||
const acquireConnectionTimeout = 120 * 1000;
|
const acquireConnectionTimeout = 120 * 1000;
|
||||||
|
|
||||||
R.setup("sqlite", {
|
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||||
filename: Database.path,
|
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
||||||
|
|
||||||
|
const knexInstance = knex({
|
||||||
|
client: Dialect,
|
||||||
|
connection: {
|
||||||
|
filename: Database.path,
|
||||||
|
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||||
|
},
|
||||||
useNullAsDefault: true,
|
useNullAsDefault: true,
|
||||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
pool: {
|
||||||
}, {
|
min: 1,
|
||||||
min: 1,
|
max: 1,
|
||||||
max: 1,
|
idleTimeoutMillis: 120 * 1000,
|
||||||
idleTimeoutMillis: 120 * 1000,
|
propagateCreateError: false,
|
||||||
propagateCreateError: false,
|
acquireTimeoutMillis: acquireConnectionTimeout,
|
||||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
R.setup(knexInstance);
|
||||||
|
|
||||||
if (process.env.SQL_LOG === "1") {
|
if (process.env.SQL_LOG === "1") {
|
||||||
R.debug(true);
|
R.debug(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto map the model to a bean object
|
// Auto map the model to a bean object
|
||||||
R.freeze(true)
|
R.freeze(true);
|
||||||
await R.autoloadModels("./server/model");
|
await R.autoloadModels("./server/model");
|
||||||
|
|
||||||
// Change to WAL
|
// Change to WAL
|
||||||
@ -41,6 +114,7 @@ class Database {
|
|||||||
console.log("SQLite config:");
|
console.log("SQLite config:");
|
||||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||||
console.log(await R.getAll("PRAGMA cache_size"));
|
console.log(await R.getAll("PRAGMA cache_size"));
|
||||||
|
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
static async patch() {
|
static async patch() {
|
||||||
@ -58,21 +132,9 @@ class Database {
|
|||||||
} else if (version > this.latestVersion) {
|
} else if (version > this.latestVersion) {
|
||||||
console.info("Warning: Database version is newer than expected");
|
console.info("Warning: Database version is newer than expected");
|
||||||
} else {
|
} else {
|
||||||
console.info("Database patch is needed")
|
console.info("Database patch is needed");
|
||||||
|
|
||||||
console.info("Backup the db")
|
this.backup(version);
|
||||||
const backupPath = this.dataDir + "kuma.db.bak" + version;
|
|
||||||
fs.copyFileSync(Database.path, backupPath);
|
|
||||||
|
|
||||||
const shmPath = Database.path + "-shm";
|
|
||||||
if (fs.existsSync(shmPath)) {
|
|
||||||
fs.copyFileSync(shmPath, shmPath + ".bak" + version);
|
|
||||||
}
|
|
||||||
|
|
||||||
const walPath = Database.path + "-wal";
|
|
||||||
if (fs.existsSync(walPath)) {
|
|
||||||
fs.copyFileSync(walPath, walPath + ".bak" + version);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try catch anything here, if gone wrong, restore the backup
|
// Try catch anything here, if gone wrong, restore the backup
|
||||||
try {
|
try {
|
||||||
@ -83,18 +145,95 @@ class Database {
|
|||||||
console.info(`Patched ${sqlFile}`);
|
console.info(`Patched ${sqlFile}`);
|
||||||
await setSetting("database_version", i);
|
await setSetting("database_version", i);
|
||||||
}
|
}
|
||||||
console.log("Database Patched Successfully");
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
await Database.close();
|
await Database.close();
|
||||||
console.error("Patch db failed!!! Restoring the backup")
|
|
||||||
fs.copyFileSync(backupPath, Database.path);
|
|
||||||
console.error(ex)
|
|
||||||
|
|
||||||
console.error("Start Uptime-Kuma failed due to patch db failed")
|
console.error(ex);
|
||||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
|
console.error("Start Uptime-Kuma failed due to patch db failed");
|
||||||
|
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
|
this.restore();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.patch2();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call it from patch() only
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async patch2() {
|
||||||
|
console.log("Database Patch 2.0 Process");
|
||||||
|
let databasePatchedFiles = await setting("databasePatchedFiles");
|
||||||
|
|
||||||
|
if (! databasePatchedFiles) {
|
||||||
|
databasePatchedFiles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("Patched files:");
|
||||||
|
debug(databasePatchedFiles);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let sqlFilename in this.patchList) {
|
||||||
|
await this.patch2Recursion(sqlFilename, databasePatchedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.patched) {
|
||||||
|
console.log("Database Patched Successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (ex) {
|
||||||
|
await Database.close();
|
||||||
|
|
||||||
|
console.error(ex);
|
||||||
|
console.error("Start Uptime-Kuma failed due to patch db failed");
|
||||||
|
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||||
|
|
||||||
|
this.restore();
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used it patch2() only
|
||||||
|
* @param sqlFilename
|
||||||
|
* @param databasePatchedFiles
|
||||||
|
*/
|
||||||
|
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
||||||
|
let value = this.patchList[sqlFilename];
|
||||||
|
|
||||||
|
if (! value) {
|
||||||
|
console.log(sqlFilename + " skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if patched
|
||||||
|
if (! databasePatchedFiles[sqlFilename]) {
|
||||||
|
console.log(sqlFilename + " is not patched");
|
||||||
|
|
||||||
|
if (value.parents) {
|
||||||
|
console.log(sqlFilename + " need parents");
|
||||||
|
for (let parentSQLFilename of value.parents) {
|
||||||
|
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.backup(dayjs().format("YYYYMMDDHHmmss"));
|
||||||
|
|
||||||
|
console.log(sqlFilename + " is patching");
|
||||||
|
this.patched = true;
|
||||||
|
await this.importSQLFile("./db/" + sqlFilename);
|
||||||
|
databasePatchedFiles[sqlFilename] = true;
|
||||||
|
console.log(sqlFilename + " is patched successfully");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
debug(sqlFilename + " is already patched, skip");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,12 +250,12 @@ class Database {
|
|||||||
// Remove all comments (--)
|
// Remove all comments (--)
|
||||||
let lines = text.split("\n");
|
let lines = text.split("\n");
|
||||||
lines = lines.filter((line) => {
|
lines = lines.filter((line) => {
|
||||||
return ! line.startsWith("--")
|
return ! line.startsWith("--");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Split statements by semicolon
|
// Split statements by semicolon
|
||||||
// Filter out empty line
|
// Filter out empty line
|
||||||
text = lines.join("\n")
|
text = lines.join("\n");
|
||||||
|
|
||||||
let statements = text.split(";")
|
let statements = text.split(";")
|
||||||
.map((statement) => {
|
.map((statement) => {
|
||||||
@ -124,7 +263,7 @@ class Database {
|
|||||||
})
|
})
|
||||||
.filter((statement) => {
|
.filter((statement) => {
|
||||||
return statement !== "";
|
return statement !== "";
|
||||||
})
|
});
|
||||||
|
|
||||||
for (let statement of statements) {
|
for (let statement of statements) {
|
||||||
await R.exec(statement);
|
await R.exec(statement);
|
||||||
@ -140,10 +279,96 @@ class Database {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async close() {
|
static async close() {
|
||||||
if (this.sqliteInstance) {
|
const listener = (reason, p) => {
|
||||||
this.sqliteInstance.close();
|
Database.noReject = false;
|
||||||
|
};
|
||||||
|
process.addListener("unhandledRejection", listener);
|
||||||
|
|
||||||
|
console.log("Closing DB");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
Database.noReject = true;
|
||||||
|
await R.close();
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
if (Database.noReject) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
console.log("Waiting to close the db");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("SQLite closed");
|
||||||
|
|
||||||
|
process.removeListener("unhandledRejection", listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One backup one time in this process.
|
||||||
|
* Reset this.backupPath if you want to backup again
|
||||||
|
* @param version
|
||||||
|
*/
|
||||||
|
static backup(version) {
|
||||||
|
if (! this.backupPath) {
|
||||||
|
console.info("Backup the db");
|
||||||
|
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||||
|
fs.copyFileSync(Database.path, this.backupPath);
|
||||||
|
|
||||||
|
const shmPath = Database.path + "-shm";
|
||||||
|
if (fs.existsSync(shmPath)) {
|
||||||
|
this.backupShmPath = shmPath + ".bak" + version;
|
||||||
|
fs.copyFileSync(shmPath, this.backupShmPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const walPath = Database.path + "-wal";
|
||||||
|
if (fs.existsSync(walPath)) {
|
||||||
|
this.backupWalPath = walPath + ".bak" + version;
|
||||||
|
fs.copyFileSync(walPath, this.backupWalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
static restore() {
|
||||||
|
if (this.backupPath) {
|
||||||
|
console.error("Patch db failed!!! Restoring the backup");
|
||||||
|
|
||||||
|
const shmPath = Database.path + "-shm";
|
||||||
|
const walPath = Database.path + "-wal";
|
||||||
|
|
||||||
|
// Delete patch failed db
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(Database.path)) {
|
||||||
|
fs.unlinkSync(Database.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(shmPath)) {
|
||||||
|
fs.unlinkSync(shmPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(walPath)) {
|
||||||
|
fs.unlinkSync(walPath);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Restore failed, you may need to restore the backup manually");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore backup
|
||||||
|
fs.copyFileSync(this.backupPath, Database.path);
|
||||||
|
|
||||||
|
if (this.backupShmPath) {
|
||||||
|
fs.copyFileSync(this.backupShmPath, shmPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.backupWalPath) {
|
||||||
|
fs.copyFileSync(this.backupWalPath, walPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log("Nothing to restore");
|
||||||
}
|
}
|
||||||
console.log("Stopped database");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
57
server/image-data-uri.js
Normal file
57
server/image-data-uri.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
|
||||||
|
Modified with 0 dependencies
|
||||||
|
*/
|
||||||
|
let fs = require("fs");
|
||||||
|
|
||||||
|
let ImageDataURI = (() => {
|
||||||
|
|
||||||
|
function decode(dataURI) {
|
||||||
|
if (!/data:image\//.test(dataURI)) {
|
||||||
|
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let regExMatches = dataURI.match("data:(image/.*);base64,(.*)");
|
||||||
|
return {
|
||||||
|
imageType: regExMatches[1],
|
||||||
|
dataBase64: regExMatches[2],
|
||||||
|
dataBuffer: new Buffer(regExMatches[2], "base64")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(data, mediaType) {
|
||||||
|
if (!data || !mediaType) {
|
||||||
|
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType;
|
||||||
|
let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64");
|
||||||
|
let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64;
|
||||||
|
|
||||||
|
return dataImgBase64;
|
||||||
|
}
|
||||||
|
|
||||||
|
function outputFile(dataURI, filePath) {
|
||||||
|
filePath = filePath || "./";
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let imageDecoded = decode(dataURI);
|
||||||
|
|
||||||
|
fs.writeFile(filePath, imageDecoded.dataBuffer, err => {
|
||||||
|
if (err) {
|
||||||
|
return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4));
|
||||||
|
}
|
||||||
|
resolve(filePath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
decode: decode,
|
||||||
|
encode: encode,
|
||||||
|
outputFile: outputFile,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
module.exports = ImageDataURI;
|
34
server/model/group.js
Normal file
34
server/model/group.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
|
class Group extends BeanModel {
|
||||||
|
|
||||||
|
async toPublicJSON() {
|
||||||
|
let monitorBeanList = await this.getMonitorList();
|
||||||
|
let monitorList = [];
|
||||||
|
|
||||||
|
for (let bean of monitorBeanList) {
|
||||||
|
monitorList.push(await bean.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
weight: this.weight,
|
||||||
|
monitorList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMonitorList() {
|
||||||
|
return R.convertToBeans("monitor", await R.getAll(`
|
||||||
|
SELECT monitor.* FROM monitor, monitor_group
|
||||||
|
WHERE monitor.id = monitor_group.monitor_id
|
||||||
|
AND group_id = ?
|
||||||
|
ORDER BY monitor_group.weight
|
||||||
|
`, [
|
||||||
|
this.id,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Group;
|
@ -1,8 +1,8 @@
|
|||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const utc = require("dayjs/plugin/utc")
|
const utc = require("dayjs/plugin/utc");
|
||||||
let timezone = require("dayjs/plugin/timezone")
|
let timezone = require("dayjs/plugin/timezone");
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone);
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -13,6 +13,15 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
|||||||
*/
|
*/
|
||||||
class Heartbeat extends BeanModel {
|
class Heartbeat extends BeanModel {
|
||||||
|
|
||||||
|
toPublicJSON() {
|
||||||
|
return {
|
||||||
|
status: this.status,
|
||||||
|
time: this.time,
|
||||||
|
msg: "", // Hide for public
|
||||||
|
ping: this.ping,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
monitorID: this.monitor_id,
|
monitorID: this.monitor_id,
|
||||||
|
18
server/model/incident.js
Normal file
18
server/model/incident.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class Incident extends BeanModel {
|
||||||
|
|
||||||
|
toPublicJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
style: this.style,
|
||||||
|
title: this.title,
|
||||||
|
content: this.content,
|
||||||
|
pin: this.pin,
|
||||||
|
createdDate: this.createdDate,
|
||||||
|
lastUpdatedDate: this.lastUpdatedDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Incident;
|
@ -1,16 +1,16 @@
|
|||||||
const https = require("https");
|
const https = require("https");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const utc = require("dayjs/plugin/utc")
|
const utc = require("dayjs/plugin/utc");
|
||||||
let timezone = require("dayjs/plugin/timezone")
|
let timezone = require("dayjs/plugin/timezone");
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone);
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const { Notification } = require("../notification")
|
const { Notification } = require("../notification");
|
||||||
const version = require("../../package.json").version;
|
const version = require("../../package.json").version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,18 +20,35 @@ const version = require("../../package.json").version;
|
|||||||
* 2 = PENDING
|
* 2 = PENDING
|
||||||
*/
|
*/
|
||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a object that ready to parse to JSON for public
|
||||||
|
* Only show necessary data to public
|
||||||
|
*/
|
||||||
|
async toPublicJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a object that ready to parse to JSON
|
||||||
|
*/
|
||||||
async toJSON() {
|
async toJSON() {
|
||||||
|
|
||||||
let notificationIDList = {};
|
let notificationIDList = {};
|
||||||
|
|
||||||
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
let list = await R.find("monitor_notification", " monitor_id = ? ", [
|
||||||
this.id,
|
this.id,
|
||||||
])
|
]);
|
||||||
|
|
||||||
for (let bean of list) {
|
for (let bean of list) {
|
||||||
notificationIDList[bean.notification_id] = true;
|
notificationIDList[bean.notification_id] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
@ -43,6 +60,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,
|
||||||
keyword: this.keyword,
|
keyword: this.keyword,
|
||||||
ignoreTls: this.getIgnoreTls(),
|
ignoreTls: this.getIgnoreTls(),
|
||||||
upsideDown: this.isUpsideDown(),
|
upsideDown: this.isUpsideDown(),
|
||||||
@ -52,6 +70,7 @@ class Monitor extends BeanModel {
|
|||||||
dns_resolve_server: this.dns_resolve_server,
|
dns_resolve_server: this.dns_resolve_server,
|
||||||
dns_last_result: this.dns_last_result,
|
dns_last_result: this.dns_last_result,
|
||||||
notificationIDList,
|
notificationIDList,
|
||||||
|
tags: tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +79,7 @@ class Monitor extends BeanModel {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
getIgnoreTls() {
|
getIgnoreTls() {
|
||||||
return Boolean(this.ignoreTls)
|
return Boolean(this.ignoreTls);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,12 +109,12 @@ class Monitor extends BeanModel {
|
|||||||
if (! previousBeat) {
|
if (! previousBeat) {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
this.id,
|
this.id,
|
||||||
])
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFirstBeat = !previousBeat;
|
const isFirstBeat = !previousBeat;
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat")
|
let bean = R.dispense("heartbeat");
|
||||||
bean.monitor_id = this.id;
|
bean.monitor_id = this.id;
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTime(dayjs.utc());
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
@ -131,7 +150,7 @@ class Monitor extends BeanModel {
|
|||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
bean.msg = `${res.status} - ${res.statusText}`
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
// Check certificate if https is used
|
// Check certificate if https is used
|
||||||
@ -141,12 +160,12 @@ class Monitor extends BeanModel {
|
|||||||
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message !== "No TLS certificate in response") {
|
if (e.message !== "No TLS certificate in response") {
|
||||||
console.error(e.message)
|
console.error(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
|
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||||
|
|
||||||
if (this.type === "http") {
|
if (this.type === "http") {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
@ -156,26 +175,26 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
// Convert to string for object/array
|
// Convert to string for object/array
|
||||||
if (typeof data !== "string") {
|
if (typeof data !== "string") {
|
||||||
data = JSON.stringify(data)
|
data = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.includes(this.keyword)) {
|
if (data.includes(this.keyword)) {
|
||||||
bean.msg += ", keyword is found"
|
bean.msg += ", keyword is found";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(bean.msg + ", but keyword is not found")
|
throw new Error(bean.msg + ", but keyword is not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.type === "port") {
|
} else if (this.type === "port") {
|
||||||
bean.ping = await tcping(this.hostname, this.port);
|
bean.ping = await tcping(this.hostname, this.port);
|
||||||
bean.msg = ""
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
|
||||||
} else if (this.type === "ping") {
|
} else if (this.type === "ping") {
|
||||||
bean.ping = await ping(this.hostname);
|
bean.ping = await ping(this.hostname);
|
||||||
bean.msg = ""
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
} else if (this.type === "dns") {
|
} else if (this.type === "dns") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
@ -195,7 +214,7 @@ class Monitor extends BeanModel {
|
|||||||
dnsRes.forEach(record => {
|
dnsRes.forEach(record => {
|
||||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||||
});
|
});
|
||||||
dnsMessage = dnsMessage.slice(0, -2)
|
dnsMessage = dnsMessage.slice(0, -2);
|
||||||
} else if (this.dns_resolve_type == "NS") {
|
} else if (this.dns_resolve_type == "NS") {
|
||||||
dnsMessage += "Servers: ";
|
dnsMessage += "Servers: ";
|
||||||
dnsMessage += dnsRes.join(" | ");
|
dnsMessage += dnsRes.join(" | ");
|
||||||
@ -205,7 +224,7 @@ class Monitor extends BeanModel {
|
|||||||
dnsRes.forEach(record => {
|
dnsRes.forEach(record => {
|
||||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
||||||
});
|
});
|
||||||
dnsMessage = dnsMessage.slice(0, -2)
|
dnsMessage = dnsMessage.slice(0, -2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.dnsLastResult !== dnsMessage) {
|
if (this.dnsLastResult !== dnsMessage) {
|
||||||
@ -268,20 +287,20 @@ class Monitor extends BeanModel {
|
|||||||
if (!isFirstBeat || bean.status === DOWN) {
|
if (!isFirstBeat || bean.status === DOWN) {
|
||||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||||
this.id,
|
this.id,
|
||||||
])
|
]);
|
||||||
|
|
||||||
let text;
|
let text;
|
||||||
if (bean.status === UP) {
|
if (bean.status === UP) {
|
||||||
text = "✅ Up"
|
text = "✅ Up";
|
||||||
} else {
|
} else {
|
||||||
text = "🔴 Down"
|
text = "🔴 Down";
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||||
|
|
||||||
for (let notification of notificationList) {
|
for (let notification of notificationList) {
|
||||||
try {
|
try {
|
||||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
|
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Cannot send notification to " + notification.name);
|
console.error("Cannot send notification to " + notification.name);
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@ -293,16 +312,21 @@ 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());
|
||||||
Monitor.sendStats(io, this.id, this.user_id)
|
Monitor.sendStats(io, this.id, this.user_id);
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
prometheus.update(bean, tlsInfo);
|
prometheus.update(bean, tlsInfo);
|
||||||
@ -310,10 +334,10 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
};
|
||||||
|
|
||||||
beat();
|
beat();
|
||||||
}
|
}
|
||||||
@ -406,7 +430,7 @@ class Monitor extends BeanModel {
|
|||||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||||
* @param duration : int Hours
|
* @param duration : int Hours
|
||||||
*/
|
*/
|
||||||
static async sendUptime(duration, io, monitorID, userID) {
|
static async calcUptime(duration, monitorID) {
|
||||||
const timeLogger = new TimeLogger();
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
||||||
@ -459,12 +483,21 @@ class Monitor extends BeanModel {
|
|||||||
} else {
|
} else {
|
||||||
// Handle new monitor with only one beat, because the beat's duration = 0
|
// Handle new monitor with only one beat, because the beat's duration = 0
|
||||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
||||||
console.log("here???" + status);
|
|
||||||
if (status === UP) {
|
if (status === UP) {
|
||||||
uptime = 1;
|
uptime = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send Uptime
|
||||||
|
* @param duration : int Hours
|
||||||
|
*/
|
||||||
|
static async sendUptime(duration, io, monitorID, userID) {
|
||||||
|
const uptime = await this.calcUptime(duration, monitorID);
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
server/model/tag.js
Normal file
13
server/model/tag.js
Normal file
@ -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;
|
749
server/modules/apicache/apicache.js
Normal file
749
server/modules/apicache/apicache.js
Normal file
@ -0,0 +1,749 @@
|
|||||||
|
let url = require("url");
|
||||||
|
let MemoryCache = require("./memory-cache");
|
||||||
|
|
||||||
|
let t = {
|
||||||
|
ms: 1,
|
||||||
|
second: 1000,
|
||||||
|
minute: 60000,
|
||||||
|
hour: 3600000,
|
||||||
|
day: 3600000 * 24,
|
||||||
|
week: 3600000 * 24 * 7,
|
||||||
|
month: 3600000 * 24 * 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
let instances = [];
|
||||||
|
|
||||||
|
let matches = function (a) {
|
||||||
|
return function (b) {
|
||||||
|
return a === b;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let doesntMatch = function (a) {
|
||||||
|
return function (b) {
|
||||||
|
return !matches(a)(b);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let logDuration = function (d, prefix) {
|
||||||
|
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms";
|
||||||
|
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m";
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSafeHeaders(res) {
|
||||||
|
return res.getHeaders ? res.getHeaders() : res._headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApiCache() {
|
||||||
|
let memCache = new MemoryCache();
|
||||||
|
|
||||||
|
let globalOptions = {
|
||||||
|
debug: false,
|
||||||
|
defaultDuration: 3600000,
|
||||||
|
enabled: true,
|
||||||
|
appendKey: [],
|
||||||
|
jsonp: false,
|
||||||
|
redisClient: false,
|
||||||
|
headerBlacklist: [],
|
||||||
|
statusCodes: {
|
||||||
|
include: [],
|
||||||
|
exclude: [],
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
expire: undefined,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
// 'cache-control': 'no-cache' // example of header overwrite
|
||||||
|
},
|
||||||
|
trackPerformance: false,
|
||||||
|
respectCacheControl: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let middlewareOptions = [];
|
||||||
|
let instance = this;
|
||||||
|
let index = null;
|
||||||
|
let timers = {};
|
||||||
|
let performanceArray = []; // for tracking cache hit rate
|
||||||
|
|
||||||
|
instances.push(this);
|
||||||
|
this.id = instances.length;
|
||||||
|
|
||||||
|
function debug(a, b, c, d) {
|
||||||
|
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
|
||||||
|
return arg !== undefined;
|
||||||
|
});
|
||||||
|
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1;
|
||||||
|
|
||||||
|
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCacheResponse(request, response, toggle) {
|
||||||
|
let opt = globalOptions;
|
||||||
|
let codes = opt.statusCodes;
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toggle && !toggle(request, response)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addIndexEntries(key, req) {
|
||||||
|
let groupName = req.apicacheGroup;
|
||||||
|
|
||||||
|
if (groupName) {
|
||||||
|
debug("group detected \"" + groupName + "\"");
|
||||||
|
let group = (index.groups[groupName] = index.groups[groupName] || []);
|
||||||
|
group.unshift(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
index.all.unshift(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterBlacklistedHeaders(headers) {
|
||||||
|
return Object.keys(headers)
|
||||||
|
.filter(function (key) {
|
||||||
|
return globalOptions.headerBlacklist.indexOf(key) === -1;
|
||||||
|
})
|
||||||
|
.reduce(function (acc, header) {
|
||||||
|
acc[header] = headers[header];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCacheObject(status, headers, data, encoding) {
|
||||||
|
return {
|
||||||
|
status: status,
|
||||||
|
headers: filterBlacklistedHeaders(headers),
|
||||||
|
data: data,
|
||||||
|
encoding: encoding,
|
||||||
|
timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheResponse(key, value, duration) {
|
||||||
|
let redis = globalOptions.redisClient;
|
||||||
|
let expireCallback = globalOptions.events.expire;
|
||||||
|
|
||||||
|
if (redis && redis.connected) {
|
||||||
|
try {
|
||||||
|
redis.hset(key, "response", JSON.stringify(value));
|
||||||
|
redis.hset(key, "duration", duration);
|
||||||
|
redis.expire(key, duration / 1000, expireCallback || function () {});
|
||||||
|
} catch (err) {
|
||||||
|
debug("[apicache] error in redis.hset()");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memCache.add(key, value, duration, expireCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add automatic cache clearing from duration, includes max limit on setTimeout
|
||||||
|
timers[key] = setTimeout(function () {
|
||||||
|
instance.clear(key, true);
|
||||||
|
}, Math.min(duration, 2147483647));
|
||||||
|
}
|
||||||
|
|
||||||
|
function accumulateContent(res, content) {
|
||||||
|
if (content) {
|
||||||
|
if (typeof content == "string") {
|
||||||
|
res._apicache.content = (res._apicache.content || "") + content;
|
||||||
|
} else if (Buffer.isBuffer(content)) {
|
||||||
|
let oldContent = res._apicache.content;
|
||||||
|
|
||||||
|
if (typeof oldContent === "string") {
|
||||||
|
oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oldContent) {
|
||||||
|
oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
res._apicache.content = Buffer.concat(
|
||||||
|
[oldContent, content],
|
||||||
|
oldContent.length + content.length
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
res._apicache.content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
||||||
|
// monkeypatch res.end to create cache object
|
||||||
|
res._apicache = {
|
||||||
|
write: res.write,
|
||||||
|
writeHead: res.writeHead,
|
||||||
|
end: res.end,
|
||||||
|
cacheable: true,
|
||||||
|
content: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// append header overwrites if applicable
|
||||||
|
Object.keys(globalOptions.headers).forEach(function (name) {
|
||||||
|
res.setHeader(name, globalOptions.headers[name]);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeHead = function () {
|
||||||
|
// add cache control headers
|
||||||
|
if (!globalOptions.headers["cache-control"]) {
|
||||||
|
if (shouldCacheResponse(req, res, toggle)) {
|
||||||
|
res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0));
|
||||||
|
} else {
|
||||||
|
res.setHeader("cache-control", "no-cache, no-store, must-revalidate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res._apicache.headers = Object.assign({}, getSafeHeaders(res));
|
||||||
|
return res._apicache.writeHead.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// patch res.write
|
||||||
|
res.write = function (content) {
|
||||||
|
accumulateContent(res, content);
|
||||||
|
return res._apicache.write.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// patch res.end
|
||||||
|
res.end = function (content, encoding) {
|
||||||
|
if (shouldCacheResponse(req, res, toggle)) {
|
||||||
|
accumulateContent(res, content);
|
||||||
|
|
||||||
|
if (res._apicache.cacheable && res._apicache.content) {
|
||||||
|
addIndexEntries(key, req);
|
||||||
|
let headers = res._apicache.headers || getSafeHeaders(res);
|
||||||
|
let cacheObject = createCacheObject(
|
||||||
|
res.statusCode,
|
||||||
|
headers,
|
||||||
|
res._apicache.content,
|
||||||
|
encoding
|
||||||
|
);
|
||||||
|
cacheResponse(key, cacheObject, duration);
|
||||||
|
|
||||||
|
// display log entry
|
||||||
|
let elapsed = new Date() - req.apicacheTimer;
|
||||||
|
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed));
|
||||||
|
debug("_apicache.headers: ", res._apicache.headers);
|
||||||
|
debug("res.getHeaders(): ", getSafeHeaders(res));
|
||||||
|
debug("cacheObject: ", cacheObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res._apicache.end.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
||||||
|
if (toggle && !toggle(request, response)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers = getSafeHeaders(response);
|
||||||
|
|
||||||
|
// Modified by @louislam, removed Cache-control, since I don't need client side cache!
|
||||||
|
// Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
|
||||||
|
Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {}));
|
||||||
|
|
||||||
|
// only embed apicache headers when not in production environment
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
Object.assign(headers, {
|
||||||
|
"apicache-store": globalOptions.redisClient ? "redis" : "memory",
|
||||||
|
"apicache-version": "1.6.2-modified",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// unstringify buffers
|
||||||
|
let data = cacheObject.data;
|
||||||
|
if (data && data.type === "Buffer") {
|
||||||
|
data =
|
||||||
|
typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// test Etag against If-None-Match for 304
|
||||||
|
let cachedEtag = cacheObject.headers.etag;
|
||||||
|
let requestEtag = request.headers["if-none-match"];
|
||||||
|
|
||||||
|
if (requestEtag && cachedEtag === requestEtag) {
|
||||||
|
response.writeHead(304, headers);
|
||||||
|
return response.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
response.writeHead(cacheObject.status || 200, headers);
|
||||||
|
|
||||||
|
return response.end(data, cacheObject.encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncOptions() {
|
||||||
|
for (let i in middlewareOptions) {
|
||||||
|
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clear = function (target, isAutomatic) {
|
||||||
|
let group = index.groups[target];
|
||||||
|
let redis = globalOptions.redisClient;
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
debug("clearing group \"" + target + "\"");
|
||||||
|
|
||||||
|
group.forEach(function (key) {
|
||||||
|
debug("clearing cached entry for \"" + key + "\"");
|
||||||
|
clearTimeout(timers[key]);
|
||||||
|
delete timers[key];
|
||||||
|
if (!globalOptions.redisClient) {
|
||||||
|
memCache.delete(key);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
redis.del(key);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index.all = index.all.filter(doesntMatch(key));
|
||||||
|
});
|
||||||
|
|
||||||
|
delete index.groups[target];
|
||||||
|
} else if (target) {
|
||||||
|
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\"");
|
||||||
|
clearTimeout(timers[target]);
|
||||||
|
delete timers[target];
|
||||||
|
// clear actual cached entry
|
||||||
|
if (!redis) {
|
||||||
|
memCache.delete(target);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
redis.del(target);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[apicache] error in redis.del(\"" + target + "\")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove from global index
|
||||||
|
index.all = index.all.filter(doesntMatch(target));
|
||||||
|
|
||||||
|
// remove target from each group that it may exist in
|
||||||
|
Object.keys(index.groups).forEach(function (groupName) {
|
||||||
|
index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target));
|
||||||
|
|
||||||
|
// delete group if now empty
|
||||||
|
if (!index.groups[groupName].length) {
|
||||||
|
delete index.groups[groupName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
debug("clearing entire index");
|
||||||
|
|
||||||
|
if (!redis) {
|
||||||
|
memCache.clear();
|
||||||
|
} else {
|
||||||
|
// clear redis keys one by one from internal index to prevent clearing non-apicache entries
|
||||||
|
index.all.forEach(function (key) {
|
||||||
|
clearTimeout(timers[key]);
|
||||||
|
delete timers[key];
|
||||||
|
try {
|
||||||
|
redis.del(key);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[apicache] error in redis.del(\"" + key + "\")");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.resetIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getIndex();
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseDuration(duration, defaultDuration) {
|
||||||
|
if (typeof duration === "number") {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof duration === "string") {
|
||||||
|
let split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
|
||||||
|
|
||||||
|
if (split.length === 3) {
|
||||||
|
let len = parseFloat(split[1]);
|
||||||
|
let unit = split[2].replace(/s$/i, "").toLowerCase();
|
||||||
|
if (unit === "m") {
|
||||||
|
unit = "ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (len || 1) * (t[unit] || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getDuration = function (duration) {
|
||||||
|
return parseDuration(duration, globalOptions.defaultDuration);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return cache performance statistics (hit rate). Suitable for putting into a route:
|
||||||
|
* <code>
|
||||||
|
* app.get('/api/cache/performance', (req, res) => {
|
||||||
|
* res.json(apicache.getPerformance())
|
||||||
|
* })
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
this.getPerformance = function () {
|
||||||
|
return performanceArray.map(function (p) {
|
||||||
|
return p.report();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getIndex = function (group) {
|
||||||
|
if (group) {
|
||||||
|
return index.groups[group];
|
||||||
|
} else {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
|
||||||
|
let duration = instance.getDuration(strDuration);
|
||||||
|
let opt = {};
|
||||||
|
|
||||||
|
middlewareOptions.push({
|
||||||
|
options: opt,
|
||||||
|
});
|
||||||
|
|
||||||
|
let options = function (localOptions) {
|
||||||
|
if (localOptions) {
|
||||||
|
middlewareOptions.find(function (middleware) {
|
||||||
|
return middleware.options === opt;
|
||||||
|
}).localOptions = localOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncOptions();
|
||||||
|
|
||||||
|
return opt;
|
||||||
|
};
|
||||||
|
|
||||||
|
options(localOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Function for non tracking performance
|
||||||
|
*/
|
||||||
|
function NOOPCachePerformance() {
|
||||||
|
this.report = this.hit = this.miss = function () {}; // noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
|
||||||
|
*/
|
||||||
|
function CachePerformance() {
|
||||||
|
/**
|
||||||
|
* Tracks the hit rate for the last 100 requests.
|
||||||
|
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
|
||||||
|
*/
|
||||||
|
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the hit rate for the last 1000 requests.
|
||||||
|
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
|
||||||
|
*/
|
||||||
|
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the hit rate for the last 10000 requests.
|
||||||
|
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
|
||||||
|
*/
|
||||||
|
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the hit rate for the last 100000 requests.
|
||||||
|
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
|
||||||
|
*/
|
||||||
|
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of calls that have passed through the middleware since the server started.
|
||||||
|
*/
|
||||||
|
this.callCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of hits since the server started
|
||||||
|
*/
|
||||||
|
this.hitCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
|
||||||
|
*/
|
||||||
|
this.lastCacheHit = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
|
||||||
|
*/
|
||||||
|
this.lastCacheMiss = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return performance statistics
|
||||||
|
*/
|
||||||
|
this.report = function () {
|
||||||
|
return {
|
||||||
|
lastCacheHit: this.lastCacheHit,
|
||||||
|
lastCacheMiss: this.lastCacheMiss,
|
||||||
|
callCount: this.callCount,
|
||||||
|
hitCount: this.hitCount,
|
||||||
|
missCount: this.callCount - this.hitCount,
|
||||||
|
hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
|
||||||
|
hitRateLast100: this.hitRate(this.hitsLast100),
|
||||||
|
hitRateLast1000: this.hitRate(this.hitsLast1000),
|
||||||
|
hitRateLast10000: this.hitRate(this.hitsLast10000),
|
||||||
|
hitRateLast100000: this.hitRate(this.hitsLast100000),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a cache hit rate from an array of hits and misses.
|
||||||
|
* @param {Uint8Array} array An array representing hits and misses.
|
||||||
|
* @returns a number between 0 and 1, or null if the array has no hits or misses
|
||||||
|
*/
|
||||||
|
this.hitRate = function (array) {
|
||||||
|
let hits = 0;
|
||||||
|
let misses = 0;
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
let n8 = array[i];
|
||||||
|
for (let j = 0; j < 4; j++) {
|
||||||
|
switch (n8 & 3) {
|
||||||
|
case 1:
|
||||||
|
hits++;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
misses++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
n8 >>= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let total = hits + misses;
|
||||||
|
if (total == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return hits / total;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a hit or miss in the given array. It will be recorded at a position determined
|
||||||
|
* by the current value of the callCount variable.
|
||||||
|
* @param {Uint8Array} array An array representing hits and misses.
|
||||||
|
* @param {boolean} hit true for a hit, false for a miss
|
||||||
|
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
|
||||||
|
* Each hit or miss is encoded as to bits as follows:
|
||||||
|
* 00 means no hit or miss has been recorded in these bits
|
||||||
|
* 01 encodes a hit
|
||||||
|
* 10 encodes a miss
|
||||||
|
*/
|
||||||
|
this.recordHitInArray = function (array, hit) {
|
||||||
|
let arrayIndex = ~~(this.callCount / 4) % array.length;
|
||||||
|
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
||||||
|
let clearMask = ~(3 << bitOffset);
|
||||||
|
let record = (hit ? 1 : 2) << bitOffset;
|
||||||
|
array[arrayIndex] = (array[arrayIndex] & clearMask) | record;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the hit or miss in the tracking arrays and increments the call count.
|
||||||
|
* @param {boolean} hit true records a hit, false records a miss
|
||||||
|
*/
|
||||||
|
this.recordHit = function (hit) {
|
||||||
|
this.recordHitInArray(this.hitsLast100, hit);
|
||||||
|
this.recordHitInArray(this.hitsLast1000, hit);
|
||||||
|
this.recordHitInArray(this.hitsLast10000, hit);
|
||||||
|
this.recordHitInArray(this.hitsLast100000, hit);
|
||||||
|
if (hit) {
|
||||||
|
this.hitCount++;
|
||||||
|
}
|
||||||
|
this.callCount++;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a hit event, setting lastCacheMiss to the given key
|
||||||
|
* @param {string} key The key that had the cache hit
|
||||||
|
*/
|
||||||
|
this.hit = function (key) {
|
||||||
|
this.recordHit(true);
|
||||||
|
this.lastCacheHit = key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a miss event, setting lastCacheMiss to the given key
|
||||||
|
* @param {string} key The key that had the cache miss
|
||||||
|
*/
|
||||||
|
this.miss = function (key) {
|
||||||
|
this.recordHit(false);
|
||||||
|
this.lastCacheMiss = key;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance();
|
||||||
|
|
||||||
|
performanceArray.push(perf);
|
||||||
|
|
||||||
|
let cache = function (req, res, next) {
|
||||||
|
function bypass() {
|
||||||
|
debug("bypass detected, skipping cache.");
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// initial bypass chances
|
||||||
|
if (!opt.enabled) {
|
||||||
|
return bypass();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
req.headers["x-apicache-bypass"] ||
|
||||||
|
req.headers["x-apicache-force-fetch"] ||
|
||||||
|
(opt.respectCacheControl && req.headers["cache-control"] == "no-cache")
|
||||||
|
) {
|
||||||
|
return bypass();
|
||||||
|
}
|
||||||
|
|
||||||
|
// REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
|
||||||
|
// if (typeof middlewareToggle === 'function') {
|
||||||
|
// if (!middlewareToggle(req, res)) return bypass()
|
||||||
|
// } else if (middlewareToggle !== undefined && !middlewareToggle) {
|
||||||
|
// return bypass()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// embed timer
|
||||||
|
req.apicacheTimer = new Date();
|
||||||
|
|
||||||
|
// In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
|
||||||
|
let key = req.originalUrl || req.url;
|
||||||
|
|
||||||
|
// Remove querystring from key if jsonp option is enabled
|
||||||
|
if (opt.jsonp) {
|
||||||
|
key = url.parse(key).pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add appendKey (either custom function or response path)
|
||||||
|
if (typeof opt.appendKey === "function") {
|
||||||
|
key += "$$appendKey=" + opt.appendKey(req, res);
|
||||||
|
} else if (opt.appendKey.length > 0) {
|
||||||
|
let appendKey = req;
|
||||||
|
|
||||||
|
for (let i = 0; i < opt.appendKey.length; i++) {
|
||||||
|
appendKey = appendKey[opt.appendKey[i]];
|
||||||
|
}
|
||||||
|
key += "$$appendKey=" + appendKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt cache hit
|
||||||
|
let redis = opt.redisClient;
|
||||||
|
let cached = !redis ? memCache.getValue(key) : null;
|
||||||
|
|
||||||
|
// send if cache hit from memory-cache
|
||||||
|
if (cached) {
|
||||||
|
let elapsed = new Date() - req.apicacheTimer;
|
||||||
|
debug("sending cached (memory-cache) version of", key, logDuration(elapsed));
|
||||||
|
|
||||||
|
perf.hit(key);
|
||||||
|
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// send if cache hit from redis
|
||||||
|
if (redis && redis.connected) {
|
||||||
|
try {
|
||||||
|
redis.hgetall(key, function (err, obj) {
|
||||||
|
if (!err && obj && obj.response) {
|
||||||
|
let elapsed = new Date() - req.apicacheTimer;
|
||||||
|
debug("sending cached (redis) version of", key, logDuration(elapsed));
|
||||||
|
|
||||||
|
perf.hit(key);
|
||||||
|
return sendCachedResponse(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
JSON.parse(obj.response),
|
||||||
|
middlewareToggle,
|
||||||
|
next,
|
||||||
|
duration
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
perf.miss(key);
|
||||||
|
return makeResponseCacheable(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
next,
|
||||||
|
key,
|
||||||
|
duration,
|
||||||
|
strDuration,
|
||||||
|
middlewareToggle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// bypass redis on error
|
||||||
|
perf.miss(key);
|
||||||
|
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
perf.miss(key);
|
||||||
|
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.options = options;
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.options = function (options) {
|
||||||
|
if (options) {
|
||||||
|
Object.assign(globalOptions, options);
|
||||||
|
syncOptions();
|
||||||
|
|
||||||
|
if ("defaultDuration" in options) {
|
||||||
|
// Convert the default duration to a number in milliseconds (if needed)
|
||||||
|
globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalOptions.trackPerformance) {
|
||||||
|
debug("WARNING: using trackPerformance flag can cause high memory usage!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
} else {
|
||||||
|
return globalOptions;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.resetIndex = function () {
|
||||||
|
index = {
|
||||||
|
all: [],
|
||||||
|
groups: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newInstance = function (config) {
|
||||||
|
let instance = new ApiCache();
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
instance.options(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clone = function () {
|
||||||
|
return this.newInstance(this.options());
|
||||||
|
};
|
||||||
|
|
||||||
|
// initialize index
|
||||||
|
this.resetIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new ApiCache();
|
14
server/modules/apicache/index.js
Normal file
14
server/modules/apicache/index.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const apicache = require("./apicache");
|
||||||
|
|
||||||
|
apicache.options({
|
||||||
|
headerBlacklist: [
|
||||||
|
"cache-control"
|
||||||
|
],
|
||||||
|
headers: {
|
||||||
|
// Disable client side cache, only server side cache.
|
||||||
|
// BUG! Not working for the second request
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = apicache;
|
59
server/modules/apicache/memory-cache.js
Normal file
59
server/modules/apicache/memory-cache.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
function MemoryCache() {
|
||||||
|
this.cache = {};
|
||||||
|
this.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
|
||||||
|
let old = this.cache[key];
|
||||||
|
let instance = this;
|
||||||
|
|
||||||
|
let entry = {
|
||||||
|
value: value,
|
||||||
|
expire: time + Date.now(),
|
||||||
|
timeout: setTimeout(function () {
|
||||||
|
instance.delete(key);
|
||||||
|
return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key);
|
||||||
|
}, time)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cache[key] = entry;
|
||||||
|
this.size = Object.keys(this.cache).length;
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryCache.prototype.delete = function (key) {
|
||||||
|
let entry = this.cache[key];
|
||||||
|
|
||||||
|
if (entry) {
|
||||||
|
clearTimeout(entry.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.cache[key];
|
||||||
|
|
||||||
|
this.size = Object.keys(this.cache).length;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryCache.prototype.get = function (key) {
|
||||||
|
let entry = this.cache[key];
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryCache.prototype.getValue = function (key) {
|
||||||
|
let entry = this.get(key);
|
||||||
|
|
||||||
|
return entry && entry.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryCache.prototype.clear = function () {
|
||||||
|
Object.keys(this.cache).forEach(function (key) {
|
||||||
|
this.delete(key);
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = MemoryCache;
|
@ -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
Normal file
124
server/notification-providers/teams.js
Normal file
@ -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;
|
@ -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(),
|
||||||
|
151
server/routers/api-router.js
Normal file
151
server/routers/api-router.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
let express = require("express");
|
||||||
|
const { allowDevAllOrigin, getSettings, setting } = require("../util-server");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const server = require("../server");
|
||||||
|
const apicache = require("../modules/apicache");
|
||||||
|
const Monitor = require("../model/monitor");
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
let cache = apicache.middleware;
|
||||||
|
|
||||||
|
router.get("/api/entry-page", async (_, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
response.json(server.entryPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page Config
|
||||||
|
router.get("/api/status-page/config", async (_request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
let config = await getSettings("statusPage");
|
||||||
|
|
||||||
|
if (! config.statusPageTheme) {
|
||||||
|
config.statusPageTheme = "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! config.statusPagePublished) {
|
||||||
|
config.statusPagePublished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! config.title) {
|
||||||
|
config.title = "Uptime Kuma";
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page - Get the current Incident
|
||||||
|
// Can fetch only if published
|
||||||
|
router.get("/api/status-page/incident", async (_, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkPublished();
|
||||||
|
|
||||||
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
||||||
|
|
||||||
|
if (incident) {
|
||||||
|
incident = incident.toPublicJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
ok: true,
|
||||||
|
incident,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page - Monitor List
|
||||||
|
// Can fetch only if published
|
||||||
|
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkPublished();
|
||||||
|
const publicGroupList = [];
|
||||||
|
let list = await R.find("group", " public = 1 ORDER BY weight ");
|
||||||
|
|
||||||
|
for (let groupBean of list) {
|
||||||
|
publicGroupList.push(await groupBean.toPublicJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json(publicGroupList);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status Page Polling Data
|
||||||
|
// Can fetch only if published
|
||||||
|
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
|
||||||
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkPublished();
|
||||||
|
|
||||||
|
let heartbeatList = {};
|
||||||
|
let uptimeList = {};
|
||||||
|
|
||||||
|
let monitorIDList = await R.getCol(`
|
||||||
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
AND public = 1
|
||||||
|
`);
|
||||||
|
|
||||||
|
for (let monitorID of monitorIDList) {
|
||||||
|
let list = await R.getAll(`
|
||||||
|
SELECT * FROM heartbeat
|
||||||
|
WHERE monitor_id = ?
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT 50
|
||||||
|
`, [
|
||||||
|
monitorID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
list = R.convertToBeans("heartbeat", list);
|
||||||
|
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
||||||
|
|
||||||
|
const type = 24;
|
||||||
|
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
heartbeatList,
|
||||||
|
uptimeList
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
send403(response, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkPublished() {
|
||||||
|
if (! await isPublished()) {
|
||||||
|
throw new Error("The status page is not published");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default is published
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function isPublished() {
|
||||||
|
const value = await setting("statusPagePublished");
|
||||||
|
if (value === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function send403(res, msg = "") {
|
||||||
|
res.status(403).json({
|
||||||
|
"status": "fail",
|
||||||
|
"msg": msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = router;
|
782
server/server.js
782
server/server.js
File diff suppressed because it is too large
Load Diff
161
server/socket-handlers/status-page-socket-handler.js
Normal file
161
server/socket-handlers/status-page-socket-handler.js
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
const { R } = require("redbean-node");
|
||||||
|
const { checkLogin, setSettings } = require("../util-server");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const { debug } = require("../../src/util");
|
||||||
|
const ImageDataURI = require("../image-data-uri");
|
||||||
|
const Database = require("../database");
|
||||||
|
const apicache = require("../modules/apicache");
|
||||||
|
|
||||||
|
module.exports.statusPageSocketHandler = (socket) => {
|
||||||
|
|
||||||
|
// Post or edit incident
|
||||||
|
socket.on("postIncident", async (incident, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 ");
|
||||||
|
|
||||||
|
let incidentBean;
|
||||||
|
|
||||||
|
if (incident.id) {
|
||||||
|
incidentBean = await R.findOne("incident", " id = ?", [
|
||||||
|
incident.id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incidentBean == null) {
|
||||||
|
incidentBean = R.dispense("incident");
|
||||||
|
}
|
||||||
|
|
||||||
|
incidentBean.title = incident.title;
|
||||||
|
incidentBean.content = incident.content;
|
||||||
|
incidentBean.style = incident.style;
|
||||||
|
incidentBean.pin = true;
|
||||||
|
|
||||||
|
if (incident.id) {
|
||||||
|
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
||||||
|
} else {
|
||||||
|
incidentBean.createdDate = R.isoDateTime(dayjs.utc());
|
||||||
|
}
|
||||||
|
|
||||||
|
await R.store(incidentBean);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
incident: incidentBean.toPublicJSON(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("unpinIncident", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save Status Page
|
||||||
|
// imgDataUrl Only Accept PNG!
|
||||||
|
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
|
const header = "data:image/png;base64,";
|
||||||
|
|
||||||
|
// Check logo format
|
||||||
|
// If is image data url, convert to png file
|
||||||
|
// Else assume it is a url, nothing to do
|
||||||
|
if (imgDataUrl.startsWith("data:")) {
|
||||||
|
if (! imgDataUrl.startsWith(header)) {
|
||||||
|
throw new Error("Only allowed PNG logo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to file
|
||||||
|
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
|
||||||
|
config.logo = "/upload/logo.png?t=" + Date.now();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
config.icon = imgDataUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Config
|
||||||
|
await setSettings("statusPage", config);
|
||||||
|
|
||||||
|
// Save Public Group List
|
||||||
|
const groupIDList = [];
|
||||||
|
let groupOrder = 1;
|
||||||
|
|
||||||
|
for (let group of publicGroupList) {
|
||||||
|
let groupBean;
|
||||||
|
if (group.id) {
|
||||||
|
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
|
||||||
|
group.id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
groupBean = R.dispense("group");
|
||||||
|
}
|
||||||
|
|
||||||
|
groupBean.name = group.name;
|
||||||
|
groupBean.public = true;
|
||||||
|
groupBean.weight = groupOrder++;
|
||||||
|
|
||||||
|
await R.store(groupBean);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [
|
||||||
|
groupBean.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
let monitorOrder = 1;
|
||||||
|
console.log(group.monitorList);
|
||||||
|
|
||||||
|
for (let monitor of group.monitorList) {
|
||||||
|
let relationBean = R.dispense("monitor_group");
|
||||||
|
relationBean.weight = monitorOrder++;
|
||||||
|
relationBean.group_id = groupBean.id;
|
||||||
|
relationBean.monitor_id = monitor.id;
|
||||||
|
await R.store(relationBean);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIDList.push(groupBean.id);
|
||||||
|
group.id = groupBean.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete groups that not in the list
|
||||||
|
debug("Delete groups that not in the list");
|
||||||
|
const slots = groupIDList.map(() => "?").join(",");
|
||||||
|
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
publicGroupList,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
@ -23,7 +23,7 @@ exports.initJWTSecret = async () => {
|
|||||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
|
||||||
await R.store(jwtSecretBean);
|
await R.store(jwtSecretBean);
|
||||||
return jwtSecretBean;
|
return jwtSecretBean;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.tcping = function (hostname, port) {
|
exports.tcping = function (hostname, port) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -44,7 +44,7 @@ exports.tcping = function (hostname, port) {
|
|||||||
resolve(Math.round(data.max));
|
resolve(Math.round(data.max));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.ping = async (hostname) => {
|
exports.ping = async (hostname) => {
|
||||||
try {
|
try {
|
||||||
@ -57,7 +57,7 @@ exports.ping = async (hostname) => {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.pingAsync = function (hostname, ipv6 = false) {
|
exports.pingAsync = function (hostname, ipv6 = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -69,13 +69,13 @@ exports.pingAsync = function (hostname, ipv6 = false) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else if (ms === null) {
|
} else if (ms === null) {
|
||||||
reject(new Error(stdout))
|
reject(new Error(stdout));
|
||||||
} else {
|
} else {
|
||||||
resolve(Math.round(ms))
|
resolve(Math.round(ms));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
@ -98,8 +98,8 @@ exports.dnsResolve = function (hostname, resolver_server, rrtype) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.setting = async function (key) {
|
exports.setting = async function (key) {
|
||||||
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||||
@ -108,29 +108,29 @@ exports.setting = async function (key) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const v = JSON.parse(value);
|
const v = JSON.parse(value);
|
||||||
debug(`Get Setting: ${key}: ${v}`)
|
debug(`Get Setting: ${key}: ${v}`);
|
||||||
return v;
|
return v;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.setSetting = async function (key, value) {
|
exports.setSetting = async function (key, value) {
|
||||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
key,
|
key,
|
||||||
])
|
]);
|
||||||
if (!bean) {
|
if (!bean) {
|
||||||
bean = R.dispense("setting")
|
bean = R.dispense("setting");
|
||||||
bean.key = key;
|
bean.key = key;
|
||||||
}
|
}
|
||||||
bean.value = JSON.stringify(value);
|
bean.value = JSON.stringify(value);
|
||||||
await R.store(bean)
|
await R.store(bean);
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.getSettings = async function (type) {
|
exports.getSettings = async function (type) {
|
||||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||||
type,
|
type,
|
||||||
])
|
]);
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ exports.getSettings = async function (type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.setSettings = async function (type, data) {
|
exports.setSettings = async function (type, data) {
|
||||||
let keyList = Object.keys(data);
|
let keyList = Object.keys(data);
|
||||||
@ -163,12 +163,12 @@ exports.setSettings = async function (type, data) {
|
|||||||
|
|
||||||
if (bean.type === type) {
|
if (bean.type === type) {
|
||||||
bean.value = JSON.stringify(data[key]);
|
bean.value = JSON.stringify(data[key]);
|
||||||
promiseList.push(R.store(bean))
|
promiseList.push(R.store(bean));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promiseList);
|
await Promise.all(promiseList);
|
||||||
}
|
};
|
||||||
|
|
||||||
// ssl-checker by @dyaa
|
// ssl-checker by @dyaa
|
||||||
// param: res - response object from axios
|
// param: res - response object from axios
|
||||||
@ -218,7 +218,7 @@ exports.checkCertificate = function (res) {
|
|||||||
issuer,
|
issuer,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
// Check if the provided status code is within the accepted ranges
|
// Check if the provided status code is within the accepted ranges
|
||||||
// Param: status - the status code to check
|
// Param: status - the status code to check
|
||||||
@ -247,7 +247,7 @@ exports.checkStatusCode = function (status, accepted_codes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
exports.getTotalClientInRoom = (io, roomName) => {
|
exports.getTotalClientInRoom = (io, roomName) => {
|
||||||
|
|
||||||
@ -270,4 +270,31 @@ exports.getTotalClientInRoom = (io, roomName) => {
|
|||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
exports.genSecret = () => {
|
||||||
|
let secret = "";
|
||||||
|
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let charsLength = chars.length;
|
||||||
|
for ( let i = 0; i < 64; i++ ) {
|
||||||
|
secret += chars.charAt(Math.floor(Math.random() * charsLength));
|
||||||
|
}
|
||||||
|
return secret;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.allowDevAllOrigin = (res) => {
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
exports.allowAllOrigin(res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.allowAllOrigin = (res) => {
|
||||||
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
|
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.checkLogin = (socket) => {
|
||||||
|
if (! socket.userID) {
|
||||||
|
throw new Error("You are not logged in.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -144,7 +144,9 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shadow-box {
|
.shadow-box {
|
||||||
background-color: $dark-bg;
|
&:not(.alert) {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-check-input {
|
.form-check-input {
|
||||||
@ -255,6 +257,18 @@ h2 {
|
|||||||
background-color: $dark-bg;
|
background-color: $dark-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monitor-list {
|
||||||
|
.item {
|
||||||
|
&:hover {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 550px) {
|
@media (max-width: 550px) {
|
||||||
.table-shadow-box {
|
.table-shadow-box {
|
||||||
tbody {
|
tbody {
|
||||||
@ -268,6 +282,16 @@ h2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
&.bg-info,
|
||||||
|
&.bg-warning,
|
||||||
|
&.bg-danger,
|
||||||
|
&.bg-light {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -288,3 +312,119 @@ h2 {
|
|||||||
transform: translateY(50px);
|
transform: translateY(50px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-fade-right-enter-active {
|
||||||
|
transition: all 0.2s $easing-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-right-leave-active {
|
||||||
|
transition: all 0.2s $easing-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-right-enter-from,
|
||||||
|
.slide-fade-right-leave-to {
|
||||||
|
transform: translateX(50px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-list {
|
||||||
|
&.scrollbar {
|
||||||
|
min-height: calc(100vh - 240px);
|
||||||
|
max-height: calc(100vh - 30px);
|
||||||
|
overflow-y: auto;
|
||||||
|
position: sticky;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 13px 15px 10px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all ease-in-out 0.15s;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $highlight-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #cdf8f4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
color: #122f21;
|
||||||
|
background-color: $primary;
|
||||||
|
border-color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
color: #055160;
|
||||||
|
background-color: #cff4fc;
|
||||||
|
border-color: #cff4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
color: #842029;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border-color: #f8d7da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #4caf50;
|
||||||
|
border-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable=true] {
|
||||||
|
transition: all $easing-in 0.2s;
|
||||||
|
background-color: rgba(239, 239, 239, 0.7);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0 solid #eee;
|
||||||
|
background-color: rgba(245, 245, 245, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(239, 239, 239, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: rgba(239, 239, 239, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
&::after {
|
||||||
|
margin-left: 5px;
|
||||||
|
content: "🖊️";
|
||||||
|
font-size: 13px;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
transition: all $easing-in 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-image-crop-upload .vicp-wrap {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
@ -25,6 +25,10 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
heartbeatList: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -38,8 +42,15 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If heartbeatList is null, get it from $root.heartbeatList
|
||||||
|
*/
|
||||||
beatList() {
|
beatList() {
|
||||||
return this.$root.heartbeatList[this.monitorId]
|
if (this.heartbeatList === null) {
|
||||||
|
return this.$root.heartbeatList[this.monitorId];
|
||||||
|
} else {
|
||||||
|
return this.heartbeatList;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
shortBeatList() {
|
shortBeatList() {
|
||||||
@ -118,8 +129,10 @@ export default {
|
|||||||
window.removeEventListener("resize", this.resize);
|
window.removeEventListener("resize", this.resize);
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
if (this.heartbeatList === null) {
|
||||||
this.$root.heartbeatList[this.monitorId] = [];
|
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||||
|
this.$root.heartbeatList[this.monitorId] = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -4,16 +4,23 @@
|
|||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
<h1 class="h3 mb-3 fw-normal" />
|
<h1 class="h3 mb-3 fw-normal" />
|
||||||
|
|
||||||
<div class="form-floating">
|
<div v-if="!tokenRequired" class="form-floating">
|
||||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
|
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
|
||||||
<label for="floatingInput">{{ $t("Username") }}</label>
|
<label for="floatingInput">{{ $t("Username") }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mt-3">
|
<div v-if="!tokenRequired" class="form-floating mt-3">
|
||||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
|
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
|
||||||
<label for="floatingPassword">{{ $t("Password") }}</label>
|
<label for="floatingPassword">{{ $t("Password") }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="tokenRequired">
|
||||||
|
<div class="form-floating mt-3">
|
||||||
|
<input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
|
||||||
|
<label for="floatingToken">{{ $t("Token") }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
|
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
|
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
|
||||||
@ -42,16 +49,24 @@ export default {
|
|||||||
processing: false,
|
processing: false,
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
token: "",
|
||||||
res: null,
|
res: null,
|
||||||
|
tokenRequired: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.$root.login(this.username, this.password, (res) => {
|
|
||||||
|
this.$root.login(this.username, this.password, this.token, (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.res = res;
|
console.log(res)
|
||||||
|
|
||||||
|
if (res.tokenRequired) {
|
||||||
|
this.tokenRequired = true;
|
||||||
|
} else {
|
||||||
|
this.res = res;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,44 +1,69 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shadow-box list mb-3" :class="{ scrollbar: scrollbar }">
|
<div class="shadow-box mb-3">
|
||||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
<div class="list-header">
|
||||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
<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>
|
||||||
|
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<Uptime :monitor="item" type="24" :pill="true" />
|
<Uptime :monitor="item" type="24" :pill="true" />
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
<div class="tags">
|
||||||
|
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
||||||
|
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
|
||||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||||
|
</div>
|
||||||
</div>
|
</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,57 +126,51 @@ export default {
|
|||||||
padding-right: 5px !important;
|
padding-right: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list-header {
|
||||||
&.scrollbar {
|
border-bottom: 1px solid #dee2e6;
|
||||||
min-height: calc(100vh - 240px);
|
border-radius: 10px 10px 0 0;
|
||||||
max-height: calc(100vh - 30px);
|
margin: -10px;
|
||||||
overflow-y: auto;
|
margin-bottom: 10px;
|
||||||
position: sticky;
|
padding: 10px;
|
||||||
top: 10px;
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
|
||||||
.item {
|
.dark & {
|
||||||
display: block;
|
background-color: #161b22;
|
||||||
text-decoration: none;
|
border-bottom: 0;
|
||||||
padding: 13px 15px 10px 15px;
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all ease-in-out 0.15s;
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $highlight-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: #cdf8f4;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
@media (max-width: 770px) {
|
||||||
.list {
|
.list-header {
|
||||||
.item {
|
margin: -20px;
|
||||||
&:hover {
|
margin-bottom: 10px;
|
||||||
background-color: $dark-bg2;
|
padding: 5px;
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $dark-bg2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
padding: 10px;
|
||||||
|
color: #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
max-width: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
.monitorItem {
|
.monitorItem {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
padding-left: 62px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -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">
|
||||||
|
|
||||||
@ -410,7 +418,7 @@
|
|||||||
|
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input v-model="notification.applyExisting" class="form-check-input" type="checkbox">
|
<input v-model="notification.applyExisting" class="form-check-input" type="checkbox">
|
||||||
<label class="form-check-label">{{ $t("Also apply to existing monitors") }}</label>
|
<label class="form-check-label">{{ $t("Apply on all existing monitors") }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -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: {},
|
||||||
|
144
src/components/PublicGroupList.vue
Normal file
144
src/components/PublicGroupList.vue
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Group List -->
|
||||||
|
<Draggable
|
||||||
|
v-model="$root.publicGroupList"
|
||||||
|
:disabled="!editMode"
|
||||||
|
item-key="id"
|
||||||
|
:animation="100"
|
||||||
|
>
|
||||||
|
<template #item="group">
|
||||||
|
<div class="mb-5 ">
|
||||||
|
<!-- Group Title -->
|
||||||
|
<h2 class="group-title">
|
||||||
|
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
|
||||||
|
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
|
||||||
|
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" />
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="shadow-box monitor-list mt-4 position-relative">
|
||||||
|
<div v-if="group.element.monitorList.length === 0" class="text-center no-monitor-msg">
|
||||||
|
{{ $t("No Monitors") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Monitor List -->
|
||||||
|
<!-- animation is not working, no idea why -->
|
||||||
|
<Draggable
|
||||||
|
v-model="group.element.monitorList"
|
||||||
|
class="monitor-list"
|
||||||
|
group="same-group"
|
||||||
|
:disabled="!editMode"
|
||||||
|
:animation="100"
|
||||||
|
item-key="id"
|
||||||
|
>
|
||||||
|
<template #item="monitor">
|
||||||
|
<div class="item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9 col-md-8 small-padding">
|
||||||
|
<div class="info">
|
||||||
|
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
|
||||||
|
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
|
||||||
|
|
||||||
|
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||||
|
{{ monitor.element.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
|
||||||
|
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Draggable from "vuedraggable";
|
||||||
|
import HeartbeatBar from "./HeartbeatBar.vue";
|
||||||
|
import Uptime from "./Uptime.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Draggable,
|
||||||
|
HeartbeatBar,
|
||||||
|
Uptime,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
editMode: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showGroupDrag() {
|
||||||
|
return (this.$root.publicGroupList.length >= 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeGroup(index) {
|
||||||
|
this.$root.publicGroupList.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeMonitor(groupIndex, index) {
|
||||||
|
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars";
|
||||||
|
|
||||||
|
.no-monitor-msg {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-list {
|
||||||
|
min-height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-list-move {
|
||||||
|
transition: transform 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-move {
|
||||||
|
transition: transform 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag {
|
||||||
|
color: #bbb;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
.item {
|
||||||
|
padding: 13px 0 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
73
src/components/Tag.vue
Normal file
73
src/components/Tag.vue
Normal file
@ -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
Normal file
405
src/components/TagsManager.vue
Normal file
@ -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>
|
178
src/components/TwoFADialog.vue
Normal file
178
src/components/TwoFADialog.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
{{ $t("Setup 2FA") }}
|
||||||
|
<span v-if="twoFAStatus == true" class="badge bg-primary">{{ $t("Active") }}</span>
|
||||||
|
<span v-if="twoFAStatus == false" class="badge bg-primary">{{ $t("Inactive") }}</span>
|
||||||
|
</h5>
|
||||||
|
<button :disabled="processing" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div v-if="uri && twoFAStatus == false" class="mx-auto text-center" style="width: 210px;">
|
||||||
|
<vue-qrcode :key="uri" :value="uri" type="image/png" :quality="1" :color="{ light: '#ffffffff' }" />
|
||||||
|
<button v-show="!showURI" type="button" class="btn btn-outline-primary btn-sm mt-2" @click="showURI = true">{{ $t("Show URI") }}</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
|
||||||
|
|
||||||
|
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
|
||||||
|
{{ $t("Enable 2FA") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="twoFAStatus == true" class="btn btn-danger" type="button" :disabled="processing" @click="confirmDisableTwoFA()">
|
||||||
|
{{ $t("Disable 2FA") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="uri && twoFAStatus == false" class="mt-3">
|
||||||
|
<label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input v-model="token" type="text" maxlength="6" class="form-control">
|
||||||
|
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
|
||||||
|
</div>
|
||||||
|
<p v-show="tokenValid" class="mt-2" style="color: green">{{ $t("tokenValidSettingsMsg") }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="uri && twoFAStatus == false" class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="processing || tokenValid == false" @click="confirmEnableTwoFA()">
|
||||||
|
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Confirm ref="confirmEnableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="save2FA">
|
||||||
|
{{ $t("confirmEnableTwoFAMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDisableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disable2FA">
|
||||||
|
{{ $t("confirmDisableTwoFAMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Modal } from "bootstrap"
|
||||||
|
import Confirm from "./Confirm.vue";
|
||||||
|
import VueQrcode from "vue-qrcode"
|
||||||
|
import { useToast } from "vue-toastification"
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
VueQrcode,
|
||||||
|
},
|
||||||
|
props: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
uri: null,
|
||||||
|
tokenValid: false,
|
||||||
|
twoFAStatus: null,
|
||||||
|
token: null,
|
||||||
|
showURI: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal)
|
||||||
|
this.getStatus();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
show() {
|
||||||
|
this.modal.show()
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmEnableTwoFA() {
|
||||||
|
this.$refs.confirmEnableTwoFA.show()
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDisableTwoFA() {
|
||||||
|
this.$refs.confirmDisableTwoFA.show()
|
||||||
|
},
|
||||||
|
|
||||||
|
prepare2FA() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("prepare2FA", (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.uri = res.uri;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
save2FA() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("save2FA", (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.$root.toastRes(res)
|
||||||
|
this.getStatus();
|
||||||
|
this.modal.hide();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
disable2FA() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("disable2FA", (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.$root.toastRes(res)
|
||||||
|
this.getStatus();
|
||||||
|
this.modal.hide();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyToken() {
|
||||||
|
this.$root.getSocket().emit("verifyToken", this.token, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.tokenValid = res.valid;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
this.$root.getSocket().emit("twoFAStatus", (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.twoFAStatus = res.status;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.modal-dialog .form-text, .modal-dialog p {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -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
Normal file
29
src/components/notifications/Teams.vue
Normal file
@ -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>
|
50
src/i18n.js
Normal file
50
src/i18n.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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 ptBR from "./languages/pt-BR";
|
||||||
|
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,
|
||||||
|
"pt-BR": ptBR,
|
||||||
|
"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: true,
|
||||||
|
messages: languageList,
|
||||||
|
});
|
64
src/icon.js
64
src/icon.js
@ -1,10 +1,60 @@
|
|||||||
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 } from "@fortawesome/free-solid-svg-icons"
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
//import { fa } from '@fortawesome/free-regular-svg-icons'
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
|
||||||
|
|
||||||
// Add Free Font Awesome Icons here
|
// Add Free Font Awesome Icons
|
||||||
// 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);
|
import {
|
||||||
|
faArrowAltCircleUp,
|
||||||
|
faCog,
|
||||||
|
faEdit,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faList,
|
||||||
|
faPause,
|
||||||
|
faPlay,
|
||||||
|
faPlus,
|
||||||
|
faSearch,
|
||||||
|
faTachometerAlt,
|
||||||
|
faTimes,
|
||||||
|
faTimesCircle,
|
||||||
|
faTrash,
|
||||||
|
faCheckCircle,
|
||||||
|
faStream,
|
||||||
|
faSave,
|
||||||
|
faExclamationCircle,
|
||||||
|
faBullhorn,
|
||||||
|
faArrowsAltV,
|
||||||
|
faUnlink,
|
||||||
|
faQuestionCircle,
|
||||||
|
faImages, faUpload,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faArrowAltCircleUp,
|
||||||
|
faCog,
|
||||||
|
faEdit,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faList,
|
||||||
|
faPause,
|
||||||
|
faPlay,
|
||||||
|
faPlus,
|
||||||
|
faSearch,
|
||||||
|
faTachometerAlt,
|
||||||
|
faTimes,
|
||||||
|
faTimesCircle,
|
||||||
|
faTrash,
|
||||||
|
faCheckCircle,
|
||||||
|
faStream,
|
||||||
|
faSave,
|
||||||
|
faExclamationCircle,
|
||||||
|
faBullhorn,
|
||||||
|
faArrowsAltV,
|
||||||
|
faUnlink,
|
||||||
|
faQuestionCircle,
|
||||||
|
faImages,
|
||||||
|
faUpload,
|
||||||
|
);
|
||||||
|
|
||||||
|
export { FontAwesomeIcon };
|
||||||
|
|
||||||
export { FontAwesomeIcon }
|
|
||||||
|
@ -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. 😏
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ export default {
|
|||||||
Down: "Inaktiv",
|
Down: "Inaktiv",
|
||||||
Pending: "Afventer",
|
Pending: "Afventer",
|
||||||
Unknown: "Ukendt",
|
Unknown: "Ukendt",
|
||||||
Pause: "Pause",
|
Pause: "Stands",
|
||||||
pauseDashboardHome: "Pauset",
|
pauseDashboardHome: "Standset",
|
||||||
Name: "Navn",
|
Name: "Navn",
|
||||||
Status: "Status",
|
Status: "Status",
|
||||||
DateTime: "Dato / Tid",
|
DateTime: "Dato / Tid",
|
||||||
@ -36,8 +36,7 @@ export default {
|
|||||||
hour: "Timer",
|
hour: "Timer",
|
||||||
"-hour": "-Timer",
|
"-hour": "-Timer",
|
||||||
checkEverySecond: "Tjek hvert {0} sekund",
|
checkEverySecond: "Tjek hvert {0} sekund",
|
||||||
"Avg.": "Gennemsnit",
|
Response: "Respons",
|
||||||
Response: " Respons",
|
|
||||||
Ping: "Ping",
|
Ping: "Ping",
|
||||||
"Monitor Type": "Overvåger Type",
|
"Monitor Type": "Overvåger Type",
|
||||||
Keyword: "Nøgleord",
|
Keyword: "Nøgleord",
|
||||||
@ -103,29 +102,81 @@ export default {
|
|||||||
"Resolver Server": "Navne-server",
|
"Resolver Server": "Navne-server",
|
||||||
rrtypeDescription: "Vælg den type RR, du vil overvåge.",
|
rrtypeDescription: "Vælg den type RR, du vil overvåge.",
|
||||||
"Last Result": "Seneste resultat",
|
"Last Result": "Seneste resultat",
|
||||||
pauseMonitorMsg: "Er du sikker på, at du vil pause Overvågeren?",
|
pauseMonitorMsg: "Er du sikker på, at du vil standse Overvågeren?",
|
||||||
"Create your admin account": "Opret din administratorkonto",
|
"Create your admin account": "Opret din administratorkonto",
|
||||||
"Repeat Password": "Gentag adgangskoden",
|
"Repeat Password": "Gentag adgangskoden",
|
||||||
"Resource Record Type": "Resource Record Type",
|
"Resource Record Type": "Resource Record Type",
|
||||||
respTime: "Resp. Time (ms)",
|
respTime: "Resp. Tid (ms)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A",
|
||||||
Create: "Create",
|
Create: "Opret",
|
||||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
clearEventsMsg: "Er du sikker på vil slette alle events for denne Overvåger?",
|
||||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
clearHeartbeatsMsg: "Er du sikker på vil slette alle heartbeats for denne Overvåger?",
|
||||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
confirmClearStatisticsMsg: "Vil du helt sikkert slette ALLE statistikker?",
|
||||||
"Clear Data": "Clear Data",
|
"Clear Data": "Ryd Data",
|
||||||
Events: "Events",
|
Events: "Events",
|
||||||
Heartbeats: "Heartbeats",
|
Heartbeats: "Heartbeats",
|
||||||
"Auto Get": "Auto Get",
|
"Auto Get": "Auto-hent",
|
||||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
enableDefaultNotificationDescription: "For hver ny overvåger aktiveres denne underretning som standard. Du kan stadig deaktivere underretningen separat for hver skærm.",
|
||||||
"Default enabled": "Default enabled",
|
"Default enabled": "Standard aktiveret",
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
"Also apply to existing monitors": "Anvend også på eksisterende overvågere",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
Export: "Eksport",
|
||||||
Export: "Export",
|
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
backupDescription: "Du kan sikkerhedskopiere alle Overvågere og alle underretninger til en JSON-fil.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
backupDescription2: "PS: Historik og hændelsesdata er ikke inkluderet.",
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
backupDescription3: "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.",
|
||||||
alertNoFile: "Please select a file to import.",
|
alertNoFile: "Vælg en fil der skal importeres.",
|
||||||
alertWrongFileType: "Please select a JSON file."
|
alertWrongFileType: "Vælg venligst en JSON-fil.",
|
||||||
}
|
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",
|
||||||
|
"Show URI": "Show 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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -36,8 +36,7 @@ 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",
|
||||||
Keyword: "Schlüsselwort",
|
Keyword: "Schlüsselwort",
|
||||||
@ -113,19 +112,70 @@ 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)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A",
|
||||||
"Default enabled": "Standardmäßig aktiviert",
|
"Default enabled": "Standardmäßig aktiviert",
|
||||||
"Also apply to existing monitors": "Auch für alle existierenden Monitore aktivieren",
|
"Apply on all existing monitors": "Auf alle existierenden Monitore anwenden",
|
||||||
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
|
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
|
||||||
Create: "Erstellen",
|
Create: "Erstellen",
|
||||||
"Auto Get": "Auto Get",
|
"Auto Get": "Auto Get",
|
||||||
backupDescription: "Es können alle Monitore und alle Benachrichtigungen in einer JSON-Datei gesichert werden.",
|
backupDescription: "Es können alle Monitore und Benachrichtigungen in einer JSON-Datei gesichert werden.",
|
||||||
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
|
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
|
||||||
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
|
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
|
||||||
alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
|
alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
|
||||||
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
|
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
|
||||||
}
|
"Clear all statistics": "Lösche alle Statistiken",
|
||||||
|
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",
|
||||||
|
"Verify Token": "Token verifizieren",
|
||||||
|
"Setup 2FA": "2FA Einrichten",
|
||||||
|
"Enable 2FA": "2FA Aktivieren",
|
||||||
|
"Disable 2FA": "2FA deaktivieren",
|
||||||
|
"2FA Settings": "2FA Einstellungen",
|
||||||
|
confirmEnableTwoFAMsg: "Bist du sicher das du 2FA aktivieren möchtest?",
|
||||||
|
confirmDisableTwoFAMsg: "Bist du sicher das du 2FA deaktivieren möchtest?",
|
||||||
|
tokenValidSettingsMsg: "Token gültig! Du kannst jetzt die 2FA Einstellungen speichern.",
|
||||||
|
"Two Factor Authentication": "Zwei Faktor Authentifizierung",
|
||||||
|
Active: "Aktiv",
|
||||||
|
Inactive: "Inaktiv",
|
||||||
|
Token: "Token",
|
||||||
|
"Show URI": "URI Anzeigen",
|
||||||
|
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": "Durchsch. Ping",
|
||||||
|
"Avg. Response": "Durchsch. Antwort",
|
||||||
|
"Entry Page": "Einstiegsseite",
|
||||||
|
"statusPageNothing": "Nichts ist hier, bitte füge eine Gruppe oder Monitor hinzu.",
|
||||||
|
"No Services": "Keine Dienste",
|
||||||
|
"All Systems Operational": "Alle Systeme Betriebsbereit",
|
||||||
|
"Partially Degraded Service": "Teilweise beeinträchtigter Dienst",
|
||||||
|
"Degraded Service": "Eingeschränkter Dienst",
|
||||||
|
"Add Group": "Gruppe hinzufügen",
|
||||||
|
"Add a monitor": "Monitor hinzufügen",
|
||||||
|
"Edit Status Page": "Bearbeite Statusseite",
|
||||||
|
"Go to Dashboard": "Gehe zum Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,12 @@ 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",
|
||||||
|
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?",
|
||||||
Settings: "Settings",
|
Settings: "Settings",
|
||||||
Dashboard: "Dashboard",
|
Dashboard: "Dashboard",
|
||||||
"New Update": "New Update",
|
"New Update": "New Update",
|
||||||
@ -64,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",
|
||||||
@ -111,13 +118,14 @@ 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)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A",
|
||||||
"Default enabled": "Default enabled",
|
"Default enabled": "Default enabled",
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
"Apply on all existing monitors": "Apply on all existing monitors",
|
||||||
Create: "Create",
|
Create: "Create",
|
||||||
"Clear Data": "Clear Data",
|
"Clear Data": "Clear Data",
|
||||||
Events: "Events",
|
Events: "Events",
|
||||||
@ -127,5 +135,47 @@ export default {
|
|||||||
backupDescription2: "PS: History and event data is not included.",
|
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.",
|
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",
|
||||||
|
"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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,12 +119,64 @@ 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.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
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.",
|
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.",
|
||||||
}
|
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",
|
||||||
|
"Show URI": "Show 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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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.",
|
||||||
@ -10,7 +9,7 @@ export default {
|
|||||||
passwordNotMatchMsg: "Salasõnad ei kattu.",
|
passwordNotMatchMsg: "Salasõnad ei kattu.",
|
||||||
notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.",
|
notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.",
|
||||||
keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
|
keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
|
||||||
pauseDashboardHome: "Seiskamine",
|
pauseDashboardHome: "Seismas",
|
||||||
deleteMonitorMsg: "Kas soovid eemaldada seire?",
|
deleteMonitorMsg: "Kas soovid eemaldada seire?",
|
||||||
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?",
|
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?",
|
||||||
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
|
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
|
||||||
@ -109,23 +108,75 @@ export default {
|
|||||||
"Repeat Password": "korda salasõna",
|
"Repeat Password": "korda salasõna",
|
||||||
respTime: "Reageerimisaeg (ms)",
|
respTime: "Reageerimisaeg (ms)",
|
||||||
notAvailableShort: "N/A",
|
notAvailableShort: "N/A",
|
||||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
enableDefaultNotificationDescription: "Kõik järgnevalt lisatud seired kasutavad seda teavitusmeetodit. Seiretelt võib teavitusmeetodi ühekaupa eemaldada.",
|
||||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
clearEventsMsg: "Kas soovid seire kõik sündmused kustutada?",
|
||||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
clearHeartbeatsMsg: "Kas soovid seire kõik tuksed kustutada?",
|
||||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
confirmClearStatisticsMsg: "Kas soovid KÕIK statistika kustutada?",
|
||||||
"Import/Export Backup": "Import/Export Backup",
|
Export: "Eksport",
|
||||||
Export: "Export",
|
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
"Default enabled": "Default enabled",
|
"Default enabled": "Kasuta vaikimisi",
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
"Also apply to existing monitors": "Aktiveeri teavitusmeetod olemasolevatel seiretel",
|
||||||
Create: "Create",
|
Create: "Loo konto",
|
||||||
"Clear Data": "Clear Data",
|
"Clear Data": "Eemalda andmed",
|
||||||
Events: "Events",
|
Events: "Sündmused",
|
||||||
Heartbeats: "Heartbeats",
|
Heartbeats: "Tuksed",
|
||||||
"Auto Get": "Auto Get",
|
"Auto Get": "Hangi automaatselt",
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
backupDescription: "Varunda kõik seired ja teavitused JSON faili.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
backupDescription2: "PS: Varukoopia EI sisalda seirete ajalugu ja sündmustikku.",
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
backupDescription3: "Varukoopiad sisaldavad teavitusmeetodite pääsuvõtmeid.",
|
||||||
alertNoFile: "Please select a file to import.",
|
alertNoFile: "Palun lisa fail, mida importida.",
|
||||||
alertWrongFileType: "Please select a JSON file."
|
alertWrongFileType: "Palun lisa JSON-formaadis fail.",
|
||||||
}
|
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",
|
||||||
|
"Show URI": "Show 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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,12 +119,64 @@ 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.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
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.",
|
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.",
|
||||||
}
|
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",
|
||||||
|
"Show URI": "Show 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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,22 +118,64 @@ 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",
|
||||||
}
|
"Setup 2FA": "Imposta l'autenticazione a due fattori",
|
||||||
|
"Enable 2FA": "Abilita l'autenticazione a due fattori",
|
||||||
|
"Disable 2FA": "Disabilita l'autenticazione a due fattori",
|
||||||
|
"2FA Settings": "Impostazioni autenticazione a due fattori",
|
||||||
|
"Two Factor Authentication": "Autenticazione a due fattori",
|
||||||
|
Active: "Attivata",
|
||||||
|
Inactive: "Disattivata",
|
||||||
|
Token: "Token",
|
||||||
|
"Show URI": "Mostra URI",
|
||||||
|
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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,12 +119,64 @@ 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.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
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.",
|
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.",
|
||||||
}
|
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",
|
||||||
|
"Show URI": "Show 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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,12 +119,64 @@ 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.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
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.",
|
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.",
|
||||||
}
|
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",
|
||||||
|
"Show URI": "Show 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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,25 +114,69 @@ 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",
|
Token: "Token",
|
||||||
Export: "Export",
|
"Show URI": "Toon URI",
|
||||||
Import: "Import",
|
"Clear all statistics": "Wis alle statistieken",
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
retryCheckEverySecond: "Retry every {0} seconds.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
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.",
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
||||||
alertNoFile: "Please select a file to import.",
|
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
||||||
alertWrongFileType: "Please select a JSON file."
|
"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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,22 +109,74 @@ 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",
|
||||||
}
|
tokenValidSettingsMsg: "Token jest poprawny! Możesz teraz zapisać ustawienia 2FA.",
|
||||||
|
confirmEnableTwoFAMsg: "Jesteś pewien że chcesz włączyć 2FA?",
|
||||||
|
confirmDisableTwoFAMsg: "Jesteś pewien że chcesz wyłączyć 2FA?",
|
||||||
|
"Apply on all existing monitors": "Zastosuj do wszystki obecnych monitorów",
|
||||||
|
"Verify Token": "Weryfikuj token",
|
||||||
|
"Setup 2FA": "Konfiguracja 2FA",
|
||||||
|
"Enable 2FA": "Włącz 2FA",
|
||||||
|
"Disable 2FA": "Wyłącz 2FA",
|
||||||
|
"2FA Settings": "Ustawienia 2FA",
|
||||||
|
"Two Factor Authentication": "Uwierzytelnienie dwuskładnikowe",
|
||||||
|
Active: "Włączone",
|
||||||
|
Inactive: "Wyłączone",
|
||||||
|
Token: "Token",
|
||||||
|
"Show URI": "Pokaż URI",
|
||||||
|
"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ź",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
182
src/languages/pt-BR.js
Normal file
182
src/languages/pt-BR.js
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
export default {
|
||||||
|
languageName: "Português (Brasileiro)",
|
||||||
|
checkEverySecond: "Verificar cada {0} segundos.",
|
||||||
|
retryCheckEverySecond: "Tentar novamente a cada {0} segundos.",
|
||||||
|
retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada",
|
||||||
|
ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS",
|
||||||
|
upsideDownModeDescription: "Inverta o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.",
|
||||||
|
maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Defina como 0 para desativar redirecionamentos.",
|
||||||
|
acceptedStatusCodesDescription: "Selecione os códigos de status que são considerados uma resposta bem-sucedida.",
|
||||||
|
passwordNotMatchMsg: "A senha repetida não corresponde.",
|
||||||
|
notificationDescription: "Atribua uma notificação ao (s) monitor (es) para que funcione.",
|
||||||
|
keywordDescription: "Pesquise a palavra-chave em html simples ou resposta JSON e diferencia maiúsculas de minúsculas",
|
||||||
|
pauseDashboardHome: "Pausar",
|
||||||
|
deleteMonitorMsg: "Tem certeza de que deseja excluir este monitor?",
|
||||||
|
deleteNotificationMsg: "Tem certeza de que deseja excluir esta notificação para todos os monitores?",
|
||||||
|
resoverserverDescription: "Cloudflare é o servidor padrão, você pode alterar o servidor resolvedor a qualquer momento.",
|
||||||
|
rrtypeDescription: "Selecione o RR-Type que você deseja monitorar",
|
||||||
|
pauseMonitorMsg: "Tem certeza que deseja fazer uma pausa?",
|
||||||
|
enableDefaultNotificationDescription: "Para cada novo monitor, esta notificação será habilitada por padrão. Você ainda pode desativar a notificação separadamente para cada monitor.",
|
||||||
|
clearEventsMsg: "Tem certeza de que deseja excluir todos os eventos deste monitor?",
|
||||||
|
clearHeartbeatsMsg: "Tem certeza de que deseja excluir todos os heartbeats deste monitor?",
|
||||||
|
confirmClearStatisticsMsg: "Tem certeza que deseja excluir TODAS as estatísticas?",
|
||||||
|
importHandleDescription: "Escolha 'Ignorar existente' se quiser ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.",
|
||||||
|
confirmImportMsg: "Tem certeza que deseja importar o backup? Certifique-se de que selecionou a opção de importação correta.",
|
||||||
|
twoFAVerifyLabel: "Digite seu token para verificar se 2FA está funcionando",
|
||||||
|
tokenValidSettingsMsg: "O token é válido! Agora você pode salvar as configurações 2FA.",
|
||||||
|
confirmEnableTwoFAMsg: "Tem certeza de que deseja habilitar 2FA?",
|
||||||
|
confirmDisableTwoFAMsg: "Tem certeza de que deseja desativar 2FA?",
|
||||||
|
Settings: "Configurações",
|
||||||
|
Dashboard: "Dashboard",
|
||||||
|
"New Update": "Nova Atualização",
|
||||||
|
Language: "Linguagem",
|
||||||
|
Appearance: "Aparência",
|
||||||
|
Theme: "Tema",
|
||||||
|
General: "Geral",
|
||||||
|
Version: "Versão",
|
||||||
|
"Check Update On GitHub": "Verificar atualização no Github",
|
||||||
|
List: "Lista",
|
||||||
|
Add: "Adicionar",
|
||||||
|
"Add New Monitor": "Adicionar novo monitor",
|
||||||
|
"Quick Stats": "Estatísticas rápidas",
|
||||||
|
Up: "On",
|
||||||
|
Down: "Off",
|
||||||
|
Pending: "Pendente",
|
||||||
|
Unknown: "Desconhecido",
|
||||||
|
Pause: "Pausar",
|
||||||
|
Name: "Nome",
|
||||||
|
Status: "Status",
|
||||||
|
DateTime: "Data hora",
|
||||||
|
Message: "Mensagem",
|
||||||
|
"No important events": "Nenhum evento importante",
|
||||||
|
Resume: "Resumo",
|
||||||
|
Edit: "Editar",
|
||||||
|
Delete: "Deletar",
|
||||||
|
Current: "Atual",
|
||||||
|
Uptime: "Tempo de atividade",
|
||||||
|
"Cert Exp.": "Cert Exp.",
|
||||||
|
days: "dias",
|
||||||
|
day: "dia",
|
||||||
|
"-day": "-dia",
|
||||||
|
hour: "hora",
|
||||||
|
"-hour": "-hora",
|
||||||
|
Response: "Resposta",
|
||||||
|
Ping: "Ping",
|
||||||
|
"Monitor Type": "Tipo de Monitor",
|
||||||
|
Keyword: "Palavra-Chave",
|
||||||
|
"Friendly Name": "Nome Amigável",
|
||||||
|
URL: "URL",
|
||||||
|
Hostname: "Hostname",
|
||||||
|
Port: "Porta",
|
||||||
|
"Heartbeat Interval": "Intervalo de Heartbeat",
|
||||||
|
Retries: "Novas tentativas",
|
||||||
|
"Heartbeat Retry Interval": "Intervalo de repetição de Heartbeat",
|
||||||
|
Advanced: "Avançado",
|
||||||
|
"Upside Down Mode": "Modo de cabeça para baixo",
|
||||||
|
"Max. Redirects": "Redirecionamento Máx.",
|
||||||
|
"Accepted Status Codes": "Status Code Aceitáveis",
|
||||||
|
Save: "Salvar",
|
||||||
|
Notifications: "Notificações",
|
||||||
|
"Not available, please setup.": "Não disponível, por favor configure.",
|
||||||
|
"Setup Notification": "Configurar Notificação",
|
||||||
|
Light: "Claro",
|
||||||
|
Dark: "Escuro",
|
||||||
|
Auto: "Auto",
|
||||||
|
"Theme - Heartbeat Bar": "Tema - Barra de Heartbeat",
|
||||||
|
Normal: "Normal",
|
||||||
|
Bottom: "Inferior",
|
||||||
|
None: "Nenhum",
|
||||||
|
Timezone: "Fuso horário",
|
||||||
|
"Search Engine Visibility": "Visibilidade do mecanismo de pesquisa",
|
||||||
|
"Allow indexing": "Permitir Indexação",
|
||||||
|
"Discourage search engines from indexing site": "Desencoraje os motores de busca de indexar o site",
|
||||||
|
"Change Password": "Mudar senha",
|
||||||
|
"Current Password": "Senha atual",
|
||||||
|
"New Password": "Nova Senha",
|
||||||
|
"Repeat New Password": "Repetir Nova Senha",
|
||||||
|
"Update Password": "Atualizar Senha",
|
||||||
|
"Disable Auth": "Desativar Autenticação",
|
||||||
|
"Enable Auth": "Ativar Autenticação",
|
||||||
|
Logout: "Deslogar",
|
||||||
|
Leave: "Sair",
|
||||||
|
"I understand, please disable": "Eu entendo, por favor desative.",
|
||||||
|
Confirm: "Confirmar",
|
||||||
|
Yes: "Sim",
|
||||||
|
No: "Não",
|
||||||
|
Username: "Usuário",
|
||||||
|
Password: "Senha",
|
||||||
|
"Remember me": "Lembre-me",
|
||||||
|
Login: "Autenticar",
|
||||||
|
"No Monitors, please": "Nenhum monitor, por favor",
|
||||||
|
"add one": "adicionar um",
|
||||||
|
"Notification Type": "Tipo de Notificação",
|
||||||
|
Email: "Email",
|
||||||
|
Test: "Testar",
|
||||||
|
"Certificate Info": "Info. do Certificado ",
|
||||||
|
"Resolver Server": "Resolver Servidor",
|
||||||
|
"Resource Record Type": "Tipo de registro de aplicação",
|
||||||
|
"Last Result": "Último resultado",
|
||||||
|
"Create your admin account": "Crie sua conta de admin",
|
||||||
|
"Repeat Password": "Repita a senha",
|
||||||
|
"Import Backup": "Importar Backup",
|
||||||
|
"Export Backup": "Exportar Backup",
|
||||||
|
Export: "Exportar",
|
||||||
|
Import: "Importar",
|
||||||
|
respTime: "Tempo de Resp. (ms)",
|
||||||
|
notAvailableShort: "N/A",
|
||||||
|
"Default enabled": "Padrão habilitado",
|
||||||
|
"Apply on all existing monitors": "Aplicar em todos os monitores existentes",
|
||||||
|
Create: "Criar",
|
||||||
|
"Clear Data": "Limpar Dados",
|
||||||
|
Events: "Eventos",
|
||||||
|
Heartbeats: "Heartbeats",
|
||||||
|
"Auto Get": "Obter Automático",
|
||||||
|
backupDescription: "Você pode fazer backup de todos os monitores e todas as notificações em um arquivo JSON.",
|
||||||
|
backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.",
|
||||||
|
backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantenha-o com cuidado.",
|
||||||
|
alertNoFile: "Selecione um arquivo para importar.",
|
||||||
|
alertWrongFileType: "Selecione um arquivo JSON.",
|
||||||
|
"Clear all statistics": "Limpar todas as estatísticas",
|
||||||
|
"Skip existing": "Pular existente",
|
||||||
|
Overwrite: "Sobrescrever",
|
||||||
|
Options: "Opções",
|
||||||
|
"Keep both": "Manter os dois",
|
||||||
|
"Verify Token": "Verificar Token",
|
||||||
|
"Setup 2FA": "Configurar 2FA",
|
||||||
|
"Enable 2FA": "Ativar 2FA",
|
||||||
|
"Disable 2FA": "Desativar 2FA",
|
||||||
|
"2FA Settings": "Configurações do 2FA ",
|
||||||
|
"Two Factor Authentication": "Autenticação e Dois Fatores",
|
||||||
|
Active: "Ativo",
|
||||||
|
Inactive: "Inativo",
|
||||||
|
Token: "Token",
|
||||||
|
"Show URI": "Mostrar URI",
|
||||||
|
Tags: "Tag",
|
||||||
|
"Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...",
|
||||||
|
"Tag with this name already exist.": "Já existe uma etiqueta com este nome.",
|
||||||
|
"Tag with this value already exist.": "Já existe uma etiqueta com este valor.",
|
||||||
|
color: "cor",
|
||||||
|
"value (optional)": "valor (opcional)",
|
||||||
|
Gray: "Cinza",
|
||||||
|
Red: "Vermelho",
|
||||||
|
Orange: "Laranja",
|
||||||
|
Green: "Verde",
|
||||||
|
Blue: "Azul",
|
||||||
|
Indigo: "Índigo",
|
||||||
|
Purple: "Roxo",
|
||||||
|
Pink: "Rosa",
|
||||||
|
"Search...": "Buscar...",
|
||||||
|
"Avg. Ping": "Ping Médio.",
|
||||||
|
"Avg. Response": "Resposta Média. ",
|
||||||
|
"Status Page": "Página de Status",
|
||||||
|
"Entry Page": "Página de entrada",
|
||||||
|
"statusPageNothing": "Nada aqui, por favor, adicione um grupo ou monitor.",
|
||||||
|
"No Services": "Nenhum Serviço",
|
||||||
|
"All Systems Operational": "Todos os Serviços Operacionais",
|
||||||
|
"Partially Degraded Service": "Serviço parcialmente degradado",
|
||||||
|
"Degraded Service": "Serviço Degradado",
|
||||||
|
"Add Group": "Adicionar Grupo",
|
||||||
|
"Add a monitor": "Adicionar um monitor",
|
||||||
|
"Edit Status Page": "Editar Página de Status",
|
||||||
|
"Go to Dashboard": "Ir para a dashboard",
|
||||||
|
};
|
@ -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,25 +106,77 @@ 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",
|
||||||
}
|
tokenValidSettingsMsg: "Токен действителен! Теперь вы можете сохранить настройки 2FA.",
|
||||||
|
confirmEnableTwoFAMsg: "Вы действительно хотите включить 2FA?",
|
||||||
|
confirmDisableTwoFAMsg: "Вы действительно хотите выключить 2FA?",
|
||||||
|
"Apply on all existing monitors": "Применить ко всем существующим мониторам",
|
||||||
|
"Verify Token": "Проверить токен",
|
||||||
|
"Setup 2FA": "Настройка 2FA",
|
||||||
|
"Enable 2FA": "Включить 2FA",
|
||||||
|
"Disable 2FA": "Выключить 2FA",
|
||||||
|
"2FA Settings": "Настройки 2FA",
|
||||||
|
"Two Factor Authentication": "Двухфакторная аутентификация",
|
||||||
|
Active: "Активно",
|
||||||
|
Inactive: "Неактивно",
|
||||||
|
Token: "Токен",
|
||||||
|
"Show URI": "Показать URI",
|
||||||
|
"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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,12 +119,64 @@ 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.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
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.",
|
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.",
|
||||||
}
|
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",
|
||||||
|
"Show URI": "Show 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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,12 +119,64 @@ 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.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
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.",
|
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.",
|
||||||
}
|
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",
|
||||||
|
"Show URI": "Show 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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -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,12 +119,64 @@ 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.",
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
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.",
|
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.",
|
||||||
}
|
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",
|
||||||
|
"Show URI": "Show 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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
181
src/languages/tr-TR.js
Normal file
181
src/languages/tr-TR.js
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
@ -1,7 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
languageName: "简体中文",
|
languageName: "简体中文",
|
||||||
checkEverySecond: "检测频率 {0} 秒",
|
checkEverySecond: "检测频率 {0} 秒",
|
||||||
"Avg.": "平均",
|
|
||||||
retriesDescription: "最大重试失败次数",
|
retriesDescription: "最大重试失败次数",
|
||||||
ignoreTLSError: "忽略HTTPS站点的证书错误",
|
ignoreTLSError: "忽略HTTPS站点的证书错误",
|
||||||
upsideDownModeDescription: "反向状态监控(状态码范围外为有效状态,反之为无效)",
|
upsideDownModeDescription: "反向状态监控(状态码范围外为有效状态,反之为无效)",
|
||||||
@ -120,12 +119,64 @@ export default {
|
|||||||
enableDefaultNotificationDescription: "新的监控项将默认启用,你也可以在每个监控项中分别设置",
|
enableDefaultNotificationDescription: "新的监控项将默认启用,你也可以在每个监控项中分别设置",
|
||||||
"Default enabled": "默认开启",
|
"Default enabled": "默认开启",
|
||||||
"Also apply to existing monitors": "应用到所有监控项",
|
"Also apply to existing monitors": "应用到所有监控项",
|
||||||
"Import/Export Backup": "导入/导出备份",
|
|
||||||
Export: "导出",
|
Export: "导出",
|
||||||
Import: "导入",
|
Import: "导入",
|
||||||
backupDescription: "你可以将所有的监控项和消息通知备份到一个 JSON 文件中",
|
backupDescription: "你可以将所有的监控项和消息通知备份到一个 JSON 文件中",
|
||||||
backupDescription2: "注意: 不包括历史状态和事件数据",
|
backupDescription2: "注意: 不包括历史状态和事件数据",
|
||||||
backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!",
|
backupDescription3: "导出的文件中可能包含敏感信息,如消息通知的 Token 信息,请小心存放!",
|
||||||
alertNoFile: "请选择一个文件导入",
|
alertNoFile: "请选择一个文件导入",
|
||||||
alertWrongFileType: "请选择一个 JSON 格式的文件"
|
alertWrongFileType: "请选择一个 JSON 格式的文件",
|
||||||
}
|
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": "应用到所有监控项",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -36,7 +36,6 @@ export default {
|
|||||||
hour: "小時",
|
hour: "小時",
|
||||||
"-hour": "小時",
|
"-hour": "小時",
|
||||||
checkEverySecond: "每 {0} 秒檢查一次",
|
checkEverySecond: "每 {0} 秒檢查一次",
|
||||||
"Avg.": "平均",
|
|
||||||
Response: "反應時間",
|
Response: "反應時間",
|
||||||
Ping: "反應時間",
|
Ping: "反應時間",
|
||||||
"Monitor Type": "監測器類型",
|
"Monitor Type": "監測器類型",
|
||||||
@ -120,12 +119,64 @@ export default {
|
|||||||
enableDefaultNotificationDescription: "新增監測器時這個通知會預設啟用,當然每個監測器亦可分別控制開關。",
|
enableDefaultNotificationDescription: "新增監測器時這個通知會預設啟用,當然每個監測器亦可分別控制開關。",
|
||||||
"Default enabled": "預設通知",
|
"Default enabled": "預設通知",
|
||||||
"Also apply to existing monitors": "同時取用至目前所有監測器",
|
"Also apply to existing monitors": "同時取用至目前所有監測器",
|
||||||
"Import/Export Backup": "匯入/匯出 備份",
|
|
||||||
Export: "匯出",
|
Export: "匯出",
|
||||||
Import: "匯入",
|
Import: "匯入",
|
||||||
backupDescription: "您可以備份所有監測器及所有通知。",
|
backupDescription: "您可以備份所有監測器及所有通知。",
|
||||||
backupDescription2: "註:此備份不包括歷史記錄。",
|
backupDescription2: "註:此備份不包括歷史記錄。",
|
||||||
backupDescription3: "此備份可能包含了一些敏感資料如通知裡的 Token,請小心保存備份。",
|
backupDescription3: "此備份可能包含了一些敏感資料如通知裡的 Token,請小心保存備份。",
|
||||||
alertNoFile: "請選擇一個檔案",
|
alertNoFile: "請選擇一個檔案",
|
||||||
alertWrongFileType: "請選擇 JSON 檔案"
|
alertWrongFileType: "請選擇 JSON 檔案",
|
||||||
}
|
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": "套用至目前所有監測器",
|
||||||
|
"Verify Token": "驗証 Token",
|
||||||
|
"Setup 2FA": "設定 2FA",
|
||||||
|
"Enable 2FA": "開啟 2FA",
|
||||||
|
"Disable 2FA": "關閉 2FA",
|
||||||
|
"2FA Settings": "2FA 設定",
|
||||||
|
"Two Factor Authentication": "雙重認證",
|
||||||
|
Active: "生效",
|
||||||
|
Inactive: "未生效",
|
||||||
|
Token: "Token",
|
||||||
|
"Show URI": "顯示 URI",
|
||||||
|
"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",
|
||||||
|
"Entry Page": "Entry Page",
|
||||||
|
"statusPageNothing": "Nothing here, please add a group or a monitor.",
|
||||||
|
"No Services": "No Services",
|
||||||
|
"All Systems Operational": "All Systems Operational",
|
||||||
|
"Partially Degraded Service": "Partially Degraded Service",
|
||||||
|
"Degraded Service": "Degraded Service",
|
||||||
|
"Add Group": "Add Group",
|
||||||
|
"Add a monitor": "Add a monitor",
|
||||||
|
"Edit Status Page": "Edit Status Page",
|
||||||
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
|
};
|
||||||
|
@ -18,7 +18,12 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
<li class="nav-item">
|
<li class="nav-item me-2">
|
||||||
|
<a href="/status" class="nav-link status-page">
|
||||||
|
<font-awesome-icon icon="stream" /> {{ $t("Status Page") }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item me-2">
|
||||||
<router-link to="/dashboard" class="nav-link">
|
<router-link to="/dashboard" class="nav-link">
|
||||||
<font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }}
|
<font-awesome-icon icon="tachometer-alt" /> {{ $t("Dashboard") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
@ -81,7 +86,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@ -105,29 +110,29 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
|
||||||
this.init();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
|
||||||
if (this.$route.name === "root") {
|
|
||||||
this.$router.push("/dashboard")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
&.status-page {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bottom-nav {
|
.bottom-nav {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
135
src/main.js
135
src/main.js
@ -1,141 +1,28 @@
|
|||||||
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 contenteditable from "vue-contenteditable"
|
||||||
import "vue-toastification/dist/index.css";
|
import "vue-toastification/dist/index.css";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import "./assets/app.scss";
|
import "./assets/app.scss";
|
||||||
|
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 publicMixin from "./mixins/public";
|
||||||
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 { router } from "./router";
|
||||||
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,
|
||||||
theme,
|
theme,
|
||||||
mobile,
|
mobile,
|
||||||
datetime
|
datetime,
|
||||||
|
publicMixin,
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -153,7 +40,7 @@ const options = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
app.use(Toast, options);
|
app.use(Toast, options);
|
||||||
|
app.component("Editable", contenteditable);
|
||||||
|
app.component("FontAwesomeIcon", FontAwesomeIcon);
|
||||||
|
|
||||||
app.component("FontAwesomeIcon", FontAwesomeIcon)
|
app.mount("#app");
|
||||||
|
|
||||||
app.mount("#app")
|
|
||||||
|
@ -3,23 +3,34 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
windowWidth: window.innerWidth,
|
windowWidth: window.innerWidth,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener("resize", this.onResize);
|
window.addEventListener("resize", this.onResize);
|
||||||
|
this.updateBody();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onResize() {
|
onResize() {
|
||||||
this.windowWidth = window.innerWidth;
|
this.windowWidth = window.innerWidth;
|
||||||
|
this.updateBody();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateBody() {
|
||||||
|
if (this.isMobile) {
|
||||||
|
document.body.classList.add("mobile");
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove("mobile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isMobile() {
|
isMobile() {
|
||||||
return this.windowWidth <= 767.98;
|
return this.windowWidth <= 767.98;
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
|
||||||
}
|
};
|
||||||
|
40
src/mixins/public.js
Normal file
40
src/mixins/public.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const env = process.env.NODE_ENV || "production";
|
||||||
|
|
||||||
|
// change the axios base url for development
|
||||||
|
if (env === "development" || localStorage.dev === "dev") {
|
||||||
|
axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
publicGroupList: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
publicMonitorList() {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
for (let group of this.publicGroupList) {
|
||||||
|
for (let monitor of group.monitorList) {
|
||||||
|
result[monitor.id] = monitor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
publicLastHeartbeatList() {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
for (let monitorID in this.publicMonitorList) {
|
||||||
|
if (this.lastHeartbeatList[monitorID]) {
|
||||||
|
result[monitorID] = this.lastHeartbeatList[monitorID];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
@ -1,9 +1,15 @@
|
|||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
const toast = useToast()
|
const toast = useToast();
|
||||||
|
|
||||||
let socket;
|
let socket;
|
||||||
|
|
||||||
|
const noSocketIOPages = [
|
||||||
|
"/status-page",
|
||||||
|
"/status",
|
||||||
|
"/"
|
||||||
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -14,6 +20,7 @@ export default {
|
|||||||
firstConnect: true,
|
firstConnect: true,
|
||||||
connected: false,
|
connected: false,
|
||||||
connectCount: 0,
|
connectCount: 0,
|
||||||
|
initedSocketIO: false,
|
||||||
},
|
},
|
||||||
remember: (localStorage.remember !== "0"),
|
remember: (localStorage.remember !== "0"),
|
||||||
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
||||||
@ -26,165 +33,186 @@ export default {
|
|||||||
certInfoList: {},
|
certInfoList: {},
|
||||||
notificationList: [],
|
notificationList: [],
|
||||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener("resize", this.onResize);
|
window.addEventListener("resize", this.onResize);
|
||||||
|
this.initSocketIO();
|
||||||
let wsHost;
|
|
||||||
const env = process.env.NODE_ENV || "production";
|
|
||||||
if (env === "development" || localStorage.dev === "dev") {
|
|
||||||
wsHost = ":3001"
|
|
||||||
} else {
|
|
||||||
wsHost = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
socket = io(wsHost, {
|
|
||||||
transports: ["websocket"],
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("info", (info) => {
|
|
||||||
this.info = info;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("setup", (monitorID, data) => {
|
|
||||||
this.$router.push("/setup")
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("autoLogin", (monitorID, data) => {
|
|
||||||
this.loggedIn = true;
|
|
||||||
this.storage().token = "autoLogin";
|
|
||||||
this.allowLoginDialog = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("monitorList", (data) => {
|
|
||||||
// Add Helper function
|
|
||||||
Object.entries(data).forEach(([monitorID, monitor]) => {
|
|
||||||
monitor.getUrl = () => {
|
|
||||||
try {
|
|
||||||
return new URL(monitor.url);
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
this.monitorList = data;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("notificationList", (data) => {
|
|
||||||
this.notificationList = data;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("heartbeat", (data) => {
|
|
||||||
if (! (data.monitorID in this.heartbeatList)) {
|
|
||||||
this.heartbeatList[data.monitorID] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.heartbeatList[data.monitorID].push(data)
|
|
||||||
|
|
||||||
// Add to important list if it is important
|
|
||||||
// Also toast
|
|
||||||
if (data.important) {
|
|
||||||
|
|
||||||
if (data.status === 0) {
|
|
||||||
toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, {
|
|
||||||
timeout: false,
|
|
||||||
});
|
|
||||||
} else if (data.status === 1) {
|
|
||||||
toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, {
|
|
||||||
timeout: 20000,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! (data.monitorID in this.importantHeartbeatList)) {
|
|
||||||
this.importantHeartbeatList[data.monitorID] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.importantHeartbeatList[data.monitorID].unshift(data)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("heartbeatList", (monitorID, data, overwrite = false) => {
|
|
||||||
if (! (monitorID in this.heartbeatList) || overwrite) {
|
|
||||||
this.heartbeatList[monitorID] = data;
|
|
||||||
} else {
|
|
||||||
this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID])
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("avgPing", (monitorID, data) => {
|
|
||||||
this.avgPingList[monitorID] = data
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("uptime", (monitorID, type, data) => {
|
|
||||||
this.uptimeList[`${monitorID}_${type}`] = data
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("certInfo", (monitorID, data) => {
|
|
||||||
this.certInfoList[monitorID] = JSON.parse(data)
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("importantHeartbeatList", (monitorID, data, overwrite) => {
|
|
||||||
if (! (monitorID in this.importantHeartbeatList) || overwrite) {
|
|
||||||
this.importantHeartbeatList[monitorID] = data;
|
|
||||||
} else {
|
|
||||||
this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID])
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("connect_error", (err) => {
|
|
||||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
|
||||||
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
|
||||||
this.socket.connected = false;
|
|
||||||
this.socket.firstConnect = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
console.log("disconnect")
|
|
||||||
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
|
||||||
this.socket.connected = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("connect", () => {
|
|
||||||
console.log("connect")
|
|
||||||
this.socket.connectCount++;
|
|
||||||
this.socket.connected = true;
|
|
||||||
|
|
||||||
// Reset Heartbeat list if it is re-connect
|
|
||||||
if (this.socket.connectCount >= 2) {
|
|
||||||
this.clearData()
|
|
||||||
}
|
|
||||||
|
|
||||||
let token = this.storage().token;
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
if (token !== "autoLogin") {
|
|
||||||
this.loginByToken(token)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Timeout if it is not actually auto login
|
|
||||||
setTimeout(() => {
|
|
||||||
if (! this.loggedIn) {
|
|
||||||
this.allowLoginDialog = true;
|
|
||||||
this.$root.storage().removeItem("token");
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.allowLoginDialog = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.socket.firstConnect = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
initSocketIO(bypass = false) {
|
||||||
|
// No need to re-init
|
||||||
|
if (this.socket.initedSocketIO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to connect to the socket.io for status page
|
||||||
|
if (! bypass && noSocketIOPages.includes(location.pathname)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.initedSocketIO = true;
|
||||||
|
|
||||||
|
let protocol = (location.protocol === "https:") ? "wss://" : "ws://";
|
||||||
|
|
||||||
|
let wsHost;
|
||||||
|
const env = process.env.NODE_ENV || "production";
|
||||||
|
if (env === "development" || localStorage.dev === "dev") {
|
||||||
|
wsHost = protocol + location.hostname + ":3001";
|
||||||
|
} else {
|
||||||
|
wsHost = protocol + location.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = io(wsHost, {
|
||||||
|
transports: ["websocket"],
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("info", (info) => {
|
||||||
|
this.info = info;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("setup", (monitorID, data) => {
|
||||||
|
this.$router.push("/setup");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("autoLogin", (monitorID, data) => {
|
||||||
|
this.loggedIn = true;
|
||||||
|
this.storage().token = "autoLogin";
|
||||||
|
this.allowLoginDialog = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("monitorList", (data) => {
|
||||||
|
// Add Helper function
|
||||||
|
Object.entries(data).forEach(([monitorID, monitor]) => {
|
||||||
|
monitor.getUrl = () => {
|
||||||
|
try {
|
||||||
|
return new URL(monitor.url);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.monitorList = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("notificationList", (data) => {
|
||||||
|
this.notificationList = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("heartbeat", (data) => {
|
||||||
|
if (! (data.monitorID in this.heartbeatList)) {
|
||||||
|
this.heartbeatList[data.monitorID] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.heartbeatList[data.monitorID].push(data);
|
||||||
|
|
||||||
|
if (this.heartbeatList[data.monitorID].length >= 150) {
|
||||||
|
this.heartbeatList[data.monitorID].shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to important list if it is important
|
||||||
|
// Also toast
|
||||||
|
if (data.important) {
|
||||||
|
|
||||||
|
if (data.status === 0) {
|
||||||
|
toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, {
|
||||||
|
timeout: false,
|
||||||
|
});
|
||||||
|
} else if (data.status === 1) {
|
||||||
|
toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, {
|
||||||
|
timeout: 20000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! (data.monitorID in this.importantHeartbeatList)) {
|
||||||
|
this.importantHeartbeatList[data.monitorID] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importantHeartbeatList[data.monitorID].unshift(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("heartbeatList", (monitorID, data, overwrite = false) => {
|
||||||
|
if (! (monitorID in this.heartbeatList) || overwrite) {
|
||||||
|
this.heartbeatList[monitorID] = data;
|
||||||
|
} else {
|
||||||
|
this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("avgPing", (monitorID, data) => {
|
||||||
|
this.avgPingList[monitorID] = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("uptime", (monitorID, type, data) => {
|
||||||
|
this.uptimeList[`${monitorID}_${type}`] = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("certInfo", (monitorID, data) => {
|
||||||
|
this.certInfoList[monitorID] = JSON.parse(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("importantHeartbeatList", (monitorID, data, overwrite) => {
|
||||||
|
if (! (monitorID in this.importantHeartbeatList) || overwrite) {
|
||||||
|
this.importantHeartbeatList[monitorID] = data;
|
||||||
|
} else {
|
||||||
|
this.importantHeartbeatList[monitorID] = data.concat(this.importantHeartbeatList[monitorID]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", (err) => {
|
||||||
|
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||||
|
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
|
||||||
|
this.socket.connected = false;
|
||||||
|
this.socket.firstConnect = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("disconnect");
|
||||||
|
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||||
|
this.socket.connected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
console.log("connect");
|
||||||
|
this.socket.connectCount++;
|
||||||
|
this.socket.connected = true;
|
||||||
|
|
||||||
|
// Reset Heartbeat list if it is re-connect
|
||||||
|
if (this.socket.connectCount >= 2) {
|
||||||
|
this.clearData();
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = this.storage().token;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
if (token !== "autoLogin") {
|
||||||
|
this.loginByToken(token);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Timeout if it is not actually auto login
|
||||||
|
setTimeout(() => {
|
||||||
|
if (! this.loggedIn) {
|
||||||
|
this.allowLoginDialog = true;
|
||||||
|
this.$root.storage().removeItem("token");
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.allowLoginDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket.firstConnect = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
storage() {
|
storage() {
|
||||||
return (this.remember) ? localStorage : sessionStorage;
|
return (this.remember) ? localStorage : sessionStorage;
|
||||||
},
|
},
|
||||||
@ -201,11 +229,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
login(username, password, callback) {
|
login(username, password, token, callback) {
|
||||||
socket.emit("login", {
|
socket.emit("login", {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
|
token,
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
|
if (res.tokenRequired) {
|
||||||
|
callback(res);
|
||||||
|
}
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.storage().token = res.token;
|
this.storage().token = res.token;
|
||||||
@ -213,11 +245,11 @@ export default {
|
|||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
|
|
||||||
// Trigger Chrome Save Password
|
// Trigger Chrome Save Password
|
||||||
history.pushState({}, "")
|
history.pushState({}, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(res)
|
callback(res);
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
loginByToken(token) {
|
loginByToken(token) {
|
||||||
@ -225,11 +257,11 @@ export default {
|
|||||||
this.allowLoginDialog = true;
|
this.allowLoginDialog = true;
|
||||||
|
|
||||||
if (! res.ok) {
|
if (! res.ok) {
|
||||||
this.logout()
|
this.logout();
|
||||||
} else {
|
} else {
|
||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
@ -237,44 +269,68 @@ export default {
|
|||||||
this.socket.token = null;
|
this.socket.token = null;
|
||||||
this.loggedIn = false;
|
this.loggedIn = false;
|
||||||
|
|
||||||
this.clearData()
|
this.clearData();
|
||||||
|
},
|
||||||
|
|
||||||
|
prepare2FA(callback) {
|
||||||
|
socket.emit("prepare2FA", callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
save2FA(secret, callback) {
|
||||||
|
socket.emit("save2FA", callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
disable2FA(callback) {
|
||||||
|
socket.emit("disable2FA", callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyToken(token, callback) {
|
||||||
|
socket.emit("verifyToken", token, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteMonitor(monitorID, callback) {
|
deleteMonitor(monitorID, callback) {
|
||||||
socket.emit("deleteMonitor", monitorID, callback)
|
socket.emit("deleteMonitor", monitorID, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearData() {
|
clearData() {
|
||||||
console.log("reset heartbeat list")
|
console.log("reset heartbeat list");
|
||||||
this.heartbeatList = {}
|
this.heartbeatList = {};
|
||||||
this.importantHeartbeatList = {}
|
this.importantHeartbeatList = {};
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadBackup(uploadedJSON, callback) {
|
uploadBackup(uploadedJSON, importHandle, callback) {
|
||||||
socket.emit("uploadBackup", uploadedJSON, callback)
|
socket.emit("uploadBackup", uploadedJSON, importHandle, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearEvents(monitorID, callback) {
|
clearEvents(monitorID, callback) {
|
||||||
socket.emit("clearEvents", monitorID, callback)
|
socket.emit("clearEvents", monitorID, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearHeartbeats(monitorID, callback) {
|
clearHeartbeats(monitorID, callback) {
|
||||||
socket.emit("clearHeartbeats", monitorID, callback)
|
socket.emit("clearHeartbeats", monitorID, callback);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearStatistics(callback) {
|
clearStatistics(callback) {
|
||||||
socket.emit("clearStatistics", callback)
|
socket.emit("clearStatistics", callback);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
lastHeartbeatList() {
|
lastHeartbeatList() {
|
||||||
let result = {}
|
let result = {};
|
||||||
|
|
||||||
for (let monitorID in this.heartbeatList) {
|
for (let monitorID in this.heartbeatList) {
|
||||||
let index = this.heartbeatList[monitorID].length - 1;
|
let index = this.heartbeatList[monitorID].length - 1;
|
||||||
@ -285,15 +341,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
statusList() {
|
statusList() {
|
||||||
let result = {}
|
let result = {};
|
||||||
|
|
||||||
let unknown = {
|
let unknown = {
|
||||||
text: "Unknown",
|
text: "Unknown",
|
||||||
color: "secondary",
|
color: "secondary",
|
||||||
}
|
};
|
||||||
|
|
||||||
for (let monitorID in this.lastHeartbeatList) {
|
for (let monitorID in this.lastHeartbeatList) {
|
||||||
let lastHeartBeat = this.lastHeartbeatList[monitorID]
|
let lastHeartBeat = this.lastHeartbeatList[monitorID];
|
||||||
|
|
||||||
if (! lastHeartBeat) {
|
if (! lastHeartBeat) {
|
||||||
result[monitorID] = unknown;
|
result[monitorID] = unknown;
|
||||||
@ -326,14 +382,22 @@ export default {
|
|||||||
// Reload the SPA if the server version is changed.
|
// Reload the SPA if the server version is changed.
|
||||||
"info.version"(to, from) {
|
"info.version"(to, from) {
|
||||||
if (from && from !== to) {
|
if (from && from !== to) {
|
||||||
window.location.reload()
|
window.location.reload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
remember() {
|
remember() {
|
||||||
localStorage.remember = (this.remember) ? "1" : "0"
|
localStorage.remember = (this.remember) ? "1" : "0";
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reconnect the socket io, if status-page to dashboard
|
||||||
|
"$route.fullPath"(newValue, oldValue) {
|
||||||
|
if (noSocketIOPages.includes(newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.initSocketIO();
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
};
|
||||||
|
@ -5,6 +5,8 @@ export default {
|
|||||||
system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
|
system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
|
||||||
userTheme: localStorage.theme,
|
userTheme: localStorage.theme,
|
||||||
userHeartbeatBar: localStorage.heartbeatBarTheme,
|
userHeartbeatBar: localStorage.heartbeatBarTheme,
|
||||||
|
statusPageTheme: "light",
|
||||||
|
path: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -25,14 +27,28 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
theme() {
|
theme() {
|
||||||
if (this.userTheme === "auto") {
|
|
||||||
return this.system;
|
// Entry no need dark
|
||||||
|
if (this.path === "") {
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.path === "/status-page" || this.path === "/status") {
|
||||||
|
return this.statusPageTheme;
|
||||||
|
} else {
|
||||||
|
if (this.userTheme === "auto") {
|
||||||
|
return this.system;
|
||||||
|
}
|
||||||
|
return this.userTheme;
|
||||||
}
|
}
|
||||||
return this.userTheme;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
"$route.fullPath"(path) {
|
||||||
|
this.path = path;
|
||||||
|
},
|
||||||
|
|
||||||
userTheme(to, from) {
|
userTheme(to, from) {
|
||||||
localStorage.theme = to;
|
localStorage.theme = to;
|
||||||
},
|
},
|
||||||
@ -62,5 +78,5 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shadow-box table-shadow-box" style="overflow-x: scroll;">
|
<div class="shadow-box table-shadow-box" style="overflow-x: hidden;">
|
||||||
<table class="table table-borderless table-hover">
|
<table class="table table-borderless table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -178,5 +178,10 @@ table {
|
|||||||
tr {
|
tr {
|
||||||
transition: all ease-in-out 0.2ms;
|
transition: all ease-in-out 0.2ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
table-layout: fixed;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -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' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
|
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :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>
|
||||||
@ -46,7 +49,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">
|
||||||
@ -55,7 +58,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>
|
||||||
@ -213,6 +216,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: {
|
||||||
@ -224,6 +228,7 @@ export default {
|
|||||||
Status,
|
Status,
|
||||||
Pagination,
|
Pagination,
|
||||||
PingChart,
|
PingChart,
|
||||||
|
Tag,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -235,14 +240,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];
|
||||||
@ -373,6 +370,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>
|
||||||
@ -503,4 +513,12 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags > div:first-child {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -50,7 +50,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 -->
|
||||||
@ -106,6 +106,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' || monitor.type === 'keyword' " class="my-3 form-check">
|
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||||
@ -158,6 +166,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>
|
||||||
@ -197,6 +209,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||||
|
import TagsManager from "../components/TagsManager.vue";
|
||||||
import { useToast } from "vue-toastification"
|
import { useToast } from "vue-toastification"
|
||||||
import VueMultiselect from "vue-multiselect"
|
import VueMultiselect from "vue-multiselect"
|
||||||
import { isDev } from "../util.ts";
|
import { isDev } from "../util.ts";
|
||||||
@ -205,6 +218,7 @@ const toast = useToast()
|
|||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotificationDialog,
|
NotificationDialog,
|
||||||
|
TagsManager,
|
||||||
VueMultiselect,
|
VueMultiselect,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -219,7 +233,9 @@ export default {
|
|||||||
dnsresolvetypeOptions: [],
|
dnsresolvetypeOptions: [],
|
||||||
|
|
||||||
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
|
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
|
||||||
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
|
||||||
|
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])$"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -248,6 +264,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();
|
||||||
@ -289,6 +311,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,
|
||||||
@ -308,6 +331,11 @@ export default {
|
|||||||
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
|
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.monitor = res.monitor;
|
this.monitor = res.monitor;
|
||||||
|
|
||||||
|
// Handling for monitors that are created before 1.7.0
|
||||||
|
if (this.monitor.retryInterval === 0) {
|
||||||
|
this.monitor.retryInterval = this.monitor.interval;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg)
|
toast.error(res.msg)
|
||||||
}
|
}
|
||||||
@ -316,25 +344,32 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
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();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -356,6 +391,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 {
|
||||||
@ -372,9 +409,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;
|
||||||
|
20
src/pages/Entry.vue
Normal file
20
src/pages/Entry.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async mounted() {
|
||||||
|
let entryPage = (await axios.get("/api/entry-page")).data;
|
||||||
|
|
||||||
|
if (entryPage === "statusPage") {
|
||||||
|
this.$router.push("/status");
|
||||||
|
} else {
|
||||||
|
this.$router.push("/dashboard");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
@ -83,6 +83,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ $t("Entry Page") }}</label>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="entryPageYes" v-model="settings.entryPage" class="form-check-input" type="radio" name="statusPage" value="dashboard" required>
|
||||||
|
<label class="form-check-label" for="entryPageYes">
|
||||||
|
{{ $t("Dashboard") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="entryPageNo" v-model="settings.entryPage" class="form-check-input" type="radio" name="statusPage" value="statusPage" required>
|
||||||
|
<label class="form-check-label" for="entryPageNo">
|
||||||
|
{{ $t("Status Page") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" type="submit">
|
<button class="btn btn-primary" type="submit">
|
||||||
{{ $t("Save") }}
|
{{ $t("Save") }}
|
||||||
@ -120,34 +138,68 @@
|
|||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<h2 class="mt-5 mb-2">{{ $t("Import/Export Backup") }}</h2>
|
<div v-if="! settings.disableAuth" class="mt-5 mb-3">
|
||||||
|
<h2 class="mb-2">
|
||||||
|
{{ $t("Two Factor Authentication") }}
|
||||||
|
</h2>
|
||||||
|
<button class="btn btn-primary me-2" type="button" @click="$refs.TwoFADialog.show()">{{ $t("2FA Settings") }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
|
||||||
{{ $t("Import") }}
|
|
||||||
</button>
|
|
||||||
<input id="importBackup" type="file" class="form-control" accept="application/json">
|
|
||||||
</div>
|
|
||||||
<div v-if="importAlert" class="alert alert-danger mt-3" style="padding: 6px 16px;">
|
|
||||||
{{ importAlert }}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p><strong>{{ $t("backupDescription3") }}</strong></p>
|
<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>
|
||||||
|
{{ $t("Import") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="importAlert" class="alert alert-danger mt-3" style="padding: 6px 16px;">
|
||||||
|
{{ importAlert }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
||||||
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">{{ $t("Enable Auth") }}</button>
|
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1 mb-1" @click="enableAuth">{{ $t("Enable Auth") }}</button>
|
||||||
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
|
<button v-if="! settings.disableAuth" class="btn btn-primary me-1 mb-1" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button>
|
||||||
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">{{ $t("Logout") }}</button>
|
<button v-if="! settings.disableAuth" class="btn btn-danger me-1 mb-1" @click="$root.logout">{{ $t("Logout") }}</button>
|
||||||
<button class="btn btn-outline-danger me-1" @click="confirmClearStatistics">{{ $t("Clear all Statistics") }}</button>
|
<button class="btn btn-outline-danger me-1 mb-1" @click="confirmClearStatistics">{{ $t("Clear all statistics") }}</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -173,19 +225,17 @@
|
|||||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||||
{{ $t("Setup Notification") }}
|
{{ $t("Setup Notification") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<h2 class="mt-5">Info</h2>
|
||||||
|
|
||||||
|
{{ $t("Version") }}: {{ $root.info.version }} <br />
|
||||||
|
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div class="container-fluid">
|
|
||||||
Uptime Kuma -
|
|
||||||
{{ $t("Version") }}: {{ $root.info.version }} -
|
|
||||||
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<NotificationDialog ref="notificationDialog" />
|
<NotificationDialog ref="notificationDialog" />
|
||||||
|
<TwoFADialog ref="TwoFADialog" />
|
||||||
|
|
||||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
|
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
|
||||||
<template v-if="$i18n.locale === 'es-ES' ">
|
<template v-if="$i18n.locale === 'es-ES' ">
|
||||||
@ -194,6 +244,12 @@
|
|||||||
<p>Por favor usar con cuidado.</p>
|
<p>Por favor usar con cuidado.</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="$i18n.locale === 'pt-BR' ">
|
||||||
|
<p>Você tem certeza que deseja <strong>desativar a autenticação</strong>?</p>
|
||||||
|
<p>Isso é para <strong>alguém que tem autenticação de terceiros</strong> na frente do 'UpTime Kuma' como o Cloudflare Access.</p>
|
||||||
|
<p>Por favor, utilize isso com cautela.</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else-if="$i18n.locale === 'zh-HK' ">
|
<template v-else-if="$i18n.locale === 'zh-HK' ">
|
||||||
<p>你是否確認<strong>取消登入認証</strong>?</p>
|
<p>你是否確認<strong>取消登入認証</strong>?</p>
|
||||||
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。</p>
|
<p>這個功能是設計給已有<strong>第三方認証</strong>的用家,例如 Cloudflare Access。</p>
|
||||||
@ -224,6 +280,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>
|
||||||
@ -248,6 +310,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>
|
||||||
@ -259,6 +327,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>
|
||||||
@ -266,19 +337,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import Confirm from "../components/Confirm.vue";
|
import Confirm from "../components/Confirm.vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc"
|
import utc from "dayjs/plugin/utc";
|
||||||
import timezone from "dayjs/plugin/timezone"
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||||
dayjs.extend(utc)
|
import TwoFADialog from "../components/TwoFADialog.vue";
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
import { timezoneList } from "../util-frontend";
|
import { timezoneList } from "../util-frontend";
|
||||||
import { useToast } from "vue-toastification"
|
import { useToast } from "vue-toastification";
|
||||||
const toast = useToast()
|
const toast = useToast();
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotificationDialog,
|
NotificationDialog,
|
||||||
|
TwoFADialog,
|
||||||
Confirm,
|
Confirm,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -297,8 +370,9 @@ export default {
|
|||||||
},
|
},
|
||||||
loaded: false,
|
loaded: false,
|
||||||
importAlert: null,
|
importAlert: null,
|
||||||
|
importHandle: "skip",
|
||||||
processing: false,
|
processing: false,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"password.repeatNewPassword"() {
|
"password.repeatNewPassword"() {
|
||||||
@ -326,13 +400,13 @@ export default {
|
|||||||
this.invalidPassword = true;
|
this.invalidPassword = true;
|
||||||
} else {
|
} else {
|
||||||
this.$root.getSocket().emit("changePassword", this.password, (res) => {
|
this.$root.getSocket().emit("changePassword", this.password, (res) => {
|
||||||
this.$root.toastRes(res)
|
this.$root.toastRes(res);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.password.currentPassword = ""
|
this.password.currentPassword = "";
|
||||||
this.password.newPassword = ""
|
this.password.newPassword = "";
|
||||||
this.password.repeatNewPassword = ""
|
this.password.repeatNewPassword = "";
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -344,15 +418,19 @@ export default {
|
|||||||
this.settings.searchEngineIndex = false;
|
this.settings.searchEngineIndex = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.settings.entryPage === undefined) {
|
||||||
|
this.settings.entryPage = "dashboard";
|
||||||
|
}
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
saveSettings() {
|
saveSettings() {
|
||||||
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
|
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
this.loadSettings();
|
this.loadSettings();
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmDisableAuth() {
|
confirmDisableAuth() {
|
||||||
@ -363,6 +441,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();
|
||||||
@ -382,10 +464,10 @@ export default {
|
|||||||
version: this.$root.info.version,
|
version: this.$root.info.version,
|
||||||
notificationList: this.$root.notificationList,
|
notificationList: this.$root.notificationList,
|
||||||
monitorList: monitorList,
|
monitorList: monitorList,
|
||||||
}
|
};
|
||||||
exportData = JSON.stringify(exportData);
|
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();
|
||||||
},
|
},
|
||||||
@ -396,19 +478,19 @@ export default {
|
|||||||
|
|
||||||
if (uploadItem.length <= 0) {
|
if (uploadItem.length <= 0) {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
return this.importAlert = this.$t("alertNoFile")
|
return this.importAlert = this.$t("alertNoFile");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploadItem.item(0).type !== "application/json") {
|
if (uploadItem.item(0).type !== "application/json") {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
return this.importAlert = this.$t("alertWrongFileType")
|
return this.importAlert = this.$t("alertWrongFileType");
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileReader = new FileReader();
|
let fileReader = new FileReader();
|
||||||
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) {
|
||||||
@ -416,8 +498,8 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
clearStatistics() {
|
clearStatistics() {
|
||||||
@ -427,10 +509,10 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -87,7 +87,7 @@ export default {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
this.$root.login(this.username, this.password, (res) => {
|
this.$root.login(this.username, this.password, "", (res) => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.$router.push("/")
|
this.$router.push("/")
|
||||||
})
|
})
|
||||||
|
653
src/pages/StatusPage.vue
Normal file
653
src/pages/StatusPage.vue
Normal file
@ -0,0 +1,653 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="loadedTheme" class="container mt-3">
|
||||||
|
<!-- Logo & Title -->
|
||||||
|
<h1 class="mb-4">
|
||||||
|
<!-- Logo -->
|
||||||
|
<span class="logo-wrapper" @click="showImageCropUploadMethod">
|
||||||
|
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
|
||||||
|
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Uploader -->
|
||||||
|
<!-- url="/api/status-page/upload-logo" -->
|
||||||
|
<ImageCropUpload v-model="showImageCropUpload"
|
||||||
|
field="img"
|
||||||
|
:width="128"
|
||||||
|
:height="128"
|
||||||
|
:langType="$i18n.locale"
|
||||||
|
img-format="png"
|
||||||
|
:noCircle="true"
|
||||||
|
:noSquare="false"
|
||||||
|
@crop-success="cropSuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Admin functions -->
|
||||||
|
<div v-if="hasToken" class="mb-4">
|
||||||
|
<div v-if="!enableEditMode">
|
||||||
|
<button class="btn btn-info me-2" @click="edit">
|
||||||
|
<font-awesome-icon icon="edit" />
|
||||||
|
{{ $t("Edit Status Page") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="/dashboard" class="btn btn-info">
|
||||||
|
<font-awesome-icon icon="tachometer-alt" />
|
||||||
|
{{ $t("Go to Dashboard") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<button class="btn btn-success me-2" @click="save">
|
||||||
|
<font-awesome-icon icon="save" />
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-danger me-2" @click="discard">
|
||||||
|
<font-awesome-icon icon="save" />
|
||||||
|
{{ $t("Discard") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
|
||||||
|
<font-awesome-icon icon="bullhorn" />
|
||||||
|
{{ $t("Create Incident") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<button v-if="isPublished" class="btn btn-light me-2" @click="">
|
||||||
|
<font-awesome-icon icon="save" />
|
||||||
|
{{ $t("Unpublish") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="!isPublished" class="btn btn-info me-2" @click="">
|
||||||
|
<font-awesome-icon icon="save" />
|
||||||
|
{{ $t("Publish") }}
|
||||||
|
</button>-->
|
||||||
|
|
||||||
|
<!-- Set Default Language -->
|
||||||
|
<!-- Set theme -->
|
||||||
|
<button v-if="theme == 'dark'" class="btn btn-light me-2" @click="changeTheme('light')">
|
||||||
|
<font-awesome-icon icon="save" />
|
||||||
|
{{ $t("Switch to Light Theme") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="theme == 'light'" class="btn btn-dark me-2" @click="changeTheme('dark')">
|
||||||
|
<font-awesome-icon icon="save" />
|
||||||
|
{{ $t("Switch to Dark Theme") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Incident -->
|
||||||
|
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
|
||||||
|
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
|
||||||
|
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
|
||||||
|
|
||||||
|
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
|
||||||
|
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
|
||||||
|
|
||||||
|
<!-- Incident Date -->
|
||||||
|
<div class="date mt-3">
|
||||||
|
Created: {{ incident.createdDate }} ({{ createdDateFromNow }})<br />
|
||||||
|
<span v-if="incident.lastUpdatedDate">
|
||||||
|
Last Updated: {{ incident.lastUpdatedDate }} ({{ lastUpdatedDateFromNow }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="editMode" class="mt-3">
|
||||||
|
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
|
||||||
|
<font-awesome-icon icon="bullhorn" />
|
||||||
|
{{ $t("Post") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
|
||||||
|
<font-awesome-icon icon="edit" />
|
||||||
|
{{ $t("Edit") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident">
|
||||||
|
<font-awesome-icon icon="times" />
|
||||||
|
{{ $t("Cancel") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
|
||||||
|
<button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Style: {{ incident.style }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
||||||
|
<li><a class="dropdown-item" href="#" @click="incident.style = 'info'">info</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">warning</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">danger</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">primary</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click="incident.style = 'light'">light</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">dark</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
|
||||||
|
<font-awesome-icon icon="unlink" />
|
||||||
|
{{ $t("Unpin") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overall Status -->
|
||||||
|
<div class="shadow-box list p-4 overall-status mb-4">
|
||||||
|
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
|
||||||
|
<font-awesome-icon icon="question-circle" class="ok" />
|
||||||
|
{{ $t("No Services") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="allUp">
|
||||||
|
<font-awesome-icon icon="check-circle" class="ok" />
|
||||||
|
{{ $t("All Systems Operational") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="partialDown">
|
||||||
|
<font-awesome-icon icon="exclamation-circle" class="warning" />
|
||||||
|
{{ $t("Partially Degraded Service") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="allDown">
|
||||||
|
<font-awesome-icon icon="times-circle" class="danger" />
|
||||||
|
{{ $t("Degraded Service") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<font-awesome-icon icon="question-circle" style="color: #efefef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<strong v-if="editMode">{{ $t("Description") }}:</strong>
|
||||||
|
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
|
||||||
|
|
||||||
|
<div v-if="editMode" class="mb-4">
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary btn-add-group me-2" @click="addGroup">
|
||||||
|
<font-awesome-icon icon="plus" />
|
||||||
|
{{ $t("Add Group") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div v-if="allMonitorList.length > 0 && loadedData">
|
||||||
|
<label>{{ $t("Add a monitor") }}:</label>
|
||||||
|
<select v-model="selectedMonitor" class="form-control">
|
||||||
|
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center">
|
||||||
|
{{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
|
||||||
|
<!-- 👀 Nothing here, please add a group or a monitor. -->
|
||||||
|
👀 {{ $t("statusPageNothing") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PublicGroupList :edit-mode="enableEditMode" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="mt-5 mb-4">
|
||||||
|
Powered by <a target="_blank" href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios";
|
||||||
|
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||||
|
import ImageCropUpload from "vue-image-crop-upload";
|
||||||
|
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
|
||||||
|
|
||||||
|
let feedInterval;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PublicGroupList,
|
||||||
|
ImageCropUpload
|
||||||
|
},
|
||||||
|
|
||||||
|
// Leave Page for vue route change
|
||||||
|
beforeRouteLeave(to, from, next) {
|
||||||
|
if (this.editMode) {
|
||||||
|
const answer = window.confirm(leavePageMsg);
|
||||||
|
if (answer) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
enableEditMode: false,
|
||||||
|
enableEditIncidentMode: false,
|
||||||
|
hasToken: false,
|
||||||
|
config: {},
|
||||||
|
selectedMonitor: null,
|
||||||
|
incident: null,
|
||||||
|
previousIncident: null,
|
||||||
|
showImageCropUpload: false,
|
||||||
|
imgDataUrl: "/icon.svg",
|
||||||
|
loadedTheme: false,
|
||||||
|
loadedData: false,
|
||||||
|
baseURL: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
logoURL() {
|
||||||
|
if (this.imgDataUrl.startsWith("data:")) {
|
||||||
|
return this.imgDataUrl;
|
||||||
|
} else {
|
||||||
|
return this.baseURL + this.imgDataUrl;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the monitor is added to public list, which will not be in this list.
|
||||||
|
*/
|
||||||
|
allMonitorList() {
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
for (let id in this.$root.monitorList) {
|
||||||
|
if (this.$root.monitorList[id] && ! (id in this.$root.publicMonitorList)) {
|
||||||
|
let monitor = this.$root.monitorList[id];
|
||||||
|
result.push(monitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
editMode() {
|
||||||
|
return this.enableEditMode && this.$root.socket.connected;
|
||||||
|
},
|
||||||
|
|
||||||
|
editIncidentMode() {
|
||||||
|
return this.enableEditIncidentMode;
|
||||||
|
},
|
||||||
|
|
||||||
|
isPublished() {
|
||||||
|
return this.config.statusPagePublished;
|
||||||
|
},
|
||||||
|
|
||||||
|
theme() {
|
||||||
|
return this.config.statusPageTheme;
|
||||||
|
},
|
||||||
|
|
||||||
|
logoClass() {
|
||||||
|
if (this.editMode) {
|
||||||
|
return {
|
||||||
|
"edit-mode": true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
incidentClass() {
|
||||||
|
return "bg-" + this.incident.style;
|
||||||
|
},
|
||||||
|
|
||||||
|
overallStatus() {
|
||||||
|
|
||||||
|
if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = STATUS_PAGE_ALL_UP;
|
||||||
|
let hasUp = false;
|
||||||
|
|
||||||
|
for (let id in this.$root.publicLastHeartbeatList) {
|
||||||
|
let beat = this.$root.publicLastHeartbeatList[id];
|
||||||
|
|
||||||
|
if (beat.status === UP) {
|
||||||
|
hasUp = true;
|
||||||
|
} else {
|
||||||
|
status = STATUS_PAGE_PARTIAL_DOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! hasUp) {
|
||||||
|
status = STATUS_PAGE_ALL_DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
},
|
||||||
|
|
||||||
|
allUp() {
|
||||||
|
return this.overallStatus === STATUS_PAGE_ALL_UP;
|
||||||
|
},
|
||||||
|
|
||||||
|
partialDown() {
|
||||||
|
return this.overallStatus === STATUS_PAGE_PARTIAL_DOWN;
|
||||||
|
},
|
||||||
|
|
||||||
|
allDown() {
|
||||||
|
return this.overallStatus === STATUS_PAGE_ALL_DOWN;
|
||||||
|
},
|
||||||
|
|
||||||
|
createdDateFromNow() {
|
||||||
|
return dayjs.utc(this.incident.createdDate).fromNow();
|
||||||
|
},
|
||||||
|
|
||||||
|
lastUpdatedDateFromNow() {
|
||||||
|
return dayjs.utc(this.incident. lastUpdatedDate).fromNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected a monitor and add to the list.
|
||||||
|
*/
|
||||||
|
selectedMonitor(monitor) {
|
||||||
|
if (monitor) {
|
||||||
|
if (this.$root.publicGroupList.length === 0) {
|
||||||
|
this.addGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstGroup = this.$root.publicGroupList[0];
|
||||||
|
|
||||||
|
firstGroup.monitorList.push(monitor);
|
||||||
|
this.selectedMonitor = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Set Theme
|
||||||
|
"config.statusPageTheme"() {
|
||||||
|
this.$root.statusPageTheme = this.config.statusPageTheme;
|
||||||
|
this.loadedTheme = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
"config.title"(title) {
|
||||||
|
document.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.hasToken = ("token" in this.$root.storage());
|
||||||
|
|
||||||
|
// Browser change page
|
||||||
|
// https://stackoverflow.com/questions/7317273/warn-user-before-leaving-web-page-with-unsaved-changes
|
||||||
|
window.addEventListener("beforeunload", (e) => {
|
||||||
|
if (this.editMode) {
|
||||||
|
(e || window.event).returnValue = leavePageMsg;
|
||||||
|
return leavePageMsg;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Special handle for dev
|
||||||
|
const env = process.env.NODE_ENV;
|
||||||
|
if (env === "development" || localStorage.dev === "dev") {
|
||||||
|
this.baseURL = location.protocol + "//" + location.hostname + ":3001";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
axios.get("/api/status-page/config").then((res) => {
|
||||||
|
this.config = res.data;
|
||||||
|
|
||||||
|
if (this.config.logo) {
|
||||||
|
this.imgDataUrl = this.config.logo;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
axios.get("/api/status-page/incident").then((res) => {
|
||||||
|
if (res.data.ok) {
|
||||||
|
this.incident = res.data.incident;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
axios.get("/api/status-page/monitor-list").then((res) => {
|
||||||
|
this.$root.publicGroupList = res.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5mins a loop
|
||||||
|
this.updateHeartbeatList();
|
||||||
|
feedInterval = setInterval(() => {
|
||||||
|
this.updateHeartbeatList();
|
||||||
|
}, (300 + 10) * 1000);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
updateHeartbeatList() {
|
||||||
|
// If editMode, it will use the data from websocket.
|
||||||
|
if (! this.editMode) {
|
||||||
|
axios.get("/api/status-page/heartbeat").then((res) => {
|
||||||
|
this.$root.heartbeatList = res.data.heartbeatList;
|
||||||
|
this.$root.uptimeList = res.data.uptimeList;
|
||||||
|
this.loadedData = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
edit() {
|
||||||
|
this.$root.initSocketIO(true);
|
||||||
|
this.enableEditMode = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.enableEditMode = false;
|
||||||
|
this.$root.publicGroupList = res.publicGroupList;
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
monitorSelectorLabel(monitor) {
|
||||||
|
return `${monitor.name}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
addGroup() {
|
||||||
|
let groupName = "Untitled Group";
|
||||||
|
|
||||||
|
if (this.$root.publicGroupList.length === 0) {
|
||||||
|
groupName = "Services";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$root.publicGroupList.push({
|
||||||
|
name: groupName,
|
||||||
|
monitorList: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
discard() {
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
changeTheme(name) {
|
||||||
|
this.config.statusPageTheme = name;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crop Success
|
||||||
|
*/
|
||||||
|
cropSuccess(imgDataUrl) {
|
||||||
|
this.imgDataUrl = imgDataUrl;
|
||||||
|
},
|
||||||
|
|
||||||
|
showImageCropUploadMethod() {
|
||||||
|
if (this.editMode) {
|
||||||
|
this.showImageCropUpload = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createIncident() {
|
||||||
|
this.enableEditIncidentMode = true;
|
||||||
|
|
||||||
|
if (this.incident) {
|
||||||
|
this.previousIncident = this.incident;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.incident = {
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
style: "primary",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
postIncident() {
|
||||||
|
if (this.incident.title == "" || this.incident.content == "") {
|
||||||
|
toast.error("Please input title and content.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("postIncident", this.incident, (res) => {
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.enableEditIncidentMode = false;
|
||||||
|
this.incident = res.incident;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click Edit Button
|
||||||
|
*/
|
||||||
|
editIncident() {
|
||||||
|
this.enableEditIncidentMode = true;
|
||||||
|
this.previousIncident = Object.assign({}, this.incident);
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelIncident() {
|
||||||
|
this.enableEditIncidentMode = false;
|
||||||
|
|
||||||
|
if (this.previousIncident) {
|
||||||
|
this.incident = this.previousIncident;
|
||||||
|
this.previousIncident = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unpinIncident() {
|
||||||
|
this.$root.getSocket().emit("unpinIncident", () => {
|
||||||
|
this.incident = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.overall-status {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 25px;
|
||||||
|
|
||||||
|
.ok {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: $warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 30px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description span {
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.icon-upload {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-upload {
|
||||||
|
transition: all $easing-in 0.2s;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
left: -14px;
|
||||||
|
background-color: white;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
transition: all $easing-in 0.2s;
|
||||||
|
|
||||||
|
&.edit-mode {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.incident {
|
||||||
|
.content {
|
||||||
|
&[contenteditable=true] {
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overall-status {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
85
src/router.js
Normal file
85
src/router.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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";
|
||||||
|
import StatusPage from "./pages/StatusPage.vue";
|
||||||
|
import Entry from "./pages/Entry.vue";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
component: Entry,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If it is "/dashboard", the active link is not working
|
||||||
|
// If it is "", it overrides the "/" unexpectedly
|
||||||
|
// Give a random name to solve the problem.
|
||||||
|
path: "/empty",
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/status-page",
|
||||||
|
component: StatusPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/status",
|
||||||
|
component: StatusPage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
linkActiveClass: "active",
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
@ -1,9 +1,10 @@
|
|||||||
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);
|
||||||
|
|
||||||
function getTimezoneOffset(timeZone) {
|
function getTimezoneOffset(timeZone) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -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) => {
|
||||||
@ -398,7 +44,7 @@ export function timezoneList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
})
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
36
src/util.js
36
src/util.js
@ -1,6 +1,13 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
// Common Util for frontend and backend
|
||||||
|
//
|
||||||
|
// DOT NOT MODIFY util.js!
|
||||||
|
// Need to run "tsc" to compile if there are any changes.
|
||||||
|
//
|
||||||
|
// Backend uses the compiled file util.js
|
||||||
|
// Frontend uses util.ts
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
||||||
const _dayjs = require("dayjs");
|
const _dayjs = require("dayjs");
|
||||||
const dayjs = _dayjs;
|
const dayjs = _dayjs;
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
@ -8,6 +15,9 @@ exports.appName = "Uptime Kuma";
|
|||||||
exports.DOWN = 0;
|
exports.DOWN = 0;
|
||||||
exports.UP = 1;
|
exports.UP = 1;
|
||||||
exports.PENDING = 2;
|
exports.PENDING = 2;
|
||||||
|
exports.STATUS_PAGE_ALL_DOWN = 0;
|
||||||
|
exports.STATUS_PAGE_ALL_UP = 1;
|
||||||
|
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
|
||||||
function flipStatus(s) {
|
function flipStatus(s) {
|
||||||
if (s === exports.UP) {
|
if (s === exports.UP) {
|
||||||
return exports.DOWN;
|
return exports.DOWN;
|
||||||
@ -22,6 +32,10 @@ function sleep(ms) {
|
|||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
exports.sleep = sleep;
|
exports.sleep = sleep;
|
||||||
|
/**
|
||||||
|
* PHP's ucfirst
|
||||||
|
* @param str
|
||||||
|
*/
|
||||||
function ucfirst(str) {
|
function ucfirst(str) {
|
||||||
if (!str) {
|
if (!str) {
|
||||||
return str;
|
return str;
|
||||||
@ -37,11 +51,19 @@ function debug(msg) {
|
|||||||
}
|
}
|
||||||
exports.debug = debug;
|
exports.debug = debug;
|
||||||
function polyfill() {
|
function polyfill() {
|
||||||
|
/**
|
||||||
|
* String.prototype.replaceAll() polyfill
|
||||||
|
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
|
||||||
|
* @author Chris Ferdinandi
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
if (!String.prototype.replaceAll) {
|
if (!String.prototype.replaceAll) {
|
||||||
String.prototype.replaceAll = function (str, newStr) {
|
String.prototype.replaceAll = function (str, newStr) {
|
||||||
|
// If a regex pattern
|
||||||
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
||||||
return this.replace(str, newStr);
|
return this.replace(str, newStr);
|
||||||
}
|
}
|
||||||
|
// If a string
|
||||||
return this.replace(new RegExp(str, "g"), newStr);
|
return this.replace(new RegExp(str, "g"), newStr);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -58,10 +80,22 @@ class TimeLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.TimeLogger = TimeLogger;
|
exports.TimeLogger = TimeLogger;
|
||||||
|
/**
|
||||||
|
* Returns a random number between min (inclusive) and max (exclusive)
|
||||||
|
*/
|
||||||
function getRandomArbitrary(min, max) {
|
function getRandomArbitrary(min, max) {
|
||||||
return Math.random() * (max - min) + min;
|
return Math.random() * (max - min) + min;
|
||||||
}
|
}
|
||||||
exports.getRandomArbitrary = getRandomArbitrary;
|
exports.getRandomArbitrary = getRandomArbitrary;
|
||||||
|
/**
|
||||||
|
* From: https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range
|
||||||
|
*
|
||||||
|
* Returns a random integer between min (inclusive) and max (inclusive).
|
||||||
|
* The value is no lower than min (or the next integer greater than min
|
||||||
|
* if min isn't an integer) and no greater than max (or the next integer
|
||||||
|
* lower than max if max isn't an integer).
|
||||||
|
* Using Math.round() will give you a non-uniform distribution!
|
||||||
|
*/
|
||||||
function getRandomInt(min, max) {
|
function getRandomInt(min, max) {
|
||||||
min = Math.ceil(min);
|
min = Math.ceil(min);
|
||||||
max = Math.floor(max);
|
max = Math.floor(max);
|
||||||
|
12
src/util.ts
12
src/util.ts
@ -1,7 +1,10 @@
|
|||||||
// Common Util for frontend and backend
|
// Common Util for frontend and backend
|
||||||
|
//
|
||||||
|
// DOT NOT MODIFY util.js!
|
||||||
|
// Need to run "tsc" to compile if there are any changes.
|
||||||
|
//
|
||||||
// Backend uses the compiled file util.js
|
// Backend uses the compiled file util.js
|
||||||
// Frontend uses util.ts
|
// Frontend uses util.ts
|
||||||
// Need to run "tsc" to compile if there are any changes.
|
|
||||||
|
|
||||||
import * as _dayjs from "dayjs";
|
import * as _dayjs from "dayjs";
|
||||||
const dayjs = _dayjs;
|
const dayjs = _dayjs;
|
||||||
@ -12,6 +15,11 @@ export const DOWN = 0;
|
|||||||
export const UP = 1;
|
export const UP = 1;
|
||||||
export const PENDING = 2;
|
export const PENDING = 2;
|
||||||
|
|
||||||
|
export const STATUS_PAGE_ALL_DOWN = 0;
|
||||||
|
export const STATUS_PAGE_ALL_UP = 1;
|
||||||
|
export const STATUS_PAGE_PARTIAL_DOWN = 2;
|
||||||
|
|
||||||
|
|
||||||
export function flipStatus(s: number) {
|
export function flipStatus(s: number) {
|
||||||
if (s === UP) {
|
if (s === UP) {
|
||||||
return DOWN;
|
return DOWN;
|
||||||
@ -59,7 +67,6 @@ export function polyfill() {
|
|||||||
*/
|
*/
|
||||||
if (!String.prototype.replaceAll) {
|
if (!String.prototype.replaceAll) {
|
||||||
String.prototype.replaceAll = function (str: string, newStr: string) {
|
String.prototype.replaceAll = function (str: string, newStr: string) {
|
||||||
|
|
||||||
// If a regex pattern
|
// If a regex pattern
|
||||||
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
||||||
return this.replace(str, newStr);
|
return this.replace(str, newStr);
|
||||||
@ -67,7 +74,6 @@ export function polyfill() {
|
|||||||
|
|
||||||
// If a string
|
// If a string
|
||||||
return this.replace(new RegExp(str, "g"), newStr);
|
return this.replace(new RegExp(str, "g"), newStr);
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
test/ubuntu-nodejs16.dockerfile
Normal file
10
test/ubuntu-nodejs16.dockerfile
Normal file
@ -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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user