mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-03-14 13:24:47 +00:00
Merge branch 'master' into cert-notification
This commit is contained in:
commit
960876fb1d
58 changed files with 6617 additions and 1816 deletions
|
@ -1,14 +1,37 @@
|
||||||
/.idea
|
/.idea
|
||||||
/dist
|
/dist
|
||||||
/node_modules
|
/node_modules
|
||||||
/data/kuma.db
|
/data
|
||||||
/.do
|
/.do
|
||||||
**/.dockerignore
|
**/.dockerignore
|
||||||
**/.git
|
**/.git
|
||||||
**/.gitignore
|
**/.gitignore
|
||||||
**/docker-compose*
|
**/docker-compose*
|
||||||
**/Dockerfile*
|
**/[Dd]ockerfile*
|
||||||
LICENSE
|
LICENSE
|
||||||
README.md
|
README.md
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.vscode
|
.vscode
|
||||||
|
.eslint*
|
||||||
|
.stylelint*
|
||||||
|
/.github
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
app.json
|
||||||
|
CODE_OF_CONDUCT.md
|
||||||
|
CONTRIBUTING.md
|
||||||
|
|
||||||
|
### .gitignore content (commented rules are duplicated)
|
||||||
|
|
||||||
|
#node_modules
|
||||||
|
.DS_Store
|
||||||
|
#dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
#.idea
|
||||||
|
|
||||||
|
#/data
|
||||||
|
#!/data/.gitkeep
|
||||||
|
#.vscode
|
||||||
|
|
||||||
|
### End of .gitignore content
|
||||||
|
|
|
@ -16,3 +16,6 @@ indent_size = 2
|
||||||
|
|
||||||
[*.yml]
|
[*.yml]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.vue]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
70
.eslintrc.js
Normal file
70
.eslintrc.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
commonjs: true,
|
||||||
|
es2020: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:vue/vue3-recommended",
|
||||||
|
],
|
||||||
|
parser: "vue-eslint-parser",
|
||||||
|
parserOptions: {
|
||||||
|
parser: "@babel/eslint-parser",
|
||||||
|
sourceType: "module",
|
||||||
|
requireConfigFile: false,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// override/add rules settings here, such as:
|
||||||
|
// 'vue/no-unused-vars': 'error'
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
indent: [
|
||||||
|
"error",
|
||||||
|
4,
|
||||||
|
{
|
||||||
|
ignoredNodes: ["TemplateLiteral"],
|
||||||
|
SwitchCase: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
quotes: ["warn", "double"],
|
||||||
|
//semi: ['off', 'never'],
|
||||||
|
"vue/html-indent": ["warn", 4], // default: 2
|
||||||
|
"vue/max-attributes-per-line": "off",
|
||||||
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
|
"vue/html-self-closing": "off",
|
||||||
|
"no-multi-spaces": ["error", {
|
||||||
|
ignoreEOLComments: true,
|
||||||
|
}],
|
||||||
|
"curly": "error",
|
||||||
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
"object-curly-newline": "off",
|
||||||
|
"object-property-newline": "error",
|
||||||
|
"comma-spacing": "error",
|
||||||
|
"brace-style": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
"key-spacing": "warn",
|
||||||
|
"keyword-spacing": "warn",
|
||||||
|
"space-infix-ops": "warn",
|
||||||
|
"arrow-spacing": "warn",
|
||||||
|
"no-trailing-spaces": "warn",
|
||||||
|
"no-constant-condition": ["error", {
|
||||||
|
"checkLoops": false,
|
||||||
|
}],
|
||||||
|
"space-before-blocks": "warn",
|
||||||
|
//'no-console': 'warn',
|
||||||
|
"no-extra-boolean-cast": "off",
|
||||||
|
"no-multiple-empty-lines": ["warn", {
|
||||||
|
"max": 1,
|
||||||
|
"maxBOF": 0,
|
||||||
|
}],
|
||||||
|
"lines-between-class-members": ["warn", "always", {
|
||||||
|
exceptAfterSingleLine: true,
|
||||||
|
}],
|
||||||
|
"no-unneeded-ternary": "error",
|
||||||
|
"array-bracket-newline": ["error", "consistent"],
|
||||||
|
"eol-last": ["error", "always"],
|
||||||
|
//'prefer-template': 'error',
|
||||||
|
"comma-dangle": ["warn", "only-multiline"],
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
name: ⚠ Please go to "Discussions" Tab if you want to ask or share something
|
|
||||||
about: BUG REPORT ONLY HERE
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
BUG REPORT ONLY HERE
|
|
10
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
name: Ask for help
|
||||||
|
about: You can ask any question related to Uptime Kuma.
|
||||||
|
title: ''
|
||||||
|
labels: help
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
11
.github/workflows/reviewdog.yml
vendored
Normal file
11
.github/workflows/reviewdog.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
name: reviewdog
|
||||||
|
on: [pull_request]
|
||||||
|
jobs:
|
||||||
|
eslint:
|
||||||
|
name: runner / eslint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: reviewdog/action-eslint@v1
|
||||||
|
with:
|
||||||
|
reporter: github-pr-review
|
3
.stylelintrc
Normal file
3
.stylelintrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "stylelint-config-recommended",
|
||||||
|
}
|
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
louis@uptimekuma.louislam.net.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
104
CONTRIBUTING.md
Normal file
104
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# Project Info
|
||||||
|
|
||||||
|
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structed and commented so well, lol. Sorry about that.
|
||||||
|
|
||||||
|
The project was created with vite.js (vue3). Then I created a sub-directory called "server" for server part. Both frontend and backend share the same package.json.
|
||||||
|
|
||||||
|
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
|
||||||
|
|
||||||
|
Your IDE should follow the config in ".editorconfig". The most special thing is I set it to 4 spaces indentation. I know 2 spaces indentation became a kind of standard nowadays for js, but my eyes is not so comfortable for this. In my opinion, there is no callback-hell nowadays, it is good to go back 4 spaces world again.
|
||||||
|
|
||||||
|
# Project Styles
|
||||||
|
|
||||||
|
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
|
||||||
|
|
||||||
|
For example, recently, because I am not a python expert, I spent a 2 hours to resolve all problems in order to install and use the Apprise cli. Apprise requires so many hidden requirements, I have to figure out myself how to solve the problems by Google search for my OS. That is painful. I do not want Uptime Kuma to be like this way, so:
|
||||||
|
|
||||||
|
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
|
||||||
|
- Single container for Docker users, no very complex docker-composer file. Just map the volume and expose the port, then good to go
|
||||||
|
- All settings in frontend.
|
||||||
|
- Easy to use
|
||||||
|
|
||||||
|
# Tools
|
||||||
|
- Node.js >= 14
|
||||||
|
- Git
|
||||||
|
- IDE that supports .editorconfig (I am using Intellji Idea)
|
||||||
|
- A SQLite tool (I am using SQLite Expert Personal)
|
||||||
|
|
||||||
|
# Prepare the dev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
# Backend Dev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start-server
|
||||||
|
|
||||||
|
# Or
|
||||||
|
|
||||||
|
node server/server.js
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
It binds to 0.0.0.0:3001 by default.
|
||||||
|
|
||||||
|
|
||||||
|
## Backend Details
|
||||||
|
|
||||||
|
It is mainly a socket.io app + express.js.
|
||||||
|
|
||||||
|
express.js is just used for serving the frontend built files (index.html, .js and .css etc.)
|
||||||
|
|
||||||
|
# Frontend Dev
|
||||||
|
|
||||||
|
Start frontend dev server. Hot-reload enabled in this way. It binds to 0.0.0.0:3000.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
localStorage.dev = "dev";
|
||||||
|
```
|
||||||
|
|
||||||
|
So that the frontend will try to connect websocket server in 3001.
|
||||||
|
|
||||||
|
Alternately, you can specific NODE_ENV to "development".
|
||||||
|
|
||||||
|
|
||||||
|
## Build the frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Details
|
||||||
|
|
||||||
|
Uptime Kuma Frontend is a single page application (SPA). Most paths are handled by Vue Router.
|
||||||
|
|
||||||
|
The router in "src/main.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.
|
||||||
|
|
||||||
|
The data and socket logic in "src/mixins/socket.js"
|
||||||
|
|
||||||
|
# Database Migration
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
43
README.md
43
README.md
|
@ -20,7 +20,7 @@ It is a self-hosted monitoring tool like "Uptime Robot".
|
||||||
|
|
||||||
# How to Use
|
# How to Use
|
||||||
|
|
||||||
### Docker
|
## Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a volume
|
# Create a volume
|
||||||
|
@ -38,7 +38,7 @@ Change Port and Volume
|
||||||
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||||
```
|
```
|
||||||
|
|
||||||
### Without Docker
|
## Without Docker
|
||||||
|
|
||||||
Required Tools: Node.js >= 14, git and pm2.
|
Required Tools: Node.js >= 14, git and pm2.
|
||||||
|
|
||||||
|
@ -60,27 +60,56 @@ pm2 start npm --name uptime-kuma -- run start-server -- --port=80 --hostname=0.0
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
More useful commands if you have installed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 start uptime-kuma
|
||||||
|
pm2 restart uptime-kuma
|
||||||
|
pm2 stop uptime-kuma
|
||||||
|
```
|
||||||
|
|
||||||
Browse to http://localhost:3001 after started.
|
Browse to http://localhost:3001 after started.
|
||||||
|
|
||||||
### One-click Deploy to DigitalOcean
|
|
||||||
|
## (Optional) One more step for Reverse Proxy
|
||||||
|
|
||||||
|
This is optional for someone who want to do reverse proxy.
|
||||||
|
|
||||||
|
Unlikely other web apps, Uptime Kuma is based on WebSocket. You need two more headers **"Upgrade"** and **"Connection"** in order to reverse proxy WebSocket.
|
||||||
|
|
||||||
|
Please read wiki for more info:
|
||||||
|
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy
|
||||||
|
|
||||||
|
## One-click Deploy
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Abort. Heroku instance killed the server.js if idle, stupid.
|
||||||
|
[](https://heroku.com/deploy?template=https://github.com/louislam/uptime-kuma/tree/1.0.10)
|
||||||
|
-->
|
||||||
|
|
||||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434)
|
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434)
|
||||||
|
|
||||||
Choose Cheapest Plan is enough. (US$ 5)
|
|
||||||
|
|
||||||
# How to Update
|
# How to Update
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
Re-pull the latest docker image and create another container with the same volume.
|
Re-pull the latest docker image and create another container with the same volume.
|
||||||
|
|
||||||
|
For someone who used my "How-to-use" commands to install Uptime Kuma, you can update by this:
|
||||||
|
```bash
|
||||||
|
docker pull uptime-kuma:1
|
||||||
|
docker stop uptime-kuma
|
||||||
|
docker rm uptime-kuma
|
||||||
|
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||||
|
```
|
||||||
|
|
||||||
PS: For every new release, it takes some time to build the docker image, please be patient if it is not available yet.
|
PS: For every new release, it takes some time to build the docker image, please be patient if it is not available yet.
|
||||||
|
|
||||||
### Without Docker
|
### Without Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git fetch --all
|
git fetch --all
|
||||||
git checkout 1.0.6 --force
|
git checkout 1.0.10 --force
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
pm2 restart uptime-kuma
|
pm2 restart uptime-kuma
|
||||||
|
@ -119,6 +148,6 @@ If you love this project, please consider giving me a ⭐.
|
||||||
|
|
||||||
If you want to report a bug or request a new feature. Free feel to open a new issue.
|
If you want to report a bug or request a new feature. Free feel to open a new issue.
|
||||||
|
|
||||||
If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/wiki/%5BDev%5D-Setup-Development-Environment
|
If you want to modify Uptime Kuma, this guideline maybe 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.
|
||||||
|
|
7
app.json
Normal file
7
app.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Uptime Kuma",
|
||||||
|
"description": "A fancy self-hosted monitoring tool",
|
||||||
|
"repository": "https://github.com/louislam/uptime-kuma",
|
||||||
|
"logo": "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
|
||||||
|
"keywords": ["node", "express", "socket-io", "uptime-kuma", "uptime"]
|
||||||
|
}
|
40
db/patch4.sql
Normal file
40
db/patch4.sql
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
-- OK.... serious wrong, missing maxretries column
|
||||||
|
-- Developers should patch it manually if you have missing the maxretries column
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table monitor_dg_tmp
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
primary key autoincrement,
|
||||||
|
name VARCHAR(150),
|
||||||
|
active BOOLEAN default 1 not null,
|
||||||
|
user_id INTEGER
|
||||||
|
references user
|
||||||
|
on update cascade on delete set null,
|
||||||
|
interval INTEGER default 20 not null,
|
||||||
|
url TEXT,
|
||||||
|
type VARCHAR(20),
|
||||||
|
weight INTEGER default 2000,
|
||||||
|
hostname VARCHAR(255),
|
||||||
|
port INTEGER,
|
||||||
|
created_date DATETIME,
|
||||||
|
keyword VARCHAR(255),
|
||||||
|
maxretries INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ignore_tls BOOLEAN default 0 not null,
|
||||||
|
upside_down BOOLEAN default 0 not null
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword, maxretries from monitor;
|
||||||
|
|
||||||
|
drop table monitor;
|
||||||
|
|
||||||
|
alter table monitor_dg_tmp rename to monitor;
|
||||||
|
|
||||||
|
create index user_id on monitor (user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys=on;
|
70
db/patch5.sql
Normal file
70
db/patch5.sql
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
PRAGMA foreign_keys = off;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table monitor_dg_tmp (
|
||||||
|
id INTEGER not null primary key autoincrement,
|
||||||
|
name VARCHAR(150),
|
||||||
|
active BOOLEAN default 1 not null,
|
||||||
|
user_id INTEGER references user on update cascade on delete
|
||||||
|
set
|
||||||
|
null,
|
||||||
|
interval INTEGER default 20 not null,
|
||||||
|
url TEXT,
|
||||||
|
type VARCHAR(20),
|
||||||
|
weight INTEGER default 2000,
|
||||||
|
hostname VARCHAR(255),
|
||||||
|
port INTEGER,
|
||||||
|
created_date DATETIME default (DATETIME('now')) not null,
|
||||||
|
keyword VARCHAR(255),
|
||||||
|
maxretries INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ignore_tls BOOLEAN default 0 not null,
|
||||||
|
upside_down BOOLEAN default 0 not null
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into
|
||||||
|
monitor_dg_tmp(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
user_id,
|
||||||
|
interval,
|
||||||
|
url,
|
||||||
|
type,
|
||||||
|
weight,
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
keyword,
|
||||||
|
maxretries,
|
||||||
|
ignore_tls,
|
||||||
|
upside_down
|
||||||
|
)
|
||||||
|
select
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
user_id,
|
||||||
|
interval,
|
||||||
|
url,
|
||||||
|
type,
|
||||||
|
weight,
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
keyword,
|
||||||
|
maxretries,
|
||||||
|
ignore_tls,
|
||||||
|
upside_down
|
||||||
|
from
|
||||||
|
monitor;
|
||||||
|
|
||||||
|
drop table monitor;
|
||||||
|
|
||||||
|
alter table
|
||||||
|
monitor_dg_tmp rename to monitor;
|
||||||
|
|
||||||
|
create index user_id on monitor (user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = on;
|
74
db/patch6.sql
Normal file
74
db/patch6.sql
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
PRAGMA foreign_keys = off;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
create table monitor_dg_tmp (
|
||||||
|
id INTEGER not null primary key autoincrement,
|
||||||
|
name VARCHAR(150),
|
||||||
|
active BOOLEAN default 1 not null,
|
||||||
|
user_id INTEGER references user on update cascade on delete
|
||||||
|
set
|
||||||
|
null,
|
||||||
|
interval INTEGER default 20 not null,
|
||||||
|
url TEXT,
|
||||||
|
type VARCHAR(20),
|
||||||
|
weight INTEGER default 2000,
|
||||||
|
hostname VARCHAR(255),
|
||||||
|
port INTEGER,
|
||||||
|
created_date DATETIME default (DATETIME('now')) not null,
|
||||||
|
keyword VARCHAR(255),
|
||||||
|
maxretries INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ignore_tls BOOLEAN default 0 not null,
|
||||||
|
upside_down BOOLEAN default 0 not null,
|
||||||
|
maxredirects INTEGER default 10 not null,
|
||||||
|
accepted_statuscodes_json TEXT default '["200-299"]' not null
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into
|
||||||
|
monitor_dg_tmp(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
user_id,
|
||||||
|
interval,
|
||||||
|
url,
|
||||||
|
type,
|
||||||
|
weight,
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
created_date,
|
||||||
|
keyword,
|
||||||
|
maxretries,
|
||||||
|
ignore_tls,
|
||||||
|
upside_down
|
||||||
|
)
|
||||||
|
select
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
user_id,
|
||||||
|
interval,
|
||||||
|
url,
|
||||||
|
type,
|
||||||
|
weight,
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
created_date,
|
||||||
|
keyword,
|
||||||
|
maxretries,
|
||||||
|
ignore_tls,
|
||||||
|
upside_down
|
||||||
|
from
|
||||||
|
monitor;
|
||||||
|
|
||||||
|
drop table monitor;
|
||||||
|
|
||||||
|
alter table
|
||||||
|
monitor_dg_tmp rename to monitor;
|
||||||
|
|
||||||
|
create index user_id on monitor (user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = on;
|
22
dockerfile
22
dockerfile
|
@ -11,26 +11,16 @@ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
|
||||||
# Touching above code may causes sqlite3 re-compile again, painful slow.
|
# Touching above code may causes sqlite3 re-compile again, painful slow.
|
||||||
|
|
||||||
# Install apprise
|
# Install apprise
|
||||||
# Hate pip!!! I never run pip install successfully in first run for anything in my life without Google :/
|
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
|
||||||
# Compilation Fail 1 => Google Search "alpine ffi.h" => Add libffi-dev
|
RUN pip3 --no-cache-dir install apprise && \
|
||||||
# Compilation Fail 2 => Google Search "alpine cargo" => Add cargo
|
rm -rf /root/.cache
|
||||||
# Compilation Fail 3 => Google Search "alpine opensslv.h" => Add openssl-dev
|
|
||||||
# Compilation Fail 4 => Google Search "alpine opensslv.h" again => Change to libressl-dev musl-dev
|
|
||||||
# Compilation Fail 5 => Google Search "ERROR: libressl3.3-libtls-3.3.3-r0: trying to overwrite usr/lib/libtls.so.20 owned by libretls-3.3.3-r0." again => Change back to openssl-dev with musl-dev
|
|
||||||
# Runtime Error => ModuleNotFoundError: No module named 'six' => pip3 install six
|
|
||||||
# Runtime Error 2 => ModuleNotFoundError: No module named 'six' => apk add py3-six
|
|
||||||
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
|
||||||
RUN apk add --no-cache python3 py3-pip py3-six cargo
|
|
||||||
RUN apk add --no-cache --virtual .build-deps libffi-dev musl-dev openssl-dev python3-dev && \
|
|
||||||
pip3 install apprise && \
|
|
||||||
apk del .build-deps
|
|
||||||
RUN apprise --version
|
|
||||||
|
|
||||||
# New things add here
|
# New things add here
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm install
|
RUN npm install && \
|
||||||
RUN npm run build
|
npm run build && \
|
||||||
|
npm prune
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
VOLUME ["/app/data"]
|
VOLUME ["/app/data"]
|
||||||
|
|
78
extra/update-version.js
Normal file
78
extra/update-version.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
String.prototype.replaceAll = function(str, newStr) {
|
||||||
|
|
||||||
|
// If a regex pattern
|
||||||
|
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
||||||
|
return this.replace(str, newStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a string
|
||||||
|
return this.replace(new RegExp(str, "g"), newStr);
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = require("../package.json");
|
||||||
|
const fs = require("fs");
|
||||||
|
const child_process = require("child_process");
|
||||||
|
const oldVersion = pkg.version;
|
||||||
|
const newVersion = process.argv[2];
|
||||||
|
|
||||||
|
console.log("Old Version: " + oldVersion);
|
||||||
|
console.log("New Version: " + newVersion);
|
||||||
|
|
||||||
|
if (! newVersion) {
|
||||||
|
console.error("invalid version");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = tagExists(newVersion);
|
||||||
|
|
||||||
|
if (! exists) {
|
||||||
|
// Process package.json
|
||||||
|
pkg.version = newVersion;
|
||||||
|
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
|
||||||
|
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
||||||
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||||
|
|
||||||
|
// Process README.md
|
||||||
|
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
|
||||||
|
|
||||||
|
commit(newVersion);
|
||||||
|
tag(newVersion);
|
||||||
|
} else {
|
||||||
|
console.log("version exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
function commit(version) {
|
||||||
|
let msg = "update to " + version;
|
||||||
|
|
||||||
|
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
|
||||||
|
let stdout = res.stdout.toString().trim();
|
||||||
|
console.log(stdout)
|
||||||
|
|
||||||
|
if (stdout.includes("no changes added to commit")) {
|
||||||
|
throw new Error("commit error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tag(version) {
|
||||||
|
let res = child_process.spawnSync("git", ["tag", version]);
|
||||||
|
console.log(res.stdout.toString().trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagExists(version) {
|
||||||
|
if (! version) {
|
||||||
|
throw new Error("invalid version");
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = child_process.spawnSync("git", ["tag", "-l", version]);
|
||||||
|
|
||||||
|
return res.stdout.toString().trim() === version;
|
||||||
|
}
|
|
@ -1,39 +0,0 @@
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
String.prototype.replaceAll = function(str, newStr){
|
|
||||||
|
|
||||||
// If a regex pattern
|
|
||||||
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
|
|
||||||
return this.replace(str, newStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a string
|
|
||||||
return this.replace(new RegExp(str, 'g'), newStr);
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const pkg = require('../package.json');
|
|
||||||
const fs = require("fs");
|
|
||||||
const oldVersion = pkg.version
|
|
||||||
const newVersion = process.argv[2]
|
|
||||||
|
|
||||||
console.log("Old Version: " + oldVersion)
|
|
||||||
console.log("New Version: " + newVersion)
|
|
||||||
|
|
||||||
if (newVersion) {
|
|
||||||
// Process package.json
|
|
||||||
pkg.version = newVersion
|
|
||||||
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion)
|
|
||||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion)
|
|
||||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n")
|
|
||||||
|
|
||||||
// Process README.md
|
|
||||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion))
|
|
||||||
}
|
|
||||||
|
|
3907
package-lock.json
generated
3907
package-lock.json
generated
File diff suppressed because it is too large
Load diff
50
package.json
50
package.json
|
@ -1,55 +1,75 @@
|
||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.0.6",
|
"version": "1.0.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "14.*"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
|
"start": "npm run start-server",
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"update": "",
|
"update": "",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"vite-preview-dist": "vite preview --host",
|
"vite-preview-dist": "vite preview --host",
|
||||||
"build-docker": "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.0.6 --target release . --push",
|
"build-docker": "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.0.10 --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-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
|
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
|
||||||
"setup": "git checkout 1.0.6 && npm install && npm run build",
|
"setup": "git checkout 1.0.10 && npm install && npm run build",
|
||||||
"version-global-replace": "node extra/version-global-replace.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"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.9.2",
|
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
|
"@fortawesome/vue-fontawesome": "^3.0.0-4",
|
||||||
|
"@popperjs/core": "^2.9.3",
|
||||||
"args-parser": "^1.3.0",
|
"args-parser": "^1.3.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"bootstrap": "^5.0.2",
|
"bootstrap": "^5.1.0",
|
||||||
"command-exists": "^1.2.9",
|
"command-exists": "^1.2.9",
|
||||||
"dayjs": "^1.10.6",
|
"dayjs": "^1.10.6",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-basic-auth": "^1.2.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"http-graceful-shutdown": "^3.1.2",
|
"http-graceful-shutdown": "^3.1.3",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"nodemailer": "^6.6.3",
|
"nodemailer": "^6.6.3",
|
||||||
"password-hash": "^1.2.2",
|
"password-hash": "^1.2.2",
|
||||||
"redbean-node": "0.0.20",
|
"prom-client": "^13.1.0",
|
||||||
|
"prometheus-api-metrics": "^3.2.0",
|
||||||
|
"redbean-node": "0.0.21",
|
||||||
"socket.io": "^4.1.3",
|
"socket.io": "^4.1.3",
|
||||||
"socket.io-client": "^4.1.3",
|
"socket.io-client": "^4.1.3",
|
||||||
"sqlite3": "^5.0.2",
|
"@louislam/sqlite3": "^5.0.3",
|
||||||
"tcp-ping": "^0.1.1",
|
"tcp-ping": "^0.1.1",
|
||||||
"v-pagination-3": "^0.1.6",
|
"v-pagination-3": "^0.1.6",
|
||||||
"vue": "^3.0.5",
|
"vue": "^3.1.5",
|
||||||
"vue-confirm-dialog": "^1.0.2",
|
"vue-confirm-dialog": "^1.0.2",
|
||||||
|
"vue-multiselect": "^3.0.0-alpha.2",
|
||||||
"vue-router": "^4.0.10",
|
"vue-router": "^4.0.10",
|
||||||
"vue-toastification": "^2.0.0-rc.1"
|
"vue-toastification": "^2.0.0-rc.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-legacy": "^1.4.4",
|
"@babel/eslint-parser": "^7.15.0",
|
||||||
"@vitejs/plugin-vue": "^1.2.5",
|
"@types/bootstrap": "^5.0.17",
|
||||||
|
"@vitejs/plugin-legacy": "^1.5.1",
|
||||||
|
"@vitejs/plugin-vue": "^1.3.0",
|
||||||
"@vue/compiler-sfc": "^3.1.5",
|
"@vue/compiler-sfc": "^3.1.5",
|
||||||
"core-js": "^3.15.2",
|
"core-js": "^3.16.0",
|
||||||
"sass": "^1.35.2",
|
"eslint": "^7.32.0",
|
||||||
"vite": "^2.4.2"
|
"eslint-plugin-vue": "^7.15.1",
|
||||||
|
"sass": "^1.37.5",
|
||||||
|
"stylelint": "^13.13.1",
|
||||||
|
"stylelint-config-recommended": "^5.0.0",
|
||||||
|
"stylelint-config-standard": "^22.0.0",
|
||||||
|
"typescript": "^4.3.5",
|
||||||
|
"vite": "^2.4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
51
server/auth.js
Normal file
51
server/auth.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
const basicAuth = require("express-basic-auth")
|
||||||
|
const passwordHash = require("./password-hash");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const { setting } = require("./util-server");
|
||||||
|
const { debug } = require("../src/util");
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param username : string
|
||||||
|
* @param password : string
|
||||||
|
* @returns {Promise<Bean|null>}
|
||||||
|
*/
|
||||||
|
exports.login = async function (username, password) {
|
||||||
|
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||||
|
username,
|
||||||
|
])
|
||||||
|
|
||||||
|
if (user && passwordHash.verify(password, user.password)) {
|
||||||
|
// Upgrade the hash to bcrypt
|
||||||
|
if (passwordHash.needRehash(user.password)) {
|
||||||
|
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||||
|
passwordHash.generate(password),
|
||||||
|
user.id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function myAuthorizer(username, password, callback) {
|
||||||
|
|
||||||
|
setting("disableAuth").then((result) => {
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
callback(null, true)
|
||||||
|
} else {
|
||||||
|
exports.login(username, password).then((user) => {
|
||||||
|
callback(null, user != null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.basicAuth = basicAuth({
|
||||||
|
authorizer: myAuthorizer,
|
||||||
|
authorizeAsync: true,
|
||||||
|
challenge: true,
|
||||||
|
});
|
|
@ -1,16 +1,34 @@
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const {sleep} = require("./util");
|
const { sleep } = require("../src/util");
|
||||||
const {R} = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const {setSetting, setting} = require("./util-server");
|
const { setSetting, setting } = require("./util-server");
|
||||||
|
const knex = require("knex");
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
|
|
||||||
static templatePath = "./db/kuma.db"
|
static templatePath = "./db/kuma.db"
|
||||||
static path = './data/kuma.db';
|
static path = "./data/kuma.db";
|
||||||
static latestVersion = 3;
|
static latestVersion = 6;
|
||||||
static noReject = true;
|
static noReject = true;
|
||||||
|
|
||||||
|
static connect() {
|
||||||
|
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||||
|
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
||||||
|
|
||||||
|
R.setup(knex({
|
||||||
|
client: Dialect,
|
||||||
|
connection: {
|
||||||
|
filename: Database.path,
|
||||||
|
},
|
||||||
|
useNullAsDefault: true,
|
||||||
|
pool: {
|
||||||
|
min: 1,
|
||||||
|
max: 1,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
static async patch() {
|
static async patch() {
|
||||||
let version = parseInt(await setting("database_version"));
|
let version = parseInt(await setting("database_version"));
|
||||||
|
|
||||||
|
@ -23,6 +41,8 @@ class Database {
|
||||||
|
|
||||||
if (version === this.latestVersion) {
|
if (version === this.latestVersion) {
|
||||||
console.info("Database no need to patch");
|
console.info("Database no need to patch");
|
||||||
|
} else if (version > this.latestVersion) {
|
||||||
|
console.info("Warning: Database version is newer than expected");
|
||||||
} else {
|
} else {
|
||||||
console.info("Database patch is needed")
|
console.info("Database patch is needed")
|
||||||
|
|
||||||
|
@ -95,7 +115,7 @@ class Database {
|
||||||
const listener = (reason, p) => {
|
const listener = (reason, p) => {
|
||||||
Database.noReject = false;
|
Database.noReject = false;
|
||||||
};
|
};
|
||||||
process.addListener('unhandledRejection', listener);
|
process.addListener("unhandledRejection", listener);
|
||||||
|
|
||||||
console.log("Closing DB")
|
console.log("Closing DB")
|
||||||
|
|
||||||
|
@ -112,7 +132,7 @@ class Database {
|
||||||
}
|
}
|
||||||
console.log("SQLite closed")
|
console.log("SQLite closed")
|
||||||
|
|
||||||
process.removeListener('unhandledRejection', listener);
|
process.removeListener("unhandledRejection", listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require("dayjs/plugin/utc")
|
||||||
var 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");
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
* 0 = DOWN
|
* 0 = DOWN
|
||||||
* 1 = UP
|
* 1 = UP
|
||||||
|
* 2 = PENDING
|
||||||
*/
|
*/
|
||||||
class Heartbeat extends BeanModel {
|
class Heartbeat extends BeanModel {
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,22 @@
|
||||||
|
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")
|
||||||
var 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 {debug, UP, DOWN, PENDING} = require("../util");
|
const { Prometheus } = require("../prometheus");
|
||||||
const {tcping, ping, checkCertificate} = require("../util-server");
|
const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util");
|
||||||
const {R} = require("redbean-node");
|
const { tcping, ping, checkCertificate, checkStatusCode } = require("../util-server");
|
||||||
const {BeanModel} = require("redbean-node/dist/bean-model");
|
const { R } = require("redbean-node");
|
||||||
const {Notification} = require("../notification")
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { Notification } = require("../notification")
|
||||||
// Use Custom agent to disable session reuse
|
|
||||||
// https://github.com/nodejs/node/issues/3940
|
|
||||||
const customAgent = new https.Agent({
|
|
||||||
maxCachedSessions: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
* 0 = DOWN
|
* 0 = DOWN
|
||||||
* 1 = UP
|
* 1 = UP
|
||||||
|
* 2 = PENDING
|
||||||
*/
|
*/
|
||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
async toJSON() {
|
async toJSON() {
|
||||||
|
@ -29,7 +24,7 @@ class Monitor extends BeanModel {
|
||||||
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) {
|
||||||
|
@ -48,19 +43,45 @@ class Monitor extends BeanModel {
|
||||||
type: this.type,
|
type: this.type,
|
||||||
interval: this.interval,
|
interval: this.interval,
|
||||||
keyword: this.keyword,
|
keyword: this.keyword,
|
||||||
notificationIDList
|
ignoreTls: this.getIgnoreTls(),
|
||||||
|
upsideDown: this.isUpsideDown(),
|
||||||
|
maxredirects: this.maxredirects,
|
||||||
|
accepted_statuscodes: this.getAcceptedStatuscodes(),
|
||||||
|
notificationIDList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
getIgnoreTls() {
|
||||||
|
return Boolean(this.ignoreTls)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isUpsideDown() {
|
||||||
|
return Boolean(this.upsideDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAcceptedStatuscodes() {
|
||||||
|
return JSON.parse(this.accepted_statuscodes_json);
|
||||||
|
}
|
||||||
|
|
||||||
start(io) {
|
start(io) {
|
||||||
let previousBeat = null;
|
let previousBeat = null;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
|
|
||||||
|
let prometheus = new Prometheus(this);
|
||||||
|
|
||||||
const beat = async () => {
|
const beat = async () => {
|
||||||
|
|
||||||
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,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,9 +92,13 @@ class Monitor extends BeanModel {
|
||||||
bean.time = R.isoDateTime(dayjs.utc());
|
bean.time = R.isoDateTime(dayjs.utc());
|
||||||
bean.status = DOWN;
|
bean.status = DOWN;
|
||||||
|
|
||||||
|
if (this.isUpsideDown()) {
|
||||||
|
bean.status = flipStatus(bean.status);
|
||||||
|
}
|
||||||
|
|
||||||
// Duration
|
// Duration
|
||||||
if (! isFirstBeat) {
|
if (! isFirstBeat) {
|
||||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), 'second');
|
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
|
||||||
} else {
|
} else {
|
||||||
bean.duration = 0;
|
bean.duration = 0;
|
||||||
}
|
}
|
||||||
|
@ -81,9 +106,21 @@ class Monitor extends BeanModel {
|
||||||
try {
|
try {
|
||||||
if (this.type === "http" || this.type === "keyword") {
|
if (this.type === "http" || this.type === "keyword") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
|
// Use Custom agent to disable session reuse
|
||||||
|
// https://github.com/nodejs/node/issues/3940
|
||||||
let res = await axios.get(this.url, {
|
let res = await axios.get(this.url, {
|
||||||
headers: { "User-Agent": "Uptime-Kuma" },
|
headers: {
|
||||||
httpsAgent: customAgent,
|
"User-Agent": "Uptime-Kuma",
|
||||||
|
},
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
maxCachedSessions: 0,
|
||||||
|
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||||
|
}),
|
||||||
|
maxRedirects: this.maxredirects,
|
||||||
|
validateStatus: (status) => {
|
||||||
|
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;
|
||||||
|
@ -95,9 +132,11 @@ class Monitor extends BeanModel {
|
||||||
try {
|
try {
|
||||||
await this.updateTlsInfo(checkCertificate(res));
|
await this.updateTlsInfo(checkCertificate(res));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
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")
|
||||||
|
|
||||||
|
@ -121,7 +160,6 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} 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 = ""
|
||||||
|
@ -133,14 +171,29 @@ class Monitor extends BeanModel {
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isUpsideDown()) {
|
||||||
|
bean.status = flipStatus(bean.status);
|
||||||
|
|
||||||
|
if (bean.status === DOWN) {
|
||||||
|
throw new Error("Flip UP to DOWN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
retries = 0;
|
retries = 0;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
|
||||||
|
bean.msg = error.message;
|
||||||
|
|
||||||
|
// If UP come in here, it must be upside down mode
|
||||||
|
// Just reset the retries
|
||||||
|
if (this.isUpsideDown() && bean.status === UP) {
|
||||||
|
retries = 0;
|
||||||
|
|
||||||
|
} else if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
||||||
retries++;
|
retries++;
|
||||||
bean.status = PENDING;
|
bean.status = PENDING;
|
||||||
}
|
}
|
||||||
bean.msg = error.message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||||
|
@ -165,8 +218,8 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
// Send only if the first beat is DOWN
|
// Send only if the first beat is DOWN
|
||||||
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;
|
||||||
|
@ -178,7 +231,7 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -194,11 +247,13 @@ class Monitor extends BeanModel {
|
||||||
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: ${this.interval} seconds | Type: ${this.type}`)
|
||||||
} else if (bean.status === PENDING) {
|
} else if (bean.status === PENDING) {
|
||||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Type: ${this.type}`)
|
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | 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} | Type: ${this.type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prometheus.update(bean)
|
||||||
|
|
||||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||||
|
|
||||||
await R.store(bean)
|
await R.store(bean)
|
||||||
|
@ -215,9 +270,12 @@ class Monitor extends BeanModel {
|
||||||
clearInterval(this.heartbeatInterval)
|
clearInterval(this.heartbeatInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper Method:
|
/**
|
||||||
// returns URL object for further usage
|
* Helper Method:
|
||||||
// returns null if url is invalid
|
* returns URL object for further usage
|
||||||
|
* returns null if url is invalid
|
||||||
|
* @returns {null|URL}
|
||||||
|
*/
|
||||||
getUrl() {
|
getUrl() {
|
||||||
try {
|
try {
|
||||||
return new URL(this.url);
|
return new URL(this.url);
|
||||||
|
@ -226,10 +284,14 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store TLS info to database
|
/**
|
||||||
|
* Store TLS info to database
|
||||||
|
* @param checkCertificateResult
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async updateTlsInfo(checkCertificateResult) {
|
async updateTlsInfo(checkCertificateResult) {
|
||||||
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||||
this.id
|
this.id,
|
||||||
]);
|
]);
|
||||||
if (tls_info_bean == null) {
|
if (tls_info_bean == null) {
|
||||||
tls_info_bean = R.dispense("monitor_tls_info");
|
tls_info_bean = R.dispense("monitor_tls_info");
|
||||||
|
@ -258,7 +320,7 @@ class Monitor extends BeanModel {
|
||||||
AND ping IS NOT NULL
|
AND ping IS NOT NULL
|
||||||
AND monitor_id = ? `, [
|
AND monitor_id = ? `, [
|
||||||
-duration,
|
-duration,
|
||||||
monitorID
|
monitorID,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
io.to(userID).emit("avgPing", monitorID, avgPing);
|
||||||
|
@ -266,7 +328,7 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
static async sendCertInfo(io, monitorID, userID) {
|
static async sendCertInfo(io, monitorID, userID) {
|
||||||
let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||||
monitorID
|
monitorID,
|
||||||
]);
|
]);
|
||||||
if (tls_info != null) {
|
if (tls_info != null) {
|
||||||
io.to(userID).emit("certInfo", monitorID, tls_info.info_json);
|
io.to(userID).emit("certInfo", monitorID, tls_info.info_json);
|
||||||
|
@ -290,7 +352,7 @@ class Monitor extends BeanModel {
|
||||||
WHERE time > DATETIME('now', ? || ' hours')
|
WHERE time > DATETIME('now', ? || ' hours')
|
||||||
AND monitor_id = ? `, [
|
AND monitor_id = ? `, [
|
||||||
-duration,
|
-duration,
|
||||||
monitorID
|
monitorID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let downtime = 0;
|
let downtime = 0;
|
||||||
|
@ -314,7 +376,7 @@ class Monitor extends BeanModel {
|
||||||
// Handle if heartbeat duration longer than the target duration
|
// Handle if heartbeat duration longer than the target duration
|
||||||
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
|
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
|
||||||
if (value > sec) {
|
if (value > sec) {
|
||||||
let trim = dayjs.utc().diff(dayjs(time), 'second');
|
let trim = dayjs.utc().diff(dayjs(time), "second");
|
||||||
value = sec - trim;
|
value = sec - trim;
|
||||||
|
|
||||||
if (value < 0) {
|
if (value < 0) {
|
||||||
|
@ -335,8 +397,6 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const {R} = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const FormData = require('form-data');
|
const FormData = require("form-data");
|
||||||
const nodemailer = require("nodemailer");
|
const nodemailer = require("nodemailer");
|
||||||
const child_process = require("child_process");
|
const child_process = require("child_process");
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class Notification {
|
||||||
params: {
|
params: {
|
||||||
chat_id: notification.telegramChatID,
|
chat_id: notification.telegramChatID,
|
||||||
text: msg,
|
text: msg,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class Notification {
|
||||||
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
|
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
|
||||||
"message": msg,
|
"message": msg,
|
||||||
"priority": notification.gotifyPriority || 8,
|
"priority": notification.gotifyPriority || 8,
|
||||||
"title": "Uptime-Kuma"
|
"title": "Uptime-Kuma",
|
||||||
})
|
})
|
||||||
|
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
@ -62,10 +62,10 @@ class Notification {
|
||||||
|
|
||||||
if (notification.webhookContentType === "form-data") {
|
if (notification.webhookContentType === "form-data") {
|
||||||
finalData = new FormData();
|
finalData = new FormData();
|
||||||
finalData.append('data', JSON.stringify(data));
|
finalData.append("data", JSON.stringify(data));
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
headers: finalData.getHeaders()
|
headers: finalData.getHeaders(),
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -84,41 +84,79 @@ class Notification {
|
||||||
|
|
||||||
} else if (notification.type === "discord") {
|
} else if (notification.type === "discord") {
|
||||||
try {
|
try {
|
||||||
|
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||||
|
|
||||||
// If heartbeatJSON is null, assume we're testing.
|
// If heartbeatJSON is null, assume we're testing.
|
||||||
if(heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let data = {
|
let discordtestdata = {
|
||||||
username: 'Uptime-Kuma',
|
username: discordDisplayName,
|
||||||
content: msg
|
content: msg,
|
||||||
}
|
}
|
||||||
await axios.post(notification.discordWebhookUrl, data)
|
await axios.post(notification.discordWebhookUrl, discordtestdata)
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||||
if(heartbeatJSON['status'] == 0) {
|
if (heartbeatJSON["status"] == 0) {
|
||||||
var alertColor = "16711680";
|
let discorddowndata = {
|
||||||
} else if(heartbeatJSON['status'] == 1) {
|
username: discordDisplayName,
|
||||||
var alertColor = "65280";
|
|
||||||
}
|
|
||||||
let data = {
|
|
||||||
username: 'Uptime-Kuma',
|
|
||||||
embeds: [{
|
embeds: [{
|
||||||
title: "Uptime-Kuma Alert",
|
title: "❌ One of your services went down. ❌",
|
||||||
color: alertColor,
|
color: 16711680,
|
||||||
|
timestamp: heartbeatJSON["time"],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: "Time (UTC)",
|
name: "Service Name",
|
||||||
value: heartbeatJSON["time"]
|
value: monitorJSON["name"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Message",
|
name: "Service URL",
|
||||||
value: msg
|
value: monitorJSON["url"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Time (UTC)",
|
||||||
|
value: heartbeatJSON["time"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error",
|
||||||
|
value: heartbeatJSON["msg"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
]
|
await axios.post(notification.discordWebhookUrl, discorddowndata)
|
||||||
}]
|
|
||||||
}
|
|
||||||
await axios.post(notification.discordWebhookUrl, data)
|
|
||||||
return okMsg;
|
return okMsg;
|
||||||
} catch(error) {
|
|
||||||
|
} else if (heartbeatJSON["status"] == 1) {
|
||||||
|
let discordupdata = {
|
||||||
|
username: discordDisplayName,
|
||||||
|
embeds: [{
|
||||||
|
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
|
||||||
|
color: 65280,
|
||||||
|
timestamp: heartbeatJSON["time"],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Service Name",
|
||||||
|
value: monitorJSON["name"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Service URL",
|
||||||
|
value: "[Visit Service](" + monitorJSON["url"] + ")",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Time (UTC)",
|
||||||
|
value: heartbeatJSON["time"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ping",
|
||||||
|
value: heartbeatJSON["ping"] + "ms",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
await axios.post(notification.discordWebhookUrl, discordupdata)
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
throwGeneralAxiosError(error)
|
throwGeneralAxiosError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +165,7 @@ class Notification {
|
||||||
let data = {
|
let data = {
|
||||||
"message": msg,
|
"message": msg,
|
||||||
"number": notification.signalNumber,
|
"number": notification.signalNumber,
|
||||||
"recipients": notification.signalRecipients.replace(/\s/g, '').split(",")
|
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
|
||||||
};
|
};
|
||||||
let config = {};
|
let config = {};
|
||||||
|
|
||||||
|
@ -137,10 +175,33 @@ class Notification {
|
||||||
throwGeneralAxiosError(error)
|
throwGeneralAxiosError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (notification.type === "pushy") {
|
||||||
|
try {
|
||||||
|
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
|
||||||
|
"to": notification.pushyToken,
|
||||||
|
"data": {
|
||||||
|
"message": "Uptime-Kuma"
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"body": msg,
|
||||||
|
"badge": 1,
|
||||||
|
"sound": "ping.aiff"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} else if (notification.type === "slack") {
|
} else if (notification.type === "slack") {
|
||||||
try {
|
try {
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let data = {'text': "Uptime Kuma Slack testing successful.", 'channel': notification.slackchannel, 'username': notification.slackusername, 'icon_emoji': notification.slackiconemo}
|
let data = {
|
||||||
|
"text": "Uptime Kuma Slack testing successful.",
|
||||||
|
"channel": notification.slackchannel,
|
||||||
|
"username": notification.slackusername,
|
||||||
|
"icon_emoji": notification.slackiconemo,
|
||||||
|
}
|
||||||
await axios.post(notification.slackwebhookURL, data)
|
await axios.post(notification.slackwebhookURL, data)
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
|
@ -148,27 +209,26 @@ class Notification {
|
||||||
const time = heartbeatJSON["time"];
|
const time = heartbeatJSON["time"];
|
||||||
let data = {
|
let data = {
|
||||||
"text": "Uptime Kuma Alert",
|
"text": "Uptime Kuma Alert",
|
||||||
"channel":notification.slackchannel,
|
"channel": notification.slackchannel,
|
||||||
"username": notification.slackusername,
|
"username": notification.slackusername,
|
||||||
"icon_emoji": notification.slackiconemo,
|
"icon_emoji": notification.slackiconemo,
|
||||||
"blocks": [{
|
"blocks": [{
|
||||||
"type": "header",
|
"type": "header",
|
||||||
"text": {
|
"text": {
|
||||||
"type": "plain_text",
|
"type": "plain_text",
|
||||||
"text": "Uptime Kuma Alert"
|
"text": "Uptime Kuma Alert",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "section",
|
"type": "section",
|
||||||
"fields": [{
|
"fields": [{
|
||||||
"type": "mrkdwn",
|
"type": "mrkdwn",
|
||||||
"text": '*Message*\n'+msg
|
"text": "*Message*\n" + msg,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "mrkdwn",
|
"type": "mrkdwn",
|
||||||
"text": "*Time (UTC)*\n"+time
|
"text": "*Time (UTC)*\n" + time,
|
||||||
}
|
}],
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "actions",
|
"type": "actions",
|
||||||
|
@ -180,11 +240,10 @@ class Notification {
|
||||||
"text": "Visit Uptime Kuma",
|
"text": "Visit Uptime Kuma",
|
||||||
},
|
},
|
||||||
"value": "Uptime-Kuma",
|
"value": "Uptime-Kuma",
|
||||||
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma"
|
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}],
|
||||||
]
|
|
||||||
}
|
}
|
||||||
await axios.post(notification.slackwebhookURL, data)
|
await axios.post(notification.slackwebhookURL, data)
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
@ -193,26 +252,34 @@ class Notification {
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (notification.type === "pushover") {
|
} else if (notification.type === "pushover") {
|
||||||
var pushoverlink = 'https://api.pushover.net/1/messages.json'
|
let pushoverlink = "https://api.pushover.net/1/messages.json"
|
||||||
try {
|
try {
|
||||||
if (heartbeatJSON == null) {
|
if (heartbeatJSON == null) {
|
||||||
let data = {'message': "<b>Uptime Kuma Pushover testing successful.</b>",
|
|
||||||
'user': notification.pushoveruserkey, 'token': notification.pushoverapptoken, 'sound':notification.pushoversounds,
|
|
||||||
'priority': notification.pushoverpriority, 'title':notification.pushovertitle, 'retry': "30", 'expire':"3600", 'html': 1}
|
|
||||||
await axios.post(pushoverlink, data)
|
|
||||||
return okMsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:"+msg+ '\n<b>Time (UTC)</b>:' +heartbeatJSON["time"],
|
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
|
||||||
"user":notification.pushoveruserkey,
|
"user": notification.pushoveruserkey,
|
||||||
"token": notification.pushoverapptoken,
|
"token": notification.pushoverapptoken,
|
||||||
"sound": notification.pushoversounds,
|
"sound": notification.pushoversounds,
|
||||||
"priority": notification.pushoverpriority,
|
"priority": notification.pushoverpriority,
|
||||||
"title": notification.pushovertitle,
|
"title": notification.pushovertitle,
|
||||||
"retry": "30",
|
"retry": "30",
|
||||||
"expire": "3600",
|
"expire": "3600",
|
||||||
"html": 1
|
"html": 1,
|
||||||
|
}
|
||||||
|
await axios.post(pushoverlink, data)
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
|
||||||
|
"user": notification.pushoveruserkey,
|
||||||
|
"token": notification.pushoverapptoken,
|
||||||
|
"sound": notification.pushoversounds,
|
||||||
|
"priority": notification.pushoverpriority,
|
||||||
|
"title": notification.pushovertitle,
|
||||||
|
"retry": "30",
|
||||||
|
"expire": "3600",
|
||||||
|
"html": 1,
|
||||||
}
|
}
|
||||||
await axios.post(pushoverlink, data)
|
await axios.post(pushoverlink, data)
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
@ -224,6 +291,41 @@ class Notification {
|
||||||
|
|
||||||
return Notification.apprise(notification, msg)
|
return Notification.apprise(notification, msg)
|
||||||
|
|
||||||
|
} else if (notification.type === "lunasea") {
|
||||||
|
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (heartbeatJSON == null) {
|
||||||
|
let testdata = {
|
||||||
|
"title": "Uptime Kuma Alert",
|
||||||
|
"body": "Testing Successful.",
|
||||||
|
}
|
||||||
|
await axios.post(lunaseadevice, testdata)
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON["status"] == 0) {
|
||||||
|
let downdata = {
|
||||||
|
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||||
|
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||||
|
}
|
||||||
|
await axios.post(lunaseadevice, downdata)
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heartbeatJSON["status"] == 1) {
|
||||||
|
let updata = {
|
||||||
|
"title": "UptimeKuma Alert:" + monitorJSON["name"],
|
||||||
|
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
|
||||||
|
}
|
||||||
|
await axios.post(lunaseadevice, updata)
|
||||||
|
return okMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throwGeneralAxiosError(error)
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Notification type is not supported")
|
throw new Error("Notification type is not supported")
|
||||||
}
|
}
|
||||||
|
@ -291,24 +393,23 @@ class Notification {
|
||||||
static async apprise(notification, msg) {
|
static async apprise(notification, msg) {
|
||||||
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
||||||
|
|
||||||
|
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||||
let output = (s.stdout) ? s.stdout.toString() : 'ERROR: maybe apprise not found';
|
|
||||||
|
|
||||||
if (output) {
|
if (output) {
|
||||||
|
|
||||||
if (! output.includes("ERROR")) {
|
if (! output.includes("ERROR")) {
|
||||||
return "Sent Successfully";
|
return "Sent Successfully";
|
||||||
} else {
|
|
||||||
throw new Error(output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error(output)
|
||||||
} else {
|
} else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static checkApprise() {
|
static checkApprise() {
|
||||||
let commandExistsSync = require('command-exists').sync;
|
let commandExistsSync = require("command-exists").sync;
|
||||||
let exists = commandExistsSync('apprise');
|
let exists = commandExistsSync("apprise");
|
||||||
return exists;
|
return exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const passwordHashOld = require('password-hash');
|
const passwordHashOld = require("password-hash");
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require("bcrypt");
|
||||||
const saltRounds = 10;
|
const saltRounds = 10;
|
||||||
|
|
||||||
exports.generate = function (password) {
|
exports.generate = function (password) {
|
||||||
|
@ -9,9 +9,9 @@ exports.generate = function (password) {
|
||||||
exports.verify = function (password, hash) {
|
exports.verify = function (password, hash) {
|
||||||
if (isSHA1(hash)) {
|
if (isSHA1(hash)) {
|
||||||
return passwordHashOld.verify(password, hash)
|
return passwordHashOld.verify(password, hash)
|
||||||
} else {
|
|
||||||
return bcrypt.compareSync(password, hash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return bcrypt.compareSync(password, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSHA1(hash) {
|
function isSHA1(hash) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
|
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
|
||||||
// Fixed on Windows
|
// Fixed on Windows
|
||||||
|
|
||||||
var spawn = require('child_process').spawn,
|
let spawn = require("child_process").spawn,
|
||||||
events = require('events'),
|
events = require("events"),
|
||||||
fs = require('fs'),
|
fs = require("fs"),
|
||||||
WIN = /^win/.test(process.platform),
|
WIN = /^win/.test(process.platform),
|
||||||
LIN = /^linux/.test(process.platform),
|
LIN = /^linux/.test(process.platform),
|
||||||
MAC = /^darwin/.test(process.platform);
|
MAC = /^darwin/.test(process.platform);
|
||||||
|
@ -11,8 +11,9 @@ var spawn = require('child_process').spawn,
|
||||||
module.exports = Ping;
|
module.exports = Ping;
|
||||||
|
|
||||||
function Ping(host, options) {
|
function Ping(host, options) {
|
||||||
if (!host)
|
if (!host) {
|
||||||
throw new Error('You must specify a host to ping!');
|
throw new Error("You must specify a host to ping!");
|
||||||
|
}
|
||||||
|
|
||||||
this._host = host;
|
this._host = host;
|
||||||
this._options = options = (options || {});
|
this._options = options = (options || {});
|
||||||
|
@ -20,26 +21,24 @@ function Ping(host, options) {
|
||||||
events.EventEmitter.call(this);
|
events.EventEmitter.call(this);
|
||||||
|
|
||||||
if (WIN) {
|
if (WIN) {
|
||||||
this._bin = 'c:/windows/system32/ping.exe';
|
this._bin = "c:/windows/system32/ping.exe";
|
||||||
this._args = (options.args) ? options.args : [ '-n', '1', '-w', '5000', host ];
|
this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ];
|
||||||
this._regmatch = /[><=]([0-9.]+?)ms/;
|
this._regmatch = /[><=]([0-9.]+?)ms/;
|
||||||
}
|
} else if (LIN) {
|
||||||
else if (LIN) {
|
this._bin = "/bin/ping";
|
||||||
this._bin = '/bin/ping';
|
this._args = (options.args) ? options.args : [ "-n", "-w", "2", "-c", "1", host ];
|
||||||
this._args = (options.args) ? options.args : [ '-n', '-w', '2', '-c', '1', host ];
|
|
||||||
this._regmatch = /=([0-9.]+?) ms/; // need to verify this
|
this._regmatch = /=([0-9.]+?) ms/; // need to verify this
|
||||||
}
|
} else if (MAC) {
|
||||||
else if (MAC) {
|
this._bin = "/sbin/ping";
|
||||||
this._bin = '/sbin/ping';
|
this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ];
|
||||||
this._args = (options.args) ? options.args : [ '-n', '-t', '2', '-c', '1', host ];
|
|
||||||
this._regmatch = /=([0-9.]+?) ms/;
|
this._regmatch = /=([0-9.]+?) ms/;
|
||||||
}
|
} else {
|
||||||
else {
|
throw new Error("Could not detect your ping binary.");
|
||||||
throw new Error('Could not detect your ping binary.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(this._bin))
|
if (!fs.existsSync(this._bin)) {
|
||||||
throw new Error('Could not detect '+this._bin+' on your system');
|
throw new Error("Could not detect " + this._bin + " on your system");
|
||||||
|
}
|
||||||
|
|
||||||
this._i = 0;
|
this._i = 0;
|
||||||
|
|
||||||
|
@ -51,48 +50,57 @@ Ping.prototype.__proto__ = events.EventEmitter.prototype;
|
||||||
// SEND A PING
|
// SEND A PING
|
||||||
// ===========
|
// ===========
|
||||||
Ping.prototype.send = function(callback) {
|
Ping.prototype.send = function(callback) {
|
||||||
var self = this;
|
let self = this;
|
||||||
callback = callback || function(err, ms) {
|
callback = callback || function(err, ms) {
|
||||||
if (err) return self.emit('error', err);
|
if (err) {
|
||||||
else return self.emit('result', ms);
|
return self.emit("error", err);
|
||||||
|
}
|
||||||
|
return self.emit("result", ms);
|
||||||
};
|
};
|
||||||
|
|
||||||
var _ended, _exited, _errored;
|
let _ended, _exited, _errored;
|
||||||
|
|
||||||
this._ping = spawn(this._bin, this._args); // spawn the binary
|
this._ping = spawn(this._bin, this._args); // spawn the binary
|
||||||
|
|
||||||
this._ping.on('error', function(err) { // handle binary errors
|
this._ping.on("error", function(err) { // handle binary errors
|
||||||
_errored = true;
|
_errored = true;
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._ping.stdout.on('data', function(data) { // log stdout
|
this._ping.stdout.on("data", function(data) { // log stdout
|
||||||
this._stdout = (this._stdout || '') + data;
|
this._stdout = (this._stdout || "") + data;
|
||||||
});
|
});
|
||||||
|
|
||||||
this._ping.stdout.on('end', function() {
|
this._ping.stdout.on("end", function() {
|
||||||
_ended = true;
|
_ended = true;
|
||||||
if (_exited && !_errored) onEnd.call(self._ping);
|
if (_exited && !_errored) {
|
||||||
|
onEnd.call(self._ping);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._ping.stderr.on('data', function(data) { // log stderr
|
this._ping.stderr.on("data", function(data) { // log stderr
|
||||||
this._stderr = (this._stderr || '') + data;
|
this._stderr = (this._stderr || "") + data;
|
||||||
});
|
});
|
||||||
|
|
||||||
this._ping.on('exit', function(code) { // handle complete
|
this._ping.on("exit", function(code) { // handle complete
|
||||||
_exited = true;
|
_exited = true;
|
||||||
if (_ended && !_errored) onEnd.call(self._ping);
|
if (_ended && !_errored) {
|
||||||
|
onEnd.call(self._ping);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function onEnd() {
|
function onEnd() {
|
||||||
var stdout = this.stdout._stdout,
|
let stdout = this.stdout._stdout,
|
||||||
stderr = this.stderr._stderr,
|
stderr = this.stderr._stderr,
|
||||||
ms;
|
ms;
|
||||||
|
|
||||||
if (stderr)
|
if (stderr) {
|
||||||
return callback(new Error(stderr));
|
return callback(new Error(stderr));
|
||||||
else if (!stdout)
|
}
|
||||||
return callback(new Error('No stdout detected'));
|
|
||||||
|
if (!stdout) {
|
||||||
|
return callback(new Error("No stdout detected"));
|
||||||
|
}
|
||||||
|
|
||||||
ms = stdout.match(self._regmatch); // parse out the ##ms response
|
ms = stdout.match(self._regmatch); // parse out the ##ms response
|
||||||
ms = (ms && ms[1]) ? Number(ms[1]) : ms;
|
ms = (ms && ms[1]) ? Number(ms[1]) : ms;
|
||||||
|
@ -104,7 +112,7 @@ Ping.prototype.send = function(callback) {
|
||||||
// CALL Ping#send(callback) ON A TIMER
|
// CALL Ping#send(callback) ON A TIMER
|
||||||
// ===================================
|
// ===================================
|
||||||
Ping.prototype.start = function(callback) {
|
Ping.prototype.start = function(callback) {
|
||||||
var self = this;
|
let self = this;
|
||||||
this._i = setInterval(function() {
|
this._i = setInterval(function() {
|
||||||
self.send(callback);
|
self.send(callback);
|
||||||
}, (self._options.interval || 5000));
|
}, (self._options.interval || 5000));
|
||||||
|
|
59
server/prometheus.js
Normal file
59
server/prometheus.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
const PrometheusClient = require('prom-client');
|
||||||
|
|
||||||
|
const commonLabels = [
|
||||||
|
'monitor_name',
|
||||||
|
'monitor_type',
|
||||||
|
'monitor_url',
|
||||||
|
'monitor_hostname',
|
||||||
|
'monitor_port',
|
||||||
|
]
|
||||||
|
|
||||||
|
const monitor_response_time = new PrometheusClient.Gauge({
|
||||||
|
name: 'monitor_response_time',
|
||||||
|
help: 'Monitor Response Time (ms)',
|
||||||
|
labelNames: commonLabels
|
||||||
|
});
|
||||||
|
|
||||||
|
const monitor_status = new PrometheusClient.Gauge({
|
||||||
|
name: 'monitor_status',
|
||||||
|
help: 'Monitor Status (1 = UP, 0= DOWN)',
|
||||||
|
labelNames: commonLabels
|
||||||
|
});
|
||||||
|
|
||||||
|
class Prometheus {
|
||||||
|
monitorLabelValues = {}
|
||||||
|
|
||||||
|
constructor(monitor) {
|
||||||
|
this.monitorLabelValues = {
|
||||||
|
monitor_name: monitor.name,
|
||||||
|
monitor_type: monitor.type,
|
||||||
|
monitor_url: monitor.url,
|
||||||
|
monitor_hostname: monitor.hostname,
|
||||||
|
monitor_port: monitor.port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(heartbeat) {
|
||||||
|
try {
|
||||||
|
monitor_status.set(this.monitorLabelValues, heartbeat.status)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof heartbeat.ping === 'number') {
|
||||||
|
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping)
|
||||||
|
} else {
|
||||||
|
// Is it good?
|
||||||
|
monitor_response_time.set(this.monitorLabelValues, -1)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Prometheus
|
||||||
|
}
|
275
server/server.js
275
server/server.js
|
@ -1,24 +1,47 @@
|
||||||
console.log("Welcome to Uptime Kuma ")
|
console.log("Welcome to Uptime Kuma");
|
||||||
console.log("Importing libraries")
|
console.log("Node Env: " + process.env.NODE_ENV);
|
||||||
const express = require('express');
|
|
||||||
const http = require('http');
|
|
||||||
const { Server } = require("socket.io");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const {R} = require("redbean-node");
|
|
||||||
const passwordHash = require('./password-hash');
|
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const Monitor = require("./model/monitor");
|
|
||||||
const fs = require("fs");
|
|
||||||
const {getSettings} = require("./util-server");
|
|
||||||
const {Notification} = require("./notification")
|
|
||||||
const gracefulShutdown = require('http-graceful-shutdown');
|
|
||||||
const Database = require("./database");
|
|
||||||
const {sleep} = require("./util");
|
|
||||||
const args = require('args-parser')(process.argv);
|
|
||||||
|
|
||||||
const version = require('../package.json').version;
|
const { sleep, debug } = require("../src/util");
|
||||||
const hostname = args.host || "0.0.0.0"
|
|
||||||
const port = args.port || 3001
|
console.log("Importing Node libraries")
|
||||||
|
const fs = require("fs");
|
||||||
|
const http = require("http");
|
||||||
|
|
||||||
|
console.log("Importing 3rd-party libraries")
|
||||||
|
debug("Importing express");
|
||||||
|
const express = require("express");
|
||||||
|
debug("Importing socket.io");
|
||||||
|
const { Server } = require("socket.io");
|
||||||
|
debug("Importing dayjs");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
debug("Importing redbean-node");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
debug("Importing jsonwebtoken");
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
debug("Importing http-graceful-shutdown");
|
||||||
|
const gracefulShutdown = require("http-graceful-shutdown");
|
||||||
|
debug("Importing prometheus-api-metrics");
|
||||||
|
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
||||||
|
|
||||||
|
console.log("Importing this project modules");
|
||||||
|
debug("Importing Monitor");
|
||||||
|
const Monitor = require("./model/monitor");
|
||||||
|
debug("Importing Settings");
|
||||||
|
const { getSettings, setSettings, setting } = require("./util-server");
|
||||||
|
debug("Importing Notification");
|
||||||
|
const { Notification } = require("./notification");
|
||||||
|
debug("Importing Database");
|
||||||
|
const Database = require("./database");
|
||||||
|
|
||||||
|
const { basicAuth } = require("./auth");
|
||||||
|
const { login } = require("./auth");
|
||||||
|
const passwordHash = require("./password-hash");
|
||||||
|
|
||||||
|
const args = require("args-parser")(process.argv);
|
||||||
|
|
||||||
|
const version = require("../package.json").version;
|
||||||
|
const hostname = process.env.HOST || args.host || "0.0.0.0"
|
||||||
|
const port = parseInt(process.env.PORT || args.port || 3001);
|
||||||
|
|
||||||
console.info("Version: " + version)
|
console.info("Version: " + version)
|
||||||
|
|
||||||
|
@ -52,19 +75,34 @@ let monitorList = {};
|
||||||
*/
|
*/
|
||||||
let needSetup = false;
|
let needSetup = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache Index HTML
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await initDatabase();
|
await initDatabase();
|
||||||
|
|
||||||
console.log("Adding route")
|
console.log("Adding route")
|
||||||
app.use('/', express.static("dist"));
|
|
||||||
|
|
||||||
app.get('*', function(request, response, next) {
|
// Normal Router here
|
||||||
response.sendFile(process.cwd() + '/dist/index.html');
|
|
||||||
|
app.use("/", express.static("dist"));
|
||||||
|
|
||||||
|
// Basic Auth Router here
|
||||||
|
|
||||||
|
// Prometheus API metrics /metrics
|
||||||
|
// With Basic Auth using the first user's username/password
|
||||||
|
app.get("/metrics", basicAuth, prometheusAPIMetrics())
|
||||||
|
|
||||||
|
// Universal Route Handler, must be at the end
|
||||||
|
app.get("*", function(_request, response) {
|
||||||
|
response.send(indexHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
console.log("Adding socket handler")
|
console.log("Adding socket handler")
|
||||||
io.on('connection', async (socket) => {
|
io.on("connection", async (socket) => {
|
||||||
|
|
||||||
socket.emit("info", {
|
socket.emit("info", {
|
||||||
version,
|
version,
|
||||||
|
@ -77,11 +115,13 @@ let needSetup = false;
|
||||||
socket.emit("setup")
|
socket.emit("setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on("disconnect", () => {
|
||||||
totalClient--;
|
totalClient--;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ***************************
|
||||||
// Public API
|
// Public API
|
||||||
|
// ***************************
|
||||||
|
|
||||||
socket.on("loginByToken", async (token, callback) => {
|
socket.on("loginByToken", async (token, callback) => {
|
||||||
|
|
||||||
|
@ -91,25 +131,29 @@ let needSetup = false;
|
||||||
console.log("Username from JWT: " + decoded.username)
|
console.log("Username from JWT: " + decoded.username)
|
||||||
|
|
||||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
||||||
decoded.username
|
decoded.username,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
debug("afterLogin")
|
||||||
|
|
||||||
await afterLogin(socket, user)
|
await afterLogin(socket, user)
|
||||||
|
|
||||||
|
debug("afterLogin ok")
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "The user is inactive or deleted."
|
msg: "The user is inactive or deleted.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Invalid token."
|
msg: "Invalid token.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,32 +162,21 @@ let needSetup = false;
|
||||||
socket.on("login", async (data, callback) => {
|
socket.on("login", async (data, callback) => {
|
||||||
console.log("Login")
|
console.log("Login")
|
||||||
|
|
||||||
let user = await R.findOne("user", " username = ? AND active = 1 ", [
|
let user = await login(data.username, data.password)
|
||||||
data.username
|
|
||||||
])
|
|
||||||
|
|
||||||
if (user && passwordHash.verify(data.password, user.password)) {
|
|
||||||
|
|
||||||
// Upgrade the hash to bcrypt
|
|
||||||
if (passwordHash.needRehash(user.password)) {
|
|
||||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
|
||||||
passwordHash.generate(data.password),
|
|
||||||
user.id
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (user) {
|
||||||
await afterLogin(socket, user)
|
await afterLogin(socket, user)
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
token: jwt.sign({
|
token: jwt.sign({
|
||||||
username: data.username
|
username: data.username,
|
||||||
}, jwtSecret)
|
}, jwtSecret),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "Incorrect username or password."
|
msg: "Incorrect username or password.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,19 +207,22 @@ let needSetup = false;
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Added Successfully."
|
msg: "Added Successfully.",
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ***************************
|
||||||
// Auth Only API
|
// Auth Only API
|
||||||
|
// ***************************
|
||||||
|
|
||||||
|
// Add a new monitor
|
||||||
socket.on("add", async (monitor, callback) => {
|
socket.on("add", async (monitor, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
|
@ -195,6 +231,9 @@ let needSetup = false;
|
||||||
let notificationIDList = monitor.notificationIDList;
|
let notificationIDList = monitor.notificationIDList;
|
||||||
delete monitor.notificationIDList;
|
delete monitor.notificationIDList;
|
||||||
|
|
||||||
|
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||||
|
delete monitor.accepted_statuscodes;
|
||||||
|
|
||||||
bean.import(monitor)
|
bean.import(monitor)
|
||||||
bean.user_id = socket.userID
|
bean.user_id = socket.userID
|
||||||
await R.store(bean)
|
await R.store(bean)
|
||||||
|
@ -207,17 +246,18 @@ let needSetup = false;
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Added Successfully.",
|
msg: "Added Successfully.",
|
||||||
monitorID: bean.id
|
monitorID: bean.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Edit a monitor
|
||||||
socket.on("editMonitor", async (monitor, callback) => {
|
socket.on("editMonitor", async (monitor, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
|
@ -236,6 +276,9 @@ let needSetup = false;
|
||||||
bean.maxretries = monitor.maxretries;
|
bean.maxretries = monitor.maxretries;
|
||||||
bean.port = monitor.port;
|
bean.port = monitor.port;
|
||||||
bean.keyword = monitor.keyword;
|
bean.keyword = monitor.keyword;
|
||||||
|
bean.ignoreTls = monitor.ignoreTls;
|
||||||
|
bean.upsideDown = monitor.upsideDown;
|
||||||
|
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||||
|
|
||||||
await R.store(bean)
|
await R.store(bean)
|
||||||
|
|
||||||
|
@ -250,14 +293,14 @@ let needSetup = false;
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Saved.",
|
msg: "Saved.",
|
||||||
monitorID: bean.id
|
monitorID: bean.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -281,7 +324,7 @@ let needSetup = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -295,13 +338,13 @@ let needSetup = false;
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Resumed Successfully."
|
msg: "Resumed Successfully.",
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -314,14 +357,13 @@ let needSetup = false;
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Paused Successfully."
|
msg: "Paused Successfully.",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -339,12 +381,12 @@ let needSetup = false;
|
||||||
|
|
||||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||||
monitorID,
|
monitorID,
|
||||||
socket.userID
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Deleted Successfully."
|
msg: "Deleted Successfully.",
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendMonitorList(socket);
|
await sendMonitorList(socket);
|
||||||
|
@ -352,7 +394,7 @@ let needSetup = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -366,19 +408,19 @@ let needSetup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
let user = await R.findOne("user", " id = ? AND active = 1 ", [
|
||||||
socket.userID
|
socket.userID,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (user && passwordHash.verify(password.currentPassword, user.password)) {
|
if (user && passwordHash.verify(password.currentPassword, user.password)) {
|
||||||
|
|
||||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||||
passwordHash.generate(password.newPassword),
|
passwordHash.generate(password.newPassword),
|
||||||
socket.userID
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Password has been updated successfully."
|
msg: "Password has been updated successfully.",
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Incorrect current password")
|
throw new Error("Incorrect current password")
|
||||||
|
@ -387,25 +429,43 @@ let needSetup = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getSettings", async (type, callback) => {
|
socket.on("getSettings", async (callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
|
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
data: await getSettings(type),
|
data: await getSettings("general"),
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("setSettings", async (data, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
await setSettings("general", data)
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -426,7 +486,7 @@ let needSetup = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -446,7 +506,7 @@ let needSetup = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -459,7 +519,7 @@ let needSetup = false;
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg
|
msg,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -467,7 +527,7 @@ let needSetup = false;
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message
|
msg: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -480,6 +540,22 @@ let needSetup = false;
|
||||||
callback(false);
|
callback(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debug("added all socket handlers")
|
||||||
|
|
||||||
|
// ***************************
|
||||||
|
// Better do anything after added all socket handlers here
|
||||||
|
// ***************************
|
||||||
|
|
||||||
|
debug("check auto login")
|
||||||
|
if (await setting("disableAuth")) {
|
||||||
|
console.log("Disabled Auth: auto login to admin")
|
||||||
|
await afterLogin(socket, await R.findOne("user"))
|
||||||
|
socket.emit("autoLogin")
|
||||||
|
} else {
|
||||||
|
debug("need auth")
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Init")
|
console.log("Init")
|
||||||
|
@ -492,7 +568,7 @@ let needSetup = false;
|
||||||
|
|
||||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||||
R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
||||||
monitorID
|
monitorID,
|
||||||
])
|
])
|
||||||
|
|
||||||
for (let notificationID in notificationIDList) {
|
for (let notificationID in notificationIDList) {
|
||||||
|
@ -525,7 +601,7 @@ async function sendMonitorList(socket) {
|
||||||
async function sendNotificationList(socket) {
|
async function sendNotificationList(socket) {
|
||||||
let result = [];
|
let result = [];
|
||||||
let list = await R.find("notification", " user_id = ? ", [
|
let list = await R.find("notification", " user_id = ? ", [
|
||||||
socket.userID
|
socket.userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for (let bean of list) {
|
for (let bean of list) {
|
||||||
|
@ -555,7 +631,7 @@ async function getMonitorJSONList(userID) {
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|
||||||
let monitorList = await R.find("monitor", " user_id = ? ", [
|
let monitorList = await R.find("monitor", " user_id = ? ", [
|
||||||
userID
|
userID,
|
||||||
])
|
])
|
||||||
|
|
||||||
for (let monitor of monitorList) {
|
for (let monitor of monitorList) {
|
||||||
|
@ -578,9 +654,7 @@ async function initDatabase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Connecting to Database")
|
console.log("Connecting to Database")
|
||||||
R.setup('sqlite', {
|
Database.connect();
|
||||||
filename: Database.path
|
|
||||||
});
|
|
||||||
console.log("Connected")
|
console.log("Connected")
|
||||||
|
|
||||||
// Patch the database
|
// Patch the database
|
||||||
|
@ -591,7 +665,7 @@ async function initDatabase() {
|
||||||
await R.autoloadModels("./server/model");
|
await R.autoloadModels("./server/model");
|
||||||
|
|
||||||
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
|
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
|
||||||
"jwtSecret"
|
"jwtSecret",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (! jwtSecretBean) {
|
if (! jwtSecretBean) {
|
||||||
|
@ -622,11 +696,11 @@ async function startMonitor(userID, monitorID) {
|
||||||
|
|
||||||
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [
|
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [
|
||||||
monitorID,
|
monitorID,
|
||||||
userID
|
userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let monitor = await R.findOne("monitor", " id = ? ", [
|
let monitor = await R.findOne("monitor", " id = ? ", [
|
||||||
monitorID
|
monitorID,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (monitor.id in monitorList) {
|
if (monitor.id in monitorList) {
|
||||||
|
@ -648,7 +722,7 @@ async function pauseMonitor(userID, monitorID) {
|
||||||
|
|
||||||
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [
|
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [
|
||||||
monitorID,
|
monitorID,
|
||||||
userID
|
userID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (monitorID in monitorList) {
|
if (monitorID in monitorList) {
|
||||||
|
@ -677,7 +751,7 @@ async function sendHeartbeatList(socket, monitorID) {
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
LIMIT 100
|
LIMIT 100
|
||||||
`, [
|
`, [
|
||||||
monitorID
|
monitorID,
|
||||||
])
|
])
|
||||||
|
|
||||||
let result = [];
|
let result = [];
|
||||||
|
@ -696,37 +770,15 @@ async function sendImportantHeartbeatList(socket, monitorID) {
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
`, [
|
`, [
|
||||||
monitorID
|
monitorID,
|
||||||
])
|
])
|
||||||
|
|
||||||
socket.emit("importantHeartbeatList", monitorID, list)
|
socket.emit("importantHeartbeatList", monitorID, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const startGracefulShutdown = async () => {
|
|
||||||
console.log('Shutdown requested');
|
|
||||||
|
|
||||||
|
|
||||||
await (new Promise((resolve) => {
|
|
||||||
server.close(async function () {
|
|
||||||
console.log('Stopped Express.');
|
|
||||||
process.exit(0)
|
|
||||||
setTimeout(async () =>{
|
|
||||||
await R.close();
|
|
||||||
console.log("Stopped DB")
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async function shutdownFunction(signal) {
|
async function shutdownFunction(signal) {
|
||||||
console.log('Called signal: ' + signal);
|
console.log("Shutdown requested");
|
||||||
|
console.log("Called signal: " + signal);
|
||||||
|
|
||||||
console.log("Stopping all monitors")
|
console.log("Stopping all monitors")
|
||||||
for (let id in monitorList) {
|
for (let id in monitorList) {
|
||||||
|
@ -735,17 +787,18 @@ async function shutdownFunction(signal) {
|
||||||
}
|
}
|
||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
await Database.close();
|
await Database.close();
|
||||||
|
console.log("Stopped DB")
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalFunction() {
|
function finalFunction() {
|
||||||
console.log('Graceful Shutdown')
|
console.log("Graceful Shutdown Done")
|
||||||
}
|
}
|
||||||
|
|
||||||
gracefulShutdown(server, {
|
gracefulShutdown(server, {
|
||||||
signals: 'SIGINT SIGTERM',
|
signals: "SIGINT SIGTERM",
|
||||||
timeout: 30000, // timeout: 30 secs
|
timeout: 30000, // timeout: 30 secs
|
||||||
development: false, // not in dev mode
|
development: false, // not in dev mode
|
||||||
forceExit: true, // triggers process.exit() at the end of shutdown process
|
forceExit: true, // triggers process.exit() at the end of shutdown process
|
||||||
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
|
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
|
||||||
finally: finalFunction // finally function (sync) - e.g. for logging
|
finally: finalFunction, // finally function (sync) - e.g. for logging
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const tcpp = require('tcp-ping');
|
const tcpp = require("tcp-ping");
|
||||||
const Ping = require("./ping-lite");
|
const Ping = require("./ping-lite");
|
||||||
const {R} = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
|
const { debug } = require("../src/util");
|
||||||
|
|
||||||
exports.tcping = function (hostname, port) {
|
exports.tcping = function (hostname, port) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -8,7 +9,7 @@ exports.tcping = function (hostname, port) {
|
||||||
address: hostname,
|
address: hostname,
|
||||||
port: port,
|
port: port,
|
||||||
attempts: 1,
|
attempts: 1,
|
||||||
}, function(err, data) {
|
}, function (err, data) {
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
|
@ -27,7 +28,7 @@ exports.ping = function (hostname) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const ping = new Ping(hostname);
|
const ping = new Ping(hostname);
|
||||||
|
|
||||||
ping.send(function(err, ms) {
|
ping.send(function (err, ms) {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
} else if (ms === null) {
|
} else if (ms === null) {
|
||||||
|
@ -40,37 +41,73 @@ exports.ping = function (hostname) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.setting = async function (key) {
|
exports.setting = async function (key) {
|
||||||
return await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
|
||||||
key
|
key,
|
||||||
])
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const v = JSON.parse(value);
|
||||||
|
debug(`Get Setting: ${key}: ${v}`)
|
||||||
|
return v;
|
||||||
|
} catch (e) {
|
||||||
|
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 = 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 * FROM setting WHERE `type` = ? ", [
|
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||||
type
|
type,
|
||||||
])
|
])
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|
||||||
for (let row of list) {
|
for (let row of list) {
|
||||||
|
try {
|
||||||
|
result[row.key] = JSON.parse(row.value);
|
||||||
|
} catch (e) {
|
||||||
result[row.key] = row.value;
|
result[row.key] = row.value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.setSettings = async function (type, data) {
|
||||||
|
let keyList = Object.keys(data);
|
||||||
|
|
||||||
|
let promiseList = [];
|
||||||
|
|
||||||
|
for (let key of keyList) {
|
||||||
|
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||||
|
key
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (bean == null) {
|
||||||
|
bean = R.dispense("setting");
|
||||||
|
bean.type = type;
|
||||||
|
bean.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bean.type === type) {
|
||||||
|
bean.value = JSON.stringify(data[key]);
|
||||||
|
promiseList.push(R.store(bean))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promiseList);
|
||||||
|
}
|
||||||
|
|
||||||
// ssl-checker by @dyaa
|
// ssl-checker by @dyaa
|
||||||
// param: res - response object from axios
|
// param: res - response object from axios
|
||||||
|
@ -97,7 +134,9 @@ exports.checkCertificate = function (res) {
|
||||||
} = res.request.res.socket.getPeerCertificate(false);
|
} = res.request.res.socket.getPeerCertificate(false);
|
||||||
|
|
||||||
if (!valid_from || !valid_to || !subjectaltname) {
|
if (!valid_from || !valid_to || !subjectaltname) {
|
||||||
throw { message: 'No TLS certificate in response' };
|
throw {
|
||||||
|
message: "No TLS certificate in response",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = res.request.res.socket.authorized || false;
|
const valid = res.request.res.socket.authorized || false;
|
||||||
|
@ -119,3 +158,32 @@ exports.checkCertificate = function (res) {
|
||||||
fingerprint,
|
fingerprint,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the provided status code is within the accepted ranges
|
||||||
|
// Param: status - the status code to check
|
||||||
|
// Param: accepted_codes - an array of accepted status codes
|
||||||
|
// Return: true if the status code is within the accepted ranges, false otherwise
|
||||||
|
// Will throw an error if the provided status code is not a valid range string or code string
|
||||||
|
|
||||||
|
exports.checkStatusCode = function (status, accepted_codes) {
|
||||||
|
if (accepted_codes == null || accepted_codes.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const code_range of accepted_codes) {
|
||||||
|
const code_range_split = code_range.split("-").map(string => parseInt(string));
|
||||||
|
if (code_range_split.length === 1) {
|
||||||
|
if (status === code_range_split[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (code_range_split.length === 2) {
|
||||||
|
if (status >= code_range_split[0] && status <= code_range_split[1]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid status code range");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
// Common JS cannot be used in frontend sadly
|
|
||||||
// sleep, ucfirst is duplicated in ../src/util-frontend.js
|
|
||||||
|
|
||||||
exports.DOWN = 0;
|
|
||||||
exports.UP = 1;
|
|
||||||
exports.PENDING = 2;
|
|
||||||
|
|
||||||
exports.sleep = function (ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.ucfirst = function (str) {
|
|
||||||
if (! str) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstLetter = str.substr(0, 1);
|
|
||||||
return firstLetter.toUpperCase() + str.substr(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.debug = (msg) => {
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
console.log(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,11 +3,5 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {}
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -5,8 +5,46 @@
|
||||||
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji;
|
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 31px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #CCC;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
box-shadow: 0 15px 70px rgb(0 0 0);
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.VuePagination__count {
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.shadow-box {
|
.shadow-box {
|
||||||
overflow: hidden;
|
//overflow: hidden; // Forget why add this, but multiple select hide by this
|
||||||
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
|
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
@ -29,10 +67,115 @@
|
||||||
background-color: $highlight;
|
background-color: $highlight;
|
||||||
border-color: $highlight;
|
border-color: $highlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
border-radius: 1rem;
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
// Dark Theme override here
|
||||||
|
.dark {
|
||||||
|
background-color: #090C10;
|
||||||
|
color: $dark-font-color;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-box {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-input {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switch .form-check-input {
|
||||||
|
background-color: #131a21;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.table,
|
||||||
|
.nav-link {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control,
|
||||||
|
.form-control:focus,
|
||||||
|
.form-select,
|
||||||
|
.form-select:focus {
|
||||||
|
color: $dark-font-color;
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control, .form-select {
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover > tbody > tr:hover {
|
||||||
|
--bs-table-accent-bg: #070A10;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
border-color: $dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-color: $dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
.page-item.disabled .page-link {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiselect
|
||||||
|
.multiselect__tags {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__input, .multiselect__single {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__content-wrapper {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect--above .multiselect__content-wrapper {
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--selected {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,3 +6,9 @@ $border-radius: 50rem;
|
||||||
|
|
||||||
$highlight: #7ce8a4;
|
$highlight: #7ce8a4;
|
||||||
$highlight-white: #e7faec;
|
$highlight-white: #e7faec;
|
||||||
|
|
||||||
|
$dark-font-color: #b1b8c0;
|
||||||
|
$dark-font-color2: #020b05;
|
||||||
|
$dark-bg: #0D1117;
|
||||||
|
$dark-bg2: #070A10;
|
||||||
|
$dark-border-color: #1d2634;
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="modal fade" tabindex="-1" ref="modal">
|
<div ref="modal" class="modal fade" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="exampleModalLabel">Confirm</h5>
|
<h5 id="exampleModalLabel" class="modal-title">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
Confirm
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn" :class="btnStyle" @click="yes" data-bs-dismiss="modal">Yes</button>
|
<button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button>
|
{{ yesText }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
{{ noText }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,17 +25,25 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Modal } from 'bootstrap'
|
import { Modal } from "bootstrap"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
btnStyle: {
|
btnStyle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "btn-primary"
|
default: "btn-primary",
|
||||||
}
|
},
|
||||||
|
yesText: {
|
||||||
|
type: String,
|
||||||
|
default: "Yes",
|
||||||
|
},
|
||||||
|
noText: {
|
||||||
|
type: String,
|
||||||
|
default: "No",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
modal: null
|
modal: null,
|
||||||
}),
|
}),
|
||||||
mounted() {
|
mounted() {
|
||||||
this.modal = new Modal(this.$refs.modal)
|
this.modal = new Modal(this.$refs.modal)
|
||||||
|
@ -39,12 +53,8 @@ export default {
|
||||||
this.modal.show()
|
this.modal.show()
|
||||||
},
|
},
|
||||||
yes() {
|
yes() {
|
||||||
this.$emit('yes');
|
this.$emit("yes");
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -3,26 +3,22 @@
|
||||||
<span v-else>{{ value }}</span>
|
<span v-else>{{ value }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
|
|
||||||
import {sleep} from '../util-frontend'
|
import { sleep } from "../util.ts"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: [String, Number],
|
value: [String, Number],
|
||||||
time: {
|
time: {
|
||||||
Number,
|
type: Number,
|
||||||
default: 0.3,
|
default: 0.3,
|
||||||
},
|
},
|
||||||
unit: {
|
unit: {
|
||||||
String,
|
type: String,
|
||||||
default: "ms",
|
default: "ms",
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.output = this.value;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -32,14 +28,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isNum() {
|
isNum() {
|
||||||
return typeof this.value === 'number'
|
return typeof this.value === "number"
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -61,9 +53,11 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.output = this.value;
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {},
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime"
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from "dayjs/plugin/utc"
|
||||||
import timezone from 'dayjs/plugin/timezone' // dependent on utc plugin
|
import timezone from "dayjs/plugin/timezone" // dependent on utc plugin
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
@ -28,14 +28,10 @@ export default {
|
||||||
format = "YYYY-MM-DD";
|
format = "YYYY-MM-DD";
|
||||||
}
|
}
|
||||||
return dayjs.utc(this.value).tz(this.$root.timezone).format(format);
|
return dayjs.utc(this.value).tz(this.$root.timezone).format(format);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="wrap" :style="wrapStyle" ref="wrap">
|
<div ref="wrap" class="wrap" :style="wrapStyle">
|
||||||
<div class="hp-bar-big" :style="barStyle">
|
<div class="hp-bar-big" :style="barStyle">
|
||||||
<div
|
<div
|
||||||
|
v-for="(beat, index) in shortBeatList"
|
||||||
|
:key="index"
|
||||||
class="beat"
|
class="beat"
|
||||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
||||||
:style="beatStyle"
|
:style="beatStyle"
|
||||||
v-for="(beat, index) in shortBeatList"
|
:title="beat.msg"
|
||||||
:key="index"
|
/>
|
||||||
:title="beat.msg">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "big"
|
default: "big",
|
||||||
},
|
},
|
||||||
monitorId: Number
|
monitorId: Number,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -34,26 +33,6 @@ export default {
|
||||||
maxBeat: -1,
|
maxBeat: -1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
unmounted() {
|
|
||||||
window.removeEventListener("resize", this.resize);
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.size === "small") {
|
|
||||||
this.beatWidth = 5.6;
|
|
||||||
this.beatMargin = 2.4;
|
|
||||||
this.beatHeight = 16
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("resize", this.resize);
|
|
||||||
this.resize();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
resize() {
|
|
||||||
if (this.$refs.wrap) {
|
|
||||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
beatList() {
|
beatList() {
|
||||||
|
@ -80,8 +59,6 @@ export default {
|
||||||
start = 0;
|
start = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return placeholders.concat(this.beatList.slice(start))
|
return placeholders.concat(this.beatList.slice(start))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -89,16 +66,9 @@ export default {
|
||||||
let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2);
|
let topBottom = (((this.beatHeight * this.hoverScale) - this.beatHeight) / 2);
|
||||||
let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2);
|
let leftRight = (((this.beatWidth * this.hoverScale) - this.beatWidth) / 2);
|
||||||
|
|
||||||
let width
|
|
||||||
if (this.maxBeat > 0) {
|
|
||||||
width = (this.beatWidth + this.beatMargin * 2) * this.maxBeat + (leftRight * 2) + "px"
|
|
||||||
} {
|
|
||||||
width = "100%"
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
padding: `${topBottom}px ${leftRight}px`,
|
padding: `${topBottom}px ${leftRight}px`,
|
||||||
width: width
|
width: "100%",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -111,11 +81,11 @@ export default {
|
||||||
transform: `translateX(${width}px)`,
|
transform: `translateX(${width}px)`,
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
}
|
||||||
return {
|
return {
|
||||||
transform: `translateX(0)`,
|
transform: "translateX(0)",
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
beatStyle() {
|
beatStyle() {
|
||||||
|
@ -125,7 +95,7 @@ export default {
|
||||||
margin: this.beatMargin + "px",
|
margin: this.beatMargin + "px",
|
||||||
"--hover-scale": this.hoverScale,
|
"--hover-scale": this.hoverScale,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -138,12 +108,32 @@ export default {
|
||||||
}, 300)
|
}, 300)
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
window.removeEventListener("resize", this.resize);
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.size === "small") {
|
||||||
|
this.beatWidth = 5.6;
|
||||||
|
this.beatMargin = 2.4;
|
||||||
|
this.beatHeight = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", this.resize);
|
||||||
|
this.resize();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resize() {
|
||||||
|
if (this.$refs.wrap) {
|
||||||
|
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
|
@ -160,6 +150,10 @@ export default {
|
||||||
|
|
||||||
&.empty {
|
&.empty {
|
||||||
background-color: aliceblue;
|
background-color: aliceblue;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
background-color: #d0d3d5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.down {
|
&.down {
|
||||||
|
@ -178,4 +172,10 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.hp-bar-big .beat.empty{
|
||||||
|
background-color: #848484;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,31 +2,32 @@
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
|
<h1 class="h3 mb-3 fw-normal" />
|
||||||
<h1 class="h3 mb-3 fw-normal"></h1>
|
|
||||||
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username">
|
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
|
||||||
<label for="floatingInput">Username</label>
|
<label for="floatingInput">Username</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mt-3">
|
<div class="form-floating mt-3">
|
||||||
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="password">
|
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
|
||||||
<label for="floatingPassword">Password</label>
|
<label for="floatingPassword">Password</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 type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="$root.remember">
|
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
|
||||||
|
|
||||||
<label class="form-check-label" for="remember">
|
<label class="form-check-label" for="remember">
|
||||||
Remember me
|
Remember me
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">Login</button>
|
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="alert alert-danger mt-3" role="alert" v-if="res && !res.ok">
|
<div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">
|
||||||
{{ res.msg }}
|
{{ res.msg }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -52,8 +53,8 @@ export default {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.res = res;
|
this.res = res;
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||||
<div class="modal fade" tabindex="-1" ref="modal" data-bs-backdrop="static">
|
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="exampleModalLabel">Setup Notification</h5>
|
<h5 id="exampleModalLabel" class="modal-title">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
Setup Notification
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="type" class="form-label">Notification Type</label>
|
<label for="type" class="form-label">Notification Type</label>
|
||||||
<select class="form-select" id="type" v-model="notification.type">
|
<select id="type" v-model="notification.type" class="form-select">
|
||||||
<option value="telegram">Telegram</option>
|
<option value="telegram">Telegram</option>
|
||||||
<option value="webhook">Webhook</option>
|
<option value="webhook">Webhook</option>
|
||||||
<option value="smtp">Email (SMTP)</option>
|
<option value="smtp">Email (SMTP)</option>
|
||||||
|
@ -21,28 +21,34 @@
|
||||||
<option value="gotify">Gotify</option>
|
<option value="gotify">Gotify</option>
|
||||||
<option value="slack">Slack</option>
|
<option value="slack">Slack</option>
|
||||||
<option value="pushover">Pushover</option>
|
<option value="pushover">Pushover</option>
|
||||||
|
<option value="pushy">Pushy</option>
|
||||||
|
<option value="lunasea">LunaSea</option>
|
||||||
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Friendly Name</label>
|
<label for="name" class="form-label">Friendly Name</label>
|
||||||
<input type="text" class="form-control" id="name" required v-model="notification.name">
|
<input id="name" v-model="notification.name" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="notification.type === 'telegram'">
|
<template v-if="notification.type === 'telegram'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
<label for="telegram-bot-token" class="form-label">Bot Token</label>
|
||||||
<input type="text" class="form-control" id="telegram-bot-token" required v-model="notification.telegramBotToken">
|
<input id="telegram-bot-token" v-model="notification.telegramBotToken" type="text" class="form-control" required>
|
||||||
<div class="form-text">You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.</div>
|
<div class="form-text">
|
||||||
|
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
||||||
|
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID">
|
<input id="telegram-chat-id" v-model="notification.telegramChatID" type="text" class="form-control" required>
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">Auto Get</button>
|
<button v-if="notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
|
||||||
|
Auto Get
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
|
@ -53,7 +59,6 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin-top: 8px;">
|
<p style="margin-top: 8px;">
|
||||||
|
|
||||||
<template v-if="notification.telegramBotToken">
|
<template v-if="notification.telegramBotToken">
|
||||||
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
||||||
</template>
|
</template>
|
||||||
|
@ -69,15 +74,18 @@
|
||||||
<template v-if="notification.type === 'webhook'">
|
<template v-if="notification.type === 'webhook'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="webhook-url" class="form-label">Post URL</label>
|
<label for="webhook-url" class="form-label">Post URL</label>
|
||||||
<input type="url" pattern="https?://.+" class="form-control" id="webhook-url" required v-model="notification.webhookURL">
|
<input id="webhook-url" v-model="notification.webhookURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="webhook-content-type" class="form-label">Content Type</label>
|
<label for="webhook-content-type" class="form-label">Content Type</label>
|
||||||
<select class="form-select" id="webhook-content-type" v-model="notification.webhookContentType" required>
|
<select id="webhook-content-type" v-model="notification.webhookContentType" class="form-select" required>
|
||||||
<option value="json">application/json</option>
|
<option value="json">
|
||||||
<option value="form-data">multipart/form-data</option>
|
application/json
|
||||||
|
</option>
|
||||||
|
<option value="form-data">
|
||||||
|
multipart/form-data
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
|
@ -90,70 +98,76 @@
|
||||||
<template v-if="notification.type === 'smtp'">
|
<template v-if="notification.type === 'smtp'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="hostname" class="form-label">Hostname</label>
|
<label for="hostname" class="form-label">Hostname</label>
|
||||||
<input type="text" class="form-control" id="hostname" required v-model="notification.smtpHost">
|
<input id="hostname" v-model="notification.smtpHost" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="port" class="form-label">Port</label>
|
<label for="port" class="form-label">Port</label>
|
||||||
<input type="number" class="form-control" id="port" v-model="notification.smtpPort" required min="0" max="65535" step="1">
|
<input id="port" v-model="notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="secure" v-model="notification.smtpSecure">
|
<input id="secure" v-model="notification.smtpSecure" class="form-check-input" type="checkbox" value="">
|
||||||
<label class="form-check-label" for="secure">
|
<label class="form-check-label" for="secure">
|
||||||
Secure
|
Secure
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text">Generally, true for 465, false for other ports.</div>
|
<div class="form-text">
|
||||||
|
Generally, true for 465, false for other ports.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Username</label>
|
<label for="username" class="form-label">Username</label>
|
||||||
<input type="text" class="form-control" id="username" v-model="notification.smtpUsername" autocomplete="false">
|
<input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Password</label>
|
<label for="password" class="form-label">Password</label>
|
||||||
<input type="password" class="form-control" id="password" v-model="notification.smtpPassword" autocomplete="false">
|
<input id="password" v-model="notification.smtpPassword" type="password" class="form-control" autocomplete="false">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="from-email" class="form-label">From Email</label>
|
<label for="from-email" class="form-label">From Email</label>
|
||||||
<input type="email" class="form-control" id="from-email" required v-model="notification.smtpFrom" autocomplete="false">
|
<input id="from-email" v-model="notification.smtpFrom" type="email" class="form-control" required autocomplete="false">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="to-email" class="form-label">To Email</label>
|
<label for="to-email" class="form-label">To Email</label>
|
||||||
<input type="email" class="form-control" id="to-email" required v-model="notification.smtpTo" autocomplete="false">
|
<input id="to-email" v-model="notification.smtpTo" type="email" class="form-control" required autocomplete="false">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'discord'">
|
<template v-if="notification.type === 'discord'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
|
<label for="discord-webhook-url" class="form-label">Discord Webhook URL</label>
|
||||||
<input type="text" class="form-control" id="discord-webhook-url" required v-model="notification.discordWebhookUrl" autocomplete="false">
|
<input id="discord-webhook-url" v-model="notification.discordWebhookUrl" type="text" class="form-control" required autocomplete="false">
|
||||||
<div class="form-text">You can get this by going to Server Settings -> Integrations -> Create Webhook</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="discord-username" class="form-label">Bot Display Name</label>
|
||||||
|
<input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
|
||||||
|
<div class="form-text">
|
||||||
|
You can get this by going to Server Settings -> Integrations -> Create Webhook
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'signal'">
|
<template v-if="notification.type === 'signal'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="signal-url" class="form-label">Post URL</label>
|
<label for="signal-url" class="form-label">Post URL</label>
|
||||||
<input type="url" pattern="https?://.+" class="form-control" id="signal-url" required v-model="notification.signalURL">
|
<input id="signal-url" v-model="notification.signalURL" type="url" pattern="https?://.+" class="form-control" required>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="signal-number" class="form-label">Number</label>
|
<label for="signal-number" class="form-label">Number</label>
|
||||||
<input type="text" class="form-control" id="signal-number" required v-model="notification.signalNumber">
|
<input id="signal-number" v-model="notification.signalNumber" type="text" class="form-control" required>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="signal-recipients" class="form-label">Recipients</label>
|
<label for="signal-recipients" class="form-label">Recipients</label>
|
||||||
<input type="text" class="form-control" id="signal-recipients" required v-model="notification.signalRecipients">
|
<input id="signal-recipients" v-model="notification.signalRecipients" type="text" class="form-control" required>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
You need to have a signal client with REST API.
|
You need to have a signal client with REST API.
|
||||||
|
@ -176,33 +190,33 @@
|
||||||
<template v-if="notification.type === 'gotify'">
|
<template v-if="notification.type === 'gotify'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gotify-application-token" class="form-label">Application Token</label>
|
<label for="gotify-application-token" class="form-label">Application Token</label>
|
||||||
<input type="text" class="form-control" id="gotify-application-token" required v-model="notification.gotifyapplicationToken">
|
<input id="gotify-application-token" v-model="notification.gotifyapplicationToken" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gotify-server-url" class="form-label">Server URL</label>
|
<label for="gotify-server-url" class="form-label">Server URL</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input type="text" class="form-control" id="gotify-server-url" required v-model="notification.gotifyserverurl">
|
<input id="gotify-server-url" v-model="notification.gotifyserverurl" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="gotify-priority" class="form-label">Priority</label>
|
<label for="gotify-priority" class="form-label">Priority</label>
|
||||||
<input type="number" class="form-control" id="gotify-priority" v-model="notification.gotifyPriority" required min="0" max="10" step="1">
|
<input id="gotify-priority" v-model="notification.gotifyPriority" type="number" class="form-control" required min="0" max="10" step="1">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'slack'">
|
<template v-if="notification.type === 'slack'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
|
||||||
<input type="text" class="form-control" id="slack-webhook-url" required v-model="notification.slackwebhookURL">
|
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
|
||||||
<label for="slack-username" class="form-label">Username</label>
|
<label for="slack-username" class="form-label">Username</label>
|
||||||
<input type="text" class="form-control" id="slack-username" v-model="notification.slackusername">
|
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
|
||||||
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
||||||
<input type="text" class="form-control" id="slack-iconemo" v-model="notification.slackiconemo">
|
<input id="slack-iconemo" v-model="notification.slackiconemo" type="text" class="form-control">
|
||||||
<label for="slack-channel" class="form-label">Channel Name</label>
|
<label for="slack-channel" class="form-label">Channel Name</label>
|
||||||
<input type="text" class="form-control" id="slack-channel-name" v-model="notification.slackchannel">
|
<input id="slack-channel-name" v-model="notification.slackchannel" type="text" class="form-control">
|
||||||
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
|
||||||
<input type="text" class="form-control" id="slack-button" v-model="notification.slackbutton">
|
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<span style="color:red;"><sup>*</sup></span>Required
|
<span style="color:red;"><sup>*</sup></span>Required
|
||||||
<p style="margin-top: 8px;">
|
<p style="margin-top: 8px;">
|
||||||
|
@ -221,18 +235,35 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="notification.type === 'pushy'">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pushy-app-token" class="form-label">API_KEY</label>
|
||||||
|
<input id="pushy-app-token" v-model="notification.pushyAPIKey" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="pushy-user-key" v-model="notification.pushyToken" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="notification.type === 'pushover'">
|
<template v-if="notification.type === 'pushover'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
|
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
|
||||||
<input type="text" class="form-control" id="pushover-user" required v-model="notification.pushoveruserkey">
|
<input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
|
||||||
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
|
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
|
||||||
<input type="text" class="form-control" id="pushover-app-token" required v-model="notification.pushoverapptoken">
|
<input id="pushover-app-token" v-model="notification.pushoverapptoken" type="text" class="form-control" required>
|
||||||
<label for="pushover-device" class="form-label">Device</label>
|
<label for="pushover-device" class="form-label">Device</label>
|
||||||
<input type="text" class="form-control" id="pushover-device" v-model="notification.pushoverdevice">
|
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
|
||||||
<label for="pushover-device" class="form-label">Message Title</label>
|
<label for="pushover-device" class="form-label">Message Title</label>
|
||||||
<input type="text" class="form-control" id="pushover-title" v-model="notification.pushovertitle">
|
<input id="pushover-title" v-model="notification.pushovertitle" type="text" class="form-control">
|
||||||
<label for="pushover-priority" class="form-label">Priority</label>
|
<label for="pushover-priority" class="form-label">Priority</label>
|
||||||
<select class="form-select" id="pushover-priority" v-model="notification.pushoverpriority">
|
<select id="pushover-priority" v-model="notification.pushoverpriority" class="form-select">
|
||||||
<option>-2</option>
|
<option>-2</option>
|
||||||
<option>-1</option>
|
<option>-1</option>
|
||||||
<option>0</option>
|
<option>0</option>
|
||||||
|
@ -240,7 +271,7 @@
|
||||||
<option>2</option>
|
<option>2</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="pushover-sound" class="form-label">Notification Sound</label>
|
<label for="pushover-sound" class="form-label">Notification Sound</label>
|
||||||
<select class="form-select" id="pushover-sound" v-model="notification.pushoversounds">
|
<select id="pushover-sound" v-model="notification.pushoversounds" class="form-select">
|
||||||
<option>pushover</option>
|
<option>pushover</option>
|
||||||
<option>bike</option>
|
<option>bike</option>
|
||||||
<option>bugle</option>
|
<option>bugle</option>
|
||||||
|
@ -282,7 +313,7 @@
|
||||||
<template v-if="notification.type === 'apprise'">
|
<template v-if="notification.type === 'apprise'">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="apprise-url" class="form-label">Apprise URL</label>
|
<label for="apprise-url" class="form-label">Apprise URL</label>
|
||||||
<input type="text" class="form-control" id="apprise-url" required v-model="notification.appriseURL">
|
<input id="apprise-url" v-model="notification.appriseURL" type="text" class="form-control" required>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
|
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -293,40 +324,56 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p>
|
<p>
|
||||||
Status:
|
Status:
|
||||||
<span class="text-primary" v-if="appriseInstalled">Apprise is installed</span>
|
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
|
||||||
<span class="text-danger" v-else>Apprise is not installed. <a href="https://github.com/caronc/apprise">Read more</a></span>
|
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise" target="_blank">Read more</a></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="notification.type === 'lunasea'">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color:red;"><sup>*</sup></span></label>
|
||||||
|
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
|
||||||
|
<div class="form-text">
|
||||||
|
<p><span style="color:red;"><sup>*</sup></span>Required</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-danger" @click="deleteConfirm" :disabled="processing" v-if="id">Delete</button>
|
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||||
<button type="button" class="btn btn-warning" @click="test" :disabled="processing">Test</button>
|
Delete
|
||||||
<button type="submit" class="btn btn-primary" :disabled="processing">Save</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<Confirm ref="confirmDelete" @yes="deleteNotification" btn-style="btn-danger">Are you sure want to delete this notification for all monitors?</Confirm>
|
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteNotification">
|
||||||
|
Are you sure want to delete this notification for all monitors?
|
||||||
|
</Confirm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { Modal } from 'bootstrap'
|
import { Modal } from "bootstrap"
|
||||||
import { ucfirst } from '../util-frontend'
|
import { ucfirst } from "../util.ts"
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from "vue-toastification"
|
||||||
import Confirm from "./Confirm.vue";
|
import Confirm from "./Confirm.vue";
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {Confirm},
|
components: {
|
||||||
props: {
|
Confirm,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
props: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
model: null,
|
model: null,
|
||||||
|
@ -335,11 +382,37 @@ export default {
|
||||||
notification: {
|
notification: {
|
||||||
name: "",
|
name: "",
|
||||||
type: null,
|
type: null,
|
||||||
gotifyPriority: 8
|
gotifyPriority: 8,
|
||||||
},
|
},
|
||||||
appriseInstalled: false,
|
appriseInstalled: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
telegramGetUpdatesURL() {
|
||||||
|
let token = "<YOUR BOT TOKEN HERE>"
|
||||||
|
|
||||||
|
if (this.notification.telegramBotToken) {
|
||||||
|
token = this.notification.telegramBotToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"notification.type"(to, from) {
|
||||||
|
let oldName;
|
||||||
|
|
||||||
|
if (from) {
|
||||||
|
oldName = `My ${ucfirst(from)} Alert (1)`;
|
||||||
|
} else {
|
||||||
|
oldName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! this.notification.name || this.notification.name === oldName) {
|
||||||
|
this.notification.name = `My ${ucfirst(to)} Alert (1)`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.modal = new Modal(this.$refs.modal)
|
this.modal = new Modal(this.$refs.modal)
|
||||||
|
|
||||||
|
@ -437,35 +510,15 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
telegramGetUpdatesURL() {
|
|
||||||
let token = "<YOUR BOT TOKEN HERE>"
|
|
||||||
|
|
||||||
if (this.notification.telegramBotToken) {
|
|
||||||
token = this.notification.telegramBotToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
"notification.type"(to, from) {
|
|
||||||
let oldName;
|
|
||||||
|
|
||||||
if (from) {
|
|
||||||
oldName = `My ${ucfirst(from)} Alert (1)`;
|
|
||||||
} else {
|
|
||||||
oldName = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! this.notification.name || this.notification.name === oldName) {
|
|
||||||
this.notification.name = `My ${ucfirst(to)} Alert (1)`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.modal-dialog .form-text, .modal-dialog p {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,39 +5,47 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
status: Number
|
status: Number,
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
color() {
|
color() {
|
||||||
if (this.status === 0) {
|
if (this.status === 0) {
|
||||||
return "danger"
|
return "danger"
|
||||||
} else if (this.status === 1) {
|
|
||||||
return "primary"
|
|
||||||
} else if (this.status === 2) {
|
|
||||||
return "warning"
|
|
||||||
} else {
|
|
||||||
return "secondary"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.status === 1) {
|
||||||
|
return "primary"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === 2) {
|
||||||
|
return "warning"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "secondary"
|
||||||
},
|
},
|
||||||
|
|
||||||
text() {
|
text() {
|
||||||
if (this.status === 0) {
|
if (this.status === 0) {
|
||||||
return "Down"
|
return "Down"
|
||||||
} else if (this.status === 1) {
|
}
|
||||||
|
|
||||||
|
if (this.status === 1) {
|
||||||
return "Up"
|
return "Up"
|
||||||
} else if (this.status === 2) {
|
}
|
||||||
|
|
||||||
|
if (this.status === 2) {
|
||||||
return "Pending"
|
return "Pending"
|
||||||
} else {
|
}
|
||||||
|
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
span {
|
span {
|
||||||
width: 54px;
|
width: 64px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
monitor : Object,
|
monitor: Object,
|
||||||
type: String,
|
type: String,
|
||||||
pill: {
|
pill: {
|
||||||
Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -20,44 +20,50 @@ export default {
|
||||||
|
|
||||||
if (this.$root.uptimeList[key] !== undefined) {
|
if (this.$root.uptimeList[key] !== undefined) {
|
||||||
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
|
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
|
||||||
} else {
|
|
||||||
return "N/A"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "N/A"
|
||||||
},
|
},
|
||||||
|
|
||||||
color() {
|
color() {
|
||||||
if (this.lastHeartBeat.status === 0) {
|
if (this.lastHeartBeat.status === 0) {
|
||||||
return "danger"
|
return "danger"
|
||||||
} else if (this.lastHeartBeat.status === 1) {
|
|
||||||
return "primary"
|
|
||||||
} else if (this.lastHeartBeat.status === 2) {
|
|
||||||
return "warning"
|
|
||||||
} else {
|
|
||||||
return "secondary"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.lastHeartBeat.status === 1) {
|
||||||
|
return "primary"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lastHeartBeat.status === 2) {
|
||||||
|
return "warning"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "secondary"
|
||||||
},
|
},
|
||||||
|
|
||||||
lastHeartBeat() {
|
lastHeartBeat() {
|
||||||
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
||||||
return this.$root.lastHeartbeatList[this.monitor.id]
|
return this.$root.lastHeartbeatList[this.monitor.id]
|
||||||
} else {
|
}
|
||||||
return { status: -1 }
|
|
||||||
|
return {
|
||||||
|
status: -1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
if (this.pill) {
|
if (this.pill) {
|
||||||
return `badge rounded-pill bg-${this.color}`;
|
return `badge rounded-pill bg-${this.color}`;
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
|
return "";
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
|
.badge {
|
||||||
|
min-width: 62px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
10
src/icon.js
Normal file
10
src/icon.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { library } from "@fortawesome/fontawesome-svg-core"
|
||||||
|
import { faCog, faEdit, faList, faPause, faPlay, faPlus, faTachometerAlt, faTrash } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
//import { fa } from '@fortawesome/free-regular-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
||||||
|
|
||||||
|
// Add Free Font Awesome Icons here
|
||||||
|
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
||||||
|
library.add(faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList)
|
||||||
|
|
||||||
|
export { FontAwesomeIcon }
|
|
@ -3,11 +3,5 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {}
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,28 +1,36 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div :class="$root.theme">
|
||||||
<div class="lost-connection" v-if="! $root.socket.connected && ! $root.socket.firstConnect">
|
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
Lost connection to the socket server. Reconnecting...
|
{{ $root.connectionErrorMsg }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Desktop header -->
|
<!-- Desktop header -->
|
||||||
<header class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom" v-if="! $root.isMobile">
|
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
|
||||||
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||||
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo"></object>
|
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo" />
|
||||||
<span class="fs-4 title">Uptime Kuma</span>
|
<span class="fs-4 title">Uptime Kuma</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<ul class="nav nav-pills" >
|
<ul class="nav nav-pills">
|
||||||
<li class="nav-item"><router-link to="/dashboard" class="nav-link">📊 Dashboard</router-link></li>
|
<li class="nav-item">
|
||||||
<li class="nav-item"><router-link to="/settings" class="nav-link">🔧 Settings</router-link></li>
|
<router-link to="/dashboard" class="nav-link">
|
||||||
|
<font-awesome-icon icon="tachometer-alt" /> Dashboard
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<router-link to="/settings" class="nav-link">
|
||||||
|
<font-awesome-icon icon="cog" /> Settings
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Mobile header -->
|
<!-- Mobile header -->
|
||||||
<header class="d-flex flex-wrap justify-content-center mt-3 mb-3" v-else>
|
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
|
||||||
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
|
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
|
||||||
<object class="bi" width="40" height="40" data="/icon.svg"></object>
|
<object class="bi" width="40" height="40" data="/icon.svg" />
|
||||||
<span class="fs-4 title ms-2">Uptime Kuma</span>
|
<span class="fs-4 title ms-2">Uptime Kuma</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</header>
|
</header>
|
||||||
|
@ -42,13 +50,29 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Mobile Only -->
|
<!-- Mobile Only -->
|
||||||
<div style="width: 100%;height: 60px;" v-if="$root.isMobile"></div>
|
<div v-if="$root.isMobile" style="width: 100%;height: 60px;" />
|
||||||
<nav class="bottom-nav" v-if="$root.isMobile">
|
<nav v-if="$root.isMobile" class="bottom-nav">
|
||||||
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList"><div>📊</div>Dashboard</router-link>
|
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList">
|
||||||
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile"><div>📃</div>List</a>
|
<div><font-awesome-icon icon="tachometer-alt" /></div>
|
||||||
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList"><div>➕</div>Add</router-link>
|
Dashboard
|
||||||
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList"><div>🔧</div>Settings</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile">
|
||||||
|
<div><font-awesome-icon icon="list" /></div>
|
||||||
|
List
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList">
|
||||||
|
<div><font-awesome-icon icon="plus" /></div>
|
||||||
|
Add
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList">
|
||||||
|
<div><font-awesome-icon icon="cog" /></div>
|
||||||
|
Settings
|
||||||
|
</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -56,23 +80,19 @@ import Login from "../components/Login.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Login
|
Login,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {}
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.init();
|
|
||||||
},
|
},
|
||||||
|
computed: {},
|
||||||
watch: {
|
watch: {
|
||||||
$route (to, from) {
|
$route (to, from) {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
init() {
|
||||||
|
@ -81,11 +101,11 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
.bottom-nav {
|
.bottom-nav {
|
||||||
|
@ -99,7 +119,7 @@ export default {
|
||||||
box-shadow: 0 15px 47px 0 rgba(0, 0, 0, 0.05), 0 5px 14px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 15px 47px 0 rgba(0, 0, 0, 0.05), 0 5px 14px 0 rgba(0, 0, 0, 0.05);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0 35px;
|
padding: 0 10px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -137,16 +157,28 @@ export default {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
color: #AAA;
|
color: #AAA;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-bottom: 30px;
|
margin-top: 10px;
|
||||||
|
padding-bottom: 30px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
header {
|
||||||
|
background-color: #161B22;
|
||||||
|
border-bottom-color: #161B22 !important;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #F0F6FC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-nav {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
63
src/main.js
63
src/main.js
|
@ -1,58 +1,60 @@
|
||||||
import {createApp, h} from "vue";
|
import "bootstrap";
|
||||||
import {createRouter, createWebHistory} from 'vue-router'
|
import { createApp, h } from "vue";
|
||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import App from './App.vue'
|
import Toast from "vue-toastification";
|
||||||
import Layout from './layouts/Layout.vue'
|
import "vue-toastification/dist/index.css";
|
||||||
import EmptyLayout from './layouts/EmptyLayout.vue'
|
import App from "./App.vue";
|
||||||
import Settings from "./pages/Settings.vue";
|
import "./assets/app.scss";
|
||||||
|
import { FontAwesomeIcon } from "./icon.js";
|
||||||
|
import EmptyLayout from "./layouts/EmptyLayout.vue";
|
||||||
|
import Layout from "./layouts/Layout.vue";
|
||||||
|
import socket from "./mixins/socket";
|
||||||
|
import theme from "./mixins/theme";
|
||||||
import Dashboard from "./pages/Dashboard.vue";
|
import Dashboard from "./pages/Dashboard.vue";
|
||||||
import DashboardHome from "./pages/DashboardHome.vue";
|
import DashboardHome from "./pages/DashboardHome.vue";
|
||||||
import Details from "./pages/Details.vue";
|
import Details from "./pages/Details.vue";
|
||||||
import socket from "./mixins/socket"
|
|
||||||
import "./assets/app.scss"
|
|
||||||
import EditMonitor from "./pages/EditMonitor.vue";
|
import EditMonitor from "./pages/EditMonitor.vue";
|
||||||
import Toast from "vue-toastification";
|
import Settings from "./pages/Settings.vue";
|
||||||
import "vue-toastification/dist/index.css";
|
|
||||||
import "bootstrap"
|
|
||||||
import Setup from "./pages/Setup.vue";
|
import Setup from "./pages/Setup.vue";
|
||||||
|
import { appName } from "./util.ts";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: "/",
|
||||||
component: Layout,
|
component: Layout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: "root",
|
name: "root",
|
||||||
path: '',
|
path: "",
|
||||||
component: Dashboard,
|
component: Dashboard,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: "DashboardHome",
|
name: "DashboardHome",
|
||||||
path: '/dashboard',
|
path: "/dashboard",
|
||||||
component: DashboardHome,
|
component: DashboardHome,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/dashboard/:id',
|
path: "/dashboard/:id",
|
||||||
component: EmptyLayout,
|
component: EmptyLayout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
component: Details,
|
component: Details,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit/:id',
|
path: "/edit/:id",
|
||||||
component: EditMonitor,
|
component: EditMonitor,
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add',
|
path: "/add",
|
||||||
component: EditMonitor,
|
component: EditMonitor,
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: "/settings",
|
||||||
component: Settings,
|
component: Settings,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -62,13 +64,13 @@ const routes = [
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/setup',
|
path: "/setup",
|
||||||
component: Setup,
|
component: Setup,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
linkActiveClass: 'active',
|
linkActiveClass: "active",
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
@ -76,17 +78,24 @@ const router = createRouter({
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
mixins: [
|
mixins: [
|
||||||
socket,
|
socket,
|
||||||
|
theme
|
||||||
],
|
],
|
||||||
render: ()=>h(App)
|
data() {
|
||||||
|
return {
|
||||||
|
appName: appName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: () => h(App),
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
position: "bottom-right"
|
position: "bottom-right",
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use(Toast, options);
|
app.use(Toast, options);
|
||||||
|
|
||||||
app.mount('#app')
|
app.component("FontAwesomeIcon", FontAwesomeIcon)
|
||||||
|
|
||||||
|
app.mount("#app")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {io} from "socket.io-client";
|
|
||||||
import { useToast } from 'vue-toastification'
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { io } from "socket.io-client";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
let socket;
|
let socket;
|
||||||
|
@ -29,11 +29,12 @@ export default {
|
||||||
notificationList: [],
|
notificationList: [],
|
||||||
windowWidth: window.innerWidth,
|
windowWidth: window.innerWidth,
|
||||||
showListMobile: false,
|
showListMobile: false,
|
||||||
|
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener('resize', this.onResize);
|
window.addEventListener("resize", this.onResize);
|
||||||
|
|
||||||
let wsHost;
|
let wsHost;
|
||||||
const env = process.env.NODE_ENV || "production";
|
const env = process.env.NODE_ENV || "production";
|
||||||
|
@ -44,21 +45,23 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
socket = io(wsHost, {
|
socket = io(wsHost, {
|
||||||
transports: ['websocket']
|
transports: ["websocket"],
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("connect_error", (err) => {
|
socket.on("info", (info) => {
|
||||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('info', (info) => {
|
|
||||||
this.info = info;
|
this.info = info;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('setup', (monitorID, data) => {
|
socket.on("setup", (monitorID, data) => {
|
||||||
this.$router.push("/setup")
|
this.$router.push("/setup")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("autoLogin", (monitorID, data) => {
|
||||||
|
this.loggedIn = true;
|
||||||
|
this.storage().token = "autoLogin";
|
||||||
|
this.allowLoginDialog = false;
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("monitorList", (data) => {
|
socket.on("monitorList", (data) => {
|
||||||
// Add Helper function
|
// Add Helper function
|
||||||
Object.entries(data).forEach(([monitorID, monitor]) => {
|
Object.entries(data).forEach(([monitorID, monitor]) => {
|
||||||
|
@ -73,11 +76,11 @@ export default {
|
||||||
this.monitorList = data;
|
this.monitorList = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('notificationList', (data) => {
|
socket.on("notificationList", (data) => {
|
||||||
this.notificationList = data;
|
this.notificationList = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('heartbeat', (data) => {
|
socket.on("heartbeat", (data) => {
|
||||||
if (! (data.monitorID in this.heartbeatList)) {
|
if (! (data.monitorID in this.heartbeatList)) {
|
||||||
this.heartbeatList[data.monitorID] = [];
|
this.heartbeatList[data.monitorID] = [];
|
||||||
}
|
}
|
||||||
|
@ -100,7 +103,6 @@ export default {
|
||||||
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
|
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (! (data.monitorID in this.importantHeartbeatList)) {
|
if (! (data.monitorID in this.importantHeartbeatList)) {
|
||||||
this.importantHeartbeatList[data.monitorID] = [];
|
this.importantHeartbeatList[data.monitorID] = [];
|
||||||
}
|
}
|
||||||
|
@ -109,7 +111,7 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('heartbeatList', (monitorID, data) => {
|
socket.on("heartbeatList", (monitorID, data) => {
|
||||||
if (! (monitorID in this.heartbeatList)) {
|
if (! (monitorID in this.heartbeatList)) {
|
||||||
this.heartbeatList[monitorID] = data;
|
this.heartbeatList[monitorID] = data;
|
||||||
} else {
|
} else {
|
||||||
|
@ -117,19 +119,19 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('avgPing', (monitorID, data) => {
|
socket.on("avgPing", (monitorID, data) => {
|
||||||
this.avgPingList[monitorID] = data
|
this.avgPingList[monitorID] = data
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('uptime', (monitorID, type, data) => {
|
socket.on("uptime", (monitorID, type, data) => {
|
||||||
this.uptimeList[`${monitorID}_${type}`] = data
|
this.uptimeList[`${monitorID}_${type}`] = data
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('certInfo', (monitorID, data) => {
|
socket.on("certInfo", (monitorID, data) => {
|
||||||
this.certInfoList[monitorID] = JSON.parse(data)
|
this.certInfoList[monitorID] = JSON.parse(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('importantHeartbeatList', (monitorID, data) => {
|
socket.on("importantHeartbeatList", (monitorID, data) => {
|
||||||
if (! (monitorID in this.importantHeartbeatList)) {
|
if (! (monitorID in this.importantHeartbeatList)) {
|
||||||
this.importantHeartbeatList[monitorID] = data;
|
this.importantHeartbeatList[monitorID] = data;
|
||||||
} else {
|
} else {
|
||||||
|
@ -137,12 +139,20 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
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")
|
console.log("disconnect")
|
||||||
|
this.connectionErrorMsg = "Lost connection to the socket server. Reconnecting...";
|
||||||
this.socket.connected = false;
|
this.socket.connected = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on("connect", () => {
|
||||||
console.log("connect")
|
console.log("connect")
|
||||||
this.socket.connectCount++;
|
this.socket.connectCount++;
|
||||||
this.socket.connected = true;
|
this.socket.connected = true;
|
||||||
|
@ -152,8 +162,22 @@ export default {
|
||||||
this.clearData()
|
this.clearData()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.storage().token) {
|
let token = this.storage().token;
|
||||||
this.loginByToken(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 {
|
} else {
|
||||||
this.allowLoginDialog = true;
|
this.allowLoginDialog = true;
|
||||||
}
|
}
|
||||||
|
@ -201,7 +225,7 @@ export default {
|
||||||
this.loggedIn = true;
|
this.loggedIn = true;
|
||||||
|
|
||||||
// Trigger Chrome Save Password
|
// Trigger Chrome Save Password
|
||||||
history.pushState({}, '')
|
history.pushState({}, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(res)
|
callback(res)
|
||||||
|
@ -254,10 +278,9 @@ export default {
|
||||||
|
|
||||||
if (this.userTimezone === "auto") {
|
if (this.userTimezone === "auto") {
|
||||||
return dayjs.tz.guess()
|
return dayjs.tz.guess()
|
||||||
} else {
|
|
||||||
return this.userTimezone
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.userTimezone
|
||||||
},
|
},
|
||||||
|
|
||||||
lastHeartbeatList() {
|
lastHeartbeatList() {
|
||||||
|
@ -276,7 +299,7 @@ export default {
|
||||||
|
|
||||||
let unknown = {
|
let unknown = {
|
||||||
text: "Unknown",
|
text: "Unknown",
|
||||||
color: "secondary"
|
color: "secondary",
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let monitorID in this.lastHeartbeatList) {
|
for (let monitorID in this.lastHeartbeatList) {
|
||||||
|
@ -287,17 +310,17 @@ export default {
|
||||||
} else if (lastHeartBeat.status === 1) {
|
} else if (lastHeartBeat.status === 1) {
|
||||||
result[monitorID] = {
|
result[monitorID] = {
|
||||||
text: "Up",
|
text: "Up",
|
||||||
color: "primary"
|
color: "primary",
|
||||||
};
|
};
|
||||||
} else if (lastHeartBeat.status === 0) {
|
} else if (lastHeartBeat.status === 0) {
|
||||||
result[monitorID] = {
|
result[monitorID] = {
|
||||||
text: "Down",
|
text: "Down",
|
||||||
color: "danger"
|
color: "danger",
|
||||||
};
|
};
|
||||||
} else if (lastHeartBeat.status === 2) {
|
} else if (lastHeartBeat.status === 2) {
|
||||||
result[monitorID] = {
|
result[monitorID] = {
|
||||||
text: "Pending",
|
text: "Pending",
|
||||||
color: "warning"
|
color: "warning",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
result[monitorID] = unknown;
|
result[monitorID] = unknown;
|
||||||
|
@ -305,7 +328,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -319,9 +342,8 @@ export default {
|
||||||
|
|
||||||
remember() {
|
remember() {
|
||||||
localStorage.remember = (this.remember) ? "1" : "0"
|
localStorage.remember = (this.remember) ? "1" : "0"
|
||||||
}
|
},
|
||||||
|
|
||||||
}
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
39
src/mixins/theme.js
Normal file
39
src/mixins/theme.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
export default {
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
system: (window.matchMedia("(prefers-color-scheme: dark)")) ? "dark" : "light",
|
||||||
|
userTheme: localStorage.theme,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// Default Light
|
||||||
|
if (! this.userTheme) {
|
||||||
|
this.userTheme = "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.classList.add(this.theme);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
theme() {
|
||||||
|
if (this.userTheme === "auto") {
|
||||||
|
return this.system;
|
||||||
|
}
|
||||||
|
return this.userTheme;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
userTheme(to, from) {
|
||||||
|
localStorage.theme = to;
|
||||||
|
},
|
||||||
|
|
||||||
|
theme(to, from) {
|
||||||
|
document.body.classList.remove(from);
|
||||||
|
document.body.classList.add(this.theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,36 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-5 col-xl-4">
|
<div class="col-12 col-md-5 col-xl-4">
|
||||||
<div v-if="! $root.isMobile">
|
<div v-if="! $root.isMobile">
|
||||||
<router-link to="/add" class="btn btn-primary">Add New Monitor</router-link>
|
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> Add New Monitor</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shadow-box list mb-4" v-if="showList">
|
<div v-if="showList" class="shadow-box list mb-4">
|
||||||
|
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||||
<div class="text-center mt-3" v-if="Object.keys($root.monitorList).length === 0">
|
|
||||||
No Monitors, please <router-link to="/add">add one</router-link>.
|
No Monitors, please <router-link to="/add">add one</router-link>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="(item, index) in sortedMonitorList" @click="$root.cancelActiveList" :key="index">
|
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" @click="$root.cancelActiveList">
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 col-md-8 small-padding">
|
<div class="col-6 col-md-8 small-padding">
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-md-4">
|
<div class="col-6 col-md-4">
|
||||||
<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>
|
</div>
|
||||||
<div class="col-12 col-md-7 col-xl-8">
|
<div class="col-12 col-md-7 col-xl-8">
|
||||||
|
@ -38,7 +31,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -49,12 +41,10 @@ import Uptime from "../components/Uptime.vue";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Uptime,
|
Uptime,
|
||||||
HeartbeatBar
|
HeartbeatBar,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {}
|
||||||
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sortedMonitorList() {
|
sortedMonitorList() {
|
||||||
|
@ -94,12 +84,12 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
monitorURL(id) {
|
monitorURL(id) {
|
||||||
return "/dashboard/" + id;
|
return "/dashboard/" + id;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
|
@ -107,9 +97,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
margin-top: 25px;
|
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: calc(100vh - 200px);
|
min-height: calc(100vh - 240px);
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -138,13 +127,23 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
|
||||||
min-width: 58px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-padding {
|
.small-padding {
|
||||||
padding-left: 5px !important;
|
padding-left: 5px !important;
|
||||||
padding-right: 5px !important;
|
padding-right: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.list {
|
||||||
|
.item {
|
||||||
|
&:hover {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<div v-if="$route.name === 'DashboardHome'">
|
<div v-if="$route.name === 'DashboardHome'">
|
||||||
<h1 class="mb-3">Quick Stats</h1>
|
<h1 class="mb-3">
|
||||||
|
Quick Stats
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div class="shadow-box big-padding text-center">
|
<div class="shadow-box big-padding text-center">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -22,16 +23,16 @@
|
||||||
<span class="num text-secondary">{{ stats.pause }}</span>
|
<span class="num text-secondary">{{ stats.pause }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" v-if="false">
|
<div v-if="false" class="row">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<h3>Uptime</h3>
|
<h3>Uptime</h3>
|
||||||
<p>(24-hour)</p>
|
<p>(24-hour)</p>
|
||||||
<span class="num"></span>
|
<span class="num" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<h3>Uptime</h3>
|
<h3>Uptime</h3>
|
||||||
<p>(30-day)</p>
|
<p>(30-day)</p>
|
||||||
<span class="num"></span>
|
<span class="num" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,7 +56,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr v-if="importantHeartBeatList.length === 0">
|
<tr v-if="importantHeartBeatList.length === 0">
|
||||||
<td colspan="4">No important events</td>
|
<td colspan="4">
|
||||||
|
No important events
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -63,8 +66,9 @@
|
||||||
<div class="d-flex justify-content-center kuma_pagination">
|
<div class="d-flex justify-content-center kuma_pagination">
|
||||||
<pagination
|
<pagination
|
||||||
v-model="page"
|
v-model="page"
|
||||||
:records=importantHeartBeatList.length
|
:records="importantHeartBeatList.length"
|
||||||
:per-page="perPage" />
|
:per-page="perPage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,11 +146,13 @@ export default {
|
||||||
result.sort((a, b) => {
|
result.sort((a, b) => {
|
||||||
if (a.time > b.time) {
|
if (a.time > b.time) {
|
||||||
return -1;
|
return -1;
|
||||||
} else if (a.time < b.time) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (a.time < b.time) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.heartBeatList = result;
|
this.heartBeatList = result;
|
||||||
|
@ -159,11 +165,11 @@ export default {
|
||||||
const endIndex = startIndex + this.perPage;
|
const endIndex = startIndex + this.perPage;
|
||||||
return this.heartBeatList.slice(startIndex, endIndex);
|
return this.heartBeatList.slice(startIndex, endIndex);
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import "../assets/vars";
|
@import "../assets/vars";
|
||||||
|
|
||||||
.num {
|
.num {
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<h1> {{ monitor.name }}</h1>
|
<h1> {{ monitor.name }}</h1>
|
||||||
<p class="url">
|
<p class="url">
|
||||||
<a :href="monitor.url" target="_blank" v-if="monitor.type === 'http' || monitor.type === 'keyword' ">{{ 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>
|
||||||
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
|
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
|
||||||
<span v-if="monitor.type === 'keyword'">
|
<span v-if="monitor.type === 'keyword'">
|
||||||
<br />
|
<br>
|
||||||
<span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span>
|
<span>Keyword:</span> <span class="keyword">{{ monitor.keyword }}</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="functions">
|
<div class="functions">
|
||||||
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button>
|
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
|
||||||
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button>
|
<font-awesome-icon icon="pause" /> Pause
|
||||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">Edit</router-link>
|
</button>
|
||||||
<button class="btn btn-danger" @click="deleteDialog">Delete</button>
|
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||||
|
<font-awesome-icon icon="play" /> Resume
|
||||||
|
</button>
|
||||||
|
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
||||||
|
<font-awesome-icon icon="edit" /> Edit
|
||||||
|
</router-link>
|
||||||
|
<button class="btn btn-danger" @click="deleteDialog">
|
||||||
|
<font-awesome-icon icon="trash" /> Delete
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shadow-box">
|
<div class="shadow-box">
|
||||||
|
@ -37,7 +45,7 @@
|
||||||
<span class="num"><CountUp :value="ping" /></span>
|
<span class="num"><CountUp :value="ping" /></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4>Avg.{{ pingTitle }}</h4>
|
<h4>Avg. {{ pingTitle }}</h4>
|
||||||
<p>(24-hour)</p>
|
<p>(24-hour)</p>
|
||||||
<span class="num"><CountUp :value="avgPing" /></span>
|
<span class="num"><CountUp :value="avgPing" /></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,40 +60,50 @@
|
||||||
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
|
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col" v-if="certInfo">
|
<div v-if="certInfo" class="col">
|
||||||
<h4>CertExp.</h4>
|
<h4>Cert Exp.</h4>
|
||||||
<p>(<Datetime :value="certInfo.validTo" date-only />)</p>
|
<p>(<Datetime :value="certInfo.validTo" date-only />)</p>
|
||||||
<span class="num" >
|
<span class="num">
|
||||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{certInfo.daysRemaining}} days</a>
|
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} days</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shadow-box big-padding text-center" v-if="showCertInfoBox">
|
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4>Certificate Info</h4>
|
<h4>Certificate Info</h4>
|
||||||
<table class="text-start">
|
<table class="text-start">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Valid: </td>
|
<td class="px-3">
|
||||||
|
Valid:
|
||||||
|
</td>
|
||||||
<td>{{ certInfo.valid }}</td>
|
<td>{{ certInfo.valid }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Valid To: </td>
|
<td class="px-3">
|
||||||
|
Valid To:
|
||||||
|
</td>
|
||||||
<td><Datetime :value="certInfo.validTo" /></td>
|
<td><Datetime :value="certInfo.validTo" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Days Remaining: </td>
|
<td class="px-3">
|
||||||
|
Days Remaining:
|
||||||
|
</td>
|
||||||
<td>{{ certInfo.daysRemaining }}</td>
|
<td>{{ certInfo.daysRemaining }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Issuer: </td>
|
<td class="px-3">
|
||||||
|
Issuer:
|
||||||
|
</td>
|
||||||
<td>{{ certInfo.issuer }}</td>
|
<td>{{ certInfo.issuer }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="my-3">
|
<tr class="my-3">
|
||||||
<td class="px-3">Fingerprint: </td>
|
<td class="px-3">
|
||||||
|
Fingerprint:
|
||||||
|
</td>
|
||||||
<td>{{ certInfo.fingerprint }}</td>
|
<td>{{ certInfo.fingerprint }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -111,7 +129,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr v-if="importantHeartBeatList.length === 0">
|
<tr v-if="importantHeartBeatList.length === 0">
|
||||||
<td colspan="3">No important events</td>
|
<td colspan="3">
|
||||||
|
No important events
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -119,8 +139,9 @@
|
||||||
<div class="d-flex justify-content-center kuma_pagination">
|
<div class="d-flex justify-content-center kuma_pagination">
|
||||||
<pagination
|
<pagination
|
||||||
v-model="page"
|
v-model="page"
|
||||||
:records=importantHeartBeatList.length
|
:records="importantHeartBeatList.length"
|
||||||
:per-page="perPage" />
|
:per-page="perPage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -128,13 +149,13 @@
|
||||||
Are you sure want to pause?
|
Are you sure want to pause?
|
||||||
</Confirm>
|
</Confirm>
|
||||||
|
|
||||||
<Confirm ref="confirmDelete" btnStyle="btn-danger" @yes="deleteMonitor">
|
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteMonitor">
|
||||||
Are you sure want to delete this monitor?
|
Are you sure want to delete this monitor?
|
||||||
</Confirm>
|
</Confirm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from "vue-toastification"
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
import Confirm from "../components/Confirm.vue";
|
import Confirm from "../components/Confirm.vue";
|
||||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||||
|
@ -153,9 +174,6 @@ export default {
|
||||||
Confirm,
|
Confirm,
|
||||||
Status,
|
Status,
|
||||||
Pagination,
|
Pagination,
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -170,9 +188,9 @@ export default {
|
||||||
pingTitle() {
|
pingTitle() {
|
||||||
if (this.monitor.type === "http") {
|
if (this.monitor.type === "http") {
|
||||||
return "Response"
|
return "Response"
|
||||||
} else {
|
|
||||||
return "Ping"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "Ping"
|
||||||
},
|
},
|
||||||
|
|
||||||
monitor() {
|
monitor() {
|
||||||
|
@ -183,50 +201,52 @@ export default {
|
||||||
lastHeartBeat() {
|
lastHeartBeat() {
|
||||||
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
|
||||||
return this.$root.lastHeartbeatList[this.monitor.id]
|
return this.$root.lastHeartbeatList[this.monitor.id]
|
||||||
} else {
|
}
|
||||||
return { status: -1 }
|
|
||||||
|
return {
|
||||||
|
status: -1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
ping() {
|
ping() {
|
||||||
if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) {
|
if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) {
|
||||||
return this.lastHeartBeat.ping;
|
return this.lastHeartBeat.ping;
|
||||||
} else {
|
|
||||||
return "N/A"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "N/A"
|
||||||
},
|
},
|
||||||
|
|
||||||
avgPing() {
|
avgPing() {
|
||||||
if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) {
|
if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) {
|
||||||
return this.$root.avgPingList[this.monitor.id];
|
return this.$root.avgPingList[this.monitor.id];
|
||||||
} else {
|
|
||||||
return "N/A"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "N/A"
|
||||||
},
|
},
|
||||||
|
|
||||||
importantHeartBeatList() {
|
importantHeartBeatList() {
|
||||||
if (this.$root.importantHeartbeatList[this.monitor.id]) {
|
if (this.$root.importantHeartbeatList[this.monitor.id]) {
|
||||||
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
|
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
|
||||||
return this.$root.importantHeartbeatList[this.monitor.id]
|
return this.$root.importantHeartbeatList[this.monitor.id]
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
status() {
|
status() {
|
||||||
if (this.$root.statusList[this.monitor.id]) {
|
if (this.$root.statusList[this.monitor.id]) {
|
||||||
return this.$root.statusList[this.monitor.id]
|
return this.$root.statusList[this.monitor.id]
|
||||||
} else {
|
|
||||||
return { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { }
|
||||||
},
|
},
|
||||||
|
|
||||||
certInfo() {
|
certInfo() {
|
||||||
if (this.$root.certInfoList[this.monitor.id]) {
|
if (this.$root.certInfoList[this.monitor.id]) {
|
||||||
return this.$root.certInfoList[this.monitor.id]
|
return this.$root.certInfoList[this.monitor.id]
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
|
|
||||||
showCertInfoBox() {
|
showCertInfoBox() {
|
||||||
|
@ -238,6 +258,9 @@ export default {
|
||||||
const endIndex = startIndex + this.perPage;
|
const endIndex = startIndex + this.perPage;
|
||||||
return this.heartBeatList.slice(startIndex, endIndex);
|
return this.heartBeatList.slice(startIndex, endIndex);
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
testNotification() {
|
testNotification() {
|
||||||
|
@ -274,9 +297,9 @@ export default {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -329,4 +352,14 @@ table {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyword {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.keyword {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,74 +1,134 @@
|
||||||
<template>
|
<template>
|
||||||
<h1 class="mb-3">{{ pageName }}</h1>
|
<h1 class="mb-3">{{ pageName }}</h1>
|
||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
|
|
||||||
<div class="shadow-box">
|
<div class="shadow-box">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>General</h2>
|
<h2 class="mb-2">General</h2>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="my-3">
|
||||||
<label for="type" class="form-label">Monitor Type</label>
|
<label for="type" class="form-label">Monitor Type</label>
|
||||||
<select class="form-select" aria-label="Default select example" id="type" v-model="monitor.type">
|
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
|
||||||
<option value="http">HTTP(s)</option>
|
<option value="http">
|
||||||
<option value="port">TCP Port</option>
|
HTTP(s)
|
||||||
<option value="ping">Ping</option>
|
</option>
|
||||||
<option value="keyword">HTTP(s) - Keyword</option>
|
<option value="port">
|
||||||
|
TCP Port
|
||||||
|
</option>
|
||||||
|
<option value="ping">
|
||||||
|
Ping
|
||||||
|
</option>
|
||||||
|
<option value="keyword">
|
||||||
|
HTTP(s) - Keyword
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="my-3">
|
||||||
<label for="name" class="form-label">Friendly Name</label>
|
<label for="name" class="form-label">Friendly Name</label>
|
||||||
<input type="text" class="form-control" id="name" v-model="monitor.name" required>
|
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3" v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
|
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||||
<label for="url" class="form-label">URL</label>
|
<label for="url" class="form-label">URL</label>
|
||||||
<input type="url" class="form-control" id="url" v-model="monitor.url" pattern="https?://.+" required>
|
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3" v-if="monitor.type === 'keyword' ">
|
<div v-if="monitor.type === 'keyword' " class="my-3">
|
||||||
<label for="keyword" class="form-label">Keyword</label>
|
<label for="keyword" class="form-label">Keyword</label>
|
||||||
<input type="text" class="form-control" id="keyword" v-model="monitor.keyword" required>
|
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
||||||
<div class="form-text">Search keyword in plain html or JSON response and it is case-sensitive</div>
|
<div class="form-text">
|
||||||
|
Search keyword in plain html or JSON response and it is case-sensitive
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3" v-if="monitor.type === 'port' || monitor.type === 'ping' ">
|
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="my-3">
|
||||||
<label for="hostname" class="form-label">Hostname</label>
|
<label for="hostname" class="form-label">Hostname</label>
|
||||||
<input type="text" class="form-control" id="hostname" v-model="monitor.hostname" required>
|
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3" v-if="monitor.type === 'port' ">
|
<div v-if="monitor.type === 'port' " class="my-3">
|
||||||
<label for="port" class="form-label">Port</label>
|
<label for="port" class="form-label">Port</label>
|
||||||
<input type="number" class="form-control" id="port" v-model="monitor.port" required min="0" max="65535" step="1">
|
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="my-3">
|
||||||
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
|
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
|
||||||
<input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20" step="1">
|
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="my-3">
|
||||||
<label for="maxRetries" class="form-label">Retries</label>
|
<label for="maxRetries" class="form-label">Retries</label>
|
||||||
<input type="number" class="form-control" id="maxRetries" v-model="monitor.maxretries" required min="0" step="1">
|
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
|
||||||
<div class="form-text">Maximum retries before the service is marked as down and a notification is sent</div>
|
<div class="form-text">
|
||||||
|
Maximum retries before the service is marked as down and a notification is sent
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<h2 class="mt-5 mb-2">Advanced</h2>
|
||||||
|
|
||||||
|
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||||
|
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||||
|
<label class="form-check-label" for="ignore-tls">
|
||||||
|
Ignore TLS/SSL error for HTTPS websites
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3 form-check">
|
||||||
|
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||||
|
<label class="form-check-label" for="upside-down">
|
||||||
|
Upside Down Mode
|
||||||
|
</label>
|
||||||
|
<div class="form-text">
|
||||||
|
Flip the status upside down. If the service is reachable, it is DOWN.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="maxRedirects" class="form-label">Max. Redirects</label>
|
||||||
|
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
||||||
|
<div class="form-text">
|
||||||
|
Maximum number of redirects to follow. Set to 0 to disable redirects.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="acceptedStatusCodes" class="form-label">Accepted Status Codes</label>
|
||||||
|
|
||||||
|
<VueMultiselect
|
||||||
|
id="acceptedStatusCodes"
|
||||||
|
v-model="monitor.accepted_statuscodes"
|
||||||
|
:options="acceptedStatusCodeOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:close-on-select="false"
|
||||||
|
:clear-on-select="false"
|
||||||
|
:preserve-search="true"
|
||||||
|
placeholder="Pick Accepted Status Codes..."
|
||||||
|
:preselect-first="false"
|
||||||
|
:max-height="600"
|
||||||
|
:taggable="true"
|
||||||
|
></VueMultiselect>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
Select status codes which are considered as a successful response.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 mb-1">
|
||||||
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button>
|
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
<div v-if="$root.isMobile" class="mt-3" />
|
||||||
|
|
||||||
<div class="mt-3" v-if="$root.isMobile"></div>
|
<h2 class="mb-2">Notifications</h2>
|
||||||
|
<p v-if="$root.notificationList.length === 0">
|
||||||
|
Not available, please setup.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>Notifications</h2>
|
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3">
|
||||||
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
|
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
|
||||||
|
|
||||||
<div class="form-check form-switch mb-3" :key="notification.id" v-for="notification in $root.notificationList">
|
|
||||||
<input class="form-check-input" type="checkbox" :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]">
|
|
||||||
|
|
||||||
<label class="form-check-label" :for=" 'notification' + notification.id">
|
<label class="form-check-label" :for=" 'notification' + notification.id">
|
||||||
{{ notification.name }}
|
{{ notification.name }}
|
||||||
|
@ -76,7 +136,9 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button>
|
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||||
|
Setup Notification
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,24 +149,26 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from "vue-toastification"
|
||||||
|
import VueMultiselect from "vue-multiselect"
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotificationDialog
|
NotificationDialog,
|
||||||
},
|
VueMultiselect,
|
||||||
mounted() {
|
|
||||||
this.init();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
monitor: {
|
monitor: {
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
},
|
},
|
||||||
|
acceptedStatusCodeOptions: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
pageName() {
|
pageName() {
|
||||||
return (this.isAdd) ? "Add New Monitor" : "Edit"
|
return (this.isAdd) ? "Add New Monitor" : "Edit"
|
||||||
|
@ -114,7 +178,29 @@ export default {
|
||||||
},
|
},
|
||||||
isEdit() {
|
isEdit() {
|
||||||
return this.$route.path.startsWith("/edit");
|
return this.$route.path.startsWith("/edit");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"$route.fullPath" () {
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
let acceptedStatusCodeOptions = [
|
||||||
|
"100-199",
|
||||||
|
"200-299",
|
||||||
|
"300-399",
|
||||||
|
"400-499",
|
||||||
|
"500-599",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 100; i <= 999; i++) {
|
||||||
|
acceptedStatusCodeOptions.push(i.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
init() {
|
||||||
|
@ -127,6 +213,10 @@ export default {
|
||||||
interval: 60,
|
interval: 60,
|
||||||
maxretries: 0,
|
maxretries: 0,
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
|
ignoreTls: false,
|
||||||
|
upsideDown: false,
|
||||||
|
maxredirects: 10,
|
||||||
|
accepted_statuscodes: ["200-299"],
|
||||||
}
|
}
|
||||||
} else if (this.isEdit) {
|
} else if (this.isEdit) {
|
||||||
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
|
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
|
||||||
|
@ -161,16 +251,45 @@ export default {
|
||||||
this.$root.toastRes(res)
|
this.$root.toastRes(res)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
'$route.fullPath' () {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.multiselect__tags {
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect--active .multiselect__tags {
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--highlight {
|
||||||
|
background: $primary !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__option--highlight::after {
|
||||||
|
background: $primary !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tag {
|
||||||
|
border-radius: 50rem;
|
||||||
|
background: $primary !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.multiselect__tag {
|
||||||
|
color: $dark-font-color2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.shadow-box {
|
.shadow-box {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
|
@ -1,94 +1,136 @@
|
||||||
<template>
|
<template>
|
||||||
<h1 class="mb-3">Settings</h1>
|
<h1 class="mb-3">
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div class="shadow-box">
|
<div class="shadow-box">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>General</h2>
|
<h2 class="mb-2">General</h2>
|
||||||
<form class="mb-3" @submit.prevent="saveGeneral">
|
<form class="mb-3" @submit.prevent="saveGeneral">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="timezone" class="form-label">Timezone</label>
|
<label for="timezone" class="form-label">Timezone</label>
|
||||||
<select class="form-select" id="timezone" v-model="$root.userTimezone">
|
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||||
<option value="auto">Auto: {{ guessTimezone }}</option>
|
<option value="auto">
|
||||||
<option v-for="(timezone, index) in timezoneList" :value="timezone.value" :key="index">{{ timezone.name }}</option>
|
Auto: {{ guessTimezone }}
|
||||||
|
</option>
|
||||||
|
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
|
||||||
|
{{ timezone.name }}
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||||
|
<input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light">
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck1">Light</label>
|
||||||
|
|
||||||
|
<input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark">
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck2">Dark</label>
|
||||||
|
|
||||||
|
<input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto">
|
||||||
|
<label class="btn btn-outline-primary" for="btncheck3">Auto</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" type="submit">Save</button>
|
<button class="btn btn-primary" type="submit">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>Change Password</h2>
|
<template v-if="loaded">
|
||||||
|
<template v-if="! settings.disableAuth">
|
||||||
|
<h2 class="mt-5 mb-2">Change Password</h2>
|
||||||
<form class="mb-3" @submit.prevent="savePassword">
|
<form class="mb-3" @submit.prevent="savePassword">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="current-password" class="form-label">Current Password</label>
|
<label for="current-password" class="form-label">Current Password</label>
|
||||||
<input type="password" class="form-control" id="current-password" required v-model="password.currentPassword">
|
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="new-password" class="form-label">New Password</label>
|
<label for="new-password" class="form-label">New Password</label>
|
||||||
<input type="password" class="form-control" id="new-password" required v-model="password.newPassword">
|
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
||||||
<input type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" id="repeat-new-password" required v-model="password.repeatNewPassword">
|
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
The repeat password does not match.
|
The repeat password does not match.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" type="submit">Update Password</button>
|
<button class="btn btn-primary" type="submit">
|
||||||
|
Update Password
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div>
|
<h2 class="mt-5 mb-2">Advanced</h2>
|
||||||
<button class="btn btn-danger" @click="$root.logout">Logout</button>
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button>
|
||||||
|
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button>
|
||||||
|
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="notification-list col-md-6">
|
||||||
|
<div v-if="$root.isMobile" class="mt-3" />
|
||||||
<div class="mt-3" v-if="$root.isMobile"></div>
|
|
||||||
|
|
||||||
<h2>Notifications</h2>
|
<h2>Notifications</h2>
|
||||||
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
|
<p v-if="$root.notificationList.length === 0">
|
||||||
<p v-else>Please assign a notification to monitor(s) to get it to work.</p>
|
Not available, please setup.
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
Please assign a notification to monitor(s) to get it to work.
|
||||||
|
</p>
|
||||||
|
|
||||||
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||||
<li class="list-group-item" v-for="(notification, index) in $root.notificationList" :key="index">
|
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
|
||||||
{{ notification.name }}<br />
|
{{ notification.name }}<br>
|
||||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<button class="btn btn-primary me-2" @click="$refs.notificationDialog.show()" type="button">Setup Notification</button>
|
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||||
|
Setup Notification
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NotificationDialog ref="notificationDialog" />
|
<NotificationDialog ref="notificationDialog" />
|
||||||
|
|
||||||
|
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
|
||||||
|
<p>Are you sure want to <strong>disable auth</strong>?</p>
|
||||||
|
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
|
||||||
|
<p>Please use it carefully.</p>
|
||||||
|
</Confirm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
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)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
import {timezoneList} from "../util-frontend";
|
|
||||||
import { useToast } from 'vue-toastification'
|
import { timezoneList } from "../util-frontend";
|
||||||
|
import { useToast } from "vue-toastification"
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotificationDialog
|
NotificationDialog,
|
||||||
|
Confirm,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -100,12 +142,21 @@ export default {
|
||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
repeatNewPassword: "",
|
repeatNewPassword: "",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
|
||||||
|
},
|
||||||
|
loaded: false,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
watch: {
|
||||||
|
"password.repeatNewPassword"() {
|
||||||
|
this.invalidPassword = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.loadSettings();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -129,17 +180,51 @@ export default {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loadSettings() {
|
||||||
|
this.$root.getSocket().emit("getSettings", (res) => {
|
||||||
|
this.settings = res.data;
|
||||||
|
this.loaded = true;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
saveSettings() {
|
||||||
|
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.loadSettings();
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDisableAuth() {
|
||||||
|
this.$refs.confirmDisableAuth.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
disableAuth() {
|
||||||
|
this.settings.disableAuth = true;
|
||||||
|
this.saveSettings();
|
||||||
|
},
|
||||||
|
|
||||||
|
enableAuth() {
|
||||||
|
this.settings.disableAuth = false;
|
||||||
|
this.saveSettings();
|
||||||
|
this.$root.storage().removeItem("token");
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
"password.repeatNewPassword"() {
|
|
||||||
this.invalidPassword = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.shadow-box {
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.shadow-box {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.list-group-item {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
color: $dark-font-color;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,38 +2,42 @@
|
||||||
<div class="form-container">
|
<div class="form-container">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<object width="64" height="64" data="/icon.svg"></object>
|
<object width="64" height="64" data="/icon.svg" />
|
||||||
<div style="font-size: 28px; font-weight: bold; margin-top: 5px;">Uptime Kuma</div>
|
<div style="font-size: 28px; font-weight: bold; margin-top: 5px;">
|
||||||
|
Uptime Kuma
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-3">Create your admin account</p>
|
<p class="mt-3">
|
||||||
|
Create your admin account
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" class="form-control" id="floatingInput" placeholder="Username" v-model="username" required>
|
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" required>
|
||||||
<label for="floatingInput">Username</label>
|
<label for="floatingInput">Username</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mt-3">
|
<div class="form-floating mt-3">
|
||||||
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" v-model="password" required>
|
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" required>
|
||||||
<label for="floatingPassword">Password</label>
|
<label for="floatingPassword">Password</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mt-3">
|
<div class="form-floating mt-3">
|
||||||
<input type="password" class="form-control" id="repeat" placeholder="Repeat Password" v-model="repeatPassword" required>
|
<input id="repeat" v-model="repeatPassword" type="password" class="form-control" placeholder="Repeat Password" required>
|
||||||
<label for="repeat">Repeat Password</label>
|
<label for="repeat">Repeat Password</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">Create</button>
|
<button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from "vue-toastification"
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -70,8 +74,8 @@ export default {
|
||||||
this.$router.push("/")
|
this.$router.push("/")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,16 @@
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from 'dayjs/plugin/utc'
|
import timezone from "dayjs/plugin/timezone";
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
import utc from "dayjs/plugin/utc";
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
export function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ucfirst(str) {
|
|
||||||
if (! str) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstLetter = str.substr(0, 1);
|
|
||||||
return firstLetter.toUpperCase() + str.substr(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getTimezoneOffset(timeZone) {
|
function getTimezoneOffset(timeZone) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const tzString = now.toLocaleString('en-US', { timeZone });
|
const tzString = now.toLocaleString("en-US", {
|
||||||
const localString = now.toLocaleString('en-US');
|
timeZone,
|
||||||
|
});
|
||||||
|
const localString = now.toLocaleString("en-US");
|
||||||
const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000;
|
const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000;
|
||||||
const offset = diff + now.getTimezoneOffset() / 60;
|
const offset = diff + now.getTimezoneOffset() / 60;
|
||||||
return -offset;
|
return -offset;
|
||||||
|
@ -31,355 +19,354 @@ function getTimezoneOffset(timeZone) {
|
||||||
// From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
|
// From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
|
||||||
// TODO: Move to separate file
|
// TODO: Move to separate file
|
||||||
const aryIannaTimeZones = [
|
const aryIannaTimeZones = [
|
||||||
'Europe/Andorra',
|
"Europe/Andorra",
|
||||||
'Asia/Dubai',
|
"Asia/Dubai",
|
||||||
'Asia/Kabul',
|
"Asia/Kabul",
|
||||||
'Europe/Tirane',
|
"Europe/Tirane",
|
||||||
'Asia/Yerevan',
|
"Asia/Yerevan",
|
||||||
'Antarctica/Casey',
|
"Antarctica/Casey",
|
||||||
'Antarctica/Davis',
|
"Antarctica/Davis",
|
||||||
'Antarctica/Mawson',
|
"Antarctica/Mawson",
|
||||||
'Antarctica/Palmer',
|
"Antarctica/Palmer",
|
||||||
'Antarctica/Rothera',
|
"Antarctica/Rothera",
|
||||||
'Antarctica/Syowa',
|
"Antarctica/Syowa",
|
||||||
'Antarctica/Troll',
|
"Antarctica/Troll",
|
||||||
'Antarctica/Vostok',
|
"Antarctica/Vostok",
|
||||||
'America/Argentina/Buenos_Aires',
|
"America/Argentina/Buenos_Aires",
|
||||||
'America/Argentina/Cordoba',
|
"America/Argentina/Cordoba",
|
||||||
'America/Argentina/Salta',
|
"America/Argentina/Salta",
|
||||||
'America/Argentina/Jujuy',
|
"America/Argentina/Jujuy",
|
||||||
'America/Argentina/Tucuman',
|
"America/Argentina/Tucuman",
|
||||||
'America/Argentina/Catamarca',
|
"America/Argentina/Catamarca",
|
||||||
'America/Argentina/La_Rioja',
|
"America/Argentina/La_Rioja",
|
||||||
'America/Argentina/San_Juan',
|
"America/Argentina/San_Juan",
|
||||||
'America/Argentina/Mendoza',
|
"America/Argentina/Mendoza",
|
||||||
'America/Argentina/San_Luis',
|
"America/Argentina/San_Luis",
|
||||||
'America/Argentina/Rio_Gallegos',
|
"America/Argentina/Rio_Gallegos",
|
||||||
'America/Argentina/Ushuaia',
|
"America/Argentina/Ushuaia",
|
||||||
'Pacific/Pago_Pago',
|
"Pacific/Pago_Pago",
|
||||||
'Europe/Vienna',
|
"Europe/Vienna",
|
||||||
'Australia/Lord_Howe',
|
"Australia/Lord_Howe",
|
||||||
'Antarctica/Macquarie',
|
"Antarctica/Macquarie",
|
||||||
'Australia/Hobart',
|
"Australia/Hobart",
|
||||||
'Australia/Currie',
|
"Australia/Currie",
|
||||||
'Australia/Melbourne',
|
"Australia/Melbourne",
|
||||||
'Australia/Sydney',
|
"Australia/Sydney",
|
||||||
'Australia/Broken_Hill',
|
"Australia/Broken_Hill",
|
||||||
'Australia/Brisbane',
|
"Australia/Brisbane",
|
||||||
'Australia/Lindeman',
|
"Australia/Lindeman",
|
||||||
'Australia/Adelaide',
|
"Australia/Adelaide",
|
||||||
'Australia/Darwin',
|
"Australia/Darwin",
|
||||||
'Australia/Perth',
|
"Australia/Perth",
|
||||||
'Australia/Eucla',
|
"Australia/Eucla",
|
||||||
'Asia/Baku',
|
"Asia/Baku",
|
||||||
'America/Barbados',
|
"America/Barbados",
|
||||||
'Asia/Dhaka',
|
"Asia/Dhaka",
|
||||||
'Europe/Brussels',
|
"Europe/Brussels",
|
||||||
'Europe/Sofia',
|
"Europe/Sofia",
|
||||||
'Atlantic/Bermuda',
|
"Atlantic/Bermuda",
|
||||||
'Asia/Brunei',
|
"Asia/Brunei",
|
||||||
'America/La_Paz',
|
"America/La_Paz",
|
||||||
'America/Noronha',
|
"America/Noronha",
|
||||||
'America/Belem',
|
"America/Belem",
|
||||||
'America/Fortaleza',
|
"America/Fortaleza",
|
||||||
'America/Recife',
|
"America/Recife",
|
||||||
'America/Araguaina',
|
"America/Araguaina",
|
||||||
'America/Maceio',
|
"America/Maceio",
|
||||||
'America/Bahia',
|
"America/Bahia",
|
||||||
'America/Sao_Paulo',
|
"America/Sao_Paulo",
|
||||||
'America/Campo_Grande',
|
"America/Campo_Grande",
|
||||||
'America/Cuiaba',
|
"America/Cuiaba",
|
||||||
'America/Santarem',
|
"America/Santarem",
|
||||||
'America/Porto_Velho',
|
"America/Porto_Velho",
|
||||||
'America/Boa_Vista',
|
"America/Boa_Vista",
|
||||||
'America/Manaus',
|
"America/Manaus",
|
||||||
'America/Eirunepe',
|
"America/Eirunepe",
|
||||||
'America/Rio_Branco',
|
"America/Rio_Branco",
|
||||||
'America/Nassau',
|
"America/Nassau",
|
||||||
'Asia/Thimphu',
|
"Asia/Thimphu",
|
||||||
'Europe/Minsk',
|
"Europe/Minsk",
|
||||||
'America/Belize',
|
"America/Belize",
|
||||||
'America/St_Johns',
|
"America/St_Johns",
|
||||||
'America/Halifax',
|
"America/Halifax",
|
||||||
'America/Glace_Bay',
|
"America/Glace_Bay",
|
||||||
'America/Moncton',
|
"America/Moncton",
|
||||||
'America/Goose_Bay',
|
"America/Goose_Bay",
|
||||||
'America/Blanc-Sablon',
|
"America/Blanc-Sablon",
|
||||||
'America/Toronto',
|
"America/Toronto",
|
||||||
'America/Nipigon',
|
"America/Nipigon",
|
||||||
'America/Thunder_Bay',
|
"America/Thunder_Bay",
|
||||||
'America/Iqaluit',
|
"America/Iqaluit",
|
||||||
'America/Pangnirtung',
|
"America/Pangnirtung",
|
||||||
'America/Atikokan',
|
"America/Atikokan",
|
||||||
'America/Winnipeg',
|
"America/Winnipeg",
|
||||||
'America/Rainy_River',
|
"America/Rainy_River",
|
||||||
'America/Resolute',
|
"America/Resolute",
|
||||||
'America/Rankin_Inlet',
|
"America/Rankin_Inlet",
|
||||||
'America/Regina',
|
"America/Regina",
|
||||||
'America/Swift_Current',
|
"America/Swift_Current",
|
||||||
'America/Edmonton',
|
"America/Edmonton",
|
||||||
'America/Cambridge_Bay',
|
"America/Cambridge_Bay",
|
||||||
'America/Yellowknife',
|
"America/Yellowknife",
|
||||||
'America/Inuvik',
|
"America/Inuvik",
|
||||||
'America/Creston',
|
"America/Creston",
|
||||||
'America/Dawson_Creek',
|
"America/Dawson_Creek",
|
||||||
'America/Fort_Nelson',
|
"America/Fort_Nelson",
|
||||||
'America/Vancouver',
|
"America/Vancouver",
|
||||||
'America/Whitehorse',
|
"America/Whitehorse",
|
||||||
'America/Dawson',
|
"America/Dawson",
|
||||||
'Indian/Cocos',
|
"Indian/Cocos",
|
||||||
'Europe/Zurich',
|
"Europe/Zurich",
|
||||||
'Africa/Abidjan',
|
"Africa/Abidjan",
|
||||||
'Pacific/Rarotonga',
|
"Pacific/Rarotonga",
|
||||||
'America/Santiago',
|
"America/Santiago",
|
||||||
'America/Punta_Arenas',
|
"America/Punta_Arenas",
|
||||||
'Pacific/Easter',
|
"Pacific/Easter",
|
||||||
'Asia/Shanghai',
|
"Asia/Shanghai",
|
||||||
'Asia/Urumqi',
|
"Asia/Urumqi",
|
||||||
'America/Bogota',
|
"America/Bogota",
|
||||||
'America/Costa_Rica',
|
"America/Costa_Rica",
|
||||||
'America/Havana',
|
"America/Havana",
|
||||||
'Atlantic/Cape_Verde',
|
"Atlantic/Cape_Verde",
|
||||||
'America/Curacao',
|
"America/Curacao",
|
||||||
'Indian/Christmas',
|
"Indian/Christmas",
|
||||||
'Asia/Nicosia',
|
"Asia/Nicosia",
|
||||||
'Asia/Famagusta',
|
"Asia/Famagusta",
|
||||||
'Europe/Prague',
|
"Europe/Prague",
|
||||||
'Europe/Berlin',
|
"Europe/Berlin",
|
||||||
'Europe/Copenhagen',
|
"Europe/Copenhagen",
|
||||||
'America/Santo_Domingo',
|
"America/Santo_Domingo",
|
||||||
'Africa/Algiers',
|
"Africa/Algiers",
|
||||||
'America/Guayaquil',
|
"America/Guayaquil",
|
||||||
'Pacific/Galapagos',
|
"Pacific/Galapagos",
|
||||||
'Europe/Tallinn',
|
"Europe/Tallinn",
|
||||||
'Africa/Cairo',
|
"Africa/Cairo",
|
||||||
'Africa/El_Aaiun',
|
"Africa/El_Aaiun",
|
||||||
'Europe/Madrid',
|
"Europe/Madrid",
|
||||||
'Africa/Ceuta',
|
"Africa/Ceuta",
|
||||||
'Atlantic/Canary',
|
"Atlantic/Canary",
|
||||||
'Europe/Helsinki',
|
"Europe/Helsinki",
|
||||||
'Pacific/Fiji',
|
"Pacific/Fiji",
|
||||||
'Atlantic/Stanley',
|
"Atlantic/Stanley",
|
||||||
'Pacific/Chuuk',
|
"Pacific/Chuuk",
|
||||||
'Pacific/Pohnpei',
|
"Pacific/Pohnpei",
|
||||||
'Pacific/Kosrae',
|
"Pacific/Kosrae",
|
||||||
'Atlantic/Faroe',
|
"Atlantic/Faroe",
|
||||||
'Europe/Paris',
|
"Europe/Paris",
|
||||||
'Europe/London',
|
"Europe/London",
|
||||||
'Asia/Tbilisi',
|
"Asia/Tbilisi",
|
||||||
'America/Cayenne',
|
"America/Cayenne",
|
||||||
'Africa/Accra',
|
"Africa/Accra",
|
||||||
'Europe/Gibraltar',
|
"Europe/Gibraltar",
|
||||||
'America/Godthab',
|
"America/Godthab",
|
||||||
'America/Danmarkshavn',
|
"America/Danmarkshavn",
|
||||||
'America/Scoresbysund',
|
"America/Scoresbysund",
|
||||||
'America/Thule',
|
"America/Thule",
|
||||||
'Europe/Athens',
|
"Europe/Athens",
|
||||||
'Atlantic/South_Georgia',
|
"Atlantic/South_Georgia",
|
||||||
'America/Guatemala',
|
"America/Guatemala",
|
||||||
'Pacific/Guam',
|
"Pacific/Guam",
|
||||||
'Africa/Bissau',
|
"Africa/Bissau",
|
||||||
'America/Guyana',
|
"America/Guyana",
|
||||||
'Asia/Hong_Kong',
|
"Asia/Hong_Kong",
|
||||||
'America/Tegucigalpa',
|
"America/Tegucigalpa",
|
||||||
'America/Port-au-Prince',
|
"America/Port-au-Prince",
|
||||||
'Europe/Budapest',
|
"Europe/Budapest",
|
||||||
'Asia/Jakarta',
|
"Asia/Jakarta",
|
||||||
'Asia/Pontianak',
|
"Asia/Pontianak",
|
||||||
'Asia/Makassar',
|
"Asia/Makassar",
|
||||||
'Asia/Jayapura',
|
"Asia/Jayapura",
|
||||||
'Europe/Dublin',
|
"Europe/Dublin",
|
||||||
'Asia/Jerusalem',
|
"Asia/Jerusalem",
|
||||||
'Asia/Kolkata',
|
"Asia/Kolkata",
|
||||||
'Indian/Chagos',
|
"Indian/Chagos",
|
||||||
'Asia/Baghdad',
|
"Asia/Baghdad",
|
||||||
'Asia/Tehran',
|
"Asia/Tehran",
|
||||||
'Atlantic/Reykjavik',
|
"Atlantic/Reykjavik",
|
||||||
'Europe/Rome',
|
"Europe/Rome",
|
||||||
'America/Jamaica',
|
"America/Jamaica",
|
||||||
'Asia/Amman',
|
"Asia/Amman",
|
||||||
'Asia/Tokyo',
|
"Asia/Tokyo",
|
||||||
'Africa/Nairobi',
|
"Africa/Nairobi",
|
||||||
'Asia/Bishkek',
|
"Asia/Bishkek",
|
||||||
'Pacific/Tarawa',
|
"Pacific/Tarawa",
|
||||||
'Pacific/Enderbury',
|
"Pacific/Enderbury",
|
||||||
'Pacific/Kiritimati',
|
"Pacific/Kiritimati",
|
||||||
'Asia/Pyongyang',
|
"Asia/Pyongyang",
|
||||||
'Asia/Seoul',
|
"Asia/Seoul",
|
||||||
'Asia/Almaty',
|
"Asia/Almaty",
|
||||||
'Asia/Qyzylorda',
|
"Asia/Qyzylorda",
|
||||||
'Asia/Aqtobe',
|
"Asia/Aqtobe",
|
||||||
'Asia/Aqtau',
|
"Asia/Aqtau",
|
||||||
'Asia/Atyrau',
|
"Asia/Atyrau",
|
||||||
'Asia/Oral',
|
"Asia/Oral",
|
||||||
'Asia/Beirut',
|
"Asia/Beirut",
|
||||||
'Asia/Colombo',
|
"Asia/Colombo",
|
||||||
'Africa/Monrovia',
|
"Africa/Monrovia",
|
||||||
'Europe/Vilnius',
|
"Europe/Vilnius",
|
||||||
'Europe/Luxembourg',
|
"Europe/Luxembourg",
|
||||||
'Europe/Riga',
|
"Europe/Riga",
|
||||||
'Africa/Tripoli',
|
"Africa/Tripoli",
|
||||||
'Africa/Casablanca',
|
"Africa/Casablanca",
|
||||||
'Europe/Monaco',
|
"Europe/Monaco",
|
||||||
'Europe/Chisinau',
|
"Europe/Chisinau",
|
||||||
'Pacific/Majuro',
|
"Pacific/Majuro",
|
||||||
'Pacific/Kwajalein',
|
"Pacific/Kwajalein",
|
||||||
'Asia/Yangon',
|
"Asia/Yangon",
|
||||||
'Asia/Ulaanbaatar',
|
"Asia/Ulaanbaatar",
|
||||||
'Asia/Hovd',
|
"Asia/Hovd",
|
||||||
'Asia/Choibalsan',
|
"Asia/Choibalsan",
|
||||||
'Asia/Macau',
|
"Asia/Macau",
|
||||||
'America/Martinique',
|
"America/Martinique",
|
||||||
'Europe/Malta',
|
"Europe/Malta",
|
||||||
'Indian/Mauritius',
|
"Indian/Mauritius",
|
||||||
'Indian/Maldives',
|
"Indian/Maldives",
|
||||||
'America/Mexico_City',
|
"America/Mexico_City",
|
||||||
'America/Cancun',
|
"America/Cancun",
|
||||||
'America/Merida',
|
"America/Merida",
|
||||||
'America/Monterrey',
|
"America/Monterrey",
|
||||||
'America/Matamoros',
|
"America/Matamoros",
|
||||||
'America/Mazatlan',
|
"America/Mazatlan",
|
||||||
'America/Chihuahua',
|
"America/Chihuahua",
|
||||||
'America/Ojinaga',
|
"America/Ojinaga",
|
||||||
'America/Hermosillo',
|
"America/Hermosillo",
|
||||||
'America/Tijuana',
|
"America/Tijuana",
|
||||||
'America/Bahia_Banderas',
|
"America/Bahia_Banderas",
|
||||||
'Asia/Kuala_Lumpur',
|
"Asia/Kuala_Lumpur",
|
||||||
'Asia/Kuching',
|
"Asia/Kuching",
|
||||||
'Africa/Maputo',
|
"Africa/Maputo",
|
||||||
'Africa/Windhoek',
|
"Africa/Windhoek",
|
||||||
'Pacific/Noumea',
|
"Pacific/Noumea",
|
||||||
'Pacific/Norfolk',
|
"Pacific/Norfolk",
|
||||||
'Africa/Lagos',
|
"Africa/Lagos",
|
||||||
'America/Managua',
|
"America/Managua",
|
||||||
'Europe/Amsterdam',
|
"Europe/Amsterdam",
|
||||||
'Europe/Oslo',
|
"Europe/Oslo",
|
||||||
'Asia/Kathmandu',
|
"Asia/Kathmandu",
|
||||||
'Pacific/Nauru',
|
"Pacific/Nauru",
|
||||||
'Pacific/Niue',
|
"Pacific/Niue",
|
||||||
'Pacific/Auckland',
|
"Pacific/Auckland",
|
||||||
'Pacific/Chatham',
|
"Pacific/Chatham",
|
||||||
'America/Panama',
|
"America/Panama",
|
||||||
'America/Lima',
|
"America/Lima",
|
||||||
'Pacific/Tahiti',
|
"Pacific/Tahiti",
|
||||||
'Pacific/Marquesas',
|
"Pacific/Marquesas",
|
||||||
'Pacific/Gambier',
|
"Pacific/Gambier",
|
||||||
'Pacific/Port_Moresby',
|
"Pacific/Port_Moresby",
|
||||||
'Pacific/Bougainville',
|
"Pacific/Bougainville",
|
||||||
'Asia/Manila',
|
"Asia/Manila",
|
||||||
'Asia/Karachi',
|
"Asia/Karachi",
|
||||||
'Europe/Warsaw',
|
"Europe/Warsaw",
|
||||||
'America/Miquelon',
|
"America/Miquelon",
|
||||||
'Pacific/Pitcairn',
|
"Pacific/Pitcairn",
|
||||||
'America/Puerto_Rico',
|
"America/Puerto_Rico",
|
||||||
'Asia/Gaza',
|
"Asia/Gaza",
|
||||||
'Asia/Hebron',
|
"Asia/Hebron",
|
||||||
'Europe/Lisbon',
|
"Europe/Lisbon",
|
||||||
'Atlantic/Madeira',
|
"Atlantic/Madeira",
|
||||||
'Atlantic/Azores',
|
"Atlantic/Azores",
|
||||||
'Pacific/Palau',
|
"Pacific/Palau",
|
||||||
'America/Asuncion',
|
"America/Asuncion",
|
||||||
'Asia/Qatar',
|
"Asia/Qatar",
|
||||||
'Indian/Reunion',
|
"Indian/Reunion",
|
||||||
'Europe/Bucharest',
|
"Europe/Bucharest",
|
||||||
'Europe/Belgrade',
|
"Europe/Belgrade",
|
||||||
'Europe/Kaliningrad',
|
"Europe/Kaliningrad",
|
||||||
'Europe/Moscow',
|
"Europe/Moscow",
|
||||||
'Europe/Simferopol',
|
"Europe/Simferopol",
|
||||||
'Europe/Kirov',
|
"Europe/Kirov",
|
||||||
'Europe/Astrakhan',
|
"Europe/Astrakhan",
|
||||||
'Europe/Volgograd',
|
"Europe/Volgograd",
|
||||||
'Europe/Saratov',
|
"Europe/Saratov",
|
||||||
'Europe/Ulyanovsk',
|
"Europe/Ulyanovsk",
|
||||||
'Europe/Samara',
|
"Europe/Samara",
|
||||||
'Asia/Yekaterinburg',
|
"Asia/Yekaterinburg",
|
||||||
'Asia/Omsk',
|
"Asia/Omsk",
|
||||||
'Asia/Novosibirsk',
|
"Asia/Novosibirsk",
|
||||||
'Asia/Barnaul',
|
"Asia/Barnaul",
|
||||||
'Asia/Tomsk',
|
"Asia/Tomsk",
|
||||||
'Asia/Novokuznetsk',
|
"Asia/Novokuznetsk",
|
||||||
'Asia/Krasnoyarsk',
|
"Asia/Krasnoyarsk",
|
||||||
'Asia/Irkutsk',
|
"Asia/Irkutsk",
|
||||||
'Asia/Chita',
|
"Asia/Chita",
|
||||||
'Asia/Yakutsk',
|
"Asia/Yakutsk",
|
||||||
'Asia/Khandyga',
|
"Asia/Khandyga",
|
||||||
'Asia/Vladivostok',
|
"Asia/Vladivostok",
|
||||||
'Asia/Ust-Nera',
|
"Asia/Ust-Nera",
|
||||||
'Asia/Magadan',
|
"Asia/Magadan",
|
||||||
'Asia/Sakhalin',
|
"Asia/Sakhalin",
|
||||||
'Asia/Srednekolymsk',
|
"Asia/Srednekolymsk",
|
||||||
'Asia/Kamchatka',
|
"Asia/Kamchatka",
|
||||||
'Asia/Anadyr',
|
"Asia/Anadyr",
|
||||||
'Asia/Riyadh',
|
"Asia/Riyadh",
|
||||||
'Pacific/Guadalcanal',
|
"Pacific/Guadalcanal",
|
||||||
'Indian/Mahe',
|
"Indian/Mahe",
|
||||||
'Africa/Khartoum',
|
"Africa/Khartoum",
|
||||||
'Europe/Stockholm',
|
"Europe/Stockholm",
|
||||||
'Asia/Singapore',
|
"Asia/Singapore",
|
||||||
'America/Paramaribo',
|
"America/Paramaribo",
|
||||||
'Africa/Juba',
|
"Africa/Juba",
|
||||||
'Africa/Sao_Tome',
|
"Africa/Sao_Tome",
|
||||||
'America/El_Salvador',
|
"America/El_Salvador",
|
||||||
'Asia/Damascus',
|
"Asia/Damascus",
|
||||||
'America/Grand_Turk',
|
"America/Grand_Turk",
|
||||||
'Africa/Ndjamena',
|
"Africa/Ndjamena",
|
||||||
'Indian/Kerguelen',
|
"Indian/Kerguelen",
|
||||||
'Asia/Bangkok',
|
"Asia/Bangkok",
|
||||||
'Asia/Dushanbe',
|
"Asia/Dushanbe",
|
||||||
'Pacific/Fakaofo',
|
"Pacific/Fakaofo",
|
||||||
'Asia/Dili',
|
"Asia/Dili",
|
||||||
'Asia/Ashgabat',
|
"Asia/Ashgabat",
|
||||||
'Africa/Tunis',
|
"Africa/Tunis",
|
||||||
'Pacific/Tongatapu',
|
"Pacific/Tongatapu",
|
||||||
'Europe/Istanbul',
|
"Europe/Istanbul",
|
||||||
'America/Port_of_Spain',
|
"America/Port_of_Spain",
|
||||||
'Pacific/Funafuti',
|
"Pacific/Funafuti",
|
||||||
'Asia/Taipei',
|
"Asia/Taipei",
|
||||||
'Europe/Kiev',
|
"Europe/Kiev",
|
||||||
'Europe/Uzhgorod',
|
"Europe/Uzhgorod",
|
||||||
'Europe/Zaporozhye',
|
"Europe/Zaporozhye",
|
||||||
'Pacific/Wake',
|
"Pacific/Wake",
|
||||||
'America/New_York',
|
"America/New_York",
|
||||||
'America/Detroit',
|
"America/Detroit",
|
||||||
'America/Kentucky/Louisville',
|
"America/Kentucky/Louisville",
|
||||||
'America/Kentucky/Monticello',
|
"America/Kentucky/Monticello",
|
||||||
'America/Indiana/Indianapolis',
|
"America/Indiana/Indianapolis",
|
||||||
'America/Indiana/Vincennes',
|
"America/Indiana/Vincennes",
|
||||||
'America/Indiana/Winamac',
|
"America/Indiana/Winamac",
|
||||||
'America/Indiana/Marengo',
|
"America/Indiana/Marengo",
|
||||||
'America/Indiana/Petersburg',
|
"America/Indiana/Petersburg",
|
||||||
'America/Indiana/Vevay',
|
"America/Indiana/Vevay",
|
||||||
'America/Chicago',
|
"America/Chicago",
|
||||||
'America/Indiana/Tell_City',
|
"America/Indiana/Tell_City",
|
||||||
'America/Indiana/Knox',
|
"America/Indiana/Knox",
|
||||||
'America/Menominee',
|
"America/Menominee",
|
||||||
'America/North_Dakota/Center',
|
"America/North_Dakota/Center",
|
||||||
'America/North_Dakota/New_Salem',
|
"America/North_Dakota/New_Salem",
|
||||||
'America/North_Dakota/Beulah',
|
"America/North_Dakota/Beulah",
|
||||||
'America/Denver',
|
"America/Denver",
|
||||||
'America/Boise',
|
"America/Boise",
|
||||||
'America/Phoenix',
|
"America/Phoenix",
|
||||||
'America/Los_Angeles',
|
"America/Los_Angeles",
|
||||||
'America/Anchorage',
|
"America/Anchorage",
|
||||||
'America/Juneau',
|
"America/Juneau",
|
||||||
'America/Sitka',
|
"America/Sitka",
|
||||||
'America/Metlakatla',
|
"America/Metlakatla",
|
||||||
'America/Yakutat',
|
"America/Yakutat",
|
||||||
'America/Nome',
|
"America/Nome",
|
||||||
'America/Adak',
|
"America/Adak",
|
||||||
'Pacific/Honolulu',
|
"Pacific/Honolulu",
|
||||||
'America/Montevideo',
|
"America/Montevideo",
|
||||||
'Asia/Samarkand',
|
"Asia/Samarkand",
|
||||||
'Asia/Tashkent',
|
"Asia/Tashkent",
|
||||||
'America/Caracas',
|
"America/Caracas",
|
||||||
'Asia/Ho_Chi_Minh',
|
"Asia/Ho_Chi_Minh",
|
||||||
'Pacific/Efate',
|
"Pacific/Efate",
|
||||||
'Pacific/Wallis',
|
"Pacific/Wallis",
|
||||||
'Pacific/Apia',
|
"Pacific/Apia",
|
||||||
'Africa/Johannesburg',
|
"Africa/Johannesburg",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export function timezoneList() {
|
export function timezoneList() {
|
||||||
|
|
||||||
let result = [];
|
let result = [];
|
||||||
|
@ -404,12 +391,14 @@ export function timezoneList() {
|
||||||
result.sort((a, b) => {
|
result.sort((a, b) => {
|
||||||
if (a.time > b.time) {
|
if (a.time > b.time) {
|
||||||
return 1;
|
return 1;
|
||||||
} else if (b.time > a.time) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (b.time > a.time) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
})
|
})
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
}
|
||||||
|
|
35
src/util.js
Normal file
35
src/util.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = exports.appName = void 0;
|
||||||
|
exports.appName = "Uptime Kuma";
|
||||||
|
exports.DOWN = 0;
|
||||||
|
exports.UP = 1;
|
||||||
|
exports.PENDING = 2;
|
||||||
|
function flipStatus(s) {
|
||||||
|
if (s === exports.UP) {
|
||||||
|
return exports.DOWN;
|
||||||
|
}
|
||||||
|
if (s === exports.DOWN) {
|
||||||
|
return exports.UP;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
exports.flipStatus = flipStatus;
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
exports.sleep = sleep;
|
||||||
|
function ucfirst(str) {
|
||||||
|
if (!str) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
const firstLetter = str.substr(0, 1);
|
||||||
|
return firstLetter.toUpperCase() + str.substr(1);
|
||||||
|
}
|
||||||
|
exports.ucfirst = ucfirst;
|
||||||
|
function debug(msg) {
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.debug = debug;
|
44
src/util.ts
Normal file
44
src/util.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// Common Util for frontend and backend
|
||||||
|
// Backend uses the compiled file util.js
|
||||||
|
// Frontend uses util.ts
|
||||||
|
// Need to run "tsc" to compile if there are any changes.
|
||||||
|
|
||||||
|
export const appName = "Uptime Kuma";
|
||||||
|
export const DOWN = 0;
|
||||||
|
export const UP = 1;
|
||||||
|
export const PENDING = 2;
|
||||||
|
|
||||||
|
export function flipStatus(s) {
|
||||||
|
if (s === UP) {
|
||||||
|
return DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s === DOWN) {
|
||||||
|
return UP;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHP's ucfirst
|
||||||
|
* @param str
|
||||||
|
*/
|
||||||
|
export function ucfirst(str) {
|
||||||
|
if (! str) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstLetter = str.substr(0, 1);
|
||||||
|
return firstLetter.toUpperCase() + str.substr(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debug(msg) {
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
}
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compileOnSave": true,
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2018",
|
||||||
|
"module": "commonjs",
|
||||||
|
"removeComments": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"sourceMap": false,
|
||||||
|
"files.insertFinalNewline": true
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./server/util.ts"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue