mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-01 11:22:34 +02:00
Merge branch 'master' into reduce_docker_image_size
This commit is contained in:
commit
713bbe0014
27 changed files with 825 additions and 228 deletions
|
@ -16,3 +16,6 @@ indent_size = 2
|
||||||
|
|
||||||
[*.yml]
|
[*.yml]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.vue]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
38
.eslintrc.js
38
.eslintrc.js
|
@ -2,48 +2,43 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
commonjs: true,
|
commonjs: true,
|
||||||
es2017: true,
|
es2020: true,
|
||||||
node: true,
|
node: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:vue/vue3-recommended",
|
"plugin:vue/vue3-recommended",
|
||||||
],
|
],
|
||||||
|
parser: "vue-eslint-parser",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2018,
|
parser: "@babel/eslint-parser",
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
|
requireConfigFile: false,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// override/add rules settings here, such as:
|
// override/add rules settings here, such as:
|
||||||
// 'vue/no-unused-vars': 'error'
|
// 'vue/no-unused-vars': 'error'
|
||||||
"no-unused-vars": "warn",
|
"no-unused-vars": "warn",
|
||||||
indent: ["error", 4],
|
indent: [
|
||||||
|
"error",
|
||||||
|
4,
|
||||||
|
{
|
||||||
|
ignoredNodes: ["TemplateLiteral"],
|
||||||
|
SwitchCase: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
quotes: ["warn", "double"],
|
quotes: ["warn", "double"],
|
||||||
//semi: ['off', 'never'],
|
//semi: ['off', 'never'],
|
||||||
"vue/html-indent": ["warn", 4], // default: 2
|
"vue/html-indent": ["warn", 4], // default: 2
|
||||||
"vue/max-attributes-per-line": "off",
|
"vue/max-attributes-per-line": "off",
|
||||||
"vue/singleline-html-element-content-newline": "off",
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
|
"vue/html-self-closing": "off",
|
||||||
"no-multi-spaces": ["error", {
|
"no-multi-spaces": ["error", {
|
||||||
ignoreEOLComments: true,
|
ignoreEOLComments: true,
|
||||||
}],
|
}],
|
||||||
"curly": "error",
|
"curly": "error",
|
||||||
"object-curly-spacing": ["error", "always"],
|
"object-curly-spacing": ["error", "always"],
|
||||||
"object-curly-newline": ["error", {
|
"object-curly-newline": "off",
|
||||||
"ObjectExpression": {
|
|
||||||
"minProperties": 1,
|
|
||||||
},
|
|
||||||
"ObjectPattern": {
|
|
||||||
"multiline": true,
|
|
||||||
"minProperties": 2,
|
|
||||||
},
|
|
||||||
"ImportDeclaration": {
|
|
||||||
"multiline": true,
|
|
||||||
},
|
|
||||||
"ExportDeclaration": {
|
|
||||||
"multiline": true,
|
|
||||||
//'minProperties': 2,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
"object-property-newline": "error",
|
"object-property-newline": "error",
|
||||||
"comma-spacing": "error",
|
"comma-spacing": "error",
|
||||||
"brace-style": "error",
|
"brace-style": "error",
|
||||||
|
@ -53,6 +48,9 @@ module.exports = {
|
||||||
"space-infix-ops": "warn",
|
"space-infix-ops": "warn",
|
||||||
"arrow-spacing": "warn",
|
"arrow-spacing": "warn",
|
||||||
"no-trailing-spaces": "warn",
|
"no-trailing-spaces": "warn",
|
||||||
|
"no-constant-condition": ["error", {
|
||||||
|
"checkLoops": false,
|
||||||
|
}],
|
||||||
"space-before-blocks": "warn",
|
"space-before-blocks": "warn",
|
||||||
//'no-console': 'warn',
|
//'no-console': 'warn',
|
||||||
"no-extra-boolean-cast": "off",
|
"no-extra-boolean-cast": "off",
|
||||||
|
@ -70,6 +68,6 @@ module.exports = {
|
||||||
"array-bracket-newline": ["error", "consistent"],
|
"array-bracket-newline": ["error", "consistent"],
|
||||||
"eol-last": ["error", "always"],
|
"eol-last": ["error", "always"],
|
||||||
//'prefer-template': 'error',
|
//'prefer-template': 'error',
|
||||||
"comma-dangle": ["warn", "always-multiline"],
|
"comma-dangle": ["warn", "only-multiline"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
35
README.md
35
README.md
|
@ -15,12 +15,12 @@ It is a self-hosted monitoring tool like "Uptime Robot".
|
||||||
|
|
||||||
* Monitoring uptime for HTTP(s) / TCP / Ping.
|
* Monitoring uptime for HTTP(s) / TCP / Ping.
|
||||||
* Fancy, Reactive, Fast UI/UX.
|
* Fancy, Reactive, Fast UI/UX.
|
||||||
* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.
|
* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.
|
||||||
* 20 seconds interval.
|
* 20 seconds interval.
|
||||||
|
|
||||||
# How to Use
|
# How to Use
|
||||||
|
|
||||||
### Docker
|
## Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a volume
|
# Create a volume
|
||||||
|
@ -38,9 +38,9 @@ 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.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/louislam/uptime-kuma.git
|
git clone https://github.com/louislam/uptime-kuma.git
|
||||||
|
@ -62,12 +62,25 @@ pm2 start npm --name uptime-kuma -- run start-server -- --port=80 --hostname=0.0
|
||||||
|
|
||||||
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.8)
|
||||||
|
-->
|
||||||
|
|
||||||
[](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
|
||||||
|
@ -88,7 +101,7 @@ pm2 restart uptime-kuma
|
||||||
|
|
||||||
# What's Next?
|
# What's Next?
|
||||||
|
|
||||||
I will mark requests/issues to the next milestone.
|
I will mark requests/issues to the next milestone.
|
||||||
https://github.com/louislam/uptime-kuma/milestones
|
https://github.com/louislam/uptime-kuma/milestones
|
||||||
|
|
||||||
# More Screenshots
|
# More Screenshots
|
||||||
|
@ -104,10 +117,10 @@ Telegram Notification Sample:
|
||||||
|
|
||||||
# Motivation
|
# Motivation
|
||||||
|
|
||||||
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained.
|
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained.
|
||||||
* Want to build a fancy UI.
|
* Want to build a fancy UI.
|
||||||
* Learn Vue 3 and vite.js.
|
* Learn Vue 3 and vite.js.
|
||||||
* Show the power of Bootstrap 5.
|
* Show the power of Bootstrap 5.
|
||||||
* Try to use WebSocket with SPA instead of REST API.
|
* Try to use WebSocket with SPA instead of REST API.
|
||||||
* Deploy my first Docker image to Docker Hub.
|
* Deploy my first Docker image to Docker Hub.
|
||||||
|
|
||||||
|
@ -119,6 +132,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.
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
-- 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;
|
PRAGMA foreign_keys=off;
|
||||||
|
|
||||||
BEGIN TRANSACTION;
|
BEGIN TRANSACTION;
|
||||||
|
@ -20,11 +22,12 @@ create table monitor_dg_tmp
|
||||||
port INTEGER,
|
port INTEGER,
|
||||||
created_date DATETIME,
|
created_date DATETIME,
|
||||||
keyword VARCHAR(255),
|
keyword VARCHAR(255),
|
||||||
|
maxretries INTEGER NOT NULL DEFAULT 0,
|
||||||
ignore_tls BOOLEAN default 0 not null,
|
ignore_tls BOOLEAN default 0 not null,
|
||||||
upside_down 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) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor;
|
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;
|
drop table monitor;
|
||||||
|
|
||||||
|
|
23
dockerfile
23
dockerfile
|
@ -11,21 +11,9 @@ 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 py3-six cargo
|
|
||||||
RUN apk add --no-cache --virtual .build-deps python3 py3-pip libffi-dev musl-dev openssl-dev python3-dev && \
|
|
||||||
pip3 install apprise && \
|
|
||||||
pip3 cache purge && \
|
|
||||||
rm -rf /root/.cache && \
|
|
||||||
apk del .build-deps
|
|
||||||
RUN apprise --version
|
RUN apprise --version
|
||||||
|
|
||||||
# New things add here
|
# New things add here
|
||||||
|
@ -33,8 +21,9 @@ RUN apprise --version
|
||||||
FROM release-base AS build
|
FROM release-base AS build
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm install
|
RUN npm install && \
|
||||||
RUN npm run build
|
npm run build && \
|
||||||
|
npm prune
|
||||||
|
|
||||||
FROM release-base AS release-final
|
FROM release-base AS release-final
|
||||||
|
|
||||||
|
|
50
package-lock.json
generated
50
package-lock.json
generated
|
@ -96,6 +96,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@babel/eslint-parser": {
|
||||||
|
"version": "7.14.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.14.7.tgz",
|
||||||
|
"integrity": "sha512-6WPwZqO5priAGIwV6msJcdc9TsEPzYeYdS/Xuoap+/ihkgN6dzHp2bcAAwyWZ5bLzk0vvjDmKvRwkqNaiJ8BiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"eslint-scope": "^5.1.1",
|
||||||
|
"eslint-visitor-keys": "^2.1.0",
|
||||||
|
"semver": "^6.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@babel/generator": {
|
"@babel/generator": {
|
||||||
"version": "7.14.8",
|
"version": "7.14.8",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz",
|
||||||
|
@ -600,6 +619,16 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/bootstrap": {
|
||||||
|
"version": "5.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.0.17.tgz",
|
||||||
|
"integrity": "sha512-uQQQ3p+zw10VjZLvtCuKWI6QgVCYEnK/yHnno3gyEhikfQdiZexS2XPxjWRboGmX135o470GkmCta9eAgQMVLQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@popperjs/core": "^2.9.2",
|
||||||
|
"@types/jquery": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/component-emitter": {
|
"@types/component-emitter": {
|
||||||
"version": "1.2.10",
|
"version": "1.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
||||||
|
@ -676,6 +705,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.1.tgz",
|
||||||
"integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q=="
|
"integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q=="
|
||||||
},
|
},
|
||||||
|
"@types/jquery": {
|
||||||
|
"version": "3.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.6.tgz",
|
||||||
|
"integrity": "sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/sizzle": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/keygrip": {
|
"@types/keygrip": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz",
|
||||||
|
@ -760,6 +798,12 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/sizzle": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/unist": {
|
"@types/unist": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
||||||
|
@ -6673,6 +6717,12 @@
|
||||||
"is-typedarray": "^1.0.0"
|
"is-typedarray": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"typescript": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"unc-path-regex": {
|
"unc-path-regex": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": "14.*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
|
@ -56,6 +56,8 @@
|
||||||
"vue-toastification": "^2.0.0-rc.1"
|
"vue-toastification": "^2.0.0-rc.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/eslint-parser": "^7.13.10",
|
||||||
|
"@types/bootstrap": "^5.0.17",
|
||||||
"@vitejs/plugin-legacy": "^1.5.0",
|
"@vitejs/plugin-legacy": "^1.5.0",
|
||||||
"@vitejs/plugin-vue": "^1.3.0",
|
"@vitejs/plugin-vue": "^1.3.0",
|
||||||
"@vue/compiler-sfc": "^3.1.5",
|
"@vue/compiler-sfc": "^3.1.5",
|
||||||
|
@ -66,6 +68,7 @@
|
||||||
"stylelint": "^13.13.1",
|
"stylelint": "^13.13.1",
|
||||||
"stylelint-config-recommended": "^5.0.0",
|
"stylelint-config-recommended": "^5.0.0",
|
||||||
"stylelint-config-standard": "^22.0.0",
|
"stylelint-config-standard": "^22.0.0",
|
||||||
|
"typescript": "^4.3.5",
|
||||||
"vite": "^2.4.4"
|
"vite": "^2.4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
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");
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
|
|
||||||
|
@ -95,7 +96,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 +113,7 @@ class Database {
|
||||||
}
|
}
|
||||||
console.log("SQLite closed")
|
console.log("SQLite closed")
|
||||||
|
|
||||||
process.removeListener('unhandledRejection', listener);
|
process.removeListener("unhandledRejection", listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,16 @@
|
||||||
const https = require('https');
|
const https = require("https");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const utc = require('dayjs/plugin/utc')
|
const utc = require("dayjs/plugin/utc")
|
||||||
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 {Prometheus} = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const {debug, UP, DOWN, PENDING} = require("../util");
|
const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util");
|
||||||
const {tcping, ping, checkCertificate} = require("../util-server");
|
const { tcping, ping, checkCertificate } = require("../util-server");
|
||||||
const {R} = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const {BeanModel} = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
const {Notification} = require("../notification")
|
const { Notification } = require("../notification")
|
||||||
|
|
||||||
// Use Custom agent to disable session reuse
|
|
||||||
// https://github.com/nodejs/node/issues/3940
|
|
||||||
const customAgent = new https.Agent({
|
|
||||||
maxCachedSessions: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
|
@ -30,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) {
|
||||||
|
@ -49,10 +43,28 @@ 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(),
|
||||||
|
notificationIDList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
getIgnoreTls() {
|
||||||
|
return Boolean(this.ignoreTls)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isUpsideDown() {
|
||||||
|
return Boolean(this.upsideDown);
|
||||||
|
}
|
||||||
|
|
||||||
start(io) {
|
start(io) {
|
||||||
let previousBeat = null;
|
let previousBeat = null;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
|
@ -63,7 +75,7 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
if (! previousBeat) {
|
if (! previousBeat) {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
this.id
|
this.id,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,9 +86,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;
|
||||||
}
|
}
|
||||||
|
@ -84,9 +100,17 @@ 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(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
bean.msg = `${res.status} - ${res.statusText}`
|
bean.msg = `${res.status} - ${res.statusText}`
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
@ -124,7 +148,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 = ""
|
||||||
|
@ -136,14 +159,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]
|
||||||
|
@ -168,8 +206,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;
|
||||||
|
@ -181,7 +219,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,7 +232,6 @@ class Monitor extends BeanModel {
|
||||||
bean.important = false;
|
bean.important = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -221,9 +258,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);
|
||||||
|
@ -232,10 +272,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");
|
||||||
|
@ -264,15 +308,15 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
@ -294,7 +338,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;
|
||||||
|
@ -318,7 +362,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) {
|
||||||
|
@ -339,8 +383,6 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -235,6 +235,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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,46 @@
|
||||||
console.log("Welcome to Uptime Kuma ")
|
console.log("Welcome to Uptime Kuma")
|
||||||
console.log("Importing libraries")
|
|
||||||
const express = require("express");
|
const { sleep, debug } = require("../src/util");
|
||||||
const http = require("http");
|
|
||||||
const { Server } = require("socket.io");
|
console.log("Importing Node libraries")
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const { R } = require("redbean-node");
|
|
||||||
const jwt = require("jsonwebtoken");
|
|
||||||
const Monitor = require("./model/monitor");
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { getSettings } = require("./util-server");
|
const http = require("http");
|
||||||
const { Notification } = require("./notification")
|
|
||||||
|
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");
|
const gracefulShutdown = require("http-graceful-shutdown");
|
||||||
const Database = require("./database");
|
debug("Importing prometheus-api-metrics");
|
||||||
const { sleep } = require("./util");
|
|
||||||
const args = require("args-parser")(process.argv);
|
|
||||||
const prometheusAPIMetrics = require("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 { basicAuth } = require("./auth");
|
||||||
const { login } = require("./auth");
|
const { login } = require("./auth");
|
||||||
const passwordHash = require("./password-hash");
|
const passwordHash = require("./password-hash");
|
||||||
|
|
||||||
|
const args = require("args-parser")(process.argv);
|
||||||
|
|
||||||
const version = require("../package.json").version;
|
const version = require("../package.json").version;
|
||||||
const hostname = args.host || "0.0.0.0"
|
const hostname = process.env.HOST || args.host || "0.0.0.0"
|
||||||
const port = process.env.PORT || args.port || 3001
|
const port = parseInt(process.env.PORT || args.port || 3001);
|
||||||
|
|
||||||
console.info("Version: " + version)
|
console.info("Version: " + version)
|
||||||
|
|
||||||
|
@ -94,11 +114,18 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
socket.emit("setup")
|
socket.emit("setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await setting("disableAuth")) {
|
||||||
|
console.log("Disabled Auth: auto login to admin")
|
||||||
|
await afterLogin(socket, await R.findOne("user", " username = 'admin' "))
|
||||||
|
}
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
totalClient--;
|
totalClient--;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ***************************
|
||||||
// Public API
|
// Public API
|
||||||
|
// ***************************
|
||||||
|
|
||||||
socket.on("loginByToken", async (token, callback) => {
|
socket.on("loginByToken", async (token, callback) => {
|
||||||
|
|
||||||
|
@ -191,8 +218,11 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ***************************
|
||||||
// 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)
|
||||||
|
@ -224,6 +254,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Edit a monitor
|
||||||
socket.on("editMonitor", async (monitor, callback) => {
|
socket.on("editMonitor", async (monitor, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket)
|
checkLogin(socket)
|
||||||
|
@ -242,6 +273,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
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;
|
||||||
|
|
||||||
await R.store(bean)
|
await R.store(bean)
|
||||||
|
|
||||||
|
@ -397,13 +430,32 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("setSettings", async (data, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket)
|
||||||
|
|
||||||
|
await setSettings("general", data)
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved"
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -553,6 +605,8 @@ async function afterLogin(socket, user) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendNotificationList(socket)
|
sendNotificationList(socket)
|
||||||
|
|
||||||
|
socket.emit("autoLogin")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMonitorJSONList(userID) {
|
async function getMonitorJSONList(userID) {
|
||||||
|
|
|
@ -40,9 +40,15 @@ 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 {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (e) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.setSetting = async function (key, value) {
|
exports.setSetting = async function (key, value) {
|
||||||
|
@ -53,24 +59,53 @@ exports.setSetting = async function (key, value) {
|
||||||
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) {
|
||||||
result[row.key] = row.value;
|
try {
|
||||||
|
result[row.key] = JSON.parse(row.value);
|
||||||
|
} catch (e) {
|
||||||
|
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
|
||||||
// return an object containing the certificate information
|
// return an object containing the certificate information
|
||||||
|
|
|
@ -1,25 +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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,10 +13,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
|
<button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
|
||||||
Yes
|
{{ yesText }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
No
|
{{ noText }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,6 +33,14 @@ export default {
|
||||||
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,
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
<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 {
|
||||||
|
|
||||||
|
|
|
@ -13,33 +13,16 @@
|
||||||
<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 id="type" v-model="notification.type" class="form-select">
|
<select id="type" v-model="notification.type" class="form-select">
|
||||||
<option value="telegram">
|
<option value="telegram">Telegram</option>
|
||||||
Telegram
|
<option value="webhook">Webhook</option>
|
||||||
</option>
|
<option value="smtp">Email (SMTP)</option>
|
||||||
<option value="webhook">
|
<option value="discord">Discord</option>
|
||||||
Webhook
|
<option value="signal">Signal</option>
|
||||||
</option>
|
<option value="gotify">Gotify</option>
|
||||||
<option value="smtp">
|
<option value="slack">Slack</option>
|
||||||
Email (SMTP)
|
<option value="pushover">Pushover</option>
|
||||||
</option>
|
<option value="lunasea">LunaSea</option>
|
||||||
<option value="discord">
|
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||||
Discord
|
|
||||||
</option>
|
|
||||||
<option value="signal">
|
|
||||||
Signal
|
|
||||||
</option>
|
|
||||||
<option value="gotify">
|
|
||||||
Gotify
|
|
||||||
</option>
|
|
||||||
<option value="slack">
|
|
||||||
Slack
|
|
||||||
</option>
|
|
||||||
<option value="pushover">
|
|
||||||
Pushover
|
|
||||||
</option>
|
|
||||||
<option value="apprise">
|
|
||||||
Apprise (Support 50+ Notification services)
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -323,6 +306,17 @@
|
||||||
</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 v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||||
|
@ -345,9 +339,9 @@
|
||||||
</Confirm>
|
</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";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
|
<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>
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ export default {
|
||||||
notificationList: [],
|
notificationList: [],
|
||||||
windowWidth: window.innerWidth,
|
windowWidth: window.innerWidth,
|
||||||
showListMobile: false,
|
showListMobile: false,
|
||||||
|
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -47,10 +48,6 @@ export default {
|
||||||
transports: ["websocket"],
|
transports: ["websocket"],
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("connect_error", (err) => {
|
|
||||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("info", (info) => {
|
socket.on("info", (info) => {
|
||||||
this.info = info;
|
this.info = info;
|
||||||
});
|
});
|
||||||
|
@ -59,6 +56,11 @@ export default {
|
||||||
this.$router.push("/setup")
|
this.$router.push("/setup")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("autoLogin", (monitorID, data) => {
|
||||||
|
this.loggedIn = true;
|
||||||
|
this.storage().token = "autoLogin"
|
||||||
|
});
|
||||||
|
|
||||||
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]) => {
|
||||||
|
@ -136,8 +138,16 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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", () => {
|
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -151,8 +161,12 @@ 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 {
|
} else {
|
||||||
this.allowLoginDialog = true;
|
this.allowLoginDialog = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<font-awesome-icon icon="pause" /> Pause
|
<font-awesome-icon icon="pause" /> Pause
|
||||||
</button>
|
</button>
|
||||||
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||||
<font-awesome-icon icon="pause" /> Resume
|
<font-awesome-icon icon="play" /> Resume
|
||||||
</button>
|
</button>
|
||||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
||||||
<font-awesome-icon icon="edit" /> Edit
|
<font-awesome-icon icon="edit" /> Edit
|
||||||
|
|
|
@ -67,6 +67,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2>Advanced</h2>
|
||||||
|
|
||||||
|
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-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="mb-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>
|
<div>
|
||||||
<button class="btn btn-primary" type="submit" :disabled="processing">
|
<button class="btn btn-primary" type="submit" :disabled="processing">
|
||||||
Save
|
Save
|
||||||
|
@ -149,6 +168,8 @@ export default {
|
||||||
interval: 60,
|
interval: 60,
|
||||||
maxretries: 0,
|
maxretries: 0,
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
|
ignoreTls: false,
|
||||||
|
upsideDown: false,
|
||||||
}
|
}
|
||||||
} 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) => {
|
||||||
|
|
|
@ -27,38 +27,44 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>Change Password</h2>
|
<template v-if="loaded">
|
||||||
<form class="mb-3" @submit.prevent="savePassword">
|
<template v-if="! settings.disableAuth">
|
||||||
<div class="mb-3">
|
<h2>Change Password</h2>
|
||||||
<label for="current-password" class="form-label">Current Password</label>
|
<form class="mb-3" @submit.prevent="savePassword">
|
||||||
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
<div class="mb-3">
|
||||||
</div>
|
<label for="current-password" class="form-label">Current Password</label>
|
||||||
|
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new-password" class="form-label">New Password</label>
|
||||||
|
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
||||||
|
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
The repeat password does not match.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
Update Password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<h2>Advanced</h2>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="new-password" class="form-label">New Password</label>
|
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button>
|
||||||
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
<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 class="mb-3">
|
|
||||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
|
||||||
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
|
||||||
<div class="invalid-feedback">
|
|
||||||
The repeat password does not match.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary" type="submit">
|
|
||||||
Update Password
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-danger" @click="$root.logout">
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -87,15 +93,23 @@
|
||||||
</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 { timezoneList } from "../util-frontend";
|
||||||
import { useToast } from "vue-toastification"
|
import { useToast } from "vue-toastification"
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
@ -103,6 +117,7 @@ const toast = useToast()
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotificationDialog,
|
NotificationDialog,
|
||||||
|
Confirm,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -115,6 +130,10 @@ export default {
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
repeatNewPassword: "",
|
repeatNewPassword: "",
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
|
||||||
|
},
|
||||||
|
loaded: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -124,7 +143,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.loadSettings();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -148,6 +167,36 @@ 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().token = null;
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,19 +5,6 @@ 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", {
|
const tzString = now.toLocaleString("en-US", {
|
||||||
|
|
34
src/util.js
Normal file
34
src/util.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = void 0;
|
||||||
|
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;
|
43
src/util.ts
Normal file
43
src/util.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// 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 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