mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-11 05:16:55 +08:00
Compare commits
215 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4d7c2d329b | ||
|
1982e2f8b8 | ||
|
2819094377 | ||
|
06c4523ce3 | ||
|
ac3732f6cc | ||
|
bf3e9dccd2 | ||
|
5b18a6a518 | ||
|
caec933186 | ||
|
51ac7a58dc | ||
|
db26b7d123 | ||
|
7b8459c73a | ||
|
d0c63ebe3e | ||
|
803f0d6219 | ||
|
d556509d07 | ||
|
3cc4955cad | ||
|
48f82b55f8 | ||
|
280ba84aca | ||
|
0dbecca10f | ||
|
8279368b4d | ||
|
2450b3d082 | ||
|
6b72d5033a | ||
|
4d262bbb6a | ||
|
d1370a62bd | ||
|
063fd6ef43 | ||
|
1d4d7fa9c4 | ||
|
248b5292dc | ||
|
77fbfc23be | ||
|
b7a32d4ab6 | ||
|
5a219554b3 | ||
|
70b1f197c1 | ||
|
32a5e838ba | ||
|
86e18ac11d | ||
|
f20ab4b0e3 | ||
|
96c60dd94a | ||
|
ccda6f05f5 | ||
|
03b3bb5b30 | ||
|
7e4a1ad279 | ||
|
916b9da0dc | ||
|
a64ce81457 | ||
|
c575afc8e0 | ||
|
1e42343aee | ||
|
afd4cf2425 | ||
|
e02eb72863 | ||
|
1c0dc18d72 | ||
|
c00612c1a9 | ||
|
32345fcbe9 | ||
|
fd90458e77 | ||
|
d89e6f4649 | ||
|
c4ca8e2acb | ||
|
94b5a557bf | ||
|
14e1d1f105 | ||
|
8b905b6b12 | ||
|
fa57d40c3c | ||
|
62e231e92b | ||
|
02b4dfc100 | ||
|
657acf748b | ||
|
054269ecf0 | ||
|
71dd68bb6d | ||
|
02230930c5 | ||
|
a8b102ad4a | ||
|
58d029445d | ||
|
77af41bfff | ||
|
058032a26a | ||
|
cbb9d3f91b | ||
|
5bd3184ebf | ||
|
59ebe134f1 | ||
|
851ceef3d5 | ||
|
efd7608ba2 | ||
|
05fdaf0c96 | ||
|
01b0e82d52 | ||
|
d2ccfd5366 | ||
|
7cba9ce231 | ||
|
69e8c56e3e | ||
|
9928ea8c30 | ||
|
25c370c9ff | ||
|
9227ff6ea3 | ||
|
2d943620c7 | ||
|
f7bd67c413 | ||
|
9ca2444dab | ||
|
1d45a7606d | ||
|
16f363ac38 | ||
|
d6b9403f60 | ||
|
92e5ddd97c | ||
|
23611e540c | ||
|
f9274557f3 | ||
|
44b66cbd2e | ||
|
66037e236c | ||
|
386c002fda | ||
|
7c1aab6a15 | ||
|
37884cfd08 | ||
|
ce6841eae7 | ||
|
7c94c3b502 | ||
|
268c8e50f5 | ||
|
13c9244e3f | ||
|
13b3a5be9c | ||
|
2e31e780a1 | ||
|
6a2b5f9dd8 | ||
|
16767dc042 | ||
|
fb3e000dc3 | ||
|
6f3ea21864 | ||
|
403137280e | ||
|
d94894b7e0 | ||
|
a173700cd4 | ||
|
9c248776e7 | ||
|
309caa4279 | ||
|
28b14ceb70 | ||
|
9f9c42c30b | ||
|
2bff62cade | ||
|
bfb4a5bcd4 | ||
|
e87b78501b | ||
|
637422494f | ||
|
db34484ff2 | ||
|
790c071f2c | ||
|
149688e669 | ||
|
7dae5279fb | ||
|
c203317b3b | ||
|
01f2fccb23 | ||
|
7808aef58f | ||
|
ce2d78f45a | ||
|
20cad50593 | ||
|
829a2a191d | ||
|
65b320d06b | ||
|
1935da5b16 | ||
|
78f5d2cd8b | ||
|
f62b70c9a9 | ||
|
dfa9b3a0ca | ||
|
b3bff8d735 | ||
|
f2af5bc064 | ||
|
91b736f391 | ||
|
275f77d4bb | ||
|
b00524067a | ||
|
25a93b05dc | ||
|
53e203d2f9 | ||
|
f48f957ba9 | ||
|
bfb117cb76 | ||
|
2b8e33caed | ||
|
60493f0f86 | ||
|
63c6e29e62 | ||
|
5f6d5588a6 | ||
|
18744d834f | ||
|
8dd5b97b79 | ||
|
386c8bfdf1 | ||
|
126f00e739 | ||
|
80466ac957 | ||
|
3b52433202 | ||
|
137f5da3da | ||
|
338d002d42 | ||
|
77ab9fbc57 | ||
|
b6b7835d7e | ||
|
d4fe5908f5 | ||
|
af838d62e8 | ||
|
5a6e83b777 | ||
|
c81930cacc | ||
|
6d4694da43 | ||
|
9c23cd09ce | ||
|
a60bf1528a | ||
|
1f3b337806 | ||
|
010ebea210 | ||
|
b3a5d868a7 | ||
|
312dec7393 | ||
|
be1ef24cce | ||
|
c5de82b220 | ||
|
fef41b44a8 | ||
|
6af65b688d | ||
|
3e4a98b6bc | ||
|
866bf56319 | ||
|
99afdabcac | ||
|
0f1a95fde9 | ||
|
edbab8163e | ||
|
551d00fc24 | ||
|
3e4cdbecf2 | ||
|
622681470d | ||
|
d9e2c230bf | ||
|
010302395f | ||
|
e053ee6573 | ||
|
c4bc95927f | ||
|
3e305b79b2 | ||
|
c6237277c0 | ||
|
900219deb1 | ||
|
7ebeee3455 | ||
|
0abd3b2d16 | ||
|
f452bf6b13 | ||
|
8cd90d1e96 | ||
|
789094a2ee | ||
|
5515437eab | ||
|
7acb347012 | ||
|
9d57e93367 | ||
|
dae92d0ae4 | ||
|
1259ff5368 | ||
|
8debce82b1 | ||
|
11a2adcb7c | ||
|
ad615d1a90 | ||
|
613c42b6d8 | ||
|
a6e16116f2 | ||
|
c7dfb36349 | ||
|
459dde2761 | ||
|
cb94ab3bb5 | ||
|
0176857a2c | ||
|
763d7f2683 | ||
|
c436ef4e05 | ||
|
56fcfc9369 | ||
|
a9d19ae06a | ||
|
35ce54f30c | ||
|
e4f38d833d | ||
|
8b83266b00 | ||
|
6fb1b344f6 | ||
|
b15b44e290 | ||
|
66d991bd05 | ||
|
673d3c124c | ||
|
e568cad22c | ||
|
f84f7aca75 | ||
|
b198b3dde4 | ||
|
0b294815c7 | ||
|
83935a2cf4 | ||
|
838913f0a1 |
@@ -2,3 +2,13 @@
|
||||
/dist
|
||||
/node_modules
|
||||
/data/kuma.db
|
||||
/.do
|
||||
**/.dockerignore
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
LICENSE
|
||||
README.md
|
||||
.editorconfig
|
||||
.vscode
|
||||
|
@@ -13,3 +13,6 @@ trim_trailing_whitespace = false
|
||||
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
|
10
.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/--please-go-to--discussion--tab-if-you-want-to-ask-or-share-something.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
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
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- Uptime Kuma Version:
|
||||
- Using Docker?: Yes/No
|
||||
- OS:
|
||||
- Browser:
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ dist-ssr
|
||||
|
||||
/data
|
||||
!/data/.gitkeep
|
||||
.vscode
|
55
README.md
55
README.md
@@ -1,5 +1,8 @@
|
||||
# Uptime Kuma
|
||||
|
||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a>
|
||||
|
||||
|
||||
<div align="center" width="100%">
|
||||
<img src="./public/icon.svg" width="128" alt="" />
|
||||
</div>
|
||||
@@ -12,18 +15,29 @@ It is a self-hosted monitoring tool like "Uptime Robot".
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / Ping.
|
||||
* Fancy, Reactive, Fast UI/UX.
|
||||
* Notifications via Webhook, Telegram, Discord and email (SMTP).
|
||||
* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.
|
||||
* 20 seconds interval.
|
||||
|
||||
# How to Use
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker run -d --restart=always -p 3001:3001 louislam/uptime-kuma
|
||||
# Create a volume
|
||||
docker volume create uptime-kuma
|
||||
|
||||
# Start the container
|
||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
|
||||
Change Port and Volume
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
Required Tools: Node.js >= 14, git and pm2.
|
||||
@@ -36,11 +50,14 @@ npm run setup
|
||||
# Option 1. Try it
|
||||
npm run start-server
|
||||
|
||||
# (Recommanded)
|
||||
# (Recommended)
|
||||
# Option 2. Run in background using PM2
|
||||
# Install PM2 if you don't have: npm install pm2 -g
|
||||
pm2 start npm --name uptime-kuma -- run start-server
|
||||
|
||||
# Listen to different port or hostname
|
||||
pm2 start npm --name uptime-kuma -- run start-server -- --port=80 --hostname=0.0.0.0
|
||||
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
@@ -49,6 +66,30 @@ Browse to http://localhost:3001 after started.
|
||||
|
||||
[](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
|
||||
|
||||
### Docker
|
||||
|
||||
Re-pull the latest docker image and create another container with the same volume.
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
git fetch --all
|
||||
git checkout 1.0.7 --force
|
||||
npm install
|
||||
npm run build
|
||||
pm2 restart uptime-kuma
|
||||
```
|
||||
|
||||
# What's Next?
|
||||
|
||||
I will mark requests/issues to the next milestone.
|
||||
https://github.com/louislam/uptime-kuma/milestones
|
||||
|
||||
# More Screenshots
|
||||
|
||||
@@ -73,3 +114,11 @@ Telegram Notification Sample:
|
||||
|
||||
If you love this project, please consider giving me a ⭐.
|
||||
|
||||
|
||||
# Contribute
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
BIN
db/kuma.db
BIN
db/kuma.db
Binary file not shown.
37
db/patch1.sql
Normal file
37
db/patch1.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
-- Change Monitor.created_date from "TIMESTAMP" to "DATETIME"
|
||||
-- SQL Generated by Intellij Idea
|
||||
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)
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
drop table monitor;
|
||||
|
||||
alter table monitor_dg_tmp rename to monitor;
|
||||
|
||||
create index user_id on monitor (user_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys=on;
|
9
db/patch2.sql
Normal file
9
db/patch2.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE monitor_tls_info (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_id INTEGER NOT NULL,
|
||||
info_json TEXT
|
||||
);
|
||||
|
||||
COMMIT;
|
37
db/patch3.sql
Normal file
37
db/patch3.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
-- Add maxretries column to monitor
|
||||
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
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
drop table monitor;
|
||||
|
||||
alter table monitor_dg_tmp rename to monitor;
|
||||
|
||||
create index user_id on monitor (user_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys=on;
|
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
# Simple docker-composer.yml
|
||||
# You can change your port or volume location
|
||||
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma
|
||||
container_name: uptime-kuma
|
||||
volumes:
|
||||
- ./uptime-kuma:/app/data
|
||||
ports:
|
||||
- 3001:3001
|
35
dockerfile
35
dockerfile
@@ -1,10 +1,41 @@
|
||||
FROM node:14
|
||||
|
||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||
FROM node:14-alpine3.12 AS release
|
||||
WORKDIR /app
|
||||
|
||||
# split the sqlite install here, so that it can caches the arm prebuilt
|
||||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
|
||||
ln -s /usr/bin/python3 /usr/bin/python && \
|
||||
npm install sqlite3@5.0.2 bcrypt@5.0.1 && \
|
||||
apk del .build-deps
|
||||
|
||||
# Touching above code may causes sqlite3 re-compile again, painful slow.
|
||||
|
||||
# Install apprise
|
||||
# Hate pip!!! I never run pip install successfully in first run for anything in my life without Google :/
|
||||
# Compilation Fail 1 => Google Search "alpine ffi.h" => Add libffi-dev
|
||||
# Compilation Fail 2 => Google Search "alpine cargo" => Add cargo
|
||||
# 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
|
||||
|
||||
COPY . .
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js
|
||||
CMD ["npm", "run", "start-server"]
|
||||
|
||||
FROM release AS nightly
|
||||
RUN npm run mark-as-nightly
|
||||
|
19
extra/healthcheck.js
Normal file
19
extra/healthcheck.js
Normal file
@@ -0,0 +1,19 @@
|
||||
var http = require("http");
|
||||
var options = {
|
||||
host: "localhost",
|
||||
port: "3001",
|
||||
timeout: 2000,
|
||||
};
|
||||
var request = http.request(options, (res) => {
|
||||
console.log(`STATUS: ${res.statusCode}`);
|
||||
if (res.statusCode == 200) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
request.on("error", function (err) {
|
||||
console.log("ERROR");
|
||||
process.exit(1);
|
||||
});
|
||||
request.end();
|
40
extra/mark-as-nightly.js
Normal file
40
extra/mark-as-nightly.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 = oldVersion + "-nightly"
|
||||
|
||||
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
|
||||
if (fs.existsSync("README.md")) {
|
||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion))
|
||||
}
|
||||
}
|
39
extra/version-global-replace.js
Normal file
39
extra/version-global-replace.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
|
@@ -3,7 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#5cdd8b" />
|
||||
<meta name="description" content="Uptime Kuma monitoring tool" />
|
||||
<title>Uptime Kuma</title>
|
||||
</head>
|
||||
<body>
|
||||
|
464
package-lock.json
generated
464
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.6",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -29,21 +29,6 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@discordjs/collection": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz",
|
||||
"integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ=="
|
||||
},
|
||||
"@discordjs/form-data": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz",
|
||||
"integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz",
|
||||
@@ -55,37 +40,43 @@
|
||||
"integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
|
||||
},
|
||||
"@types/cookie": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
|
||||
"integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg=="
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||
},
|
||||
"@types/cors": {
|
||||
"version": "2.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
|
||||
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
|
||||
"version": "2.8.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
},
|
||||
"@types/estree": {
|
||||
"version": "0.0.48",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
|
||||
"integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "15.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.4.tgz",
|
||||
"integrity": "sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA=="
|
||||
"version": "16.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.3.tgz",
|
||||
"integrity": "sha512-8h7k1YgQKxKXWckzFCMfsIwn0Y61UK6tlD6y2lOb3hTOIMlK3t9/QwHOhc81TwU+RMf0As5fj7NPjroERCnejQ=="
|
||||
},
|
||||
"@vitejs/plugin-legacy": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.4.3.tgz",
|
||||
"integrity": "sha512-lxZUJaMWYMQuqvZM1wPzDP6KABQgA/drVL5fnaygEPcz9adc2OHhfFNN/SvvHQ1V0rP8gybIc7uA+iI1gAdkVQ==",
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.4.4.tgz",
|
||||
"integrity": "sha512-pVYeQUDPG5InWwrTu7acy187WWjGonJnL/GMqMLmeKCFiwkZ6UcsoUjojiKmCUI0nAJTrrKH5lhjTqkccY9Iow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/standalone": "^7.14.7",
|
||||
"core-js": "^3.15.1",
|
||||
"core-js": "^3.15.2",
|
||||
"magic-string": "^0.25.7",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"systemjs": "^6.10.1"
|
||||
"systemjs": "^6.10.2"
|
||||
}
|
||||
},
|
||||
"@vitejs/plugin-vue": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.2.3.tgz",
|
||||
"integrity": "sha512-LlnLpObkGKZ+b7dcpL4T24l13nPSHLjo+6Oc7MbZiKz5PMAUzADfNJ3EKfYIQ0l0969nxf2jp/9vsfnuJ7h6fw==",
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.2.5.tgz",
|
||||
"integrity": "sha512-GIR31mdXTEfvElmBUaRhDc5v7lfdkEdawWQqJRiaRL/5qKsH+xusukglkvJz5y7+c6dEpxgmvcATv2BbB7+fzQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
@@ -110,17 +101,18 @@
|
||||
}
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.1.tgz",
|
||||
"integrity": "sha512-lSgMsZaYHF+bAgryq5aUqpvyfhu52GJI2/4LoiJCE5uaxc6FCZfxfgqgw/d9ltiZghv+HiISFtmQVAVvlsk+/w==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.5.tgz",
|
||||
"integrity": "sha512-mtMY6xMvZeSRx9MTa1+NgJWndrkzVTdJ1pQAmAKQuxyb5LsHVvrgP7kcQFvxPHVpLVTORbTJWHaiqoKrJvi1iA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.13.9",
|
||||
"@babel/types": "^7.13.0",
|
||||
"@vue/compiler-core": "3.1.1",
|
||||
"@vue/compiler-dom": "3.1.1",
|
||||
"@vue/compiler-ssr": "3.1.1",
|
||||
"@vue/shared": "3.1.1",
|
||||
"@types/estree": "^0.0.48",
|
||||
"@vue/compiler-core": "3.1.5",
|
||||
"@vue/compiler-dom": "3.1.5",
|
||||
"@vue/compiler-ssr": "3.1.5",
|
||||
"@vue/shared": "3.1.5",
|
||||
"consolidate": "^0.16.0",
|
||||
"estree-walker": "^2.0.1",
|
||||
"hash-sum": "^2.0.0",
|
||||
@@ -131,16 +123,78 @@
|
||||
"postcss-modules": "^4.0.0",
|
||||
"postcss-selector-parser": "^6.0.4",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.5.tgz",
|
||||
"integrity": "sha512-TXBhFinoBaXKDykJzY26UEuQU1K07FOp/0Ie+OXySqqk0bS0ZO7Xvl7UmiTUPYcLrWbxWBR7Bs/y55AI0MNc2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.12.0",
|
||||
"@babel/types": "^7.12.0",
|
||||
"@vue/shared": "3.1.5",
|
||||
"estree-walker": "^2.0.1",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.5.tgz",
|
||||
"integrity": "sha512-ZsL3jqJ52OjGU/YiT/9XiuZAmWClKInZM2aFJh9gnsAPqOrj2JIELMbkIFpVKR/CrVO/f2VxfPiiQdQTr65jcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.1.5",
|
||||
"@vue/shared": "3.1.5"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vue/compiler-ssr": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.1.tgz",
|
||||
"integrity": "sha512-7H6krZtVt3h/YzfNp7eYK41hMDz8ZskiBy+Wby+EDRINX6BD9JQ5C8zyy2xAa7T6Iz2VrQzsaJ/Bb52lTPSS5A==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.5.tgz",
|
||||
"integrity": "sha512-CU5N7Di/a4lyJ18LGJxJYZS2a8PlLdWpWHX9p/XcsjT2TngMpj3QvHVRkuik2u8QrIDZ8OpYmTyj1WDNsOV+Dg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.1.1",
|
||||
"@vue/shared": "3.1.1"
|
||||
"@vue/compiler-dom": "3.1.5",
|
||||
"@vue/shared": "3.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.5.tgz",
|
||||
"integrity": "sha512-TXBhFinoBaXKDykJzY26UEuQU1K07FOp/0Ie+OXySqqk0bS0ZO7Xvl7UmiTUPYcLrWbxWBR7Bs/y55AI0MNc2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.12.0",
|
||||
"@babel/types": "^7.12.0",
|
||||
"@vue/shared": "3.1.5",
|
||||
"estree-walker": "^2.0.1",
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.5.tgz",
|
||||
"integrity": "sha512-ZsL3jqJ52OjGU/YiT/9XiuZAmWClKInZM2aFJh9gnsAPqOrj2JIELMbkIFpVKR/CrVO/f2VxfPiiQdQTr65jcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.1.5",
|
||||
"@vue/shared": "3.1.5"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vue/devtools-api": {
|
||||
@@ -185,14 +239,6 @@
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"requires": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
@@ -202,6 +248,14 @@
|
||||
"negotiator": "0.6.2"
|
||||
}
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"requires": {
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -243,6 +297,11 @@
|
||||
"readable-stream": "^2.0.6"
|
||||
}
|
||||
},
|
||||
"args-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/args-parser/-/args-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-If3Zi4BSjlQIJ9fgAhSiKi0oJtgMzSqh0H4wvl7XSeO16FKx7QqaHld8lZeEajPX7y1C5qKKeNgyrfyvmjmjUQ=="
|
||||
},
|
||||
"arr-diff": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
|
||||
@@ -333,6 +392,11 @@
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-add-module-exports": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz",
|
||||
"integrity": "sha1-mumh9KjcZ/DN7E9K7aHkOl/2XiU="
|
||||
},
|
||||
"backo2": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
|
||||
@@ -403,6 +467,70 @@
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
|
||||
},
|
||||
"bcrypt": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
|
||||
"integrity": "sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==",
|
||||
"requires": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.0",
|
||||
"node-addon-api": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz",
|
||||
"integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==",
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nopt": "^5.0.0",
|
||||
"npmlog": "^4.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.4",
|
||||
"tar": "^6.1.0"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
|
||||
"requires": {
|
||||
"abbrev": "1"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
@@ -477,9 +605,9 @@
|
||||
}
|
||||
},
|
||||
"bootstrap": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.0.1.tgz",
|
||||
"integrity": "sha512-Fl79+wsLOZKoiU345KeEaWD0ik8WKRI5zm0YSPj2oF1Qr+BO7z0fco6GbUtqjoG1h4VI89PeKJnMsMMVQdKKTw=="
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.0.2.tgz",
|
||||
"integrity": "sha512-1Ge963tyEQWJJ+8qtXFU6wgmAVj9gweEjibUdbmcCEYsn38tVwRk8107rk2vzt6cfQcRr3SlZ8aQBqaD8aqf+Q=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
@@ -601,6 +729,11 @@
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"command-exists": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
|
||||
"integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
|
||||
},
|
||||
"commander": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
|
||||
@@ -699,9 +832,9 @@
|
||||
}
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.5.tgz",
|
||||
"integrity": "sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g=="
|
||||
"version": "1.10.6",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz",
|
||||
"integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.1",
|
||||
@@ -788,28 +921,6 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
||||
},
|
||||
"discord.js": {
|
||||
"version": "12.5.3",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz",
|
||||
"integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==",
|
||||
"requires": {
|
||||
"@discordjs/collection": "^0.1.6",
|
||||
"@discordjs/form-data": "^3.0.1",
|
||||
"abort-controller": "^3.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prism-media": "^1.2.9",
|
||||
"setimmediate": "^1.0.5",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"ws": "^7.4.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -866,9 +977,9 @@
|
||||
}
|
||||
},
|
||||
"engine.io-client": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.1.1.tgz",
|
||||
"integrity": "sha512-jPFpw2HLL0lhZ2KY0BpZhIJdleQcUO9W1xkIpo0h3d6s+5D6+EV/xgQw9qWOmymszv2WXef/6KUUehyxEKomlQ==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.1.2.tgz",
|
||||
"integrity": "sha512-blRrgXIE0A/eurWXRzvfCLG7uUFJqfTGFsyJzXSK71srMMGJ2VraBLg8Mdw28uUxSpVicepBN9X7asqpD1mZcQ==",
|
||||
"requires": {
|
||||
"base64-arraybuffer": "0.1.4",
|
||||
"component-emitter": "~1.3.0",
|
||||
@@ -890,9 +1001,9 @@
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.12.9",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.9.tgz",
|
||||
"integrity": "sha512-MWRhAbMOJ9RJygCrt778rz/qNYgA4ZVj6aXnNPxFjs7PmIpb0fuB9Gmg5uWrr6n++XKwwm/RmSz6RR5JL2Ocsw==",
|
||||
"version": "0.12.15",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.15.tgz",
|
||||
"integrity": "sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==",
|
||||
"dev": true
|
||||
},
|
||||
"escape-html": {
|
||||
@@ -915,11 +1026,6 @@
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
||||
},
|
||||
"event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||
},
|
||||
"expand-brackets": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
|
||||
@@ -1244,6 +1350,14 @@
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"requires": {
|
||||
"minipass": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -1484,6 +1598,14 @@
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"http-graceful-shutdown": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/http-graceful-shutdown/-/http-graceful-shutdown-3.1.2.tgz",
|
||||
"integrity": "sha512-2vmU3kWOsZqZy4Kn4EZp00CF+6glpNNN/NAYJPkO9bnMX/D8sRl29TsxIu9Vgyo8ygtCWazWJp720zHfqhSdXg==",
|
||||
"requires": {
|
||||
"debug": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
@@ -1495,6 +1617,15 @@
|
||||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||
"requires": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -1926,6 +2057,21 @@
|
||||
"sourcemap-codec": "^1.4.4"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"requires": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"make-iterator": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
|
||||
@@ -1952,6 +2098,11 @@
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
|
||||
},
|
||||
"merge": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz",
|
||||
"integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w=="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
@@ -2099,6 +2250,37 @@
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
|
||||
"integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"requires": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mixin-deep": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||
@@ -2118,6 +2300,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@@ -2317,9 +2504,9 @@
|
||||
}
|
||||
},
|
||||
"nodemailer": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.2.tgz",
|
||||
"integrity": "sha512-YSzu7TLbI+bsjCis/TZlAXBoM4y93HhlIgo0P5oiA2ua9Z4k+E2Fod//ybIzdJxOlXGRcHIh/WaeCBehvxZb/Q=="
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.3.tgz",
|
||||
"integrity": "sha512-faZFufgTMrphYoDjvyVpbpJcYzwyFnbAMmQtj1lVBYAUSm3SOy2fIdd9+Mr4UxPosBa0JRw9bJoIwQn+nswiew=="
|
||||
},
|
||||
"nopt": {
|
||||
"version": "3.0.6",
|
||||
@@ -2653,11 +2840,6 @@
|
||||
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"prism-media": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.1.tgz",
|
||||
"integrity": "sha512-nyYAa3KB4qteJIqdguKmwxTJgy55xxUtkJ3uRnOvO5jO+frci+9zpRXw6QZVcfDeva3S654fU9+26P2OSTzjHw=="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -2877,9 +3059,9 @@
|
||||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.52.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.2.tgz",
|
||||
"integrity": "sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA==",
|
||||
"version": "2.53.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.53.2.tgz",
|
||||
"integrity": "sha512-1CtEYuS5CRCzFZ7SNW5528SlDlk4VDXIRGwbm/2POQxA/G4+7/crIqJwkmnj8Q/74hGx4oVlNvh4E1CJQ5hZ6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fsevents": "~2.3.2"
|
||||
@@ -2904,9 +3086,9 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.35.1.tgz",
|
||||
"integrity": "sha512-oCisuQJstxMcacOPmxLNiLlj4cUyN2+8xJnG7VanRoh2GOLr9RqkvI4AxA4a6LHVg/rsu+PmxXeGhrdSF9jCiQ==",
|
||||
"version": "1.35.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.35.2.tgz",
|
||||
"integrity": "sha512-jhO5KAR+AMxCEwIH3v+4zbB2WB0z67V1X0jbapfVwQQdjHZUGUyukpnoM6+iCMfsIUC016w9OPKQ5jrNOS9uXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0"
|
||||
@@ -3001,11 +3183,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
@@ -3132,19 +3309,19 @@
|
||||
}
|
||||
},
|
||||
"socket.io": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.1.2.tgz",
|
||||
"integrity": "sha512-xK0SD1C7hFrh9+bYoYCdVt+ncixkSLKtNLCax5aEy1o3r5PaO5yQhVb97exIe67cE7lAK+EpyMytXWTWmyZY8w==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.1.3.tgz",
|
||||
"integrity": "sha512-tLkaY13RcO4nIRh1K2hT5iuotfTaIQw7cVIe0FUykN3SuQi0cm7ALxuyT5/CtDswOMWUzMGTibxYNx/gU7In+Q==",
|
||||
"requires": {
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/cors": "^2.8.8",
|
||||
"@types/cors": "^2.8.10",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io": "~5.1.0",
|
||||
"socket.io-adapter": "~2.3.0",
|
||||
"socket.io-parser": "~4.0.3"
|
||||
"engine.io": "~5.1.1",
|
||||
"socket.io-adapter": "~2.3.1",
|
||||
"socket.io-parser": "~4.0.4"
|
||||
}
|
||||
},
|
||||
"socket.io-adapter": {
|
||||
@@ -3153,15 +3330,15 @@
|
||||
"integrity": "sha512-8cVkRxI8Nt2wadkY6u60Y4rpW3ejA1rxgcK2JuyIhmF+RMNpTy1QRtkHIDUOf3B4HlQwakMsWbKftMv/71VMmw=="
|
||||
},
|
||||
"socket.io-client": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.1.2.tgz",
|
||||
"integrity": "sha512-RDpWJP4DQT1XeexmeDyDkm0vrFc0+bUsHDKiVGaNISJvJonhQQOMqV9Vwfg0ZpPJ27LCdan7iqTI92FRSOkFWQ==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.1.3.tgz",
|
||||
"integrity": "sha512-hISFn6PDpgDifVUiNklLHVPTMv1LAk8poHArfIUdXa+gKgbr0MZbAlquDFqCqsF30yBqa+jg42wgos2FK50BHA==",
|
||||
"requires": {
|
||||
"@types/component-emitter": "^1.2.10",
|
||||
"backo2": "~1.0.2",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-client": "~5.1.1",
|
||||
"engine.io-client": "~5.1.2",
|
||||
"parseuri": "0.0.6",
|
||||
"socket.io-parser": "~4.0.4"
|
||||
}
|
||||
@@ -3317,6 +3494,31 @@
|
||||
"integrity": "sha512-PwaC0Z6Y1E6gFekY2u38EC5+5w2M65jYVrD1aAcOptpHVhCwPIwPFJvYJyryQKUyeuQ5bKKI3PBHWNjdE9aizg==",
|
||||
"dev": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
|
||||
"integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
|
||||
"requires": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^3.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"tarn": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz",
|
||||
@@ -3506,6 +3708,16 @@
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"optional": true
|
||||
},
|
||||
"v-pagination-3": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/v-pagination-3/-/v-pagination-3-0.1.6.tgz",
|
||||
"integrity": "sha512-82J8HnEIYtZijn6F3xhyP/ildI5K7Rv4Yu74VNfQWQsiPWTKntgVvZgBH8UPh/lFEjgWxty/M4N+YHvS+YbGzg==",
|
||||
"requires": {
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"merge": "^2.1.1",
|
||||
"vue": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"v8flags": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
|
||||
@@ -3531,14 +3743,14 @@
|
||||
}
|
||||
},
|
||||
"vite": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-2.3.8.tgz",
|
||||
"integrity": "sha512-QiEx+iqNnJntSgSF2fWRQvRey9pORIrtNJzNyBJXwc+BdzWs83FQolX84cTBo393cfhObrtWa6180dAa4NLDiQ==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-2.4.2.tgz",
|
||||
"integrity": "sha512-2MifxD2I9fjyDmmEzbULOo3kOUoqX90A58cT6mECxoVQlMYFuijZsPQBuA14mqSwvV3ydUsqnq+BRWXyO9Qa+w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild": "^0.12.8",
|
||||
"fsevents": "~2.3.2",
|
||||
"postcss": "^8.3.4",
|
||||
"postcss": "^8.3.5",
|
||||
"resolve": "^1.20.0",
|
||||
"rollup": "^2.38.5"
|
||||
}
|
||||
|
42
package.json
42
package.json
@@ -1,41 +1,55 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.7",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"start-server": "node server/server.js",
|
||||
"update": "",
|
||||
"build": "vite build",
|
||||
"vite-preview-dist": "vite preview --host",
|
||||
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 . --push",
|
||||
"setup": "git checkout 1.0.0 && npm install && npm run build"
|
||||
"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.7 --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-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
|
||||
"setup": "git checkout 1.0.7 && npm install && npm run build",
|
||||
"version-global-replace": "node extra/version-global-replace.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"args-parser": "^1.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^5.0.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"discord.js": "^12.5.3",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bootstrap": "^5.0.2",
|
||||
"command-exists": "^1.2.9",
|
||||
"dayjs": "^1.10.6",
|
||||
"express": "^4.17.1",
|
||||
"form-data": "^4.0.0",
|
||||
"http-graceful-shutdown": "^3.1.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemailer": "^6.6.2",
|
||||
"nodemailer": "^6.6.3",
|
||||
"password-hash": "^1.2.2",
|
||||
"redbean-node": "0.0.20",
|
||||
"socket.io": "^4.0.2",
|
||||
"socket.io-client": "^4.1.2",
|
||||
"socket.io": "^4.1.3",
|
||||
"socket.io-client": "^4.1.3",
|
||||
"sqlite3": "^5.0.2",
|
||||
"tcp-ping": "^0.1.1",
|
||||
"v-pagination-3": "^0.1.6",
|
||||
"vue": "^3.0.5",
|
||||
"vue-confirm-dialog": "^1.0.2",
|
||||
"vue-router": "^4.0.10",
|
||||
"vue-toastification": "^2.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-legacy": "^1.4.3",
|
||||
"@vitejs/plugin-vue": "^1.2.3",
|
||||
"@vue/compiler-sfc": "^3.0.5",
|
||||
"@vitejs/plugin-legacy": "^1.4.4",
|
||||
"@vitejs/plugin-vue": "^1.2.5",
|
||||
"@vue/compiler-sfc": "^3.1.5",
|
||||
"core-js": "^3.15.2",
|
||||
"sass": "^1.35.1",
|
||||
"vite": "^2.3.7"
|
||||
"sass": "^1.35.2",
|
||||
"vite": "^2.4.2"
|
||||
}
|
||||
}
|
||||
|
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
119
server/database.js
Normal file
119
server/database.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const fs = require("fs");
|
||||
const {sleep} = require("./util");
|
||||
const {R} = require("redbean-node");
|
||||
const {setSetting, setting} = require("./util-server");
|
||||
|
||||
|
||||
class Database {
|
||||
|
||||
static templatePath = "./db/kuma.db"
|
||||
static path = './data/kuma.db';
|
||||
static latestVersion = 3;
|
||||
static noReject = true;
|
||||
|
||||
static async patch() {
|
||||
let version = parseInt(await setting("database_version"));
|
||||
|
||||
if (! version) {
|
||||
version = 0;
|
||||
}
|
||||
|
||||
console.info("Your database version: " + version);
|
||||
console.info("Latest database version: " + this.latestVersion);
|
||||
|
||||
if (version === this.latestVersion) {
|
||||
console.info("Database no need to patch");
|
||||
} else {
|
||||
console.info("Database patch is needed")
|
||||
|
||||
console.info("Backup the db")
|
||||
const backupPath = "./data/kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, backupPath);
|
||||
|
||||
// Try catch anything here, if gone wrong, restore the backup
|
||||
try {
|
||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||
const sqlFile = `./db/patch${i}.sql`;
|
||||
console.info(`Patching ${sqlFile}`);
|
||||
await Database.importSQLFile(sqlFile);
|
||||
console.info(`Patched ${sqlFile}`);
|
||||
await setSetting("database_version", i);
|
||||
}
|
||||
console.log("Database Patched Successfully");
|
||||
} catch (ex) {
|
||||
await Database.close();
|
||||
console.error("Patch db failed!!! Restoring the backup")
|
||||
fs.copyFileSync(backupPath, Database.path);
|
||||
console.error(ex)
|
||||
|
||||
console.error("Start Uptime-Kuma failed due to patch db failed")
|
||||
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
|
||||
* @param filename
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async importSQLFile(filename) {
|
||||
|
||||
await R.getCell("SELECT 1");
|
||||
|
||||
let text = fs.readFileSync(filename).toString();
|
||||
|
||||
// Remove all comments (--)
|
||||
let lines = text.split("\n");
|
||||
lines = lines.filter((line) => {
|
||||
return ! line.startsWith("--")
|
||||
});
|
||||
|
||||
// Split statements by semicolon
|
||||
// Filter out empty line
|
||||
text = lines.join("\n")
|
||||
|
||||
let statements = text.split(";")
|
||||
.map((statement) => {
|
||||
return statement.trim();
|
||||
})
|
||||
.filter((statement) => {
|
||||
return statement !== "";
|
||||
})
|
||||
|
||||
for (let statement of statements) {
|
||||
await R.exec(statement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async close() {
|
||||
const listener = (reason, p) => {
|
||||
Database.noReject = false;
|
||||
};
|
||||
process.addListener('unhandledRejection', listener);
|
||||
|
||||
console.log("Closing DB")
|
||||
|
||||
while (true) {
|
||||
Database.noReject = true;
|
||||
await R.close()
|
||||
await sleep(2000)
|
||||
|
||||
if (Database.noReject) {
|
||||
break;
|
||||
} else {
|
||||
console.log("Waiting to close the db")
|
||||
}
|
||||
}
|
||||
console.log("SQLite closed")
|
||||
|
||||
process.removeListener('unhandledRejection', listener);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Database;
|
@@ -3,8 +3,6 @@ const utc = require('dayjs/plugin/utc')
|
||||
var timezone = require('dayjs/plugin/timezone')
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const axios = require("axios");
|
||||
const {R} = require("redbean-node");
|
||||
const {BeanModel} = require("redbean-node/dist/bean-model");
|
||||
|
||||
|
||||
|
@@ -1,22 +1,29 @@
|
||||
|
||||
const https = require('https');
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
var timezone = require('dayjs/plugin/timezone')
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const axios = require("axios");
|
||||
const {tcping, ping} = require("../util-server");
|
||||
const {debug, UP, DOWN, PENDING} = require("../util");
|
||||
const {tcping, ping, checkCertificate} = require("../util-server");
|
||||
const {R} = require("redbean-node");
|
||||
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:
|
||||
* 0 = DOWN
|
||||
* 1 = UP
|
||||
*/
|
||||
class Monitor extends BeanModel {
|
||||
|
||||
async toJSON() {
|
||||
|
||||
let notificationIDList = {};
|
||||
@@ -35,6 +42,7 @@ class Monitor extends BeanModel {
|
||||
url: this.url,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
weight: this.weight,
|
||||
active: this.active,
|
||||
type: this.type,
|
||||
@@ -46,9 +54,9 @@ class Monitor extends BeanModel {
|
||||
|
||||
start(io) {
|
||||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
|
||||
const beat = async () => {
|
||||
console.log(`Monitor ${this.id}: Heartbeat`)
|
||||
|
||||
if (! previousBeat) {
|
||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||
@@ -56,13 +64,15 @@ class Monitor extends BeanModel {
|
||||
])
|
||||
}
|
||||
|
||||
const isFirstBeat = !previousBeat;
|
||||
|
||||
let bean = R.dispense("heartbeat")
|
||||
bean.monitor_id = this.id;
|
||||
bean.time = R.isoDateTime(dayjs.utc());
|
||||
bean.status = 0;
|
||||
bean.status = DOWN;
|
||||
|
||||
// Duration
|
||||
if (previousBeat) {
|
||||
if (! isFirstBeat) {
|
||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), 'second');
|
||||
} else {
|
||||
bean.duration = 0;
|
||||
@@ -71,17 +81,40 @@ class Monitor extends BeanModel {
|
||||
try {
|
||||
if (this.type === "http" || this.type === "keyword") {
|
||||
let startTime = dayjs().valueOf();
|
||||
let res = await axios.get(this.url)
|
||||
let res = await axios.get(this.url, {
|
||||
headers: { "User-Agent": "Uptime-Kuma" },
|
||||
httpsAgent: customAgent,
|
||||
});
|
||||
bean.msg = `${res.status} - ${res.statusText}`
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
// Check certificate if https is used
|
||||
|
||||
let certInfoStartTime = dayjs().valueOf();
|
||||
if (this.getUrl()?.protocol === "https:") {
|
||||
try {
|
||||
await this.updateTlsInfo(checkCertificate(res));
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
|
||||
|
||||
if (this.type === "http") {
|
||||
bean.status = 1;
|
||||
bean.status = UP;
|
||||
} else {
|
||||
|
||||
if (res.data.includes(this.keyword)) {
|
||||
let data = res.data;
|
||||
|
||||
// Convert to string for object/array
|
||||
if (typeof data !== "string") {
|
||||
data = JSON.stringify(data)
|
||||
}
|
||||
|
||||
if (data.includes(this.keyword)) {
|
||||
bean.msg += ", keyword is found"
|
||||
bean.status = 1;
|
||||
bean.status = UP;
|
||||
} else {
|
||||
throw new Error(bean.msg + ", but keyword is not found")
|
||||
}
|
||||
@@ -92,32 +125,52 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "port") {
|
||||
bean.ping = await tcping(this.hostname, this.port);
|
||||
bean.msg = ""
|
||||
bean.status = 1;
|
||||
bean.status = UP;
|
||||
|
||||
} else if (this.type === "ping") {
|
||||
bean.ping = await ping(this.hostname);
|
||||
bean.msg = ""
|
||||
bean.status = 1;
|
||||
bean.status = UP;
|
||||
}
|
||||
|
||||
retries = 0;
|
||||
|
||||
} catch (error) {
|
||||
if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
||||
retries++;
|
||||
bean.status = PENDING;
|
||||
}
|
||||
bean.msg = error.message;
|
||||
}
|
||||
|
||||
// Mark as important if status changed
|
||||
if (! previousBeat || previousBeat.status !== bean.status) {
|
||||
// * ? -> ANY STATUS = important [isFirstBeat]
|
||||
// UP -> PENDING = not important
|
||||
// * UP -> DOWN = important
|
||||
// UP -> UP = not important
|
||||
// PENDING -> PENDING = not important
|
||||
// * PENDING -> DOWN = important
|
||||
// PENDING -> UP = not important
|
||||
// DOWN -> PENDING = this case not exists
|
||||
// DOWN -> DOWN = not important
|
||||
// * DOWN -> UP = important
|
||||
let isImportant = isFirstBeat ||
|
||||
(previousBeat.status === UP && bean.status === DOWN) ||
|
||||
(previousBeat.status === DOWN && bean.status === UP) ||
|
||||
(previousBeat.status === PENDING && bean.status === DOWN);
|
||||
|
||||
// Mark as important if status changed, ignore pending pings,
|
||||
// Don't notify if disrupted changes to up
|
||||
if (isImportant) {
|
||||
bean.important = true;
|
||||
|
||||
// Do not send if first beat is UP
|
||||
if (previousBeat || bean.status !== 1) {
|
||||
// Send only if the first beat is DOWN
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
let notificationList = await R.getAll(`SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id `, [
|
||||
this.id
|
||||
])
|
||||
|
||||
let promiseList = [];
|
||||
|
||||
let text;
|
||||
if (bean.status === 1) {
|
||||
if (bean.status === UP) {
|
||||
text = "✅ Up"
|
||||
} else {
|
||||
text = "🔴 Down"
|
||||
@@ -126,16 +179,26 @@ class Monitor extends BeanModel {
|
||||
let msg = `[${this.name}] [${text}] ${bean.msg}`;
|
||||
|
||||
for(let notification of notificationList) {
|
||||
promiseList.push(Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()));
|
||||
try {
|
||||
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
|
||||
} catch (e) {
|
||||
console.error("Cannot send notification to " + notification.name)
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promiseList);
|
||||
}
|
||||
|
||||
} else {
|
||||
bean.important = false;
|
||||
}
|
||||
|
||||
if (bean.status === UP) {
|
||||
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
|
||||
} else if (bean.status === PENDING) {
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Type: ${this.type}`)
|
||||
} else {
|
||||
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
|
||||
}
|
||||
|
||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||
|
||||
await R.store(bean)
|
||||
@@ -152,10 +215,35 @@ class Monitor extends BeanModel {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
}
|
||||
|
||||
// Helper Method:
|
||||
// returns URL object for further usage
|
||||
// returns null if url is invalid
|
||||
getUrl() {
|
||||
try {
|
||||
return new URL(this.url);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Store TLS info to database
|
||||
async updateTlsInfo(checkCertificateResult) {
|
||||
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
this.id
|
||||
]);
|
||||
if (tls_info_bean == null) {
|
||||
tls_info_bean = R.dispense("monitor_tls_info");
|
||||
tls_info_bean.monitor_id = this.id;
|
||||
}
|
||||
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
|
||||
await R.store(tls_info_bean);
|
||||
}
|
||||
|
||||
static async sendStats(io, monitorID, userID) {
|
||||
Monitor.sendAvgPing(24, io, monitorID, userID);
|
||||
Monitor.sendUptime(24, io, monitorID, userID);
|
||||
Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
||||
Monitor.sendCertInfo(io, monitorID, userID);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,6 +264,15 @@ class Monitor extends BeanModel {
|
||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
||||
}
|
||||
|
||||
static async sendCertInfo(io, monitorID, userID) {
|
||||
let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
monitorID
|
||||
]);
|
||||
if (tls_info != null) {
|
||||
io.to(userID).emit("certInfo", monitorID, tls_info.info_json);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uptime with calculation
|
||||
* Calculation based on:
|
||||
@@ -224,7 +321,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
total += value;
|
||||
if (row.status === 0) {
|
||||
if (row.status === 0 || row.status === 2) {
|
||||
downtime += value;
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,22 @@ const axios = require("axios");
|
||||
const {R} = require("redbean-node");
|
||||
const FormData = require('form-data');
|
||||
const nodemailer = require("nodemailer");
|
||||
const Discord = require('discord.js');
|
||||
const child_process = require("child_process");
|
||||
|
||||
class Notification {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param notification
|
||||
* @param msg
|
||||
* @param monitorJSON
|
||||
* @param heartbeatJSON
|
||||
* @returns {Promise<string>} Successful msg
|
||||
* Throw Error with fail msg
|
||||
*/
|
||||
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully. ";
|
||||
|
||||
if (notification.type === "telegram") {
|
||||
try {
|
||||
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
|
||||
@@ -14,15 +26,32 @@ class Notification {
|
||||
text: msg,
|
||||
}
|
||||
})
|
||||
return true;
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
} else if (notification.type === "gotify") {
|
||||
try {
|
||||
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
|
||||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
||||
}
|
||||
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
|
||||
"message": msg,
|
||||
"priority": notification.gotifyPriority || 8,
|
||||
"title": "Uptime-Kuma"
|
||||
})
|
||||
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "webhook") {
|
||||
try {
|
||||
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
@@ -43,18 +72,157 @@ class Notification {
|
||||
finalData = data;
|
||||
}
|
||||
|
||||
let res = await axios.post(notification.webhookURL, finalData, config)
|
||||
return true;
|
||||
await axios.post(notification.webhookURL, finalData, config)
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return false;
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "smtp") {
|
||||
return await Notification.smtp(notification, msg)
|
||||
|
||||
} else if (notification.type === "discord") {
|
||||
return await Notification.discord(notification, msg)
|
||||
try {
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if(heartbeatJSON == null) {
|
||||
let data = {
|
||||
username: 'Uptime-Kuma',
|
||||
content: msg
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, data)
|
||||
return okMsg;
|
||||
}
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if(heartbeatJSON['status'] == 0) {
|
||||
var alertColor = "16711680";
|
||||
} else if(heartbeatJSON['status'] == 1) {
|
||||
var alertColor = "65280";
|
||||
}
|
||||
let data = {
|
||||
username: 'Uptime-Kuma',
|
||||
embeds: [{
|
||||
title: "Uptime-Kuma Alert",
|
||||
color: alertColor,
|
||||
fields: [
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"]
|
||||
},
|
||||
{
|
||||
name: "Message",
|
||||
value: msg
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
await axios.post(notification.discordWebhookUrl, data)
|
||||
return okMsg;
|
||||
} catch(error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "signal") {
|
||||
try {
|
||||
let data = {
|
||||
"message": msg,
|
||||
"number": notification.signalNumber,
|
||||
"recipients": notification.signalRecipients.replace(/\s/g, '').split(",")
|
||||
};
|
||||
let config = {};
|
||||
|
||||
await axios.post(notification.signalURL, data, config)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "slack") {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {'text': "Uptime Kuma Slack testing successful.", 'channel': notification.slackchannel, 'username': notification.slackusername, 'icon_emoji': notification.slackiconemo}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel":notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
"blocks": [{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Uptime Kuma Alert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [{
|
||||
"type": "mrkdwn",
|
||||
"text": '*Message*\n'+msg
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Time (UTC)*\n"+time
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(notification.slackwebhookURL, data)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "pushover") {
|
||||
var pushoverlink = 'https://api.pushover.net/1/messages.json'
|
||||
try {
|
||||
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 = {
|
||||
"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)
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
|
||||
} else if (notification.type === "apprise") {
|
||||
|
||||
return Notification.apprise(notification, msg)
|
||||
|
||||
} else {
|
||||
throw new Error("Notification type is not supported")
|
||||
@@ -110,27 +278,54 @@ class Notification {
|
||||
});
|
||||
|
||||
// send mail with defined transport object
|
||||
let info = await transporter.sendMail({
|
||||
await transporter.sendMail({
|
||||
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
|
||||
to: notification.smtpTo,
|
||||
subject: msg,
|
||||
text: msg,
|
||||
});
|
||||
|
||||
return true;
|
||||
return "Sent Successfully.";
|
||||
}
|
||||
|
||||
static async discord(notification, msg) {
|
||||
const client = new Discord.Client();
|
||||
await client.login(notification.discordToken)
|
||||
static async apprise(notification, msg) {
|
||||
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
|
||||
|
||||
const channel = await client.channels.fetch(notification.discordChannelID);
|
||||
await channel.send(msg);
|
||||
|
||||
client.destroy()
|
||||
let output = (s.stdout) ? s.stdout.toString() : 'ERROR: maybe apprise not found';
|
||||
|
||||
return true;
|
||||
if (output) {
|
||||
|
||||
if (! output.includes("ERROR")) {
|
||||
return "Sent Successfully";
|
||||
} else {
|
||||
throw new Error(output)
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
static checkApprise() {
|
||||
let commandExistsSync = require('command-exists').sync;
|
||||
let exists = commandExistsSync('apprise');
|
||||
return exists;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function throwGeneralAxiosError(error) {
|
||||
let msg = "Error: " + error + " ";
|
||||
|
||||
if (error.response && error.response.data) {
|
||||
if (typeof error.response.data === "string") {
|
||||
msg += error.response.data;
|
||||
} else {
|
||||
msg += JSON.stringify(error.response.data)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
23
server/password-hash.js
Normal file
23
server/password-hash.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const passwordHashOld = require('password-hash');
|
||||
const bcrypt = require('bcrypt');
|
||||
const saltRounds = 10;
|
||||
|
||||
exports.generate = function (password) {
|
||||
return bcrypt.hashSync(password, saltRounds);
|
||||
}
|
||||
|
||||
exports.verify = function (password, hash) {
|
||||
if (isSHA1(hash)) {
|
||||
return passwordHashOld.verify(password, hash)
|
||||
} else {
|
||||
return bcrypt.compareSync(password, hash);
|
||||
}
|
||||
}
|
||||
|
||||
function isSHA1(hash) {
|
||||
return (typeof hash === "string" && hash.startsWith("sha1"))
|
||||
}
|
||||
|
||||
exports.needRehash = function (hash) {
|
||||
return isSHA1(hash);
|
||||
}
|
170
server/server.js
170
server/server.js
@@ -1,43 +1,75 @@
|
||||
console.log("Welcome to Uptime Kuma ")
|
||||
console.log("Importing libraries")
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const http = require('http');
|
||||
const server = http.createServer(app);
|
||||
const { Server } = require("socket.io");
|
||||
const io = new Server(server);
|
||||
const dayjs = require("dayjs");
|
||||
const {R} = require("redbean-node");
|
||||
const passwordHash = require('password-hash');
|
||||
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 hostname = args.host || "0.0.0.0"
|
||||
const port = args.port || 3001
|
||||
|
||||
console.info("Version: " + version)
|
||||
|
||||
console.log("Creating express and socket.io instance")
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server);
|
||||
app.use(express.json())
|
||||
|
||||
/**
|
||||
* Total WebSocket client connected to server currently, no actual use
|
||||
* @type {number}
|
||||
*/
|
||||
let totalClient = 0;
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
*/
|
||||
let jwtSecret = null;
|
||||
|
||||
/**
|
||||
* Main monitor list
|
||||
* @type {{}}
|
||||
*/
|
||||
let monitorList = {};
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
*/
|
||||
let needSetup = false;
|
||||
|
||||
(async () => {
|
||||
await initDatabase();
|
||||
|
||||
console.log("Adding route")
|
||||
app.use('/', express.static("dist"));
|
||||
|
||||
app.post('/test-webhook', function(request, response, next) {
|
||||
console.log("Test Webhook (application/json only)")
|
||||
console.log("Content-Type: " + request.header("Content-Type"))
|
||||
console.log(request.body)
|
||||
response.end();
|
||||
});
|
||||
|
||||
app.get('*', function(request, response, next) {
|
||||
response.sendFile(process.cwd() + '/dist/index.html');
|
||||
});
|
||||
|
||||
|
||||
console.log("Adding socket handler")
|
||||
io.on('connection', async (socket) => {
|
||||
console.log('a user connected');
|
||||
|
||||
socket.emit("info", {
|
||||
version,
|
||||
})
|
||||
|
||||
totalClient++;
|
||||
|
||||
if (needSetup) {
|
||||
@@ -46,7 +78,6 @@ let needSetup = false;
|
||||
}
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('user disconnected');
|
||||
totalClient--;
|
||||
});
|
||||
|
||||
@@ -93,6 +124,14 @@ let needSetup = false;
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
||||
|
||||
await afterLogin(socket, user)
|
||||
|
||||
callback({
|
||||
@@ -144,10 +183,6 @@ let needSetup = false;
|
||||
msg: e.message
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Auth Only API
|
||||
@@ -198,6 +233,7 @@ let needSetup = false;
|
||||
bean.url = monitor.url
|
||||
bean.interval = monitor.interval
|
||||
bean.hostname = monitor.hostname;
|
||||
bean.maxretries = monitor.maxretries;
|
||||
bean.port = monitor.port;
|
||||
bean.keyword = monitor.keyword;
|
||||
|
||||
@@ -218,7 +254,7 @@ let needSetup = false;
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
console.error(e)
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
@@ -419,25 +455,36 @@ let needSetup = false;
|
||||
try {
|
||||
checkLogin(socket)
|
||||
|
||||
await Notification.send(notification, notification.name + " Testing")
|
||||
let msg = await Notification.send(notification, notification.name + " Testing")
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Sent Successfully"
|
||||
msg
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("checkApprise", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket)
|
||||
callback(Notification.checkApprise());
|
||||
} catch (e) {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(3001, () => {
|
||||
console.log('Listening on 3001');
|
||||
|
||||
console.log("Init")
|
||||
server.listen(port, hostname, () => {
|
||||
console.log(`Listening on ${hostname}:${port}`);
|
||||
startMonitors();
|
||||
});
|
||||
|
||||
@@ -496,12 +543,12 @@ async function afterLogin(socket, user) {
|
||||
let monitorList = await sendMonitorList(socket)
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await sendHeartbeatList(socket, monitorID);
|
||||
await sendImportantHeartbeatList(socket, monitorID);
|
||||
await Monitor.sendStats(io, monitorID, user.id)
|
||||
sendHeartbeatList(socket, monitorID);
|
||||
sendImportantHeartbeatList(socket, monitorID);
|
||||
Monitor.sendStats(io, monitorID, user.id)
|
||||
}
|
||||
|
||||
await sendNotificationList(socket)
|
||||
sendNotificationList(socket)
|
||||
}
|
||||
|
||||
async function getMonitorJSONList(userID) {
|
||||
@@ -525,18 +572,21 @@ function checkLogin(socket) {
|
||||
}
|
||||
|
||||
async function initDatabase() {
|
||||
const path = './data/kuma.db';
|
||||
|
||||
if (! fs.existsSync(path)) {
|
||||
console.log("Copy Database")
|
||||
fs.copyFileSync("./db/kuma.db", path);
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
console.log("Copying Database")
|
||||
fs.copyFileSync(Database.templatePath, Database.path);
|
||||
}
|
||||
|
||||
console.log("Connect to Database")
|
||||
|
||||
console.log("Connecting to Database")
|
||||
R.setup('sqlite', {
|
||||
filename: path
|
||||
filename: Database.path
|
||||
});
|
||||
console.log("Connected")
|
||||
|
||||
// Patch the database
|
||||
await Database.patch()
|
||||
|
||||
// Auto map the model to a bean object
|
||||
R.freeze(true)
|
||||
await R.autoloadModels("./server/model");
|
||||
|
||||
@@ -551,10 +601,12 @@ async function initDatabase() {
|
||||
|
||||
jwtSecretBean.value = passwordHash.generate(dayjs() + "")
|
||||
await R.store(jwtSecretBean)
|
||||
console.log("Stored JWT secret into database")
|
||||
} else {
|
||||
console.log("Load JWT secret from database.")
|
||||
}
|
||||
|
||||
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup
|
||||
if ((await R.count("user")) === 0) {
|
||||
console.log("No user, need setup")
|
||||
needSetup = true;
|
||||
@@ -649,3 +701,51 @@ async function sendImportantHeartbeatList(socket, monitorID) {
|
||||
|
||||
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) {
|
||||
console.log('Called signal: ' + signal);
|
||||
|
||||
console.log("Stopping all monitors")
|
||||
for (let id in monitorList) {
|
||||
let monitor = monitorList[id]
|
||||
monitor.stop()
|
||||
}
|
||||
await sleep(2000);
|
||||
await Database.close();
|
||||
}
|
||||
|
||||
function finalFunction() {
|
||||
console.log('Graceful Shutdown')
|
||||
}
|
||||
|
||||
gracefulShutdown(server, {
|
||||
signals: 'SIGINT SIGTERM',
|
||||
timeout: 30000, // timeout: 30 secs
|
||||
development: false, // not in dev mode
|
||||
forceExit: true, // triggers process.exit() at the end of shutdown process
|
||||
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
|
||||
finally: finalFunction // finally function (sync) - e.g. for logging
|
||||
});
|
||||
|
@@ -45,6 +45,18 @@ exports.setting = async function (key) {
|
||||
])
|
||||
}
|
||||
|
||||
exports.setSetting = async function (key, value) {
|
||||
let bean = await R.findOne("setting", " `key` = ? ", [
|
||||
key
|
||||
])
|
||||
if (! bean) {
|
||||
bean = R.dispense("setting")
|
||||
bean.key = key;
|
||||
}
|
||||
bean.value = value;
|
||||
await R.store(bean)
|
||||
}
|
||||
|
||||
exports.getSettings = async function (type) {
|
||||
let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [
|
||||
type
|
||||
@@ -56,7 +68,54 @@ exports.getSettings = async function (type) {
|
||||
result[row.key] = row.value;
|
||||
}
|
||||
|
||||
console.log(result)
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// ssl-checker by @dyaa
|
||||
// param: res - response object from axios
|
||||
// return an object containing the certificate information
|
||||
|
||||
const getDaysBetween = (validFrom, validTo) =>
|
||||
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
|
||||
|
||||
const getDaysRemaining = (validFrom, validTo) => {
|
||||
const daysRemaining = getDaysBetween(validFrom, validTo);
|
||||
if (new Date(validTo).getTime() < new Date().getTime()) {
|
||||
return -daysRemaining;
|
||||
}
|
||||
return daysRemaining;
|
||||
};
|
||||
|
||||
exports.checkCertificate = function (res) {
|
||||
const {
|
||||
valid_from,
|
||||
valid_to,
|
||||
subjectaltname,
|
||||
issuer,
|
||||
fingerprint,
|
||||
} = res.request.res.socket.getPeerCertificate(false);
|
||||
|
||||
if (!valid_from || !valid_to || !subjectaltname) {
|
||||
throw { message: 'No TLS certificate in response' };
|
||||
}
|
||||
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
|
||||
const validTo = new Date(valid_to);
|
||||
|
||||
const validFor = subjectaltname
|
||||
.replace(/DNS:|IP Address:/g, "")
|
||||
.split(", ");
|
||||
|
||||
const daysRemaining = getDaysRemaining(new Date(), validTo);
|
||||
|
||||
return {
|
||||
valid,
|
||||
validFor,
|
||||
validTo,
|
||||
daysRemaining,
|
||||
issuer,
|
||||
fingerprint,
|
||||
};
|
||||
}
|
@@ -1,15 +1,15 @@
|
||||
/*
|
||||
* Common functions - can be used in frontend or backend
|
||||
*/
|
||||
// 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;
|
||||
|
||||
|
||||
|
||||
export function sleep(ms) {
|
||||
exports.sleep = function (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function ucfirst(str) {
|
||||
exports.ucfirst = function (str) {
|
||||
if (! str) {
|
||||
return str;
|
||||
}
|
||||
@@ -18,3 +18,9 @@ export function ucfirst(str) {
|
||||
return firstLetter.toUpperCase() + str.substr(1);
|
||||
}
|
||||
|
||||
exports.debug = (msg) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(msg)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,8 @@
|
||||
$primary: #5CDD8B;
|
||||
$danger: #DC3545;
|
||||
$warning: #f8a306;
|
||||
$link-color: #111;
|
||||
$border-radius: 50rem;
|
||||
|
||||
$highlight: #7ce8a4;
|
||||
$highlight-white: #e7faec;
|
||||
$highlight-white: #e7faec;
|
@@ -5,7 +5,7 @@
|
||||
|
||||
<script>
|
||||
|
||||
import {sleep} from "../../server/util";
|
||||
import {sleep} from '../util-frontend'
|
||||
|
||||
export default {
|
||||
|
||||
|
@@ -4,9 +4,9 @@
|
||||
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone' // dependent on utc plugin
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone' // dependent on utc plugin
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(relativeTime)
|
||||
@@ -14,12 +14,23 @@ dayjs.extend(relativeTime)
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
dateOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayText() {
|
||||
let format = "YYYY-MM-DD HH:mm:ss";
|
||||
return dayjs.utc(this.value).tz(this.$root.timezone).format(format)
|
||||
if (this.value !== undefined && this.value !== "") {
|
||||
let format = "YYYY-MM-DD HH:mm:ss";
|
||||
if (this.dateOnly) {
|
||||
format = "YYYY-MM-DD";
|
||||
}
|
||||
return dayjs.utc(this.value).tz(this.$root.timezone).format(format);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div class="hp-bar-big" :style="barStyle">
|
||||
<div
|
||||
class="beat"
|
||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0) }"
|
||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
||||
:style="beatStyle"
|
||||
v-for="(beat, index) in shortBeatList"
|
||||
:key="index"
|
||||
@@ -166,6 +166,10 @@ export default {
|
||||
background-color: $danger;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
background-color: $warning;
|
||||
}
|
||||
|
||||
&:not(.empty):hover {
|
||||
transition: all ease-in-out 0.15s;
|
||||
opacity: 0.8;
|
||||
|
@@ -15,14 +15,14 @@
|
||||
<label for="floatingPassword">Password</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3 mt-3" >
|
||||
<label>
|
||||
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" value="remember-me" class="form-check-input" id="remember" v-model="$root.remember">
|
||||
|
||||
<label class="form-check-label" for="remember">
|
||||
Remember me
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">Login</button>
|
||||
|
||||
|
@@ -10,67 +10,72 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">Notification Type</label>
|
||||
<select class="form-select" id="type" v-model="notification.type">
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="smtp">Email (SMTP)</option>
|
||||
<option value="discord">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>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Friendly Name</label>
|
||||
<input type="text" class="form-control" id="name" required v-model="notification.name">
|
||||
</div>
|
||||
|
||||
<template v-if="notification.type === 'telegram'">
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">Notification Type</label>
|
||||
<select class="form-select" id="type" v-model="notification.type">
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="webhook">Webhook</option>
|
||||
<option value="smtp">Email (SMTP)</option>
|
||||
<option value="discord">Discord</option>
|
||||
</select>
|
||||
<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">
|
||||
<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 class="mb-3">
|
||||
<label for="name" class="form-label">Friendly Name</label>
|
||||
<input type="text" class="form-control" id="name" required v-model="notification.name">
|
||||
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">Auto Get</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
Support Direct Chat / Group / Channel's Chat ID
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
|
||||
<template v-if="notification.telegramBotToken">
|
||||
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{ telegramGetUpdatesURL }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="notification.type === 'telegram'">
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
<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 class="mb-3">
|
||||
<label for="telegram-chat-id" class="form-label">Chat ID</label>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" id="telegram-chat-id" required v-model="notification.telegramChatID">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID" v-if="notification.telegramBotToken">Auto Get</button>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
Support Direct Chat / Group / Channel's Chat ID
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
|
||||
<template v-if="notification.telegramBotToken">
|
||||
<a :href="telegramGetUpdatesURL" target="_blank">{{ telegramGetUpdatesURL }}</a>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{ telegramGetUpdatesURL }}
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'webhook'">
|
||||
<div class="mb-3">
|
||||
<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 type="url" pattern="https?://.+" class="form-control" id="webhook-url" required v-model="notification.webhookURL">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<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 class="form-select" id="webhook-content-type" v-model="notification.webhookContentType" required>
|
||||
<option value="json">application/json</option>
|
||||
<option value="form-data">multipart/form-data</option>
|
||||
</select>
|
||||
@@ -95,7 +100,7 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="secure" v-model="notification.smtpSecure">
|
||||
<input class="form-check-input" type="checkbox" value="" id="secure" v-model="notification.smtpSecure">
|
||||
<label class="form-check-label" for="secure">
|
||||
Secure
|
||||
</label>
|
||||
@@ -105,12 +110,12 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" required v-model="notification.smtpUsername" autocomplete="false">
|
||||
<input type="text" class="form-control" id="username" v-model="notification.smtpUsername" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" required v-model="notification.smtpPassword" autocomplete="false">
|
||||
<input type="password" class="form-control" id="password" v-model="notification.smtpPassword" autocomplete="false">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -127,22 +132,173 @@
|
||||
|
||||
<template v-if="notification.type === 'discord'">
|
||||
<div class="mb-3">
|
||||
<label for="discord-token" class="form-label">Discord Bot Token</label>
|
||||
<input type="text" class="form-control" id="discord-token" required v-model="notification.discordToken" autocomplete="false">
|
||||
<div class="form-text">You should create a Discord app and create a bot from <a href="https://discord.com/developers/applications" target="_blank">here</a>.</div>
|
||||
<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">
|
||||
<div class="form-text">You can get this by going to Server Settings -> Integrations -> Create Webhook</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'signal'">
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discordChannelID" class="form-label">Channel ID</label>
|
||||
<input type="text" class="form-control" id="discordChannelID" required v-model="notification.discordChannelID" autocomplete="false">
|
||||
<label for="signal-number" class="form-label">Number</label>
|
||||
<input type="text" class="form-control" id="signal-number" required v-model="notification.signalNumber">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="signal-recipients" class="form-label">Recipients</label>
|
||||
<input type="text" class="form-control" id="signal-recipients" required v-model="notification.signalRecipients">
|
||||
|
||||
<div class="form-text">
|
||||
You should add the bot to your channel. <br />
|
||||
<a href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-" target="_blank">Where can I find the channel id?</a><br />
|
||||
<a href="https://discordapi.com/permissions.html#8" target="_blank">How to add a bot to your channel?</a>
|
||||
You need to have a signal client with REST API.
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
You can check this url to view how to setup one:
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
<a href="https://github.com/bbernhard/signal-cli-rest-api" target="_blank">https://github.com/bbernhard/signal-cli-rest-api</a>
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 8px;">
|
||||
IMPORTANT: You cannot mix groups and numbers in recipients!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'gotify'">
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gotify-server-url" class="form-label">Server URL</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" id="gotify-server-url" required v-model="notification.gotifyserverurl">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'slack'">
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
<label for="slack-username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="slack-username" v-model="notification.slackusername">
|
||||
<label for="slack-iconemo" class="form-label">Icon Emoji</label>
|
||||
<input type="text" class="form-control" id="slack-iconemo" v-model="notification.slackiconemo">
|
||||
<label for="slack-channel" class="form-label">Channel Name</label>
|
||||
<input type="text" class="form-control" id="slack-channel-name" v-model="notification.slackchannel">
|
||||
<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">
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Enter the channel name on Slack Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'pushover'">
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
<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">
|
||||
<label for="pushover-device" class="form-label">Device</label>
|
||||
<input type="text" class="form-control" id="pushover-device" v-model="notification.pushoverdevice">
|
||||
<label for="pushover-device" class="form-label">Message Title</label>
|
||||
<input type="text" class="form-control" id="pushover-title" v-model="notification.pushovertitle">
|
||||
<label for="pushover-priority" class="form-label">Priority</label>
|
||||
<select class="form-select" id="pushover-priority" v-model="notification.pushoverpriority">
|
||||
<option>-2</option>
|
||||
<option>-1</option>
|
||||
<option>0</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
</select>
|
||||
<label for="pushover-sound" class="form-label">Notification Sound</label>
|
||||
<select class="form-select" id="pushover-sound" v-model="notification.pushoversounds">
|
||||
<option>pushover</option>
|
||||
<option>bike</option>
|
||||
<option>bugle</option>
|
||||
<option>cashregister</option>
|
||||
<option>classical</option>
|
||||
<option>cosmic</option>
|
||||
<option>falling</option>
|
||||
<option>gamelan</option>
|
||||
<option>incoming</option>
|
||||
<option>intermission</option>
|
||||
<option>mechanical</option>
|
||||
<option>pianobar</option>
|
||||
<option>siren</option>
|
||||
<option>spacealarm</option>
|
||||
<option>tugboat</option>
|
||||
<option>alien</option>
|
||||
<option>climb</option>
|
||||
<option>persistent</option>
|
||||
<option>echo</option>
|
||||
<option>updown</option>
|
||||
<option>vibrate</option>
|
||||
<option>none</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<span style="color:red;"><sup>*</sup></span>Required
|
||||
<p style="margin-top: 8px;">
|
||||
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
Emergency priority (2) has default 30 second timeout between retries and will expire after 1 hour.
|
||||
</p>
|
||||
<p style="margin-top: 8px;">
|
||||
If you want to send notifications to different devices, fill out Device field.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'apprise'">
|
||||
<div class="mb-3">
|
||||
<label for="apprise-url" class="form-label">Apprise URL</label>
|
||||
<input type="text" class="form-control" id="apprise-url" required v-model="notification.appriseURL">
|
||||
<div class="form-text">
|
||||
<p>Example: twilio://AccountSid:AuthToken@FromPhoneNo</p>
|
||||
<p>
|
||||
Read more: <a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank">https://github.com/caronc/apprise/wiki#notification-services</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<p>
|
||||
Status:
|
||||
<span class="text-primary" v-if="appriseInstalled">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>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" @click="deleteConfirm" :disabled="processing" v-if="id">Delete</button>
|
||||
@@ -160,7 +316,7 @@
|
||||
|
||||
<script>
|
||||
import { Modal } from 'bootstrap'
|
||||
import { ucfirst } from "../../server/util";
|
||||
import { ucfirst } from '../util-frontend'
|
||||
import axios from "axios";
|
||||
import { useToast } from 'vue-toastification'
|
||||
import Confirm from "./Confirm.vue";
|
||||
@@ -179,18 +335,17 @@ export default {
|
||||
notification: {
|
||||
name: "",
|
||||
type: null,
|
||||
gotifyPriority: 8
|
||||
},
|
||||
appriseInstalled: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal)
|
||||
|
||||
// TODO: for edit
|
||||
this.$root.getSocket().emit("getSettings", "notification", (data) => {
|
||||
// this.notification = data
|
||||
this.$root.getSocket().emit("checkApprise", (installed) => {
|
||||
this.appriseInstalled = installed;
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -218,6 +373,7 @@ export default {
|
||||
|
||||
// Default set to Telegram
|
||||
this.notification.type = "telegram"
|
||||
this.notification.gotifyPriority = 8
|
||||
}
|
||||
|
||||
this.modal.show()
|
||||
|
@@ -14,6 +14,8 @@ export default {
|
||||
return "danger"
|
||||
} else if (this.status === 1) {
|
||||
return "primary"
|
||||
} else if (this.status === 2) {
|
||||
return "warning"
|
||||
} else {
|
||||
return "secondary"
|
||||
}
|
||||
@@ -24,6 +26,8 @@ export default {
|
||||
return "Down"
|
||||
} else if (this.status === 1) {
|
||||
return "Up"
|
||||
} else if (this.status === 2) {
|
||||
return "Pending"
|
||||
} else {
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -34,6 +38,6 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
width: 45px;
|
||||
width: 54px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -30,6 +30,8 @@ export default {
|
||||
return "danger"
|
||||
} else if (this.lastHeartBeat.status === 1) {
|
||||
return "primary"
|
||||
} else if (this.lastHeartBeat.status === 2) {
|
||||
return "warning"
|
||||
} else {
|
||||
return "secondary"
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<!-- Desktop header -->
|
||||
<header class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom" v-if="! $root.isMobile">
|
||||
<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"></object>
|
||||
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo"></object>
|
||||
<span class="fs-4 title">Uptime Kuma</span>
|
||||
</router-link>
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container-fluid">
|
||||
Uptime Kuma -
|
||||
Version: {{ $root.info.version }} -
|
||||
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">Check Update On GitHub</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Mobile Only -->
|
||||
<div style="width: 100%;height: 60px;" v-if="$root.isMobile"></div>
|
||||
<nav class="bottom-nav" v-if="$root.isMobile">
|
||||
@@ -130,6 +138,15 @@ export default {
|
||||
}
|
||||
|
||||
main {
|
||||
margin-bottom: 30px;
|
||||
|
||||
}
|
||||
|
||||
footer {
|
||||
color: #AAA;
|
||||
font-size: 13px;
|
||||
margin-bottom: 30px;
|
||||
margin-left: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -9,6 +9,7 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
info: { },
|
||||
socket: {
|
||||
token: null,
|
||||
firstConnect: true,
|
||||
@@ -24,6 +25,7 @@ export default {
|
||||
importantHeartbeatList: { },
|
||||
avgPingList: { },
|
||||
uptimeList: { },
|
||||
certInfoList: {},
|
||||
notificationList: [],
|
||||
windowWidth: window.innerWidth,
|
||||
showListMobile: false,
|
||||
@@ -34,7 +36,8 @@ export default {
|
||||
window.addEventListener('resize', this.onResize);
|
||||
|
||||
let wsHost;
|
||||
if (localStorage.dev === "dev") {
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
wsHost = ":3001"
|
||||
} else {
|
||||
wsHost = ""
|
||||
@@ -44,11 +47,29 @@ export default {
|
||||
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) => {
|
||||
this.info = info;
|
||||
});
|
||||
|
||||
socket.on('setup', (monitorID, data) => {
|
||||
this.$router.push("/setup")
|
||||
});
|
||||
|
||||
socket.on('monitorList', (data) => {
|
||||
socket.on("monitorList", (data) => {
|
||||
// Add Helper function
|
||||
Object.entries(data).forEach(([monitorID, monitor]) => {
|
||||
monitor.getUrl = () => {
|
||||
try {
|
||||
return new URL(monitor.url);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
this.monitorList = data;
|
||||
});
|
||||
|
||||
@@ -104,6 +125,10 @@ export default {
|
||||
this.uptimeList[`${monitorID}_${type}`] = data
|
||||
});
|
||||
|
||||
socket.on('certInfo', (monitorID, data) => {
|
||||
this.certInfoList[monitorID] = JSON.parse(data)
|
||||
});
|
||||
|
||||
socket.on('importantHeartbeatList', (monitorID, data) => {
|
||||
if (! (monitorID in this.importantHeartbeatList)) {
|
||||
this.importantHeartbeatList[monitorID] = data;
|
||||
@@ -153,7 +178,7 @@ export default {
|
||||
},
|
||||
|
||||
getSocket() {
|
||||
return socket;
|
||||
return socket;
|
||||
},
|
||||
|
||||
toastRes(res) {
|
||||
@@ -269,6 +294,11 @@ export default {
|
||||
text: "Down",
|
||||
color: "danger"
|
||||
};
|
||||
} else if (lastHeartBeat.status === 2) {
|
||||
result[monitorID] = {
|
||||
text: "Pending",
|
||||
color: "warning"
|
||||
};
|
||||
} else {
|
||||
result[monitorID] = unknown;
|
||||
}
|
||||
@@ -280,6 +310,13 @@ export default {
|
||||
|
||||
watch: {
|
||||
|
||||
// Reload the SPA if the server version is changed.
|
||||
"info.version"(to, from) {
|
||||
if (from && from !== to) {
|
||||
window.location.reload()
|
||||
}
|
||||
},
|
||||
|
||||
remember() {
|
||||
localStorage.remember = (this.remember) ? "1" : "0"
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@
|
||||
No Monitors, please <router-link to="/add">add one</router-link>.
|
||||
</div>
|
||||
|
||||
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="item in sortedMonitorList" @click="$root.cancelActiveList">
|
||||
<router-link :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" v-for="(item, index) in sortedMonitorList" @click="$root.cancelActiveList" :key="index">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-8 small-padding">
|
||||
|
@@ -47,7 +47,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="beat in importantHeartBeatList">
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index">
|
||||
<td>{{ beat.name }}</td>
|
||||
<td><Status :status="beat.status" /></td>
|
||||
<td><Datetime :value="beat.time" /></td>
|
||||
@@ -59,6 +59,13 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-center kuma_pagination">
|
||||
<pagination
|
||||
v-model="page"
|
||||
:records=importantHeartBeatList.length
|
||||
:per-page="perPage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,8 +75,21 @@
|
||||
<script>
|
||||
import Status from "../components/Status.vue";
|
||||
import Datetime from "../components/Datetime.vue";
|
||||
import Pagination from "v-pagination-3";
|
||||
|
||||
export default {
|
||||
components: {Datetime, Status},
|
||||
components: {
|
||||
Datetime,
|
||||
Status,
|
||||
Pagination,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
heartBeatList: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
stats() {
|
||||
let result = {
|
||||
@@ -90,6 +110,8 @@ export default {
|
||||
result.up++;
|
||||
} else if (beat.status === 0) {
|
||||
result.down++;
|
||||
} else if (beat.status === 2) {
|
||||
result.up++;
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
@@ -127,8 +149,16 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
this.heartBeatList = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
|
||||
displayedRecords() {
|
||||
const startIndex = this.perPage * (this.page - 1);
|
||||
const endIndex = startIndex + this.perPage;
|
||||
return this.heartBeatList.slice(startIndex, endIndex);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -12,7 +12,7 @@
|
||||
|
||||
<div class="functions">
|
||||
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button>
|
||||
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button>
|
||||
<button class="btn btn-primary" @click="resumeMonitor" v-if="! monitor.active">Resume</button>
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">Edit</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">Delete</button>
|
||||
</div>
|
||||
@@ -51,6 +51,46 @@
|
||||
<p>(30-day)</p>
|
||||
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
|
||||
</div>
|
||||
|
||||
<div class="col" v-if="certInfo">
|
||||
<h4>CertExp.</h4>
|
||||
<p>(<Datetime :value="certInfo.validTo" date-only />)</p>
|
||||
<span class="num" >
|
||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{certInfo.daysRemaining}} days</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box big-padding text-center" v-if="showCertInfoBox">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>Certificate Info</h4>
|
||||
<table class="text-start">
|
||||
<tbody>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Valid: </td>
|
||||
<td>{{ certInfo.valid }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Valid To: </td>
|
||||
<td><Datetime :value="certInfo.validTo" /></td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Days Remaining: </td>
|
||||
<td>{{ certInfo.daysRemaining }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Issuer: </td>
|
||||
<td>{{ certInfo.issuer }}</td>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">Fingerprint: </td>
|
||||
<td>{{ certInfo.fingerprint }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +104,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="beat in importantHeartBeatList">
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index">
|
||||
<td><Status :status="beat.status" /></td>
|
||||
<td><Datetime :value="beat.time" /></td>
|
||||
<td>{{ beat.msg }}</td>
|
||||
@@ -75,6 +115,13 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-center kuma_pagination">
|
||||
<pagination
|
||||
v-model="page"
|
||||
:records=importantHeartBeatList.length
|
||||
:per-page="perPage" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmPause" @yes="pauseMonitor">
|
||||
@@ -95,6 +142,7 @@ import Status from "../components/Status.vue";
|
||||
import Datetime from "../components/Datetime.vue";
|
||||
import CountUp from "../components/CountUp.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import Pagination from "v-pagination-3";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -104,13 +152,17 @@ export default {
|
||||
HeartbeatBar,
|
||||
Confirm,
|
||||
Status,
|
||||
Pagination,
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
heartBeatList: [],
|
||||
toggleCertInfoBox: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -137,7 +189,7 @@ export default {
|
||||
},
|
||||
|
||||
ping() {
|
||||
if (this.lastHeartBeat.ping) {
|
||||
if (this.lastHeartBeat.ping || this.lastHeartBeat.ping === 0) {
|
||||
return this.lastHeartBeat.ping;
|
||||
} else {
|
||||
return "N/A"
|
||||
@@ -145,7 +197,7 @@ export default {
|
||||
},
|
||||
|
||||
avgPing() {
|
||||
if (this.$root.avgPingList[this.monitor.id]) {
|
||||
if (this.$root.avgPingList[this.monitor.id] || this.$root.avgPingList[this.monitor.id] === 0) {
|
||||
return this.$root.avgPingList[this.monitor.id];
|
||||
} else {
|
||||
return "N/A"
|
||||
@@ -154,6 +206,7 @@ export default {
|
||||
|
||||
importantHeartBeatList() {
|
||||
if (this.$root.importantHeartbeatList[this.monitor.id]) {
|
||||
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
|
||||
return this.$root.importantHeartbeatList[this.monitor.id]
|
||||
} else {
|
||||
return [];
|
||||
@@ -166,8 +219,25 @@ export default {
|
||||
} else {
|
||||
return { }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
certInfo() {
|
||||
if (this.$root.certInfoList[this.monitor.id]) {
|
||||
return this.$root.certInfoList[this.monitor.id]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
showCertInfoBox() {
|
||||
return this.certInfo != null && this.toggleCertInfoBox;
|
||||
},
|
||||
|
||||
displayedRecords() {
|
||||
const startIndex = this.perPage * (this.page - 1);
|
||||
const endIndex = startIndex + this.perPage;
|
||||
return this.heartBeatList.slice(startIndex, endIndex);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
testNotification() {
|
||||
@@ -251,4 +321,12 @@ table {
|
||||
font-size: 13px;
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
.stats {
|
||||
padding: 10px;
|
||||
|
||||
.col {
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -30,7 +30,7 @@
|
||||
<div class="mb-3" v-if="monitor.type === 'keyword' ">
|
||||
<label for="keyword" class="form-label">Keyword</label>
|
||||
<input type="text" class="form-control" id="keyword" v-model="monitor.keyword" required>
|
||||
<div class="form-text">Search keyword in plain html 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 class="mb-3" v-if="monitor.type === 'port' || monitor.type === 'ping' ">
|
||||
@@ -48,6 +48,12 @@
|
||||
<input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20" step="1">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<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">
|
||||
<div class="form-text">Maximum retries before the service is marked as down and a notification is sent</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button>
|
||||
</div>
|
||||
@@ -61,7 +67,7 @@
|
||||
<h2>Notifications</h2>
|
||||
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
|
||||
|
||||
<div class="form-check form-switch mb-3" v-for="notification in $root.notificationList">
|
||||
<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">
|
||||
@@ -119,6 +125,7 @@ export default {
|
||||
name: "",
|
||||
url: "https://",
|
||||
interval: 60,
|
||||
maxretries: 0,
|
||||
notificationIDList: {},
|
||||
}
|
||||
} else if (this.isEdit) {
|
||||
|
@@ -11,7 +11,7 @@
|
||||
<label for="timezone" class="form-label">Timezone</label>
|
||||
<select class="form-select" id="timezone" v-model="$root.userTimezone">
|
||||
<option value="auto">Auto: {{ guessTimezone }}</option>
|
||||
<option v-for="timezone in timezoneList" :value="timezone.value">{{ timezone.name }}</option>
|
||||
<option v-for="(timezone, index) in timezoneList" :value="timezone.value" :key="index">{{ timezone.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<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">
|
||||
<div class="invalid-feedback">
|
||||
The repeat password is not match.
|
||||
The repeat password does not match.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,10 +56,10 @@
|
||||
|
||||
<h2>Notifications</h2>
|
||||
<p v-if="$root.notificationList.length === 0">Not available, please setup.</p>
|
||||
<p v-else>Please assign the notification to monitor(s) to get it works.</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;">
|
||||
<li class="list-group-item" v-for="notification in $root.notificationList">
|
||||
<li class="list-group-item" v-for="(notification, index) in $root.notificationList" :key="index">
|
||||
{{ notification.name }}<br />
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||
</li>
|
||||
@@ -77,8 +77,8 @@
|
||||
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
@@ -1,10 +1,23 @@
|
||||
import dayjs from "dayjs";
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
|
||||
dayjs.extend(utc)
|
||||
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) {
|
||||
const now = new Date();
|
||||
@@ -16,6 +29,7 @@ function getTimezoneOffset(timeZone) {
|
||||
}
|
||||
|
||||
// From: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
|
||||
// TODO: Move to separate file
|
||||
const aryIannaTimeZones = [
|
||||
'Europe/Andorra',
|
||||
'Asia/Dubai',
|
||||
@@ -24,7 +38,6 @@ const aryIannaTimeZones = [
|
||||
'Asia/Yerevan',
|
||||
'Antarctica/Casey',
|
||||
'Antarctica/Davis',
|
||||
'Antarctica/DumontDUrville', // https://bugs.chromium.org/p/chromium/issues/detail?id=928068
|
||||
'Antarctica/Mawson',
|
||||
'Antarctica/Palmer',
|
||||
'Antarctica/Rothera',
|
||||
@@ -195,7 +208,6 @@ const aryIannaTimeZones = [
|
||||
'Asia/Seoul',
|
||||
'Asia/Almaty',
|
||||
'Asia/Qyzylorda',
|
||||
'Asia/Qostanay', // https://bugs.chromium.org/p/chromium/issues/detail?id=928068
|
||||
'Asia/Aqtobe',
|
||||
'Asia/Aqtau',
|
||||
'Asia/Atyrau',
|
||||
@@ -364,22 +376,29 @@ const aryIannaTimeZones = [
|
||||
'Pacific/Efate',
|
||||
'Pacific/Wallis',
|
||||
'Pacific/Apia',
|
||||
'Africa/Johannesburg'
|
||||
'Africa/Johannesburg',
|
||||
];
|
||||
|
||||
|
||||
export function timezoneList() {
|
||||
|
||||
let result = [];
|
||||
|
||||
for (let timezone of aryIannaTimeZones) {
|
||||
|
||||
let display = dayjs().tz(timezone).format("Z");
|
||||
try {
|
||||
let display = dayjs().tz(timezone).format("Z");
|
||||
|
||||
result.push({
|
||||
name: `(UTC${display}) ${timezone}`,
|
||||
value: timezone,
|
||||
time: getTimezoneOffset(timezone),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
console.log("Skip this timezone")
|
||||
}
|
||||
|
||||
result.push({
|
||||
name: `(UTC${display}) ${timezone}`,
|
||||
value: timezone,
|
||||
time: getTimezoneOffset(timezone),
|
||||
})
|
||||
}
|
||||
|
||||
result.sort((a, b) => {
|
||||
@@ -394,4 +413,3 @@ export function timezoneList() {
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
legacy({
|
||||
targets: ['ie >= 11'],
|
||||
targets: ['ie > 11'],
|
||||
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
|
||||
})
|
||||
]
|
||||
|
Reference in New Issue
Block a user