mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-11 22:06:59 +08:00
Compare commits
74 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
49ba5fb1b2 | ||
|
d27789a8ae | ||
|
b53582a812 | ||
|
05680472a7 | ||
|
ca3b0a0f19 | ||
|
4571a9b8c1 | ||
|
362eabab8d | ||
|
a22d0f6951 | ||
|
b1168d4cdb | ||
|
6fcc2253ec | ||
|
f21937b197 | ||
|
1e7623c459 | ||
|
22047fe932 | ||
|
209e44c2e1 | ||
|
30b8d3d0ab | ||
|
64498163e1 | ||
|
4f70a70dda | ||
|
5b5a32967c | ||
|
ae8b5eea5a | ||
|
b761aaffdf | ||
|
7ffdb2eb80 | ||
|
2d36f4cd4a | ||
|
2339405f90 | ||
|
8f5e5ad944 | ||
|
575c3ee182 | ||
|
c9aa110f6c | ||
|
bb0af35d47 | ||
|
61944d642e | ||
|
21640e1bbe | ||
|
de4515ea6e | ||
|
b41799f801 | ||
|
d8bcfcaaa2 | ||
|
cf5168a4e6 | ||
|
60b0ee2959 | ||
|
f1e5e53e8f | ||
|
432388a905 | ||
|
746c1b6acc | ||
|
269ac2410b | ||
|
f72cdcc663 | ||
|
6b3fbcd1e7 | ||
|
8a48f5dd71 | ||
|
d218661f3d | ||
|
77369bd002 | ||
|
29a89df524 | ||
|
e257fa7b2d | ||
|
6980c38a6c | ||
|
8d57df7256 | ||
|
64501bf065 | ||
|
440c178403 | ||
|
c9c51e47e1 | ||
|
5e52f230b1 | ||
|
61e758d872 | ||
|
86826fb826 | ||
|
7a32e5e6ff | ||
|
610f2f9c47 | ||
|
01e9c76a6f | ||
|
5927c2703f | ||
|
316db89b9a | ||
|
eed6d3e847 | ||
|
2a62f6daae | ||
|
e09c296410 | ||
|
d7f660ec57 | ||
|
798f39acf0 | ||
|
31d5b4fd3d | ||
|
fc76c2836b | ||
|
0b30bfff87 | ||
|
72f0724b9a | ||
|
35176a614f | ||
|
8e883c9c6a | ||
|
2f89ee4937 | ||
|
233c5661af | ||
|
91d4c15b4d | ||
|
e1a38f64f8 | ||
|
45c162583b |
@@ -71,5 +71,8 @@ module.exports = {
|
||||
"eol-last": ["error", "always"],
|
||||
//'prefer-template': 'error',
|
||||
"comma-dangle": ["warn", "only-multiline"],
|
||||
"no-empty": ["error", {
|
||||
"allowEmptyCatch": true
|
||||
}],
|
||||
},
|
||||
}
|
||||
|
6
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
6
.github/ISSUE_TEMPLATE/ask-for-help.md
vendored
@@ -9,3 +9,9 @@ assignees: ''
|
||||
**Is it a duplicate question?**
|
||||
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
|
||||
|
||||
**Info**
|
||||
Uptime Kuma Version:
|
||||
Using Docker?: Yes/No
|
||||
OS:
|
||||
Browser:
|
||||
|
||||
|
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -23,15 +23,16 @@ Steps to reproduce the behavior:
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
|
||||
**Info**
|
||||
- Uptime Kuma Version:
|
||||
- Using Docker?: Yes/No
|
||||
- OS:
|
||||
- Browser:
|
||||
|
||||
**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:
|
||||
**Error Log**
|
||||
It is easier for us to find out the problem.
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
64
README.md
64
README.md
@@ -19,75 +19,45 @@ It is a self-hosted monitoring tool like "Uptime Robot".
|
||||
|
||||
## 🔧 How to Install
|
||||
|
||||
### 🚀 Installer via cli
|
||||
|
||||
Interactive cli installer, supports Docker or without Docker.
|
||||
|
||||
```bash
|
||||
curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||
```
|
||||
|
||||
### 🐳 Docker
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
|
||||
### ☸️ Kubernetes
|
||||
|
||||
If you want to change port and volume, or need to browse via a reserve proxy, please read: https://github.com/louislam/uptime-kuma/wiki/Installation.
|
||||
See more [here](kubernetes/README.md)
|
||||
|
||||
### 💪🏻 Without Docker (Recommanded for x86/x64 only)
|
||||
|
||||
Required Tools: Node.js >= 14, git and pm2.
|
||||
### Advanced Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/louislam/uptime-kuma.git
|
||||
cd uptime-kuma
|
||||
npm run setup
|
||||
If you need more options or need to browse via a reserve proxy, please read:
|
||||
|
||||
# Option 1. Try it
|
||||
npm run start-server
|
||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
|
||||
|
||||
# (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
|
||||
|
||||
```
|
||||
|
||||
Browse to http://localhost:3001 after started.
|
||||
|
||||
If you want to change port and hostname, or need to browse via a reserve proxy, please read: https://github.com/louislam/uptime-kuma/wiki/Installation.
|
||||
|
||||
## 🆙 How to Update
|
||||
|
||||
### 🆙🐳 Docker
|
||||
Please read:
|
||||
|
||||
Re-pull the latest docker image and create another container with the same volume.
|
||||
|
||||
For someone who used my "How-to-use" commands to install Uptime Kuma, you can update by this:
|
||||
|
||||
```bash
|
||||
docker pull louislam/uptime-kuma:1
|
||||
docker stop uptime-kuma
|
||||
docker rm uptime-kuma
|
||||
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
```
|
||||
|
||||
PS: For every new release, it takes some time to build the docker image, please be patient if it is not available yet.
|
||||
|
||||
### 🆙 💪🏻 Without Docker
|
||||
|
||||
```bash
|
||||
cd <uptime-kuma-directory>
|
||||
git fetch --all
|
||||
git checkout 1.2.0 --force
|
||||
npm install
|
||||
npm run build
|
||||
pm2 restart uptime-kuma
|
||||
```
|
||||
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
|
||||
|
||||
## 🆕 What's Next?
|
||||
|
||||
I will mark requests/issues to the next milestone.
|
||||
|
||||
https://github.com/louislam/uptime-kuma/milestones
|
||||
|
||||
## 🖼 More Screenshots
|
||||
@@ -122,5 +92,3 @@ If you want to report a bug or request a new feature. Free feel to open a new is
|
||||
If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||
|
||||
English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki.
|
||||
|
||||
🐻
|
||||
|
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.x.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
https://github.com/louislam/uptime-kuma/issues
|
@@ -1 +1,2 @@
|
||||
# Must enable File Sharing in Docker Desktop
|
||||
docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh
|
245
extra/install.batsh
Normal file
245
extra/install.batsh
Normal file
@@ -0,0 +1,245 @@
|
||||
// install.sh is generated by ./extra/install.batsh, do not modify it directly.
|
||||
// "npm run compile-install-script" to compile install.sh
|
||||
// The command is working on Windows PowerShell and Docker for Windows only.
|
||||
|
||||
|
||||
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||
println("=====================");
|
||||
println("Uptime Kuma Installer");
|
||||
println("=====================");
|
||||
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
|
||||
println("---------------------------------------");
|
||||
println("This script is designed for Linux and basic usage.");
|
||||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
||||
println("---------------------------------------");
|
||||
println("");
|
||||
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
|
||||
println("Docker - Install Uptime Kuma Docker container");
|
||||
println("");
|
||||
|
||||
if ("$1" != "") {
|
||||
type = "$1";
|
||||
} else {
|
||||
call("read", "-p", "Which installation method do you prefer? [DOCKER/local]: ", "type");
|
||||
}
|
||||
|
||||
defaultPort = "3001";
|
||||
|
||||
function checkNode() {
|
||||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
||||
println("Node Version: " ++ nodeVersion);
|
||||
|
||||
if (nodeVersion < "12") {
|
||||
println("Error: Required Node.js 14");
|
||||
call("exit", "1");
|
||||
}
|
||||
|
||||
if (nodeVersion == "12") {
|
||||
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
|
||||
}
|
||||
}
|
||||
|
||||
function deb() {
|
||||
bash("nodeCheck=$(node -v)");
|
||||
bash("apt --yes update");
|
||||
|
||||
if (nodeCheck != "") {
|
||||
checkNode();
|
||||
} else {
|
||||
|
||||
// Old nodejs binary name is "nodejs"
|
||||
bash("check=$(nodejs --version)");
|
||||
if (check != "") {
|
||||
println("Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old.");
|
||||
bash("exit 1");
|
||||
}
|
||||
|
||||
bash("curlCheck=$(curl --version)");
|
||||
if (curlCheck == "") {
|
||||
println("Installing Curl");
|
||||
bash("apt --yes install curl");
|
||||
}
|
||||
|
||||
println("Installing Node.js 14");
|
||||
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
|
||||
bash("apt --yes install nodejs");
|
||||
bash("node -v");
|
||||
|
||||
bash("nodeCheckAgain=$(node -v)");
|
||||
|
||||
if (nodeCheckAgain == "") {
|
||||
println("Error during Node.js installation");
|
||||
bash("exit 1");
|
||||
}
|
||||
}
|
||||
|
||||
bash("check=$(git --version)");
|
||||
if (check == "") {
|
||||
println("Installing Git");
|
||||
bash("apt --yes install git");
|
||||
}
|
||||
}
|
||||
|
||||
if (type == "local") {
|
||||
defaultInstallPath = "/opt/uptime-kuma";
|
||||
|
||||
if (exists("/etc/redhat-release")) {
|
||||
os = call("cat", "/etc/redhat-release");
|
||||
distribution = "rhel";
|
||||
|
||||
} else if (exists("/etc/issue")) {
|
||||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
||||
if (os == "Ubuntu") {
|
||||
distribution = "ubuntu";
|
||||
}
|
||||
if (os == "Debian") {
|
||||
distribution = "debian";
|
||||
}
|
||||
}
|
||||
|
||||
bash("arch=$(uname -i)");
|
||||
|
||||
println("Your OS: " ++ os);
|
||||
println("Distribution: " ++ distribution);
|
||||
println("Arch: " ++ arch);
|
||||
|
||||
if ("$3" != "") {
|
||||
port = "$3";
|
||||
} else {
|
||||
call("read", "-p", "Listening Port [$defaultPort]: ", "port");
|
||||
|
||||
if (port == "") {
|
||||
port = defaultPort;
|
||||
}
|
||||
}
|
||||
|
||||
if ("$2" != "") {
|
||||
installPath = "$2";
|
||||
} else {
|
||||
call("read", "-p", "Installation Path [$defaultInstallPath]: ", "installPath");
|
||||
|
||||
if (installPath == "") {
|
||||
installPath = defaultInstallPath;
|
||||
}
|
||||
}
|
||||
|
||||
// CentOS
|
||||
if (distribution == "rhel") {
|
||||
bash("nodeCheck=$(node -v)");
|
||||
|
||||
if (nodeCheck != "") {
|
||||
checkNode();
|
||||
} else {
|
||||
|
||||
bash("curlCheck=$(curl --version)");
|
||||
if (curlCheck == "") {
|
||||
println("Installing Curl");
|
||||
bash("yum -y -q install curl");
|
||||
}
|
||||
|
||||
println("Installing Node.js 14");
|
||||
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
|
||||
bash("yum install -y -q nodejs");
|
||||
bash("node -v");
|
||||
|
||||
bash("nodeCheckAgain=$(node -v)");
|
||||
|
||||
if (nodeCheckAgain == "") {
|
||||
println("Error during Node.js installation");
|
||||
bash("exit 1");
|
||||
}
|
||||
}
|
||||
|
||||
bash("check=$(git --version)");
|
||||
if (check == "") {
|
||||
println("Installing Git");
|
||||
bash("yum -y -q install git");
|
||||
}
|
||||
|
||||
// Ubuntu
|
||||
} else if (distribution == "ubuntu") {
|
||||
deb();
|
||||
|
||||
// Debian
|
||||
} else if (distribution == "debian") {
|
||||
deb();
|
||||
|
||||
} else {
|
||||
// Unknown distribution
|
||||
error = 0;
|
||||
|
||||
bash("check=$(git --version)");
|
||||
if (check == "") {
|
||||
error = 1;
|
||||
println("Error: git is missing");
|
||||
}
|
||||
|
||||
bash("check=$(node -v)");
|
||||
if (check == "") {
|
||||
error = 1;
|
||||
println("Error: node is missing");
|
||||
}
|
||||
|
||||
if (error > 0) {
|
||||
println("Please install above missing software");
|
||||
bash("exit 1");
|
||||
}
|
||||
}
|
||||
|
||||
bash("check=$(pm2 --version)");
|
||||
if (check == "") {
|
||||
println("Installing PM2");
|
||||
bash("npm install pm2 -g");
|
||||
bash("pm2 startup");
|
||||
}
|
||||
|
||||
bash("mkdir -p $installPath");
|
||||
bash("cd $installPath");
|
||||
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
||||
bash("npm run setup");
|
||||
|
||||
bash("pm2 start npm --name uptime-kuma -- run start-server -- --port=$port");
|
||||
|
||||
} else {
|
||||
defaultVolume = "uptime-kuma";
|
||||
|
||||
bash("check=$(docker -v)");
|
||||
if (check == "") {
|
||||
println("Error: docker is not found!");
|
||||
bash("exit 1");
|
||||
}
|
||||
|
||||
bash("check=$(docker info)");
|
||||
|
||||
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then
|
||||
echo \"Error: docker is not running\"
|
||||
exit 1
|
||||
fi");
|
||||
|
||||
if ("$3" != "") {
|
||||
port = "$3";
|
||||
} else {
|
||||
call("read", "-p", "Expose Port [$defaultPort]: ", "port");
|
||||
|
||||
if (port == "") {
|
||||
port = defaultPort;
|
||||
}
|
||||
}
|
||||
|
||||
if ("$2" != "") {
|
||||
volume = "$2";
|
||||
} else {
|
||||
call("read", "-p", "Volume Name [$defaultVolume]: ", "volume");
|
||||
|
||||
if (volume == "") {
|
||||
volume = defaultVolume;
|
||||
}
|
||||
}
|
||||
|
||||
println("Port: $port");
|
||||
println("Volume: $volume");
|
||||
bash("docker volume create $volume");
|
||||
bash("docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1");
|
||||
}
|
||||
|
||||
println("http://localhost:$port");
|
@@ -25,9 +25,6 @@ if (! exists) {
|
||||
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
// Process README.md
|
||||
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
|
||||
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
} else {
|
||||
|
203
install.sh
Normal file
203
install.sh
Normal file
@@ -0,0 +1,203 @@
|
||||
# install.sh is generated by ./extra/install.batsh, do not modify it directly.
|
||||
# "npm run compile-install-script" to compile install.sh
|
||||
# The command is working on Windows PowerShell and Docker for Windows only.
|
||||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||
"echo" "-e" "====================="
|
||||
"echo" "-e" "Uptime Kuma Installer"
|
||||
"echo" "-e" "====================="
|
||||
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
|
||||
"echo" "-e" "---------------------------------------"
|
||||
"echo" "-e" "This script is designed for Linux and basic usage."
|
||||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
||||
"echo" "-e" "---------------------------------------"
|
||||
"echo" "-e" ""
|
||||
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
|
||||
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
||||
"echo" "-e" ""
|
||||
if [ "$1" != "" ]; then
|
||||
type="$1"
|
||||
else
|
||||
"read" "-p" "Which installation method do you prefer? [DOCKER/local]: " "type"
|
||||
fi
|
||||
defaultPort="3001"
|
||||
function checkNode {
|
||||
local _0
|
||||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
||||
"echo" "-e" "Node Version: ""$nodeVersion"
|
||||
_0="12"
|
||||
if [ $(($nodeVersion < $_0)) == 1 ]; then
|
||||
"echo" "-e" "Error: Required Node.js 14"
|
||||
"exit" "1"
|
||||
fi
|
||||
if [ "$nodeVersion" == "12" ]; then
|
||||
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
|
||||
fi
|
||||
}
|
||||
function deb {
|
||||
nodeCheck=$(node -v)
|
||||
apt --yes update
|
||||
if [ "$nodeCheck" != "" ]; then
|
||||
"checkNode"
|
||||
else
|
||||
# Old nodejs binary name is "nodejs"
|
||||
check=$(nodejs --version)
|
||||
if [ "$check" != "" ]; then
|
||||
"echo" "-e" "Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old."
|
||||
exit 1
|
||||
fi
|
||||
curlCheck=$(curl --version)
|
||||
if [ "$curlCheck" == "" ]; then
|
||||
"echo" "-e" "Installing Curl"
|
||||
apt --yes install curl
|
||||
fi
|
||||
"echo" "-e" "Installing Node.js 14"
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
|
||||
apt --yes install nodejs
|
||||
node -v
|
||||
nodeCheckAgain=$(node -v)
|
||||
if [ "$nodeCheckAgain" == "" ]; then
|
||||
"echo" "-e" "Error during Node.js installation"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
check=$(git --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Installing Git"
|
||||
apt --yes install git
|
||||
fi
|
||||
}
|
||||
if [ "$type" == "local" ]; then
|
||||
defaultInstallPath="/opt/uptime-kuma"
|
||||
if [ -e "/etc/redhat-release" ]; then
|
||||
os=$("cat" "/etc/redhat-release")
|
||||
distribution="rhel"
|
||||
else
|
||||
if [ -e "/etc/issue" ]; then
|
||||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
||||
if [ "$os" == "Ubuntu" ]; then
|
||||
distribution="ubuntu"
|
||||
fi
|
||||
if [ "$os" == "Debian" ]; then
|
||||
distribution="debian"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
arch=$(uname -i)
|
||||
"echo" "-e" "Your OS: ""$os"
|
||||
"echo" "-e" "Distribution: ""$distribution"
|
||||
"echo" "-e" "Arch: ""$arch"
|
||||
if [ "$3" != "" ]; then
|
||||
port="$3"
|
||||
else
|
||||
"read" "-p" "Listening Port [$defaultPort]: " "port"
|
||||
if [ "$port" == "" ]; then
|
||||
port="$defaultPort"
|
||||
fi
|
||||
fi
|
||||
if [ "$2" != "" ]; then
|
||||
installPath="$2"
|
||||
else
|
||||
"read" "-p" "Installation Path [$defaultInstallPath]: " "installPath"
|
||||
if [ "$installPath" == "" ]; then
|
||||
installPath="$defaultInstallPath"
|
||||
fi
|
||||
fi
|
||||
# CentOS
|
||||
if [ "$distribution" == "rhel" ]; then
|
||||
nodeCheck=$(node -v)
|
||||
if [ "$nodeCheck" != "" ]; then
|
||||
"checkNode"
|
||||
else
|
||||
curlCheck=$(curl --version)
|
||||
if [ "$curlCheck" == "" ]; then
|
||||
"echo" "-e" "Installing Curl"
|
||||
yum -y -q install curl
|
||||
fi
|
||||
"echo" "-e" "Installing Node.js 14"
|
||||
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
|
||||
yum install -y -q nodejs
|
||||
node -v
|
||||
nodeCheckAgain=$(node -v)
|
||||
if [ "$nodeCheckAgain" == "" ]; then
|
||||
"echo" "-e" "Error during Node.js installation"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
check=$(git --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Installing Git"
|
||||
yum -y -q install git
|
||||
fi
|
||||
# Ubuntu
|
||||
else
|
||||
if [ "$distribution" == "ubuntu" ]; then
|
||||
"deb"
|
||||
# Debian
|
||||
else
|
||||
if [ "$distribution" == "debian" ]; then
|
||||
"deb"
|
||||
else
|
||||
# Unknown distribution
|
||||
error=$((0))
|
||||
check=$(git --version)
|
||||
if [ "$check" == "" ]; then
|
||||
error=$((1))
|
||||
"echo" "-e" "Error: git is missing"
|
||||
fi
|
||||
check=$(node -v)
|
||||
if [ "$check" == "" ]; then
|
||||
error=$((1))
|
||||
"echo" "-e" "Error: node is missing"
|
||||
fi
|
||||
if [ $(($error > 0)) == 1 ]; then
|
||||
"echo" "-e" "Please install above missing software"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
check=$(pm2 --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Installing PM2"
|
||||
npm install pm2 -g
|
||||
pm2 startup
|
||||
fi
|
||||
mkdir -p $installPath
|
||||
cd $installPath
|
||||
git clone https://github.com/louislam/uptime-kuma.git .
|
||||
npm run setup
|
||||
pm2 start npm --name uptime-kuma -- run start-server -- --port=$port
|
||||
else
|
||||
defaultVolume="uptime-kuma"
|
||||
check=$(docker -v)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Error: docker is not found!"
|
||||
exit 1
|
||||
fi
|
||||
check=$(docker info)
|
||||
if [[ "$check" == *"Is the docker daemon running"* ]]; then
|
||||
echo "Error: docker is not running"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$3" != "" ]; then
|
||||
port="$3"
|
||||
else
|
||||
"read" "-p" "Expose Port [$defaultPort]: " "port"
|
||||
if [ "$port" == "" ]; then
|
||||
port="$defaultPort"
|
||||
fi
|
||||
fi
|
||||
if [ "$2" != "" ]; then
|
||||
volume="$2"
|
||||
else
|
||||
"read" "-p" "Volume Name [$defaultVolume]: " "volume"
|
||||
if [ "$volume" == "" ]; then
|
||||
volume="$defaultVolume"
|
||||
fi
|
||||
fi
|
||||
"echo" "-e" "Port: $port"
|
||||
"echo" "-e" "Volume: $volume"
|
||||
docker volume create $volume
|
||||
docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
||||
fi
|
||||
"echo" "-e" "http://localhost:$port"
|
28
kubernetes/README.md
Normal file
28
kubernetes/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Uptime-Kuma K8s Deployment
|
||||
## How does it work?
|
||||
|
||||
Kustomize is a tool which builds a complete deployment file for all config elements.
|
||||
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
|
||||
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
|
||||
|
||||
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service
|
||||
|
||||
## What do i have to edit?
|
||||
You have to edit the ```ingressroute.yml``` to your needs.
|
||||
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
|
||||
|
||||
- host
|
||||
- secrets and secret names
|
||||
- (Cluster)Issuer (optional)
|
||||
- the Version in the Deployment-File
|
||||
- update:
|
||||
- change to newer version and run the above commands, it will update the pods one after another
|
||||
|
||||
## How To use:
|
||||
|
||||
- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
|
||||
- Edit files mentioned above to your needs
|
||||
- run ```kustomize build > apply.yml```
|
||||
- run ```kubectl apply -f apply.yml```
|
||||
|
||||
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.
|
10
kubernetes/kustomization.yml
Normal file
10
kubernetes/kustomization.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace: uptime-kuma
|
||||
namePrefix: uptime-kuma-
|
||||
|
||||
commonLabels:
|
||||
app: uptime-kuma
|
||||
|
||||
bases:
|
||||
- uptime-kuma
|
||||
|
||||
|
42
kubernetes/uptime-kuma/deployment.yml
Normal file
42
kubernetes/uptime-kuma/deployment.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
component: uptime-kuma
|
||||
name: deployment
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
component: uptime-kuma
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
component: uptime-kuma
|
||||
spec:
|
||||
containers:
|
||||
- name: app
|
||||
image: louislam/uptime-kuma:1
|
||||
ports:
|
||||
- containerPort: 3001
|
||||
volumeMounts:
|
||||
- mountPath: /app/data
|
||||
name: storage
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- node
|
||||
- extra/healthcheck.js
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3001
|
||||
scheme: HTTP
|
||||
|
||||
volumes:
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: pvc
|
39
kubernetes/uptime-kuma/ingressroute.yml
Normal file
39
kubernetes/uptime-kuma/ingressroute.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/server-snippets: |
|
||||
location / {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
name: ingress
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- example.com
|
||||
secretName: example-com-tls
|
||||
rules:
|
||||
- host: example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: service
|
||||
port:
|
||||
number: 3001
|
5
kubernetes/uptime-kuma/kustomization.yml
Normal file
5
kubernetes/uptime-kuma/kustomization.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
resources:
|
||||
- deployment.yml
|
||||
- service.yml
|
||||
- ingressroute.yml
|
||||
- pvc.yml
|
10
kubernetes/uptime-kuma/pvc.yml
Normal file
10
kubernetes/uptime-kuma/pvc.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 4Gi
|
13
kubernetes/uptime-kuma/service.yml
Normal file
13
kubernetes/uptime-kuma/service.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: service
|
||||
spec:
|
||||
selector:
|
||||
component: uptime-kuma
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 3001
|
||||
targetPort: 3001
|
||||
protocol: TCP
|
22951
package-lock.json
generated
22951
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -17,14 +17,19 @@
|
||||
"update": "",
|
||||
"build": "vite build",
|
||||
"vite-preview-dist": "vite preview --host",
|
||||
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.2.0 --target release . --push",
|
||||
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.3.1 --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.2.0 && npm install && npm run build",
|
||||
"setup": "git checkout 1.3.1 && npm install && npm run build",
|
||||
"update-version": "node extra/update-version.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1"
|
||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
||||
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
||||
"test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
@@ -40,6 +45,7 @@
|
||||
"chart.js": "^3.5.0",
|
||||
"chartjs-adapter-dayjs": "^1.0.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"compare-versions": "^3.6.0",
|
||||
"dayjs": "^1.10.6",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
|
44
server/check-version.js
Normal file
44
server/check-version.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const { setSetting } = require("./util-server");
|
||||
const axios = require("axios");
|
||||
const { isDev } = require("../src/util");
|
||||
|
||||
exports.version = require("../package.json").version;
|
||||
exports.latestVersion = null;
|
||||
|
||||
let interval;
|
||||
|
||||
exports.startInterval = () => {
|
||||
let check = async () => {
|
||||
try {
|
||||
const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json");
|
||||
|
||||
if (typeof res.data === "string") {
|
||||
res.data = JSON.parse(res.data);
|
||||
}
|
||||
|
||||
// For debug
|
||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||
res.data.version = "1000.0.0"
|
||||
}
|
||||
|
||||
exports.latestVersion = res.data.version;
|
||||
console.log("Latest Version: " + exports.latestVersion);
|
||||
} catch (_) { }
|
||||
|
||||
};
|
||||
|
||||
check();
|
||||
interval = setInterval(check, 3600 * 48);
|
||||
};
|
||||
|
||||
exports.enableCheckUpdate = async (value) => {
|
||||
await setSetting("checkUpdate", value);
|
||||
|
||||
clearInterval(interval);
|
||||
|
||||
if (value) {
|
||||
exports.startInterval();
|
||||
}
|
||||
};
|
||||
|
||||
exports.socket = null;
|
@@ -1,8 +1,9 @@
|
||||
const fs = require("fs");
|
||||
const { sleep } = require("../src/util");
|
||||
const { sleep, debug, isDev } = require("../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const knex = require("knex");
|
||||
const sqlite3 = require("@louislam/sqlite3");
|
||||
|
||||
class Database {
|
||||
|
||||
@@ -10,23 +11,54 @@ class Database {
|
||||
static path = "./data/kuma.db";
|
||||
static latestVersion = 6;
|
||||
static noReject = true;
|
||||
static sqliteInstance = null;
|
||||
|
||||
static async connect() {
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
||||
|
||||
R.setup(knex({
|
||||
if (! this.sqliteInstance) {
|
||||
this.sqliteInstance = new sqlite3.Database(Database.path);
|
||||
this.sqliteInstance.run("PRAGMA journal_mode = WAL");
|
||||
}
|
||||
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
Dialect.prototype._driver = () => sqlite3;
|
||||
|
||||
if (isDev) {
|
||||
Dialect.prototype.acquireConnectionOrg = Dialect.prototype.acquireConnection;
|
||||
|
||||
Dialect.prototype.acquireConnection = async function () {
|
||||
let a = await this.acquireConnectionOrg();
|
||||
debug("acquired Connection");
|
||||
return a;
|
||||
};
|
||||
}
|
||||
|
||||
// Always return same connection.
|
||||
Dialect.prototype.acquireRawConnection = async function () {
|
||||
return Database.sqliteInstance;
|
||||
};
|
||||
|
||||
Dialect.prototype.destroyRawConnection = async () => { }
|
||||
|
||||
const knexInstance = knex({
|
||||
client: Dialect,
|
||||
connection: {
|
||||
filename: Database.path,
|
||||
},
|
||||
connection: { }, // Do not remove, Leave it empty is ok
|
||||
useNullAsDefault: true,
|
||||
pool: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
max: 5,
|
||||
idleTimeoutMillis: 30000,
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
console.log( knexInstance.pool)
|
||||
console.log("pool size")
|
||||
|
||||
R.setup(knexInstance);
|
||||
|
||||
if (process.env.SQL_LOG === "1") {
|
||||
R.debug(true);
|
||||
}
|
||||
|
||||
// Auto map the model to a bean object
|
||||
R.freeze(true)
|
||||
@@ -54,6 +86,16 @@ class Database {
|
||||
const backupPath = "./data/kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, backupPath);
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
if (fs.existsSync(shmPath)) {
|
||||
fs.copyFileSync(shmPath, shmPath + ".bak" + version);
|
||||
}
|
||||
|
||||
const walPath = Database.path + "-wal";
|
||||
if (fs.existsSync(walPath)) {
|
||||
fs.copyFileSync(walPath, walPath + ".bak" + version);
|
||||
}
|
||||
|
||||
// Try catch anything here, if gone wrong, restore the backup
|
||||
try {
|
||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||
@@ -116,27 +158,10 @@ class Database {
|
||||
* @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")
|
||||
}
|
||||
if (this.sqliteInstance) {
|
||||
this.sqliteInstance.close();
|
||||
}
|
||||
console.log("SQLite closed")
|
||||
|
||||
process.removeListener("unhandledRejection", listener);
|
||||
console.log("Stopped database");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,7 @@ dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util");
|
||||
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||
const { tcping, ping, checkCertificate, checkStatusCode } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
@@ -133,7 +133,6 @@ class Monitor extends BeanModel {
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
// Check certificate if https is used
|
||||
|
||||
let certInfoStartTime = dayjs().valueOf();
|
||||
if (this.getUrl()?.protocol === "https:") {
|
||||
try {
|
||||
@@ -311,10 +310,10 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
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);
|
||||
await Monitor.sendAvgPing(24, io, monitorID, userID);
|
||||
await Monitor.sendUptime(24, io, monitorID, userID);
|
||||
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
||||
await Monitor.sendCertInfo(io, monitorID, userID);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,6 +321,8 @@ class Monitor extends BeanModel {
|
||||
* @param duration : int Hours
|
||||
*/
|
||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let avgPing = parseInt(await R.getCell(`
|
||||
SELECT AVG(ping)
|
||||
FROM heartbeat
|
||||
@@ -332,6 +333,8 @@ class Monitor extends BeanModel {
|
||||
monitorID,
|
||||
]));
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] avgPing`);
|
||||
|
||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
||||
}
|
||||
|
||||
@@ -351,6 +354,8 @@ class Monitor extends BeanModel {
|
||||
* @param duration : int Hours
|
||||
*/
|
||||
static async sendUptime(duration, io, monitorID, userID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let sec = duration * 3600;
|
||||
|
||||
let heartbeatList = await R.getAll(`
|
||||
@@ -362,6 +367,8 @@ class Monitor extends BeanModel {
|
||||
monitorID,
|
||||
]);
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
|
||||
|
||||
let downtime = 0;
|
||||
let total = 0;
|
||||
let uptime;
|
||||
|
@@ -197,9 +197,9 @@ class Notification {
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
'api-key': notification.octopushAPIKey,
|
||||
'api-login': notification.octopushLogin,
|
||||
'cache-control': 'no-cache'
|
||||
"api-key": notification.octopushAPIKey,
|
||||
"api-login": notification.octopushLogin,
|
||||
"cache-control": "no-cache"
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
@@ -215,7 +215,7 @@ class Notification {
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
|
||||
await axios.post(`https://api.octopush.com/v1/public/sms-campaign/send`, data, config)
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config)
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -356,11 +356,11 @@ class Notification {
|
||||
|
||||
} else if (notification.type === "pushbullet") {
|
||||
try {
|
||||
let pushbulletUrl = `https://api.pushbullet.com/v2/pushes`;
|
||||
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
|
||||
let config = {
|
||||
headers: {
|
||||
'Access-Token': notification.pushbulletAccessToken,
|
||||
'Content-Type': 'application/json'
|
||||
"Access-Token": notification.pushbulletAccessToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
if (heartbeatJSON == null) {
|
||||
@@ -389,6 +389,53 @@ class Notification {
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
} else if (notification.type === "line") {
|
||||
try {
|
||||
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + notification.lineChannelAccessToken
|
||||
}
|
||||
};
|
||||
if (heartbeatJSON == null) {
|
||||
let testMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text":"Test Successful!"
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, testMessage, config)
|
||||
} else if (heartbeatJSON["status"] == 0) {
|
||||
let downMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text":"UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, downMessage, config)
|
||||
} else if (heartbeatJSON["status"] == 1) {
|
||||
let upMessage = {
|
||||
"to": notification.lineUserID,
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text":"UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
}
|
||||
]
|
||||
}
|
||||
await axios.post(lineAPIUrl, upMessage, config)
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
throwGeneralAxiosError(error)
|
||||
}
|
||||
} else {
|
||||
throw new Error("Notification type is not supported")
|
||||
}
|
||||
@@ -432,15 +479,21 @@ class Notification {
|
||||
|
||||
static async smtp(notification, msg) {
|
||||
|
||||
let transporter = nodemailer.createTransport({
|
||||
const config = {
|
||||
host: notification.smtpHost,
|
||||
port: notification.smtpPort,
|
||||
secure: notification.smtpSecure,
|
||||
auth: {
|
||||
};
|
||||
|
||||
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
|
||||
if (notification.smtpUsername || notification.smtpPassword) {
|
||||
config.auth = {
|
||||
user: notification.smtpUsername,
|
||||
pass: notification.smtpPassword,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
let transporter = nodemailer.createTransport(config);
|
||||
|
||||
// send mail with defined transport object
|
||||
await transporter.sendMail({
|
||||
|
@@ -7,6 +7,7 @@ const spawn = require("child_process").spawn,
|
||||
WIN = /^win/.test(process.platform),
|
||||
LIN = /^linux/.test(process.platform),
|
||||
MAC = /^darwin/.test(process.platform);
|
||||
FBSD = /^freebsd/.test(process.platform);
|
||||
const { debug } = require("../src/util");
|
||||
|
||||
module.exports = Ping;
|
||||
@@ -49,6 +50,18 @@ function Ping(host, options) {
|
||||
this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ];
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else if (FBSD) {
|
||||
this._bin = "/sbin/ping";
|
||||
|
||||
const defaultArgs = [ "-n", "-t", "2", "-c", "1", host ];
|
||||
|
||||
if (net.isIPv6(host) || options.ipv6) {
|
||||
defaultArgs.unshift("-6");
|
||||
}
|
||||
|
||||
this._args = (options.args) ? options.args : defaultArgs;
|
||||
this._regmatch = /=([0-9.]+?) ms/;
|
||||
|
||||
} else {
|
||||
throw new Error("Could not detect your ping binary.");
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
console.log("Welcome to Uptime Kuma");
|
||||
console.log("Node Env: " + process.env.NODE_ENV);
|
||||
|
||||
const { sleep, debug } = require("../src/util");
|
||||
const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util");
|
||||
|
||||
console.log("Importing Node libraries")
|
||||
const fs = require("fs");
|
||||
@@ -39,16 +39,14 @@ const passwordHash = require("./password-hash");
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
|
||||
const version = require("../package.json").version;
|
||||
const checkVersion = require("./check-version");
|
||||
console.info("Version: " + checkVersion.version);
|
||||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
const hostname = process.env.HOST || args.host;
|
||||
|
||||
const port = parseInt(process.env.PORT || args.port || 3001);
|
||||
|
||||
console.info("Version: " + version)
|
||||
|
||||
console.log("Creating express and socket.io instance")
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
@@ -119,7 +117,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
socket.emit("info", {
|
||||
version,
|
||||
version: checkVersion.version,
|
||||
latestVersion: checkVersion.latestVersion,
|
||||
})
|
||||
|
||||
totalClient++;
|
||||
@@ -584,6 +583,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
|
||||
console.log(`Listening on ${port}`);
|
||||
}
|
||||
startMonitors();
|
||||
checkVersion.startInterval();
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -644,7 +644,7 @@ async function afterLogin(socket, user) {
|
||||
|
||||
// Delay a bit, so that it let the main page to query the data first, since SQLite can process one sql at the same time only.
|
||||
// For example, query the edit data first.
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
for (let monitorID in monitorList) {
|
||||
sendHeartbeatList(socket, monitorID);
|
||||
sendImportantHeartbeatList(socket, monitorID);
|
||||
@@ -755,15 +755,22 @@ async function startMonitors() {
|
||||
let list = await R.find("monitor", " active = 1 ")
|
||||
|
||||
for (let monitor of list) {
|
||||
monitor.start(io)
|
||||
monitorList[monitor.id] = monitor;
|
||||
}
|
||||
|
||||
for (let monitor of list) {
|
||||
monitor.start(io);
|
||||
// Give some delays, so all monitors won't make request at the same moment when just start the server.
|
||||
await sleep(getRandomInt(300, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Heartbeat History list to socket
|
||||
*/
|
||||
async function sendHeartbeatList(socket, monitorID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let list = await R.find("heartbeat", `
|
||||
monitor_id = ?
|
||||
ORDER BY time DESC
|
||||
@@ -782,6 +789,8 @@ async function sendHeartbeatList(socket, monitorID) {
|
||||
}
|
||||
|
||||
async function sendImportantHeartbeatList(socket, monitorID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let list = await R.find("heartbeat", `
|
||||
monitor_id = ?
|
||||
AND important = 1
|
||||
@@ -791,6 +800,8 @@ async function sendImportantHeartbeatList(socket, monitorID) {
|
||||
monitorID,
|
||||
])
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
||||
|
||||
socket.emit("importantHeartbeatList", monitorID, list)
|
||||
}
|
||||
|
||||
@@ -805,11 +816,10 @@ async function shutdownFunction(signal) {
|
||||
}
|
||||
await sleep(2000);
|
||||
await Database.close();
|
||||
console.log("Stopped DB")
|
||||
}
|
||||
|
||||
function finalFunction() {
|
||||
console.log("Graceful Shutdown Done")
|
||||
console.log("Graceful shutdown successfully!");
|
||||
}
|
||||
|
||||
gracefulShutdown(server, {
|
||||
@@ -820,3 +830,9 @@ gracefulShutdown(server, {
|
||||
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
|
||||
finally: finalFunction, // finally function (sync) - e.g. for logging
|
||||
});
|
||||
|
||||
// Catch unexpected errors here
|
||||
process.addListener("unhandledRejection", (error, promise) => {
|
||||
console.trace(error);
|
||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||
});
|
||||
|
@@ -72,7 +72,13 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
color: white;
|
||||
|
||||
&:hover, &:active, &:focus, &.active {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark Theme override here
|
||||
.dark {
|
||||
@@ -99,6 +105,10 @@ h2 {
|
||||
.table,
|
||||
.nav-link {
|
||||
color: $dark-font-color;
|
||||
|
||||
&.btn-info {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control,
|
||||
@@ -183,3 +193,20 @@ h2 {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Transitions
|
||||
*/
|
||||
|
||||
// page-change
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.20s $easing-in;
|
||||
}
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.20s $easing-in;
|
||||
}
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(50px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
@@ -12,3 +12,7 @@ $dark-font-color2: #020b05;
|
||||
$dark-bg: #0D1117;
|
||||
$dark-bg2: #070A10;
|
||||
$dark-border-color: #1d2634;
|
||||
|
||||
$easing-in: cubic-bezier(0.54,0.78,0.55,0.97);
|
||||
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86);
|
||||
|
@@ -22,15 +22,11 @@ export default {
|
||||
|
||||
computed: {
|
||||
displayText() {
|
||||
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);
|
||||
if (this.dateOnly) {
|
||||
return this.$root.date(this.value);
|
||||
} else {
|
||||
return this.$root.datetime(this.value);
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
class="beat"
|
||||
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
|
||||
:style="beatStyle"
|
||||
:title="beat.msg"
|
||||
:title="getBeatTitle(beat)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,7 +21,10 @@ export default {
|
||||
type: String,
|
||||
default: "big",
|
||||
},
|
||||
monitorId: Number,
|
||||
monitorId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -36,14 +39,15 @@ export default {
|
||||
computed: {
|
||||
|
||||
beatList() {
|
||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||
this.$root.heartbeatList[this.monitorId] = [];
|
||||
}
|
||||
return this.$root.heartbeatList[this.monitorId]
|
||||
},
|
||||
|
||||
shortBeatList() {
|
||||
let placeholders = []
|
||||
if (! this.beatList) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let placeholders = [];
|
||||
|
||||
let start = this.beatList.length - this.maxBeat;
|
||||
|
||||
@@ -113,6 +117,11 @@ export default {
|
||||
unmounted() {
|
||||
window.removeEventListener("resize", this.resize);
|
||||
},
|
||||
beforeMount() {
|
||||
if (! (this.monitorId in this.$root.heartbeatList)) {
|
||||
this.$root.heartbeatList[this.monitorId] = [];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.size === "small") {
|
||||
this.beatWidth = 5.6;
|
||||
@@ -129,6 +138,10 @@ export default {
|
||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
|
||||
}
|
||||
},
|
||||
|
||||
getBeatTitle(beat) {
|
||||
return `${this.$root.datetime(beat.time)} - ${beat.msg}`;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
133
src/components/MonitorList.vue
Normal file
133
src/components/MonitorList.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="shadow-box list mb-4">
|
||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||
No Monitors, please <router-link to="/add">add one</router-link>.
|
||||
</div>
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
|
||||
<div class="col-12">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
export default {
|
||||
components: {
|
||||
Uptime,
|
||||
HeartbeatBar,
|
||||
},
|
||||
computed: {
|
||||
sortedMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (m1.weight !== m2.weight) {
|
||||
if (m1.weight > m2.weight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (m1.weight < m2.weight) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
})
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
monitorURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.small-padding {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
.list {
|
||||
height: auto;
|
||||
min-height: calc(100vh - 240px);
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 13px 15px 10px 15px;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.info {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #cdf8f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.list {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.monitorItem {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@@ -26,6 +26,7 @@
|
||||
<option value="lunasea">LunaSea</option>
|
||||
<option value="apprise">Apprise (Support 50+ Notification services)</option>
|
||||
<option value="pushbullet">Pushbullet</option>
|
||||
<option value="line">Line Messenger</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -383,6 +384,26 @@
|
||||
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-if="notification.type === 'line'">
|
||||
<div class="mb-3">
|
||||
<label for="line-channel-access-token" class="form-label">Channel access token</label>
|
||||
<input id="line-channel-access-token" v-model="notification.lineChannelAccessToken" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Line Developers Console - <b>Basic Settings</b>
|
||||
</div>
|
||||
<div class="mb-3" style="margin-top: 12px;">
|
||||
<label for="line-user-id" class="form-label">User ID</label>
|
||||
<input id="line-user-id" v-model="notification.lineUserID" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Line Developers Console - <b>Messaging API</b>
|
||||
</div>
|
||||
<div class="form-text" style="margin-top: 8px;">
|
||||
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<LineChart :chart-data="chartData" :height="100" :options="chartOptions" />
|
||||
<LineChart :chart-data="chartData" :options="chartOptions" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -31,6 +31,19 @@ export default {
|
||||
chartOptions() {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
onResize: (chart) => {
|
||||
chart.canvas.parentNode.style.position = "relative";
|
||||
if (screen.width < 576) {
|
||||
chart.canvas.parentNode.style.height = "275px";
|
||||
} else if (screen.width < 768) {
|
||||
chart.canvas.parentNode.style.height = "320px";
|
||||
} else if (screen.width < 992) {
|
||||
chart.canvas.parentNode.style.height = "300px";
|
||||
} else {
|
||||
chart.canvas.parentNode.style.height = "250px";
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
left: 10,
|
||||
@@ -52,12 +65,19 @@ export default {
|
||||
x: {
|
||||
type: "time",
|
||||
time: {
|
||||
unit: "minute",
|
||||
minUnit: "minute",
|
||||
round: "second",
|
||||
tooltipFormat: "YYYY-MM-DD HH:mm:ss",
|
||||
displayFormats: {
|
||||
minute: "HH:mm",
|
||||
hour: "MM-DD HH:mm",
|
||||
}
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
autoSkipPadding: 10,
|
||||
autoSkipPadding: 30,
|
||||
},
|
||||
bounds: "ticks",
|
||||
grid: {
|
||||
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
|
||||
},
|
||||
@@ -65,7 +85,7 @@ export default {
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "Response Time (ms)",
|
||||
text: "Resp. Time (ms)",
|
||||
},
|
||||
offset: false,
|
||||
grid: {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { library } from "@fortawesome/fontawesome-svg-core"
|
||||
import { faCog, faEdit, faList, faPause, faPlay, faPlus, faTachometerAlt, faTrash } from "@fortawesome/free-solid-svg-icons"
|
||||
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp } from "@fortawesome/free-solid-svg-icons"
|
||||
//import { fa } from '@fortawesome/free-regular-svg-icons'
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
|
||||
|
||||
// Add Free Font Awesome Icons here
|
||||
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
||||
library.add(faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList)
|
||||
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp);
|
||||
|
||||
export { FontAwesomeIcon }
|
||||
|
@@ -13,6 +13,10 @@
|
||||
<span class="fs-4 title">Uptime Kuma</span>
|
||||
</router-link>
|
||||
|
||||
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3">
|
||||
<font-awesome-icon icon="arrow-alt-circle-up" /> New Update
|
||||
</a>
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<router-link to="/dashboard" class="nav-link">
|
||||
@@ -52,22 +56,22 @@
|
||||
<!-- Mobile Only -->
|
||||
<div v-if="$root.isMobile" style="width: 100%;height: 60px;" />
|
||||
<nav v-if="$root.isMobile" class="bottom-nav">
|
||||
<router-link to="/dashboard" class="nav-link" @click="$root.cancelActiveList">
|
||||
<router-link to="/dashboard" class="nav-link">
|
||||
<div><font-awesome-icon icon="tachometer-alt" /></div>
|
||||
Dashboard
|
||||
</router-link>
|
||||
|
||||
<a href="#" :class=" { 'router-link-exact-active' : $root.showListMobile } " @click="$root.showListMobile = ! $root.showListMobile">
|
||||
<router-link to="/list" class="nav-link">
|
||||
<div><font-awesome-icon icon="list" /></div>
|
||||
List
|
||||
</a>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList">
|
||||
<router-link to="/add" class="nav-link">
|
||||
<div><font-awesome-icon icon="plus" /></div>
|
||||
Add
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList">
|
||||
<router-link to="/settings" class="nav-link">
|
||||
<div><font-awesome-icon icon="cog" /></div>
|
||||
Settings
|
||||
</router-link>
|
||||
@@ -77,6 +81,7 @@
|
||||
|
||||
<script>
|
||||
import Login from "../components/Login.vue";
|
||||
import compareVersions from "compare-versions";
|
||||
|
||||
export default {
|
||||
|
||||
@@ -96,12 +101,20 @@ export default {
|
||||
classes[this.$root.theme] = true;
|
||||
classes["mobile"] = this.$root.isMobile;
|
||||
return classes;
|
||||
}
|
||||
},
|
||||
|
||||
hasNewVersion() {
|
||||
if (this.$root.info.latestVersion && this.$root.info.version) {
|
||||
return compareVersions(this.$root.info.latestVersion, this.$root.info.version) >= 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route (to, from) {
|
||||
$route(to, from) {
|
||||
this.init();
|
||||
},
|
||||
},
|
||||
|
10
src/main.js
10
src/main.js
@@ -11,12 +11,15 @@ import Layout from "./layouts/Layout.vue";
|
||||
import socket from "./mixins/socket";
|
||||
import theme from "./mixins/theme";
|
||||
import mobile from "./mixins/mobile";
|
||||
import datetime from "./mixins/datetime";
|
||||
import Dashboard from "./pages/Dashboard.vue";
|
||||
import DashboardHome from "./pages/DashboardHome.vue";
|
||||
import Details from "./pages/Details.vue";
|
||||
import EditMonitor from "./pages/EditMonitor.vue";
|
||||
import Settings from "./pages/Settings.vue";
|
||||
import Setup from "./pages/Setup.vue";
|
||||
import List from "./pages/List.vue";
|
||||
|
||||
import { appName } from "./util.ts";
|
||||
|
||||
const routes = [
|
||||
@@ -52,6 +55,10 @@ const routes = [
|
||||
path: "/add",
|
||||
component: EditMonitor,
|
||||
},
|
||||
{
|
||||
path: "/list",
|
||||
component: List,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -80,7 +87,8 @@ const app = createApp({
|
||||
mixins: [
|
||||
socket,
|
||||
theme,
|
||||
mobile
|
||||
mobile,
|
||||
datetime
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
|
57
src/mixins/datetime.js
Normal file
57
src/mixins/datetime.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
/**
|
||||
* DateTime Mixin
|
||||
* Handled timezone and format
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userTimezone: localStorage.timezone || "auto",
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
datetime(value) {
|
||||
return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
|
||||
date(value) {
|
||||
return this.datetimeFormat(value, "YYYY-MM-DD");
|
||||
},
|
||||
|
||||
time(value, second = true) {
|
||||
let secondString;
|
||||
if (second) {
|
||||
secondString = ":ss";
|
||||
} else {
|
||||
secondString = "";
|
||||
}
|
||||
return this.datetimeFormat(value, "HH:mm" + secondString);
|
||||
},
|
||||
|
||||
datetimeFormat(value, format) {
|
||||
if (value !== undefined && value !== "") {
|
||||
return dayjs.utc(value).tz(this.timezone).format(format);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
timezone() {
|
||||
if (this.userTimezone === "auto") {
|
||||
return dayjs.tz.guess()
|
||||
}
|
||||
|
||||
return this.userTimezone
|
||||
},
|
||||
}
|
||||
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import dayjs from "dayjs";
|
||||
import { io } from "socket.io-client";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast()
|
||||
@@ -17,7 +16,6 @@ export default {
|
||||
connectCount: 0,
|
||||
},
|
||||
remember: (localStorage.remember !== "0"),
|
||||
userTimezone: localStorage.timezone || "auto",
|
||||
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
||||
loggedIn: false,
|
||||
monitorList: { },
|
||||
@@ -27,7 +25,6 @@ export default {
|
||||
uptimeList: { },
|
||||
certInfoList: {},
|
||||
notificationList: [],
|
||||
showListMobile: false,
|
||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
||||
}
|
||||
},
|
||||
@@ -188,10 +185,6 @@ export default {
|
||||
|
||||
methods: {
|
||||
|
||||
cancelActiveList() {
|
||||
this.$root.showListMobile = false;
|
||||
},
|
||||
|
||||
storage() {
|
||||
return (this.remember) ? localStorage : sessionStorage;
|
||||
},
|
||||
@@ -265,15 +258,6 @@ export default {
|
||||
|
||||
computed: {
|
||||
|
||||
timezone() {
|
||||
|
||||
if (this.userTimezone === "auto") {
|
||||
return dayjs.tz.guess()
|
||||
}
|
||||
|
||||
return this.userTimezone
|
||||
},
|
||||
|
||||
lastHeartbeatList() {
|
||||
let result = {}
|
||||
|
||||
|
@@ -4,6 +4,7 @@ export default {
|
||||
return {
|
||||
system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
|
||||
userTheme: localStorage.theme,
|
||||
userHeartbeatBar: localStorage.heartbeatBarTheme,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -13,6 +14,11 @@ export default {
|
||||
this.userTheme = "light";
|
||||
}
|
||||
|
||||
// Default Heartbeat Bar
|
||||
if (!this.userHeartbeatBar) {
|
||||
this.userHeartbeatBar = "normal";
|
||||
}
|
||||
|
||||
document.body.classList.add(this.theme);
|
||||
this.updateThemeColorMeta();
|
||||
},
|
||||
@@ -35,6 +41,15 @@ export default {
|
||||
document.body.classList.remove(from);
|
||||
document.body.classList.add(this.theme);
|
||||
this.updateThemeColorMeta();
|
||||
},
|
||||
|
||||
userHeartbeatBar(to, from) {
|
||||
localStorage.heartbeatBarTheme = to;
|
||||
},
|
||||
|
||||
heartbeatBarTheme(to, from) {
|
||||
document.body.classList.remove(from);
|
||||
document.body.classList.add(this.heartbeatBarTheme);
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -1,31 +1,13 @@
|
||||
<template>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-5 col-xl-4">
|
||||
<div v-if="! $root.isMobile">
|
||||
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
|
||||
<div>
|
||||
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> Add New Monitor</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="showList" class="shadow-box list mb-4">
|
||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||
No Monitors, please <router-link to="/add">add one</router-link>.
|
||||
</div>
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" @click="$root.cancelActiveList">
|
||||
<div class="row">
|
||||
<div class="col-6 col-md-8 small-padding">
|
||||
<div class="info">
|
||||
<Uptime :monitor="item" type="24" :pill="true" />
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<HeartbeatBar size="small" :monitor-id="item.id" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<MonitorList />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-7 col-xl-8">
|
||||
<router-view />
|
||||
</div>
|
||||
@@ -35,115 +17,21 @@
|
||||
|
||||
<script>
|
||||
|
||||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import MonitorList from "../components/MonitorList.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Uptime,
|
||||
HeartbeatBar,
|
||||
MonitorList,
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
sortedMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
result.sort((m1, m2) => {
|
||||
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (m1.weight !== m2.weight) {
|
||||
if (m1.weight > m2.weight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (m1.weight < m2.weight) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return m1.name.localeCompare(m2.name);
|
||||
})
|
||||
|
||||
return result;
|
||||
},
|
||||
showList() {
|
||||
return ! this.$root.isMobile || this.$root.showListMobile;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
monitorURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.container-fluid {
|
||||
width: 98%
|
||||
}
|
||||
|
||||
.list {
|
||||
height: auto;
|
||||
min-height: calc(100vh - 240px);
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 15px 15px 12px 15px;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.info {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #cdf8f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.small-padding {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.list {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -1,78 +1,79 @@
|
||||
<template>
|
||||
<div v-if="$route.name === 'DashboardHome'">
|
||||
<h1 class="mb-3">
|
||||
Quick Stats
|
||||
</h1>
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="$route.name === 'DashboardHome'">
|
||||
<h1 class="mb-3">
|
||||
Quick Stats
|
||||
</h1>
|
||||
|
||||
<div class="shadow-box big-padding text-center">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Up</h3>
|
||||
<span class="num">{{ stats.up }}</span>
|
||||
<div class="shadow-box big-padding text-center">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Up</h3>
|
||||
<span class="num">{{ stats.up }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Down</h3>
|
||||
<span class="num text-danger">{{ stats.down }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Unknown</h3>
|
||||
<span class="num text-secondary">{{ stats.unknown }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Pause</h3>
|
||||
<span class="num text-secondary">{{ stats.pause }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Down</h3>
|
||||
<span class="num text-danger">{{ stats.down }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Unknown</h3>
|
||||
<span class="num text-secondary">{{ stats.unknown }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>Pause</h3>
|
||||
<span class="num text-secondary">{{ stats.pause }}</span>
|
||||
<div v-if="false" class="row">
|
||||
<div class="col-3">
|
||||
<h3>Uptime</h3>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h3>Uptime</h3>
|
||||
<p>(30-day)</p>
|
||||
<span class="num" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="false" class="row">
|
||||
<div class="col-3">
|
||||
<h3>Uptime</h3>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h3>Uptime</h3>
|
||||
<p>(30-day)</p>
|
||||
<span class="num" />
|
||||
|
||||
<div class="shadow-box" style="margin-top: 25px;overflow-x: scroll">
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>DateTime</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
<td>{{ beat.msg }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="importantHeartBeatList.length === 0">
|
||||
<td colspan="4">
|
||||
No important events
|
||||
</td>
|
||||
</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>
|
||||
|
||||
<div class="shadow-box" style="margin-top: 25px;">
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>DateTime</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
<td>{{ beat.msg }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="importantHeartBeatList.length === 0">
|
||||
<td colspan="4">
|
||||
No important events
|
||||
</td>
|
||||
</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>
|
||||
|
||||
</transition>
|
||||
<router-view ref="child" />
|
||||
</template>
|
||||
|
||||
|
@@ -1,169 +1,175 @@
|
||||
<template>
|
||||
<h1> {{ monitor.name }}</h1>
|
||||
<p class="url">
|
||||
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
|
||||
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
|
||||
<span v-if="monitor.type === 'keyword'">
|
||||
<br>
|
||||
<span>Keyword:</span> <span class="keyword">{{ monitor.keyword }}</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="functions">
|
||||
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
|
||||
<font-awesome-icon icon="pause" /> Pause
|
||||
</button>
|
||||
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||
<font-awesome-icon icon="play" /> Resume
|
||||
</button>
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit" /> Edit
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<HeartbeatBar :monitor-id="monitor.id" />
|
||||
<span class="word">Check every {{ monitor.interval }} seconds.</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box big-padding text-center stats">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>{{ pingTitle }}</h4>
|
||||
<p>(Current)</p>
|
||||
<span class="num">
|
||||
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
|
||||
<CountUp :value="ping" />
|
||||
</a>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1> {{ monitor.name }}</h1>
|
||||
<p class="url">
|
||||
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
|
||||
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
|
||||
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
|
||||
<span v-if="monitor.type === 'keyword'">
|
||||
<br>
|
||||
<span>Keyword:</span> <span class="keyword">{{ monitor.keyword }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Avg. {{ pingTitle }}</h4>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num"><CountUp :value="avgPing" /></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Uptime</h4>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num"><Uptime :monitor="monitor" type="24" /></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Uptime</h4>
|
||||
<p>(30-day)</p>
|
||||
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
|
||||
</p>
|
||||
|
||||
<div class="functions">
|
||||
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
|
||||
<font-awesome-icon icon="pause" /> Pause
|
||||
</button>
|
||||
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||
<font-awesome-icon icon="play" /> Resume
|
||||
</button>
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit" /> Edit
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="certInfo" class="col">
|
||||
<h4>Cert Exp.</h4>
|
||||
<p>(<Datetime :value="certInfo.validTo" date-only />)</p>
|
||||
<span class="num">
|
||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} days</a>
|
||||
</span>
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<HeartbeatBar :monitor-id="monitor.id" />
|
||||
<span class="word">Check every {{ monitor.interval }} seconds.</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showPingChartBox" class="shadow-box big-padding text-center">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<PingChart :monitor-id="monitor.id" />
|
||||
<div class="shadow-box big-padding text-center stats">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>{{ pingTitle }}</h4>
|
||||
<p>(Current)</p>
|
||||
<span class="num">
|
||||
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
|
||||
<CountUp :value="ping" />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Avg. {{ pingTitle }}</h4>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num"><CountUp :value="avgPing" /></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Uptime</h4>
|
||||
<p>(24-hour)</p>
|
||||
<span class="num"><Uptime :monitor="monitor" type="24" /></span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4>Uptime</h4>
|
||||
<p>(30-day)</p>
|
||||
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
|
||||
</div>
|
||||
|
||||
<div v-if="certInfo" class="col">
|
||||
<h4>Cert Exp.</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>
|
||||
</div>
|
||||
|
||||
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4>Certificate Info</h4>
|
||||
<table class="text-start">
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
|
||||
<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>
|
||||
</transition>
|
||||
|
||||
<div v-if="showPingChartBox" class="shadow-box big-padding text-center ping-chart-wrapper">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<PingChart :monitor-id="monitor.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>DateTime</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">
|
||||
Valid:
|
||||
</td>
|
||||
<td>{{ certInfo.valid }}</td>
|
||||
<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>
|
||||
</tr>
|
||||
<tr class="my-3">
|
||||
<td class="px-3">
|
||||
Valid To:
|
||||
|
||||
<tr v-if="importantHeartBeatList.length === 0">
|
||||
<td colspan="3">
|
||||
No important events
|
||||
</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 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">
|
||||
Are you sure want to pause?
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteMonitor">
|
||||
Are you sure want to delete this monitor?
|
||||
</Confirm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
<table class="table table-borderless table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>DateTime</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
<tr v-if="importantHeartBeatList.length === 0">
|
||||
<td colspan="3">
|
||||
No important events
|
||||
</td>
|
||||
</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">
|
||||
Are you sure want to pause?
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteMonitor">
|
||||
Are you sure want to delete this monitor?
|
||||
</Confirm>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -322,6 +328,12 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.badge {
|
||||
margin-top: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.functions {
|
||||
text-align: center;
|
||||
@@ -331,6 +343,10 @@ export default {
|
||||
margin-left: 10px !important;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
|
||||
.ping-chart-wrapper {
|
||||
padding: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
|
@@ -1,150 +1,154 @@
|
||||
<template>
|
||||
<h1 class="mb-3">{{ pageName }}</h1>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">General</h2>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 class="mb-3">{{ pageName }}</h1>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">General</h2>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="type" class="form-label">Monitor Type</label>
|
||||
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
|
||||
<option value="http">
|
||||
HTTP(s)
|
||||
</option>
|
||||
<option value="port">
|
||||
TCP Port
|
||||
</option>
|
||||
<option value="ping">
|
||||
Ping
|
||||
</option>
|
||||
<option value="keyword">
|
||||
HTTP(s) - Keyword
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="type" class="form-label">Monitor Type</label>
|
||||
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
|
||||
<option value="http">
|
||||
HTTP(s)
|
||||
</option>
|
||||
<option value="port">
|
||||
TCP Port
|
||||
</option>
|
||||
<option value="ping">
|
||||
Ping
|
||||
</option>
|
||||
<option value="keyword">
|
||||
HTTP(s) - Keyword
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="name" class="form-label">Friendly Name</label>
|
||||
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="name" class="form-label">Friendly Name</label>
|
||||
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<label for="url" class="form-label">URL</label>
|
||||
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||
</div>
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<label for="url" class="form-label">URL</label>
|
||||
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'keyword' " class="my-3">
|
||||
<label for="keyword" class="form-label">Keyword</label>
|
||||
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
Search keyword in plain html or JSON response and it is case-sensitive
|
||||
<div v-if="monitor.type === 'keyword' " class="my-3">
|
||||
<label for="keyword" class="form-label">Keyword</label>
|
||||
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
Search keyword in plain html or JSON response and it is case-sensitive
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="my-3">
|
||||
<label for="hostname" class="form-label">Hostname</label>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'port' " class="my-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
|
||||
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="maxRetries" class="form-label">Retries</label>
|
||||
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
|
||||
<div class="form-text">
|
||||
Maximum retries before the service is marked as down and a notification is sent
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-5 mb-2">Advanced</h2>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls">
|
||||
Ignore TLS/SSL error for HTTPS websites
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check">
|
||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="upside-down">
|
||||
Upside Down Mode
|
||||
</label>
|
||||
<div class="form-text">
|
||||
Flip the status upside down. If the service is reachable, it is DOWN.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<label for="maxRedirects" class="form-label">Max. Redirects</label>
|
||||
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
||||
<div class="form-text">
|
||||
Maximum number of redirects to follow. Set to 0 to disable redirects.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<label for="acceptedStatusCodes" class="form-label">Accepted Status Codes</label>
|
||||
|
||||
<VueMultiselect
|
||||
id="acceptedStatusCodes"
|
||||
v-model="monitor.accepted_statuscodes"
|
||||
:options="acceptedStatusCodeOptions"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
placeholder="Pick Accepted Status Codes..."
|
||||
:preselect-first="false"
|
||||
:max-height="600"
|
||||
:taggable="true"
|
||||
></VueMultiselect>
|
||||
|
||||
<div class="form-text">
|
||||
Select status codes which are considered as a successful response.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-1">
|
||||
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="my-3">
|
||||
<label for="hostname" class="form-label">Hostname</label>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
|
||||
<div v-if="monitor.type === 'port' " class="my-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
<h2 class="mb-2">Notifications</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
Not available, please setup.
|
||||
</p>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
|
||||
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
|
||||
</div>
|
||||
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3">
|
||||
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
|
||||
|
||||
<div class="my-3">
|
||||
<label for="maxRetries" class="form-label">Retries</label>
|
||||
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
|
||||
<div class="form-text">
|
||||
Maximum retries before the service is marked as down and a notification is sent
|
||||
<label class="form-check-label" :for=" 'notification' + notification.id">
|
||||
{{ notification.name }}
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
Setup Notification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-5 mb-2">Advanced</h2>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls">
|
||||
Ignore TLS/SSL error for HTTPS websites
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check">
|
||||
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="upside-down">
|
||||
Upside Down Mode
|
||||
</label>
|
||||
<div class="form-text">
|
||||
Flip the status upside down. If the service is reachable, it is DOWN.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<label for="maxRedirects" class="form-label">Max. Redirects</label>
|
||||
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
||||
<div class="form-text">
|
||||
Maximum number of redirects to follow. Set to 0 to disable redirects.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<label for="acceptedStatusCodes" class="form-label">Accepted Status Codes</label>
|
||||
|
||||
<VueMultiselect
|
||||
id="acceptedStatusCodes"
|
||||
v-model="monitor.accepted_statuscodes"
|
||||
:options="acceptedStatusCodeOptions"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
placeholder="Pick Accepted Status Codes..."
|
||||
:preselect-first="false"
|
||||
:max-height="600"
|
||||
:taggable="true"
|
||||
></VueMultiselect>
|
||||
|
||||
<div class="form-text">
|
||||
Select status codes which are considered as a successful response.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-1">
|
||||
<button class="btn btn-primary" type="submit" :disabled="processing">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
|
||||
<h2 class="mb-2">Notifications</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
Not available, please setup.
|
||||
</p>
|
||||
|
||||
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3">
|
||||
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
|
||||
|
||||
<label class="form-check-label" :for=" 'notification' + notification.id">
|
||||
{{ notification.name }}
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
Setup Notification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -181,7 +185,7 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"$route.fullPath" () {
|
||||
"$route.fullPath"() {
|
||||
this.init();
|
||||
},
|
||||
},
|
||||
|
14
src/pages/List.vue
Normal file
14
src/pages/List.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<MonitorList />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MonitorList from "../components/MonitorList.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MonitorList,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@@ -1,139 +1,159 @@
|
||||
<template>
|
||||
<h1 class="mb-3">
|
||||
Settings
|
||||
</h1>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 v-show="show" class="mb-3">
|
||||
Settings
|
||||
</h1>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">General</h2>
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">General</h2>
|
||||
|
||||
<form class="mb-3" @submit.prevent="saveGeneral">
|
||||
<div class="mb-3">
|
||||
<label for="timezone" class="form-label">Theme</label>
|
||||
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||
<input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light">
|
||||
<label class="btn btn-outline-primary" for="btncheck1">Light</label>
|
||||
|
||||
<input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark">
|
||||
<label class="btn btn-outline-primary" for="btncheck2">Dark</label>
|
||||
|
||||
<input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto">
|
||||
<label class="btn btn-outline-primary" for="btncheck3">Auto</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="timezone" class="form-label">Timezone</label>
|
||||
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||
<option value="auto">
|
||||
Auto: {{ guessTimezone }}
|
||||
</option>
|
||||
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
|
||||
{{ timezone.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Search Engine Visibility</label>
|
||||
|
||||
<div class="form-check">
|
||||
<input id="searchEngineIndexYes" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="true" required>
|
||||
<label class="form-check-label" for="searchEngineIndexYes">
|
||||
Allow indexing
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="searchEngineIndexNo" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="false" required>
|
||||
<label class="form-check-label" for="searchEngineIndexNo">
|
||||
Discourage search engines from indexing site
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template v-if="loaded">
|
||||
<template v-if="! settings.disableAuth">
|
||||
<h2 class="mt-5 mb-2">Change Password</h2>
|
||||
<form class="mb-3" @submit.prevent="savePassword">
|
||||
<form class="mb-3" @submit.prevent="saveGeneral">
|
||||
<div class="mb-3">
|
||||
<label for="current-password" class="form-label">Current Password</label>
|
||||
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
||||
<label for="timezone" class="form-label">Theme</label>
|
||||
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||
<input id="btncheck1" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="light">
|
||||
<label class="btn btn-outline-primary" for="btncheck1">Light</label>
|
||||
|
||||
<input id="btncheck2" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="dark">
|
||||
<label class="btn btn-outline-primary" for="btncheck2">Dark</label>
|
||||
|
||||
<input id="btncheck3" v-model="$root.userTheme" type="radio" class="btn-check" name="theme" autocomplete="off" value="auto">
|
||||
<label class="btn btn-outline-primary" for="btncheck3">Auto</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new-password" class="form-label">New Password</label>
|
||||
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
||||
<label class="form-label">Theme - Heartbeat Bar</label>
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Basic checkbox toggle button group">
|
||||
<input id="btncheck4" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="normal">
|
||||
<label class="btn btn-outline-primary" for="btncheck4">Normal</label>
|
||||
|
||||
<input id="btncheck5" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="bottom">
|
||||
<label class="btn btn-outline-primary" for="btncheck5">Bottom</label>
|
||||
|
||||
<input id="btncheck6" v-model="$root.userHeartbeatBar" type="radio" class="btn-check" name="heartbeatBarTheme" autocomplete="off" value="none">
|
||||
<label class="btn btn-outline-primary" for="btncheck6">None</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
||||
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
||||
<div class="invalid-feedback">
|
||||
The repeat password does not match.
|
||||
<label for="timezone" class="form-label">Timezone</label>
|
||||
<select id="timezone" v-model="$root.userTimezone" class="form-select">
|
||||
<option value="auto">
|
||||
Auto: {{ guessTimezone }}
|
||||
</option>
|
||||
<option v-for="(timezone, index) in timezoneList" :key="index" :value="timezone.value">
|
||||
{{ timezone.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Search Engine Visibility</label>
|
||||
|
||||
<div class="form-check">
|
||||
<input id="searchEngineIndexYes" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="true" required>
|
||||
<label class="form-check-label" for="searchEngineIndexYes">
|
||||
Allow indexing
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="searchEngineIndexNo" v-model="settings.searchEngineIndex" class="form-check-input" type="radio" name="flexRadioDefault" :value="false" required>
|
||||
<label class="form-check-label" for="searchEngineIndexNo">
|
||||
Discourage search engines from indexing site
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Update Password
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<h2 class="mt-5 mb-2">Advanced</h2>
|
||||
<template v-if="loaded">
|
||||
<template v-if="! settings.disableAuth">
|
||||
<h2 class="mt-5 mb-2">Change Password</h2>
|
||||
<form class="mb-3" @submit.prevent="savePassword">
|
||||
<div class="mb-3">
|
||||
<label for="current-password" class="form-label">Current Password</label>
|
||||
<input id="current-password" v-model="password.currentPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button>
|
||||
<div class="mb-3">
|
||||
<label for="new-password" class="form-label">New Password</label>
|
||||
<input id="new-password" v-model="password.newPassword" type="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="repeat-new-password" class="form-label">Repeat New Password</label>
|
||||
<input id="repeat-new-password" v-model="password.repeatNewPassword" type="password" class="form-control" :class="{ 'is-invalid' : invalidPassword }" required>
|
||||
<div class="invalid-feedback">
|
||||
The repeat password does not match.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<h2 class="mt-5 mb-2">Advanced</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<button v-if="settings.disableAuth" class="btn btn-outline-primary me-1" @click="enableAuth">Enable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-primary me-1" @click="confirmDisableAuth">Disable Auth</button>
|
||||
<button v-if="! settings.disableAuth" class="btn btn-danger me-1" @click="$root.logout">Logout</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="notification-list col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
|
||||
<h2>Notifications</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
Not available, please setup.
|
||||
</p>
|
||||
<p v-else>
|
||||
Please assign a notification to monitor(s) to get it to work.
|
||||
</p>
|
||||
|
||||
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
|
||||
{{ notification.name }}<br>
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
Setup Notification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notification-list col-md-6">
|
||||
<div v-if="$root.isMobile" class="mt-3" />
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
|
||||
<h2>Notifications</h2>
|
||||
<p v-if="$root.notificationList.length === 0">
|
||||
Not available, please setup.
|
||||
</p>
|
||||
<p v-else>
|
||||
Please assign a notification to monitor(s) to get it to work.
|
||||
</p>
|
||||
|
||||
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
|
||||
{{ notification.name }}<br>
|
||||
<a href="#" @click="$refs.notificationDialog.show(notification.id)">Edit</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()">
|
||||
Setup Notification
|
||||
</button>
|
||||
</div>
|
||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
|
||||
<p>Are you sure want to <strong>disable auth</strong>?</p>
|
||||
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
|
||||
<p>Please use it carefully.</p>
|
||||
</Confirm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NotificationDialog ref="notificationDialog" />
|
||||
|
||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" yes-text="I understand, please disable" no-text="Leave" @yes="disableAuth">
|
||||
<p>Are you sure want to <strong>disable auth</strong>?</p>
|
||||
<p>It is for <strong>someone who have 3rd-party auth</strong> in front of Uptime Kuma such as Cloudflare Access.</p>
|
||||
<p>Please use it carefully.</p>
|
||||
</Confirm>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -158,7 +178,7 @@ export default {
|
||||
return {
|
||||
timezoneList: timezoneList(),
|
||||
guessTimezone: dayjs.tz.guess(),
|
||||
|
||||
show: true,
|
||||
invalidPassword: false,
|
||||
password: {
|
||||
currentPassword: "",
|
||||
|
28
src/util.js
28
src/util.js
@@ -1,6 +1,9 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = exports.appName = void 0;
|
||||
exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
||||
const _dayjs = require("dayjs");
|
||||
const dayjs = _dayjs;
|
||||
exports.isDev = process.env.NODE_ENV === "development";
|
||||
exports.appName = "Uptime Kuma";
|
||||
exports.DOWN = 0;
|
||||
exports.UP = 1;
|
||||
@@ -28,7 +31,7 @@ function ucfirst(str) {
|
||||
}
|
||||
exports.ucfirst = ucfirst;
|
||||
function debug(msg) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (exports.isDev) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
@@ -44,3 +47,24 @@ function polyfill() {
|
||||
}
|
||||
}
|
||||
exports.polyfill = polyfill;
|
||||
class TimeLogger {
|
||||
constructor() {
|
||||
this.startTime = dayjs().valueOf();
|
||||
}
|
||||
print(name) {
|
||||
if (exports.isDev) {
|
||||
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.TimeLogger = TimeLogger;
|
||||
function getRandomArbitrary(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
exports.getRandomArbitrary = getRandomArbitrary;
|
||||
function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
exports.getRandomInt = getRandomInt;
|
||||
|
42
src/util.ts
42
src/util.ts
@@ -4,6 +4,10 @@
|
||||
// Frontend uses util.ts
|
||||
// Need to run "tsc" to compile if there are any changes.
|
||||
|
||||
import * as _dayjs from "dayjs";
|
||||
const dayjs = _dayjs;
|
||||
|
||||
export const isDev = process.env.NODE_ENV === "development";
|
||||
export const appName = "Uptime Kuma";
|
||||
export const DOWN = 0;
|
||||
export const UP = 1;
|
||||
@@ -39,7 +43,7 @@ export function ucfirst(str) {
|
||||
}
|
||||
|
||||
export function debug(msg) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (isDev) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +56,7 @@ export function polyfill() {
|
||||
* @license MIT
|
||||
*/
|
||||
if (!String.prototype.replaceAll) {
|
||||
String.prototype.replaceAll = function(str, newStr) {
|
||||
String.prototype.replaceAll = function (str, newStr) {
|
||||
|
||||
// If a regex pattern
|
||||
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
|
||||
@@ -65,3 +69,37 @@ export function polyfill() {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeLogger {
|
||||
constructor() {
|
||||
this.startTime = dayjs().valueOf();
|
||||
}
|
||||
|
||||
print(name) {
|
||||
if (isDev) {
|
||||
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random number between min (inclusive) and max (exclusive)
|
||||
*/
|
||||
export function getRandomArbitrary(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* From: https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range
|
||||
*
|
||||
* Returns a random integer between min (inclusive) and max (inclusive).
|
||||
* The value is no lower than min (or the next integer greater than min
|
||||
* if min isn't an integer) and no greater than max (or the next integer
|
||||
* lower than max if max isn't an integer).
|
||||
* Using Math.round() will give you a non-uniform distribution!
|
||||
*/
|
||||
export function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
4
test/test_install_script/alpine3.dockerfile
Normal file
4
test/test_install_script/alpine3.dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM alpine:3
|
||||
RUN apk add --update nodejs npm git
|
||||
COPY ./install.sh .
|
||||
RUN /bin/sh install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
4
test/test_install_script/centos7.dockerfile
Normal file
4
test/test_install_script/centos7.dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM centos:7
|
||||
|
||||
COPY ./install.sh .
|
||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
4
test/test_install_script/centos8.dockerfile
Normal file
4
test/test_install_script/centos8.dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM centos:8
|
||||
|
||||
COPY ./install.sh .
|
||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
10
test/test_install_script/debian.dockerfile
Normal file
10
test/test_install_script/debian.dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM debian
|
||||
|
||||
# Test invalid node version, these commands install nodejs 10
|
||||
# RUN apt-get update
|
||||
# RUN apt --yes install nodejs
|
||||
# RUN ln -s /usr/bin/nodejs /usr/bin/node
|
||||
# RUN node -v
|
||||
|
||||
COPY ./install.sh .
|
||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
10
test/test_install_script/ubuntu.dockerfile
Normal file
10
test/test_install_script/ubuntu.dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM ubuntu
|
||||
|
||||
# Test invalid node version, these commands install nodejs 10
|
||||
# RUN apt-get update
|
||||
# RUN apt --yes install nodejs
|
||||
# RUN ln -s /usr/bin/nodejs /usr/bin/node
|
||||
# RUN node -v
|
||||
|
||||
COPY ./install.sh .
|
||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
10
test/test_install_script/ubuntu1604.dockerfile
Normal file
10
test/test_install_script/ubuntu1604.dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM ubuntu:16.04
|
||||
|
||||
# Test invalid node version, these commands install nodejs 10
|
||||
RUN apt-get update
|
||||
RUN apt --yes install nodejs
|
||||
# RUN ln -s /usr/bin/nodejs /usr/bin/node
|
||||
# RUN node -v
|
||||
|
||||
COPY ./install.sh .
|
||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
Reference in New Issue
Block a user