Compare commits

...

143 Commits
1.0.8 ... 1.2.0

Author SHA1 Message Date
LouisLam
5d0b6190c3 update to 1.2.0 2021-08-16 20:40:28 +08:00
LouisLam
cb85905c33 minor 2021-08-16 20:40:16 +08:00
LouisLam
981ed5f29f Merge remote-tracking branch 'origin/master' 2021-08-16 01:42:54 +08:00
LouisLam
0b45694f2f update all dependencies 2021-08-16 01:42:39 +08:00
Louis Lam
60531d0b15 Update README.md 2021-08-16 01:38:37 +08:00
Louis Lam
a3de63ac3c Update README.md 2021-08-16 01:29:47 +08:00
Louis Lam
80eadcb236 Merge pull request #214 from ChrisTheBaron/pushbullet
Add Pushbullet notification service
2021-08-16 01:06:27 +08:00
LouisLam
7e5a8c896b Merge remote-tracking branch 'chakflying/ping-graph' 2021-08-16 01:00:18 +08:00
Chris Taylor
efe75bde75 Add Pushbullet notification service 2021-08-13 21:18:43 +01:00
Louis Lam
af34e861c5 Merge pull request #200 from proffalken/feature/187_add_cert_checks_to_prometheus
Add certificate monitoring to the Prometheus handler
2021-08-13 00:26:58 +08:00
Louis Lam
2ae2022e62 Merge pull request #211 from AlexandreGagner/master
Add Octopush Notification Service
2021-08-13 00:26:35 +08:00
LouisLam
37f1d60f82 also change meta tag theme-color 2021-08-13 00:23:40 +08:00
LouisLam
d39b43dacc fix require problem 2021-08-13 00:13:46 +08:00
Louis Lam
7ca80fc086 fix auto theme 2021-08-12 22:17:20 +08:00
Alexandre Gagner
eb34dc6cc2 Update notification.js
Fix remove non ascii char from msg
2021-08-12 00:58:51 +02:00
Alexandre Gagner
ed93aae1c2 add octopush notification service 2021-08-12 00:15:53 +02:00
LouisLam
6a8ccf627a add version to user agent 2021-08-12 01:31:07 +08:00
Nelson Chan
8f150aaeb9 Feat: Use Async Component 2021-08-12 00:47:58 +08:00
Nelson Chan
6ed1d8cb2f Feat: Use selective import, improve tooltip UI 2021-08-12 00:31:21 +08:00
Nelson Chan
71bec74081 Feat: Add down-ed bars, improve UI 2021-08-11 23:40:56 +08:00
Nelson Chan
2bd735035c Misc: Show graph by default 2021-08-11 23:40:56 +08:00
Nelson Chan
48c6d8f19f Feat: Display recent ping chart 2021-08-11 23:40:51 +08:00
LouisLam
2d176a38af Merge remote-tracking branch 'origin/master' 2021-08-11 23:38:48 +08:00
LouisLam
b14f63491d timeout change to 80% of its interval 2021-08-11 23:12:38 +08:00
LouisLam
24b87fcd5a update vue to 3.2.1 2021-08-11 22:41:33 +08:00
LouisLam
365ea0a189 add batsh 2021-08-11 14:52:25 +08:00
Louis Lam
2461f5084e Merge pull request #205 from Ponkhy/master
Fixed function buttons for smaller screens
2021-08-11 00:50:43 +08:00
Ponkhy
1d0b332b42 Fixed function buttons for smaller screens 2021-08-10 18:29:47 +02:00
LouisLam
d5149f90b4 fix ping 2021-08-10 22:00:29 +08:00
LouisLam
e0ae9a9e73 improve space-before-function-paren 2021-08-10 21:59:15 +08:00
LouisLam
3227a2660b log undefined ping 2021-08-10 21:47:14 +08:00
LouisLam
764160f38c add eslint: space-before-function-paren 2021-08-10 21:44:29 +08:00
LouisLam
70e7945a66 fix possible race condition 2021-08-10 21:37:51 +08:00
LouisLam
b413427a37 graceful shutdown when listen error 2021-08-10 21:28:54 +08:00
LouisLam
debcac4924 run eslint 2021-08-10 14:24:05 +01:00
Matthew Macdonald-Wallace
268dd33792 Add TLS Info to Prometheus metric output 2021-08-10 14:24:05 +01:00
LouisLam
692a11e51e pass tls info to prometheus.update 2021-08-10 14:24:05 +01:00
Matthew Macdonald-Wallace
5eb4f55dfd Add the new gauges to the prometheus handler 2021-08-10 14:24:05 +01:00
LouisLam
e7cc5340e5 ping ipv6 for macos 2021-08-10 21:07:11 +08:00
LouisLam
4d4d504d6e retry ping domain with ipv6, if domain is not found 2021-08-10 21:03:14 +08:00
LouisLam
2a4695a774 add -6 to ping cmd if ipv6 address 2021-08-10 20:39:58 +08:00
LouisLam
f089bf73c3 Merge remote-tracking branch 'origin/master' 2021-08-10 20:23:29 +08:00
LouisLam
f099e4270d change to Accept: */* to better support all websites 2021-08-10 20:23:15 +08:00
Louis Lam
81636c7b44 Delete reviewdog.yml 2021-08-10 20:03:25 +08:00
Louis Lam
98fa995d3f Update reviewdog.yml 2021-08-10 19:19:55 +08:00
Louis Lam
42d24258cf Delete app.json 2021-08-10 18:41:38 +08:00
Louis Lam
3f56167198 Update reviewdog.yml 2021-08-10 17:56:30 +08:00
Louis Lam
5163e16482 Update reviewdog.yml 2021-08-10 17:36:45 +08:00
LouisLam
d93f6e2716 server.listen bind to ipv6 too 2021-08-10 16:45:37 +08:00
LouisLam
d6fad7f1ef server.listen bind to ipv6 too 2021-08-10 16:36:21 +08:00
LouisLam
5512b15162 add better token for github-pr-review for reviewdog 2021-08-10 15:39:39 +08:00
LouisLam
8979311653 Merge remote-tracking branch 'origin/master' 2021-08-10 15:04:15 +08:00
LouisLam
4f058c5b47 do not fix height for h1 2021-08-10 15:04:01 +08:00
LouisLam
9ba1743900 split mobile mixin from socket mixin 2021-08-10 15:02:46 +08:00
Louis Lam
1e4f9c7e15 Update README.md 2021-08-10 13:10:03 +08:00
Louis Lam
974672f7c1 Delete deploy.template.yaml 2021-08-10 13:09:36 +08:00
Louis Lam
01ac6d54be Merge pull request #199 from chakflying/patch-1
Fix: unify styling of theme switch btn
2021-08-10 12:50:50 +08:00
Nelson Chan
113899e278 Fix: unify styling of theme switch with UI 2021-08-10 12:20:06 +08:00
LouisLam
d1d000bd74 remove red circle around the btn-close while focus 2021-08-10 00:16:13 +08:00
Louis Lam
ef4677a640 Merge pull request #194 from Ponkhy/master
Fixed Close Button Color in Dark Mode
2021-08-10 00:04:25 +08:00
Ponkhy
e39c46ff9b Fixed Close Button Color in Dark Mode 2021-08-09 17:56:44 +02:00
Louis Lam
0e46ce42d1 Update README.md 2021-08-09 22:31:32 +08:00
LouisLam
efc9a254f4 update to 1.1.0 2021-08-09 21:03:30 +08:00
LouisLam
116d803592 minor 2021-08-09 21:03:22 +08:00
LouisLam
ba1d271afa fix jwt error 2021-08-09 20:09:01 +08:00
LouisLam
12910b23ed cache more layers for docker build 2021-08-09 19:23:18 +08:00
LouisLam
550c9703a6 fix radio button not checked 2021-08-09 18:46:57 +08:00
LouisLam
b69185ee9e control search engine visibility 2021-08-09 18:16:27 +08:00
LouisLam
ddcfa558f7 Merge remote-tracking branch 'origin/master' 2021-08-09 15:45:59 +08:00
LouisLam
478d2c4e8c add more favicon 2021-08-09 15:44:32 +08:00
Louis Lam
1352a0a162 Update bug_report.md 2021-08-09 14:53:21 +08:00
Louis Lam
7274b82143 Update ask-for-help.md 2021-08-09 14:52:55 +08:00
Louis Lam
69b1454cf5 Update feature_request.md 2021-08-09 14:52:31 +08:00
LouisLam
8f2a9fe883 chnage sqlite3 package in dockerfile 2021-08-09 14:47:53 +08:00
Louis Lam
1b8476417d Update README.md 2021-08-09 14:47:10 +08:00
Louis Lam
2a65402ad8 fix update command 2021-08-09 14:11:26 +08:00
LouisLam
59ef1f13db set longer timeout for axios request 2021-08-09 13:54:24 +08:00
LouisLam
bf33f97c9e code re-use and eslint 2021-08-09 13:49:37 +08:00
LouisLam
d0aad3400c add reset password in cli 2021-08-09 13:34:44 +08:00
LouisLam
6f489e7e0f Accepted Status Codes / Max Redirects for http/keyword only 2021-08-09 02:01:08 +08:00
LouisLam
f9cb8293f3 improve a bit ux 2021-08-09 01:58:56 +08:00
LouisLam
11b8c61079 wip: add search engine control in setting 2021-08-09 01:58:24 +08:00
Louis Lam
f69ba12c10 update reviewdog, add vue,ts ext 2021-08-09 00:33:28 +08:00
Louis Lam
e78cfaa492 Merge pull request #189 from Saibamen/save_maxredirects
Save `maxredirects` on monitor edit
2021-08-09 00:27:44 +08:00
Adam Stachowicz
9c17f59fe8 Fix few markdown lint warnings 2021-08-08 18:24:30 +02:00
Adam Stachowicz
519add4fab ESLint vite.config.js 2021-08-08 18:24:05 +02:00
Adam Stachowicz
46c7e5d058 Save maxredirects on edit 2021-08-08 18:23:51 +02:00
LouisLam
6291b7b8bb update reviewdog 2021-08-09 00:12:35 +08:00
LouisLam
3fb515e871 Merge remote-tracking branch 'origin/master' 2021-08-09 00:09:45 +08:00
LouisLam
8e440f7dff add a bot for eslint on github 2021-08-09 00:09:33 +08:00
Louis Lam
6d58c98b24 Update README.md 2021-08-08 23:51:23 +08:00
LouisLam
6ca7ca4e7e improve alignment and font size 2021-08-08 21:42:37 +08:00
Louis Lam
44391117ab Merge pull request #173 from chakflying/redirects&status
Feat: Implement Max.Redirects & Accepted Status Codes
2021-08-08 21:19:20 +08:00
LouisLam
9fa8d5c1fa improve multiselect 2021-08-08 21:14:29 +08:00
LouisLam
3265c3cbc3 improve multiselect 2021-08-08 21:03:10 +08:00
Louis Lam
b3721e03a8 Merge pull request #186 from chakflying/patch-3
Chore: Improve logging during db development
2021-08-08 19:24:50 +08:00
Nelson Chan
4ff68238c4 Chore: Improve logging during db development 2021-08-08 15:04:20 +08:00
LouisLam
7b1000d995 Merge remote-tracking branch 'chakflying/redirects&status' into redirects&status 2021-08-08 15:03:39 +08:00
LouisLam
a79e6aa338 dark theme for multiselect 2021-08-08 15:02:33 +08:00
LouisLam
3005585c0f Merge branch 'master' into redirects&status 2021-08-08 14:48:00 +08:00
Philipp Dormann
123fca43a1 FEAT: darkmode (#155)
* darkmode fixes

* fix: darkmode: empty beats in active/ hovered state

* fix: color for empty beats

* fix: navbar background color

* Update src/assets/vars.scss

Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>

* Update src/assets/app.scss

Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>

* wip, split dark theme style by .dark and store light theme to normal

* add back missing css

* working switch theme button and tuning dark theme

* finish dark theme

Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
Co-authored-by: LouisLam <louislam@users.noreply.github.com>
2021-08-08 13:47:29 +08:00
LouisLam
d5b40dfebf better code reuse and "Username" to "Bot Display Name" 2021-08-08 11:03:22 +08:00
LouisLam
c990edc87d allowElseIf for else return, since its auto fix removes "else" but without newline 2021-08-08 02:34:51 +08:00
LouisLam
2677f5dd87 run eslint for discord enhancement 2021-08-08 02:18:33 +08:00
Niyas
4469b3a19b Added discord username field 2021-08-07 11:13:25 +05:30
Niyas
ebf207c2f5 Custom embed username 2021-08-07 11:12:36 +05:30
Nelson Chan
a50aa93e84 Fix: Fix monitor creation json parsing 2021-08-07 02:10:38 +08:00
Niyas
91fce75a93 Removed UptimeKuma url field 2021-08-06 17:38:16 +05:30
Niyas
3a7414125a Updated discord embeds 2021-08-06 17:37:22 +05:30
LouisLam
5a6e5b7948 change multiselect color 2021-08-06 19:48:51 +08:00
LouisLam
adcd251076 Merge branch 'master' into redirects&status 2021-08-06 19:26:44 +08:00
LouisLam
dadc270876 Merge branch 'master' into discord-enhancements 2021-08-06 19:13:43 +08:00
LouisLam
a98ba41c8e minor 2021-08-06 19:12:49 +08:00
LouisLam
a40816b948 fix high severity vulnerabilities by using my fork sqlite3 package 2021-08-06 19:09:00 +08:00
LouisLam
d3e24df225 fix high severity vulnerabilities by using my fork sqlite3 package 2021-08-06 18:22:30 +08:00
Niyas
908176c910 Discord enhancements 2021-08-05 21:42:45 +05:30
Niyas
9ade9af1e2 Discord enhancements 2021-08-05 21:41:11 +05:30
LouisLam
8350bff629 update dependencies 2021-08-05 22:46:48 +08:00
Nelson Chan
93ea2c277a Update src/pages/EditMonitor.vue
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-05 21:06:47 +08:00
LouisLam
6251f47050 fix the min height of monitor list 2021-08-05 19:56:20 +08:00
Nelson Chan
8f7885e58a Feat: Implement MaxRedirects & StatusCodes 2021-08-05 19:04:38 +08:00
LouisLam
dffe3cf8f2 Revert "try to support subdirectory reverse proxy"
This reverts commit a03dd91e40.
2021-08-05 18:20:34 +08:00
LouisLam
d411143f3c Merge remote-tracking branch 'origin/master' 2021-08-05 17:56:50 +08:00
LouisLam
a03dd91e40 try to support subdirectory reverse proxy 2021-08-05 17:56:38 +08:00
Louis Lam
2c2ac9dc59 Delete dependabot.yml 2021-08-05 12:13:39 +08:00
Louis Lam
d06711a1a7 Update README.md 2021-08-04 15:19:06 +08:00
Louis Lam
94f2219715 Create dependabot.yml 2021-08-04 14:51:05 +08:00
LouisLam
d315e8306b update to 1.0.10 2021-08-04 13:59:42 +08:00
LouisLam
8cd0e7a058 a better script for version update 2021-08-04 13:53:21 +08:00
LouisLam
8fce62632d a better script for version update 2021-08-04 13:53:13 +08:00
LouisLam
38c0c170e7 add some comments 2021-08-04 13:31:17 +08:00
Nelson Chan
655536e457 Fix: use send() instead of end() (#161) 2021-08-04 11:56:10 +08:00
LouisLam
807db8a2d8 update to 1.0.9 2021-08-04 01:04:13 +08:00
LouisLam
d707eba046 fix disable auth 2021-08-04 01:03:40 +08:00
Philipp Dormann
e34a8e2e4a FEAT: PUSHY Notifier (#154)
FEAT: PUSHY Notifier (#154)
2021-08-03 23:14:27 +08:00
Louis Lam
6bd9d85a9a Merge pull request #150 from chakflying/created_date
Fix: [DB] Add default for created_date in monitor
2021-08-03 22:58:56 +08:00
LouisLam
f2de6299f6 update .dockerignore 2021-08-03 20:42:32 +08:00
LouisLam
a28d6eafae remove apprise --version from dockerfile 2021-08-03 20:35:41 +08:00
LouisLam
fce0edebc9 Merge remote-tracking branch 'origin/master' 2021-08-03 19:56:23 +08:00
Nelson Chan
221aad55de Chore: Add new line at EOF 2021-08-03 17:46:09 +08:00
Nelson Chan
377d475e05 Fix: Add now columns 2021-08-03 17:43:39 +08:00
Nelson Chan
0c3c59df4e Fix: [DB] Add default for created_date in monitor 2021-08-03 17:42:57 +08:00
Louis Lam
eba996b0f2 Delete codeql-analysis.yml 2021-08-03 15:17:48 +08:00
53 changed files with 2078 additions and 1010 deletions

View File

@@ -1,11 +0,0 @@
spec:
name: uptime-kuma
services:
- name: server
git:
repo_clone_url: https://github.com/louislam/uptime-kuma
branch: master
http_port: 3001
build_command: npm run setup
run_command: npm run start-server

View File

@@ -18,6 +18,8 @@ README.md
package-lock.json package-lock.json
yarn.lock yarn.lock
app.json app.json
CODE_OF_CONDUCT.md
CONTRIBUTING.md
### .gitignore content (commented rules are duplicated) ### .gitignore content (commented rules are duplicated)

View File

@@ -36,6 +36,11 @@ module.exports = {
"no-multi-spaces": ["error", { "no-multi-spaces": ["error", {
ignoreEOLComments: true, ignoreEOLComments: true,
}], }],
"space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}],
"curly": "error", "curly": "error",
"object-curly-spacing": ["error", "always"], "object-curly-spacing": ["error", "always"],
"object-curly-newline": "off", "object-curly-newline": "off",
@@ -62,9 +67,6 @@ module.exports = {
exceptAfterSingleLine: true, exceptAfterSingleLine: true,
}], }],
"no-unneeded-ternary": "error", "no-unneeded-ternary": "error",
"no-else-return": ["error", {
"allowElseIf": false,
}],
"array-bracket-newline": ["error", "consistent"], "array-bracket-newline": ["error", "consistent"],
"eol-last": ["error", "always"], "eol-last": ["error", "always"],
//'prefer-template': 'error', //'prefer-template': 'error',

View File

@@ -6,5 +6,6 @@ labels: help
assignees: '' assignees: ''
--- ---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=

View File

@@ -7,6 +7,9 @@ assignees: ''
--- ---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.

View File

@@ -6,6 +6,8 @@ labels: enhancement
assignees: '' assignees: ''
--- ---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

@@ -1,71 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '35 5 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,3 +1,3 @@
{ {
"extends": "stylelint-config-recommended", "extends": "stylelint-config-recommended"
} }

View File

@@ -2,7 +2,6 @@
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a>
<div align="center" width="100%"> <div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" /> <img src="./public/icon.svg" width="128" alt="" />
</div> </div>
@@ -11,16 +10,16 @@ It is a self-hosted monitoring tool like "Uptime Robot".
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" /> <img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" />
# Features ## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / Ping. * Monitoring uptime for HTTP(s) / TCP / Ping.
* Fancy, Reactive, Fast UI/UX. * Fancy, Reactive, Fast UI/UX.
* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise. * Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.
* 20 seconds interval. * 20 seconds interval.
# How to Use ## 🔧 How to Install
## Docker ### 🐳 Docker
```bash ```bash
# Create a volume # Create a volume
@@ -32,13 +31,10 @@ docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name upti
Browse to http://localhost:3001 after started. Browse to http://localhost:3001 after started.
Change Port and Volume
```bash 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.
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
## Without Docker ### 💪🏻 Without Docker (Recommanded for x86/x64 only)
Required Tools: Node.js >= 14, git and pm2. Required Tools: Node.js >= 14, git and pm2.
@@ -55,56 +51,50 @@ npm run start-server
# Install PM2 if you don't have: npm install pm2 -g # Install PM2 if you don't have: npm install pm2 -g
pm2 start npm --name uptime-kuma -- run start-server pm2 start npm --name uptime-kuma -- run start-server
# Listen to different port or hostname
pm2 start npm --name uptime-kuma -- run start-server -- --port=80 --hostname=0.0.0.0
``` ```
Browse to http://localhost:3001 after started. 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.
## (Optional) One more step for Reverse Proxy ## 🆙 How to Update
This is optional for someone who want to do reverse proxy. ### 🆙🐳 Docker
Unlikely other web apps, Uptime Kuma is based on WebSocket. You need two more headers **"Upgrade"** and **"Connection"** in order to reverse proxy WebSocket.
Please read wiki for more info:
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy
## One-click Deploy
<!---
Abort. Heroku instance killed the server.js if idle, stupid.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/louislam/uptime-kuma/tree/1.0.8)
-->
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/louislam/uptime-kuma/tree/master&refcode=e2c7eb658434)
# How to Update
### Docker
Re-pull the latest docker image and create another container with the same volume. Re-pull the latest docker image and create another container with the same volume.
PS: For every new release, it takes some time to build the docker image, please be patient if it is not available yet. For someone who used my "How-to-use" commands to install Uptime Kuma, you can update by this:
### Without Docker
```bash ```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 fetch --all
git checkout 1.0.8 --force git checkout 1.2.0 --force
npm install npm install
npm run build npm run build
pm2 restart uptime-kuma pm2 restart uptime-kuma
``` ```
# What's Next? ## 🆕 What's Next?
I will mark requests/issues to the next milestone. I will mark requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones https://github.com/louislam/uptime-kuma/milestones
# More Screenshots ## 🖼 More Screenshots
Dark Mode:
<img src="https://user-images.githubusercontent.com/1336778/128710166-908f8d88-9256-43f3-9c49-bfc2c56011d2.png" width="400" alt="" />
Settings Page: Settings Page:
@@ -114,8 +104,7 @@ Telegram Notification Sample:
<img src="https://louislam.net/uptimekuma/3.jpg" width="400" alt="" /> <img src="https://louislam.net/uptimekuma/3.jpg" width="400" alt="" />
## Motivation
# Motivation
* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained. * I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close one is statping. Unfortunately, it is not stable and unmaintained.
* Want to build a fancy UI. * Want to build a fancy UI.
@@ -124,14 +113,14 @@ Telegram Notification Sample:
* Try to use WebSocket with SPA instead of REST API. * Try to use WebSocket with SPA instead of REST API.
* Deploy my first Docker image to Docker Hub. * Deploy my first Docker image to Docker Hub.
If you love this project, please consider giving me a ⭐. If you love this project, please consider giving me a ⭐.
## Contribute
# Contribute
If you want to report a bug or request a new feature. Free feel to open a new issue. If you want to report a bug or request a new feature. Free feel to open a new issue.
If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki. English proofreading is needed too, because my grammar is not that great sadly. Feel free to correct my grammar in this Readme, source code or wiki.
🐻

View File

@@ -1,7 +0,0 @@
{
"name": "Uptime Kuma",
"description": "A fancy self-hosted monitoring tool",
"repository": "https://github.com/louislam/uptime-kuma",
"logo": "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
"keywords": ["node", "express", "socket-io", "uptime-kuma", "uptime"]
}

70
db/patch5.sql Normal file
View File

@@ -0,0 +1,70 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
PRAGMA foreign_keys = off;
BEGIN TRANSACTION;
create table monitor_dg_tmp (
id INTEGER not null primary key autoincrement,
name VARCHAR(150),
active BOOLEAN default 1 not null,
user_id INTEGER references user on update cascade on delete
set
null,
interval INTEGER default 20 not null,
url TEXT,
type VARCHAR(20),
weight INTEGER default 2000,
hostname VARCHAR(255),
port INTEGER,
created_date DATETIME default (DATETIME('now')) not null,
keyword VARCHAR(255),
maxretries INTEGER NOT NULL DEFAULT 0,
ignore_tls BOOLEAN default 0 not null,
upside_down BOOLEAN default 0 not null
);
insert into
monitor_dg_tmp(
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
keyword,
maxretries,
ignore_tls,
upside_down
)
select
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
keyword,
maxretries,
ignore_tls,
upside_down
from
monitor;
drop table monitor;
alter table
monitor_dg_tmp rename to monitor;
create index user_id on monitor (user_id);
COMMIT;
PRAGMA foreign_keys = on;

74
db/patch6.sql Normal file
View File

@@ -0,0 +1,74 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
PRAGMA foreign_keys = off;
BEGIN TRANSACTION;
create table monitor_dg_tmp (
id INTEGER not null primary key autoincrement,
name VARCHAR(150),
active BOOLEAN default 1 not null,
user_id INTEGER references user on update cascade on delete
set
null,
interval INTEGER default 20 not null,
url TEXT,
type VARCHAR(20),
weight INTEGER default 2000,
hostname VARCHAR(255),
port INTEGER,
created_date DATETIME default (DATETIME('now')) not null,
keyword VARCHAR(255),
maxretries INTEGER NOT NULL DEFAULT 0,
ignore_tls BOOLEAN default 0 not null,
upside_down BOOLEAN default 0 not null,
maxredirects INTEGER default 10 not null,
accepted_statuscodes_json TEXT default '["200-299"]' not null
);
insert into
monitor_dg_tmp(
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
created_date,
keyword,
maxretries,
ignore_tls,
upside_down
)
select
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
created_date,
keyword,
maxretries,
ignore_tls,
upside_down
from
monitor;
drop table monitor;
alter table
monitor_dg_tmp rename to monitor;
create index user_id on monitor (user_id);
COMMIT;
PRAGMA foreign_keys = on;

View File

@@ -5,8 +5,9 @@ WORKDIR /app
# split the sqlite install here, so that it can caches the arm prebuilt # split the sqlite install here, so that it can caches the arm prebuilt
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
ln -s /usr/bin/python3 /usr/bin/python && \ ln -s /usr/bin/python3 /usr/bin/python && \
npm install sqlite3@5.0.2 bcrypt@5.0.1 && \ npm install @louislam/sqlite3@5.0.3 bcrypt@5.0.1 && \
apk del .build-deps apk del .build-deps && \
rm -f /usr/bin/python
# Touching above code may causes sqlite3 re-compile again, painful slow. # Touching above code may causes sqlite3 re-compile again, painful slow.
@@ -14,14 +15,9 @@ RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib
RUN pip3 --no-cache-dir install apprise && \ RUN pip3 --no-cache-dir install apprise && \
rm -rf /root/.cache rm -rf /root/.cache
RUN apprise --version
# New things add here
COPY . . COPY . .
RUN npm install && \ RUN npm install && npm run build && npm prune
npm run build && \
npm prune
EXPOSE 3001 EXPOSE 3001
VOLUME ["/app/data"] VOLUME ["/app/data"]

View File

@@ -0,0 +1 @@
docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh

View File

@@ -1,19 +1,19 @@
var http = require("http"); let http = require("http");
var options = { let options = {
host: "localhost", host: "localhost",
port: "3001", port: "3001",
timeout: 2000, timeout: 2000,
}; };
var request = http.request(options, (res) => { let request = http.request(options, (res) => {
console.log(`STATUS: ${res.statusCode}`); console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode == 200) { if (res.statusCode == 200) {
process.exit(0); process.exit(0);
} else { } else {
process.exit(1); process.exit(1);
} }
}); });
request.on("error", function (err) { request.on("error", function (err) {
console.log("ERROR"); console.log("ERROR");
process.exit(1); process.exit(1);
}); });
request.end(); request.end();

View File

@@ -1,25 +1,9 @@
/** const pkg = require("../package.json");
* String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi
* @license MIT
*/
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function(str, newStr){
// If a regex pattern
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
return this.replace(str, newStr);
}
// If a string
return this.replace(new RegExp(str, 'g'), newStr);
};
}
const pkg = require('../package.json');
const fs = require("fs"); const fs = require("fs");
const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version const oldVersion = pkg.version
const newVersion = oldVersion + "-nightly" const newVersion = oldVersion + "-nightly"
@@ -35,6 +19,6 @@ if (newVersion) {
// Process README.md // Process README.md
if (fs.existsSync("README.md")) { if (fs.existsSync("README.md")) {
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion)) fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion))
} }
} }

59
extra/reset-password.js Normal file
View File

@@ -0,0 +1,59 @@
console.log("== Uptime Kuma Reset Password Tool ==");
console.log("Loading the database");
const Database = require("../server/database");
const { R } = require("redbean-node");
const readline = require("readline");
const { initJWTSecret } = require("../server/util-server");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
(async () => {
await Database.connect();
try {
const user = await R.findOne("user");
if (! user) {
throw new Error("user not found, have you installed?");
}
console.log("Found user: " + user.username);
while (true) {
let password = await question("New Password: ");
let confirmPassword = await question("Confirm New Password: ");
if (password === confirmPassword) {
await user.resetPassword(password);
// Reset all sessions by reset jwt secret
await initJWTSecret();
rl.close();
break;
} else {
console.log("Passwords do not match, please try again.");
}
}
console.log("Password reset successfully.");
} catch (e) {
console.error("Error: " + e.message);
}
await Database.close();
console.log("Finished. You should restart the Uptime Kuma server.")
})();
function question(question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer);
})
});
}

62
extra/update-version.js Normal file
View File

@@ -0,0 +1,62 @@
const pkg = require("../package.json");
const fs = require("fs");
const child_process = require("child_process");
const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version;
const newVersion = process.argv[2];
console.log("Old Version: " + oldVersion);
console.log("New Version: " + newVersion);
if (! newVersion) {
console.error("invalid version");
process.exit(1);
}
const exists = tagExists(newVersion);
if (! exists) {
// Process package.json
pkg.version = newVersion;
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
// Process README.md
fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
commit(newVersion);
tag(newVersion);
} else {
console.log("version exists")
}
function commit(version) {
let msg = "update to " + version;
let res = child_process.spawnSync("git", ["commit", "-m", msg, "-a"]);
let stdout = res.stdout.toString().trim();
console.log(stdout)
if (stdout.includes("no changes added to commit")) {
throw new Error("commit error")
}
}
function tag(version) {
let res = child_process.spawnSync("git", ["tag", version]);
console.log(res.stdout.toString().trim())
}
function tagExists(version) {
if (! version) {
throw new Error("invalid version");
}
let res = child_process.spawnSync("git", ["tag", "-l", version]);
return res.stdout.toString().trim() === version;
}

View File

@@ -1,39 +0,0 @@
/**
* String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi
* @license MIT
*/
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function(str, newStr){
// If a regex pattern
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
return this.replace(str, newStr);
}
// If a string
return this.replace(new RegExp(str, 'g'), newStr);
};
}
const pkg = require('../package.json');
const fs = require("fs");
const oldVersion = pkg.version
const newVersion = process.argv[2]
console.log("Old Version: " + oldVersion)
console.log("New Version: " + newVersion)
if (newVersion) {
// Process package.json
pkg.version = newVersion
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion)
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion)
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n")
// Process README.md
fs.writeFileSync("README.md", fs.readFileSync("README.md", 'utf8').replaceAll(oldVersion, newVersion))
}

View File

@@ -1,16 +1,16 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#5cdd8b" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="theme-color" id="theme-color" content="" />
<meta name="description" content="Uptime Kuma monitoring tool" /> <meta name="description" content="Uptime Kuma monitoring tool" />
<title>Uptime Kuma</title> <title>Uptime Kuma</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

866
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.0.8", "version": "1.2.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -13,58 +13,65 @@
"dev": "vite --host", "dev": "vite --host",
"start": "npm run start-server", "start": "npm run start-server",
"start-server": "node server/server.js", "start-server": "node server/server.js",
"start-demo-server": "set NODE_ENV=demo && node server/server.js",
"update": "", "update": "",
"build": "vite build", "build": "vite build",
"vite-preview-dist": "vite preview --host", "vite-preview-dist": "vite preview --host",
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.8 --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.2.0 --target release . --push",
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push", "build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
"setup": "git checkout 1.0.8 && npm install && npm run build", "setup": "git checkout 1.2.0 && npm install && npm run build",
"version-global-replace": "node extra/version-global-replace.js", "update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js" "mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-regular-svg-icons": "^5.15.3", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-4", "@fortawesome/vue-fontawesome": "^3.0.0-4",
"@popperjs/core": "^2.9.2", "@louislam/sqlite3": "^5.0.3",
"@popperjs/core": "^2.9.3",
"args-parser": "^1.3.0", "args-parser": "^1.3.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"bootstrap": "^5.0.2", "bootstrap": "^5.1.0",
"chart.js": "^3.5.0",
"chartjs-adapter-dayjs": "^1.0.0",
"command-exists": "^1.2.9", "command-exists": "^1.2.9",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"express": "^4.17.1", "express": "^4.17.1",
"express-basic-auth": "^1.2.0", "express-basic-auth": "^1.2.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"http-graceful-shutdown": "^3.1.2", "http-graceful-shutdown": "^3.1.3",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"nodemailer": "^6.6.3", "nodemailer": "^6.6.3",
"password-hash": "^1.2.2", "password-hash": "^1.2.2",
"prom-client": "^13.1.0", "prom-client": "^13.2.0",
"prometheus-api-metrics": "^3.2.0", "prometheus-api-metrics": "^3.2.0",
"redbean-node": "0.0.20", "redbean-node": "0.0.21",
"socket.io": "^4.1.3", "socket.io": "^4.1.3",
"socket.io-client": "^4.1.3", "socket.io-client": "^4.1.3",
"sqlite3": "^5.0.2",
"tcp-ping": "^0.1.1", "tcp-ping": "^0.1.1",
"v-pagination-3": "^0.1.6", "v-pagination-3": "^0.1.6",
"vue": "^3.0.5", "vue": "^3.2.2",
"vue-chart-3": "^0.5.7",
"vue-confirm-dialog": "^1.0.2", "vue-confirm-dialog": "^1.0.2",
"vue-router": "^4.0.10", "vue-multiselect": "^3.0.0-alpha.2",
"vue-router": "^4.0.11",
"vue-toastification": "^2.0.0-rc.1" "vue-toastification": "^2.0.0-rc.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.13.10", "@babel/eslint-parser": "^7.15.0",
"@types/bootstrap": "^5.0.17", "@types/bootstrap": "^5.1.1",
"@vitejs/plugin-legacy": "^1.5.0", "@vitejs/plugin-legacy": "^1.5.1",
"@vitejs/plugin-vue": "^1.3.0", "@vitejs/plugin-vue": "^1.4.0",
"@vue/compiler-sfc": "^3.1.5", "@vue/compiler-sfc": "^3.2.2",
"core-js": "^3.15.2", "core-js": "^3.16.1",
"eslint": "^7.31.0", "eslint": "^7.32.0",
"eslint-plugin-vue": "^7.14.0", "eslint-plugin-vue": "^7.16.0",
"sass": "^1.36.0", "sass": "^1.37.5",
"stylelint": "^13.13.1", "stylelint": "^13.13.1",
"stylelint-config-recommended": "^5.0.0", "stylelint-config-recommended": "^5.0.0",
"stylelint-config-standard": "^22.0.0", "stylelint-config-standard": "^22.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,17 +1,38 @@
const fs = require("fs"); const fs = require("fs");
const { sleep } = require("../src/util"); const { sleep } = require("../src/util");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { const { setSetting, setting } = require("./util-server");
setSetting, setting, const knex = require("knex");
} = require("./util-server");
class Database { class Database {
static templatePath = "./db/kuma.db" static templatePath = "./db/kuma.db"
static path = "./data/kuma.db"; static path = "./data/kuma.db";
static latestVersion = 4; static latestVersion = 6;
static noReject = true; static noReject = true;
static async connect() {
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => require("@louislam/sqlite3");
R.setup(knex({
client: Dialect,
connection: {
filename: Database.path,
},
useNullAsDefault: true,
pool: {
min: 1,
max: 1,
idleTimeoutMillis: 30000,
}
}));
// Auto map the model to a bean object
R.freeze(true)
await R.autoloadModels("./server/model");
}
static async patch() { static async patch() {
let version = parseInt(await setting("database_version")); let version = parseInt(await setting("database_version"));
@@ -24,6 +45,8 @@ class Database {
if (version === this.latestVersion) { if (version === this.latestVersion) {
console.info("Database no need to patch"); console.info("Database no need to patch");
} else if (version > this.latestVersion) {
console.info("Warning: Database version is newer than expected");
} else { } else {
console.info("Database patch is needed") console.info("Database patch is needed")

View File

@@ -7,10 +7,11 @@ dayjs.extend(timezone)
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util"); const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util");
const { tcping, ping, checkCertificate } = require("../util-server"); const { tcping, ping, checkCertificate, checkStatusCode } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification") const { Notification } = require("../notification")
const version = require("../../package.json").version;
/** /**
* status: * status:
@@ -45,6 +46,8 @@ class Monitor extends BeanModel {
keyword: this.keyword, keyword: this.keyword,
ignoreTls: this.getIgnoreTls(), ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(), upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(),
notificationIDList, notificationIDList,
}; };
} }
@@ -65,6 +68,10 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown); return Boolean(this.upsideDown);
} }
getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json);
}
start(io) { start(io) {
let previousBeat = null; let previousBeat = null;
let retries = 0; let retries = 0;
@@ -73,6 +80,10 @@ class Monitor extends BeanModel {
const beat = async () => { const beat = async () => {
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined;
if (! previousBeat) { if (! previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id, this.id,
@@ -104,13 +115,19 @@ class Monitor extends BeanModel {
// Use Custom agent to disable session reuse // Use Custom agent to disable session reuse
// https://github.com/nodejs/node/issues/3940 // https://github.com/nodejs/node/issues/3940
let res = await axios.get(this.url, { let res = await axios.get(this.url, {
timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"User-Agent": "Uptime-Kuma", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
}, },
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
maxCachedSessions: 0, maxCachedSessions: 0,
rejectUnauthorized: ! this.getIgnoreTls(), rejectUnauthorized: ! this.getIgnoreTls(),
}), }),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
}); });
bean.msg = `${res.status} - ${res.statusText}` bean.msg = `${res.status} - ${res.statusText}`
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
@@ -120,9 +137,11 @@ class Monitor extends BeanModel {
let certInfoStartTime = dayjs().valueOf(); let certInfoStartTime = dayjs().valueOf();
if (this.getUrl()?.protocol === "https:") { if (this.getUrl()?.protocol === "https:") {
try { try {
await this.updateTlsInfo(checkCertificate(res)); tlsInfo = await this.updateTlsInfo(checkCertificate(res));
} catch (e) { } catch (e) {
console.error(e.message) if (e.message !== "No TLS certificate in response") {
console.error(e.message)
}
} }
} }
@@ -240,7 +259,7 @@ class Monitor extends BeanModel {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`) console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
} }
prometheus.update(bean) prometheus.update(bean, tlsInfo)
io.to(this.user_id).emit("heartbeat", bean.toJSON()); io.to(this.user_id).emit("heartbeat", bean.toJSON());
@@ -275,7 +294,7 @@ class Monitor extends BeanModel {
/** /**
* Store TLS info to database * Store TLS info to database
* @param checkCertificateResult * @param checkCertificateResult
* @returns {Promise<void>} * @returns {Promise<object>}
*/ */
async updateTlsInfo(checkCertificateResult) { async updateTlsInfo(checkCertificateResult) {
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
@@ -287,6 +306,8 @@ class Monitor extends BeanModel {
} }
tls_info_bean.info_json = JSON.stringify(checkCertificateResult); tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
await R.store(tls_info_bean); await R.store(tls_info_bean);
return checkCertificateResult;
} }
static async sendStats(io, monitorID, userID) { static async sendStats(io, monitorID, userID) {

21
server/model/user.js Normal file
View File

@@ -0,0 +1,21 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const passwordHash = require("../password-hash");
const { R } = require("redbean-node");
class User extends BeanModel {
/**
* Direct execute, no need R.store()
* @param newPassword
* @returns {Promise<void>}
*/
async resetPassword(newPassword) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(newPassword),
this.id
]);
this.password = newPassword;
}
}
module.exports = User;

View File

@@ -84,40 +84,78 @@ class Notification {
} else if (notification.type === "discord") { } else if (notification.type === "discord") {
try { try {
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing. // If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
let data = { let discordtestdata = {
username: "Uptime-Kuma", username: discordDisplayName,
content: msg, content: msg,
} }
await axios.post(notification.discordWebhookUrl, data) await axios.post(notification.discordWebhookUrl, discordtestdata)
return okMsg; return okMsg;
} }
// If heartbeatJSON is not null, we go into the normal alerting loop. // If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] == 0) { if (heartbeatJSON["status"] == 0) {
var alertColor = "16711680"; let discorddowndata = {
username: discordDisplayName,
embeds: [{
title: "❌ One of your services went down. ❌",
color: 16711680,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: "Service URL",
value: monitorJSON["url"],
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Error",
value: heartbeatJSON["msg"],
},
],
}],
}
await axios.post(notification.discordWebhookUrl, discorddowndata)
return okMsg;
} else if (heartbeatJSON["status"] == 1) { } else if (heartbeatJSON["status"] == 1) {
var alertColor = "65280"; let discordupdata = {
username: discordDisplayName,
embeds: [{
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
color: 65280,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: "Service URL",
value: "[Visit Service](" + monitorJSON["url"] + ")",
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Ping",
value: heartbeatJSON["ping"] + "ms",
},
],
}],
}
await axios.post(notification.discordWebhookUrl, discordupdata)
return okMsg;
} }
let data = {
username: "Uptime-Kuma",
embeds: [{
title: "Uptime-Kuma Alert",
color: alertColor,
fields: [
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Message",
value: msg,
},
],
}],
}
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
} catch (error) { } catch (error) {
throwGeneralAxiosError(error) throwGeneralAxiosError(error)
} }
@@ -137,6 +175,52 @@ class Notification {
throwGeneralAxiosError(error) throwGeneralAxiosError(error)
} }
} else if (notification.type === "pushy") {
try {
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
"to": notification.pushyToken,
"data": {
"message": "Uptime-Kuma"
},
"notification": {
"body": msg,
"badge": 1,
"sound": "ping.aiff"
}
})
return true;
} catch (error) {
console.log(error)
return false;
}
} else if (notification.type === "octopush") {
try {
let config = {
headers: {
'api-key': notification.octopushAPIKey,
'api-login': notification.octopushLogin,
'cache-control': 'no-cache'
}
};
let data = {
"recipients": [
{
"phone_number": notification.octopushPhoneNumber
}
],
//octopush not supporting non ascii char
"text": msg.replace(/[^\x00-\x7F]/g, ""),
"type": notification.octopushSMSType,
"purpose": "alert",
"sender": notification.octopushSenderName
};
await axios.post(`https://api.octopush.com/v1/public/sms-campaign/send`, data, config)
return true;
} catch (error) {
console.log(error)
return false;
}
} else if (notification.type === "slack") { } else if (notification.type === "slack") {
try { try {
if (heartbeatJSON == null) { if (heartbeatJSON == null) {
@@ -270,6 +354,41 @@ class Notification {
throwGeneralAxiosError(error) throwGeneralAxiosError(error)
} }
} else if (notification.type === "pushbullet") {
try {
let pushbulletUrl = `https://api.pushbullet.com/v2/pushes`;
let config = {
headers: {
'Access-Token': notification.pushbulletAccessToken,
'Content-Type': 'application/json'
}
};
if (heartbeatJSON == null) {
let testdata = {
"type": "note",
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(pushbulletUrl, testdata, config)
} else if (heartbeatJSON["status"] == 0) {
let downdata = {
"type": "note",
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, downdata, config)
} else if (heartbeatJSON["status"] == 1) {
let updata = {
"type": "note",
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(pushbulletUrl, updata, config)
}
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else { } else {
throw new Error("Notification type is not supported") throw new Error("Notification type is not supported")
} }

View File

@@ -1,12 +1,13 @@
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js // https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
// Fixed on Windows // Fixed on Windows
const net = require("net");
let spawn = require("child_process").spawn, const spawn = require("child_process").spawn,
events = require("events"), events = require("events"),
fs = require("fs"), fs = require("fs"),
WIN = /^win/.test(process.platform), WIN = /^win/.test(process.platform),
LIN = /^linux/.test(process.platform), LIN = /^linux/.test(process.platform),
MAC = /^darwin/.test(process.platform); MAC = /^darwin/.test(process.platform);
const { debug } = require("../src/util");
module.exports = Ping; module.exports = Ping;
@@ -24,14 +25,30 @@ function Ping(host, options) {
this._bin = "c:/windows/system32/ping.exe"; this._bin = "c:/windows/system32/ping.exe";
this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ]; this._args = (options.args) ? options.args : [ "-n", "1", "-w", "5000", host ];
this._regmatch = /[><=]([0-9.]+?)ms/; this._regmatch = /[><=]([0-9.]+?)ms/;
} else if (LIN) { } else if (LIN) {
this._bin = "/bin/ping"; this._bin = "/bin/ping";
this._args = (options.args) ? options.args : [ "-n", "-w", "2", "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/; // need to verify this const defaultArgs = [ "-n", "-w", "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 if (MAC) { } else if (MAC) {
this._bin = "/sbin/ping";
if (net.isIPv6(host) || options.ipv6) {
this._bin = "/sbin/ping6";
} else {
this._bin = "/sbin/ping";
}
this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ]; this._args = (options.args) ? options.args : [ "-n", "-t", "2", "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/; this._regmatch = /=([0-9.]+?) ms/;
} else { } else {
throw new Error("Could not detect your ping binary."); throw new Error("Could not detect your ping binary.");
} }
@@ -49,9 +66,9 @@ Ping.prototype.__proto__ = events.EventEmitter.prototype;
// SEND A PING // SEND A PING
// =========== // ===========
Ping.prototype.send = function(callback) { Ping.prototype.send = function (callback) {
let self = this; let self = this;
callback = callback || function(err, ms) { callback = callback || function (err, ms) {
if (err) { if (err) {
return self.emit("error", err); return self.emit("error", err);
} }
@@ -62,27 +79,27 @@ Ping.prototype.send = function(callback) {
this._ping = spawn(this._bin, this._args); // spawn the binary this._ping = spawn(this._bin, this._args); // spawn the binary
this._ping.on("error", function(err) { // handle binary errors this._ping.on("error", function (err) { // handle binary errors
_errored = true; _errored = true;
callback(err); callback(err);
}); });
this._ping.stdout.on("data", function(data) { // log stdout this._ping.stdout.on("data", function (data) { // log stdout
this._stdout = (this._stdout || "") + data; this._stdout = (this._stdout || "") + data;
}); });
this._ping.stdout.on("end", function() { this._ping.stdout.on("end", function () {
_ended = true; _ended = true;
if (_exited && !_errored) { if (_exited && !_errored) {
onEnd.call(self._ping); onEnd.call(self._ping);
} }
}); });
this._ping.stderr.on("data", function(data) { // log stderr this._ping.stderr.on("data", function (data) { // log stderr
this._stderr = (this._stderr || "") + data; this._stderr = (this._stderr || "") + data;
}); });
this._ping.on("exit", function(code) { // handle complete this._ping.on("exit", function (code) { // handle complete
_exited = true; _exited = true;
if (_ended && !_errored) { if (_ended && !_errored) {
onEnd.call(self._ping); onEnd.call(self._ping);
@@ -105,15 +122,15 @@ Ping.prototype.send = function(callback) {
ms = stdout.match(self._regmatch); // parse out the ##ms response ms = stdout.match(self._regmatch); // parse out the ##ms response
ms = (ms && ms[1]) ? Number(ms[1]) : ms; ms = (ms && ms[1]) ? Number(ms[1]) : ms;
callback(null, ms); callback(null, ms, stdout);
} }
}; };
// CALL Ping#send(callback) ON A TIMER // CALL Ping#send(callback) ON A TIMER
// =================================== // ===================================
Ping.prototype.start = function(callback) { Ping.prototype.start = function (callback) {
let self = this; let self = this;
this._i = setInterval(function() { this._i = setInterval(function () {
self.send(callback); self.send(callback);
}, (self._options.interval || 5000)); }, (self._options.interval || 5000));
self.send(callback); self.send(callback);
@@ -121,6 +138,6 @@ Ping.prototype.start = function(callback) {
// STOP SENDING PINGS // STOP SENDING PINGS
// ================== // ==================
Ping.prototype.stop = function() { Ping.prototype.stop = function () {
clearInterval(this._i); clearInterval(this._i);
}; };

View File

@@ -1,22 +1,33 @@
const PrometheusClient = require('prom-client'); const PrometheusClient = require("prom-client");
const commonLabels = [ const commonLabels = [
'monitor_name', "monitor_name",
'monitor_type', "monitor_type",
'monitor_url', "monitor_url",
'monitor_hostname', "monitor_hostname",
'monitor_port', "monitor_port",
] ]
const monitor_cert_days_remaining = new PrometheusClient.Gauge({
name: "monitor_cert_days_remaining",
help: "The number of days remaining until the certificate expires",
labelNames: commonLabels
});
const monitor_cert_is_valid = new PrometheusClient.Gauge({
name: "monitor_cert_is_valid",
help: "Is the certificate still valid? (1 = Yes, 0= No)",
labelNames: commonLabels
});
const monitor_response_time = new PrometheusClient.Gauge({ const monitor_response_time = new PrometheusClient.Gauge({
name: 'monitor_response_time', name: "monitor_response_time",
help: 'Monitor Response Time (ms)', help: "Monitor Response Time (ms)",
labelNames: commonLabels labelNames: commonLabels
}); });
const monitor_status = new PrometheusClient.Gauge({ const monitor_status = new PrometheusClient.Gauge({
name: 'monitor_status', name: "monitor_status",
help: 'Monitor Status (1 = UP, 0= DOWN)', help: "Monitor Status (1 = UP, 0= DOWN)",
labelNames: commonLabels labelNames: commonLabels
}); });
@@ -33,7 +44,27 @@ class Prometheus {
} }
} }
update(heartbeat) { update(heartbeat, tlsInfo) {
if (typeof tlsInfo !== "undefined") {
try {
let is_valid = 0
if (tlsInfo.valid == true) {
is_valid = 1
} else {
is_valid = 0
}
monitor_cert_is_valid.set(this.monitorLabelValues, is_valid)
} catch (e) {
console.error(e)
}
try {
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.daysRemaining)
} catch (e) {
console.error(e)
}
}
try { try {
monitor_status.set(this.monitorLabelValues, heartbeat.status) monitor_status.set(this.monitorLabelValues, heartbeat.status)
} catch (e) { } catch (e) {
@@ -41,7 +72,7 @@ class Prometheus {
} }
try { try {
if (typeof heartbeat.ping === 'number') { if (typeof heartbeat.ping === "number") {
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping) monitor_response_time.set(this.monitorLabelValues, heartbeat.ping)
} else { } else {
// Is it good? // Is it good?

View File

@@ -1,4 +1,5 @@
console.log("Welcome to Uptime Kuma") console.log("Welcome to Uptime Kuma");
console.log("Node Env: " + process.env.NODE_ENV);
const { sleep, debug } = require("../src/util"); const { sleep, debug } = require("../src/util");
@@ -26,7 +27,7 @@ console.log("Importing this project modules");
debug("Importing Monitor"); debug("Importing Monitor");
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
debug("Importing Settings"); debug("Importing Settings");
const { getSettings, setSettings, setting } = require("./util-server"); const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server");
debug("Importing Notification"); debug("Importing Notification");
const { Notification } = require("./notification"); const { Notification } = require("./notification");
debug("Importing Database"); debug("Importing Database");
@@ -39,7 +40,11 @@ const passwordHash = require("./password-hash");
const args = require("args-parser")(process.argv); const args = require("args-parser")(process.argv);
const version = require("../package.json").version; const version = require("../package.json").version;
const hostname = process.env.HOST || args.host || "0.0.0.0"
// 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); const port = parseInt(process.env.PORT || args.port || 3001);
console.info("Version: " + version) console.info("Version: " + version)
@@ -87,17 +92,27 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
// Normal Router here // Normal Router here
app.use("/", express.static("dist")); // Robots.txt
app.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:";
if (! await setting("searchEngineIndex")) {
txt += " /";
}
response.setHeader("Content-Type", "text/plain");
response.send(txt);
});
// Basic Auth Router here // Basic Auth Router here
// Prometheus API metrics /metrics // Prometheus API metrics /metrics
// With Basic Auth using the first user's username/password // With Basic Auth using the first user's username/password
app.get("/metrics", basicAuth, prometheusAPIMetrics()) app.get("/metrics", basicAuth, prometheusAPIMetrics());
app.use("/", express.static("dist"));
// Universal Route Handler, must be at the end // Universal Route Handler, must be at the end
app.get("*", function(request, response, next) { app.get("*", async (_request, response) => {
response.end(indexHTML) response.send(indexHTML);
}); });
console.log("Adding socket handler") console.log("Adding socket handler")
@@ -114,11 +129,6 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
socket.emit("setup") socket.emit("setup")
} }
if (await setting("disableAuth")) {
console.log("Disabled Auth: auto login to admin")
await afterLogin(socket, await R.findOne("user", " username = 'admin' "))
}
socket.on("disconnect", () => { socket.on("disconnect", () => {
totalClient--; totalClient--;
}); });
@@ -139,8 +149,12 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
]) ])
if (user) { if (user) {
debug("afterLogin")
await afterLogin(socket, user) await afterLogin(socket, user)
debug("afterLogin ok")
callback({ callback({
ok: true, ok: true,
}) })
@@ -231,6 +245,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let notificationIDList = monitor.notificationIDList; let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList; delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
bean.import(monitor) bean.import(monitor)
bean.user_id = socket.userID bean.user_id = socket.userID
await R.store(bean) await R.store(bean)
@@ -275,6 +292,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
bean.keyword = monitor.keyword; bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls; bean.ignoreTls = monitor.ignoreTls;
bean.upsideDown = monitor.upsideDown; bean.upsideDown = monitor.upsideDown;
bean.maxredirects = monitor.maxredirects;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
await R.store(bean) await R.store(bean)
@@ -409,10 +428,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
if (user && passwordHash.verify(password.currentPassword, user.password)) { if (user && passwordHash.verify(password.currentPassword, user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ user.resetPassword(password.newPassword);
passwordHash.generate(password.newPassword),
socket.userID,
]);
callback({ callback({
ok: true, ok: true,
@@ -536,18 +552,44 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
callback(false); callback(false);
} }
}); });
debug("added all socket handlers")
// ***************************
// Better do anything after added all socket handlers here
// ***************************
debug("check auto login")
if (await setting("disableAuth")) {
console.log("Disabled Auth: auto login to admin")
await afterLogin(socket, await R.findOne("user"))
socket.emit("autoLogin")
} else {
debug("need auth")
}
});
console.log("Init the server")
server.once("error", async (err) => {
console.error("Cannot listen: " + err.message);
await Database.close();
}); });
console.log("Init")
server.listen(port, hostname, () => { server.listen(port, hostname, () => {
console.log(`Listening on ${hostname}:${port}`); if (hostname) {
console.log(`Listening on ${hostname}:${port}`);
} else {
console.log(`Listening on ${port}`);
}
startMonitors(); startMonitors();
}); });
})(); })();
async function updateMonitorNotification(monitorID, notificationIDList) { async function updateMonitorNotification(monitorID, notificationIDList) {
R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [ await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
monitorID, monitorID,
]) ])
@@ -598,15 +640,17 @@ async function afterLogin(socket, user) {
let monitorList = await sendMonitorList(socket) let monitorList = await sendMonitorList(socket)
for (let monitorID in monitorList) {
sendHeartbeatList(socket, monitorID);
sendImportantHeartbeatList(socket, monitorID);
Monitor.sendStats(io, monitorID, user.id)
}
sendNotificationList(socket) sendNotificationList(socket)
socket.emit("autoLogin") // 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(() => {
for (let monitorID in monitorList) {
sendHeartbeatList(socket, monitorID);
sendImportantHeartbeatList(socket, monitorID);
Monitor.sendStats(io, monitorID, user.id)
}
}, 500);
} }
async function getMonitorJSONList(userID) { async function getMonitorJSONList(userID) {
@@ -636,32 +680,22 @@ async function initDatabase() {
} }
console.log("Connecting to Database") console.log("Connecting to Database")
R.setup("sqlite", { await Database.connect();
filename: Database.path,
});
console.log("Connected") console.log("Connected")
// Patch the database // Patch the database
await Database.patch() await Database.patch()
// Auto map the model to a bean object
R.freeze(true)
await R.autoloadModels("./server/model");
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret", "jwtSecret",
]); ]);
if (! jwtSecretBean) { if (! jwtSecretBean) {
console.log("JWT secret is not found, generate one.") console.log("JWT secret is not found, generate one.");
jwtSecretBean = R.dispense("setting") jwtSecretBean = await initJWTSecret();
jwtSecretBean.key = "jwtSecret" console.log("Stored JWT secret into database");
jwtSecretBean.value = passwordHash.generate(dayjs() + "")
await R.store(jwtSecretBean)
console.log("Stored JWT secret into database")
} else { } else {
console.log("Load JWT secret from database.") console.log("Load JWT secret from database.");
} }
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup // If there is no record in user table, it is a new Uptime Kuma instance, need to setup

View File

@@ -2,6 +2,27 @@ const tcpp = require("tcp-ping");
const Ping = require("./ping-lite"); const Ping = require("./ping-lite");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { debug } = require("../src/util"); const { debug } = require("../src/util");
const passwordHash = require("./password-hash");
const dayjs = require("dayjs");
/**
* Init or reset JWT secret
* @returns {Promise<Bean>}
*/
exports.initJWTSecret = async () => {
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret",
]);
if (! jwtSecretBean) {
jwtSecretBean = R.dispense("setting");
jwtSecretBean.key = "jwtSecret";
}
jwtSecretBean.value = passwordHash.generate(dayjs() + "");
await R.store(jwtSecretBean);
return jwtSecretBean;
}
exports.tcping = function (hostname, port) { exports.tcping = function (hostname, port) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -9,7 +30,7 @@ exports.tcping = function (hostname, port) {
address: hostname, address: hostname,
port: port, port: port,
attempts: 1, attempts: 1,
}, function(err, data) { }, function (err, data) {
if (err) { if (err) {
reject(err); reject(err);
@@ -24,15 +45,30 @@ exports.tcping = function (hostname, port) {
}); });
} }
exports.ping = function (hostname) { exports.ping = async (hostname) => {
return new Promise((resolve, reject) => { try {
const ping = new Ping(hostname); return await exports.pingAsync(hostname);
} catch (e) {
// If the host cannot be resolved, try again with ipv6
if (e.message.includes("service not known")) {
return await exports.pingAsync(hostname, true);
} else {
throw e;
}
}
}
ping.send(function(err, ms) { exports.pingAsync = function (hostname, ipv6 = false) {
return new Promise((resolve, reject) => {
const ping = new Ping(hostname, {
ipv6
});
ping.send(function (err, ms, stdout) {
if (err) { if (err) {
reject(err) reject(err);
} else if (ms === null) { } else if (ms === null) {
reject(new Error("timeout")) reject(new Error(stdout))
} else { } else {
resolve(Math.round(ms)) resolve(Math.round(ms))
} }
@@ -58,7 +94,7 @@ exports.setSetting = async function (key, value) {
let bean = await R.findOne("setting", " `key` = ? ", [ let bean = await R.findOne("setting", " `key` = ? ", [
key, key,
]) ])
if (! bean) { if (!bean) {
bean = R.dispense("setting") bean = R.dispense("setting")
bean.key = key; bean.key = key;
} }
@@ -158,3 +194,32 @@ exports.checkCertificate = function (res) {
fingerprint, fingerprint,
}; };
} }
// Check if the provided status code is within the accepted ranges
// Param: status - the status code to check
// Param: accepted_codes - an array of accepted status codes
// Return: true if the status code is within the accepted ranges, false otherwise
// Will throw an error if the provided status code is not a valid range string or code string
exports.checkStatusCode = function (status, accepted_codes) {
if (accepted_codes == null || accepted_codes.length === 0) {
return false;
}
for (const code_range of accepted_codes) {
const code_range_split = code_range.split("-").map(string => parseInt(string));
if (code_range_split.length === 1) {
if (status === code_range_split[0]) {
return true;
}
} else if (code_range_split.length === 2) {
if (status >= code_range_split[0] && status <= code_range_split[1]) {
return true;
}
} else {
throw new Error("Invalid status code range");
}
}
return false;
}

View File

@@ -5,8 +5,45 @@
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji; font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji;
} }
h1 {
font-size: 32px;
}
h2 {
font-size: 26px;
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background: #CCC;
border-radius: 20px;
}
.modal {
backdrop-filter: blur(3px);
}
.modal-content {
border-radius: 1rem;
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
.dark & {
box-shadow: 0 15px 70px rgb(0 0 0);
background-color: $dark-bg;
}
}
.VuePagination__count {
font-size: 13px;
text-align: center;
}
.shadow-box { .shadow-box {
overflow: hidden; //overflow: hidden; // Forget why add this, but multiple select hide by this
box-shadow: 0 15px 70px rgba(0, 0, 0, .1); box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
padding: 10px; padding: 10px;
border-radius: 10px; border-radius: 10px;
@@ -29,10 +66,120 @@
background-color: $highlight; background-color: $highlight;
border-color: $highlight; border-color: $highlight;
} }
.dark & {
color: $dark-font-color2;
}
} }
.modal-content {
border-radius: 1rem;
backdrop-filter: blur(3px);
}
// Dark Theme override here
.dark {
background-color: #090C10;
color: $dark-font-color;
&::-webkit-scrollbar-thumb {
background: $dark-border-color;
}
.shadow-box {
background-color: $dark-bg;
}
.form-check-input {
background-color: $dark-bg2;
}
.form-switch .form-check-input {
background-color: #131a21;
}
a,
.table,
.nav-link {
color: $dark-font-color;
}
.form-control,
.form-control:focus,
.form-select,
.form-select:focus {
color: $dark-font-color;
background-color: $dark-bg2;
}
.form-control, .form-select {
border-color: $dark-border-color;
}
.table-hover > tbody > tr:hover {
--bs-table-accent-bg: #070A10;
color: $dark-font-color;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: $dark-font-color2;
}
.bg-primary {
color: $dark-font-color2;
}
.btn-secondary {
color: white;
}
.btn-close {
box-shadow: none;
filter: invert(1);
&:hover {
opacity: 0.6;
}
}
.modal-header {
border-color: $dark-bg;
}
.modal-footer {
border-color: $dark-bg;
}
// Pagination
.page-item.disabled .page-link {
background-color: $dark-bg;
border-color: $dark-border-color;
}
.page-link {
background-color: $dark-bg;
border-color: $dark-border-color;
color: $dark-font-color;
}
// Multiselect
.multiselect__tags {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect__input, .multiselect__single {
background-color: $dark-bg2;
color: $dark-font-color;
}
.multiselect__content-wrapper {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect--above .multiselect__content-wrapper {
border-color: $dark-border-color;
}
.multiselect__option--selected {
background-color: $dark-bg;
}
}

View File

@@ -6,3 +6,9 @@ $border-radius: 50rem;
$highlight: #7ce8a4; $highlight: #7ce8a4;
$highlight-white: #e7faec; $highlight-white: #e7faec;
$dark-font-color: #b1b8c0;
$dark-font-color2: #020b05;
$dark-bg: #0D1117;
$dark-bg2: #070A10;
$dark-border-color: #1d2634;

View File

@@ -133,7 +133,7 @@ export default {
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "../assets/vars.scss"; @import "../assets/vars.scss";
.wrap { .wrap {
@@ -150,6 +150,10 @@ export default {
&.empty { &.empty {
background-color: aliceblue; background-color: aliceblue;
.dark & {
background-color: #d0d3d5;
}
} }
&.down { &.down {
@@ -168,4 +172,10 @@ export default {
} }
} }
.dark {
.hp-bar-big .beat.empty{
background-color: #848484;
}
}
</style> </style>

View File

@@ -21,8 +21,11 @@
<option value="gotify">Gotify</option> <option value="gotify">Gotify</option>
<option value="slack">Slack</option> <option value="slack">Slack</option>
<option value="pushover">Pushover</option> <option value="pushover">Pushover</option>
<option value="pushy">Pushy</option>
<option value="octopush">Octopush</option>
<option value="lunasea">LunaSea</option> <option value="lunasea">LunaSea</option>
<option value="apprise">Apprise (Support 50+ Notification services)</option> <option value="apprise">Apprise (Support 50+ Notification services)</option>
<option value="pushbullet">Pushbullet</option>
</select> </select>
</div> </div>
@@ -146,6 +149,11 @@
You can get this by going to Server Settings -> Integrations -> Create Webhook You can get this by going to Server Settings -> Integrations -> Create Webhook
</div> </div>
</div> </div>
<div class="mb-3">
<label for="discord-username" class="form-label">Bot Display Name</label>
<input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
</div>
</template> </template>
<template v-if="notification.type === 'signal'"> <template v-if="notification.type === 'signal'">
@@ -229,6 +237,54 @@
</div> </div>
</template> </template>
<template v-if="notification.type === 'pushy'">
<div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label>
<input id="pushy-app-token" v-model="notification.pushyAPIKey" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
<div class="input-group mb-3">
<input id="pushy-user-key" v-model="notification.pushyToken" type="text" class="form-control" required>
</div>
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://pushy.me/docs/api/send-notifications" target="_blank">https://pushy.me/docs/api/send-notifications</a>
</p>
</template>
<template v-if="notification.type === 'octopush'">
<div class="mb-3">
<label for="octopush-key" class="form-label">API KEY</label>
<input id="octopush-key" v-model="notification.octopushAPIKey" type="text" class="form-control" required>
<label for="octopush-login" class="form-label">API LOGIN</label>
<input id="octopush-login" v-model="notification.octopushLogin" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-type-sms" class="form-label">SMS Type</label>
<select id="octopush-type-sms" v-model="notification.octopushSMSType" class="form-select">
<option value="sms_premium">Premium (Fast - recommended for alerting)</option>
<option value="sms_low_cost">Low Cost (Slow, sometimes blocked by operator)</option>
</select>
<div class="form-text">
Check octopush prices <a href="https://octopush.com/tarifs-sms-international/" target="_blank">https://octopush.com/tarifs-sms-international/</a>.
</div>
</div>
<div class="mb-3">
<label for="octopush-phone-number" class="form-label">Phone number (intl format, eg : +33612345678) </label>
<input id="octopush-phone-number" v-model="notification.octopushPhoneNumber" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="octopush-sender-name" class="form-label">SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)</label>
<input id="octopush-sender-name" v-model="notification.octopushSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://octopush.com/api-sms-documentation/envoi-de-sms/" target="_blank">https://octopush.com/api-sms-documentation/envoi-de-sms/</a>
</p>
</template>
<template v-if="notification.type === 'pushover'"> <template v-if="notification.type === 'pushover'">
<div class="mb-3"> <div class="mb-3">
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label> <label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
@@ -302,7 +358,7 @@
<p> <p>
Status: Status:
<span v-if="appriseInstalled" class="text-primary">Apprise is installed</span> <span v-if="appriseInstalled" class="text-primary">Apprise is installed</span>
<span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise">Read more</a></span> <span v-else class="text-danger">Apprise is not installed. <a href="https://github.com/caronc/apprise" target="_blank">Read more</a></span>
</p> </p>
</div> </div>
</template> </template>
@@ -317,6 +373,16 @@
</div> </div>
</template> </template>
<template v-if="notification.type === 'pushbullet'">
<div class="mb-3">
<label for="pushbullet-access-token" class="form-label">Access Token</label>
<input id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" type="text" class="form-control" required>
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
</p>
</template>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm"> <button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
@@ -490,3 +556,13 @@ export default {
}, },
} }
</script> </script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dark {
.modal-dialog .form-text, .modal-dialog p {
color: $dark-font-color;
}
}
</style>

View File

@@ -0,0 +1,152 @@
<template>
<LineChart :chart-data="chartData" :height="100" :options="chartOptions" />
</template>
<script>
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import "chartjs-adapter-dayjs";
import { LineChart } from "vue-chart-3";
dayjs.extend(utc);
dayjs.extend(timezone);
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
export default {
components: { LineChart },
props: {
monitorId: {
type: Number,
required: true,
},
},
data() {
return {
chartPeriodHrs: 6,
};
},
computed: {
chartOptions() {
return {
responsive: true,
layout: {
padding: {
left: 10,
right: 30,
top: 30,
bottom: 10,
},
},
elements: {
point: {
radius: 0,
},
bar: {
barThickness: "flex",
}
},
scales: {
x: {
type: "time",
time: {
unit: "minute",
},
ticks: {
maxRotation: 0,
autoSkipPadding: 10,
},
grid: {
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
},
},
y: {
title: {
display: true,
text: "Response Time (ms)",
},
offset: false,
grid: {
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
},
},
y1: {
display: false,
position: "right",
grid: {
drawOnChartArea: false,
},
min: 0,
max: 1,
offset: false,
},
},
bounds: "ticks",
plugins: {
tooltip: {
mode: "nearest",
intersect: false,
padding: 10,
filter: function (tooltipItem) {
return tooltipItem.datasetIndex === 0;
},
callbacks: {
label: (context) => {
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms`
},
}
},
legend: {
display: false,
},
},
}
},
chartData() {
let ping_data = [];
let down_data = [];
if (this.monitorId in this.$root.heartbeatList) {
ping_data = this.$root.heartbeatList[this.monitorId]
.filter(
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
.map((beat) => {
return {
x: dayjs.utc(beat.time).tz(this.$root.timezone).format("YYYY-MM-DD HH:mm:ss"),
y: beat.ping,
};
});
down_data = this.$root.heartbeatList[this.monitorId]
.filter(
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
.map((beat) => {
return {
x: dayjs.utc(beat.time).tz(this.$root.timezone).format("YYYY-MM-DD HH:mm:ss"),
y: beat.status === 0 ? 1 : 0,
};
});
}
return {
datasets: [
{
data: ping_data,
fill: "origin",
tension: 0.2,
borderColor: "#5CDD8B",
backgroundColor: "#5CDD8B38",
yAxisID: "y",
},
{
type: "bar",
data: down_data,
borderColor: "#00000000",
backgroundColor: "#DC354568",
yAxisID: "y1",
},
],
};
},
},
};
</script>

View File

@@ -1,109 +1,127 @@
<template> <template>
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection"> <div :class="classes">
<div class="container-fluid"> <div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
{{ $root.connectionErrorMsg }} <div class="container-fluid">
{{ $root.connectionErrorMsg }}
</div>
</div> </div>
<!-- Desktop header -->
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo" />
<span class="fs-4 title">Uptime Kuma</span>
</router-link>
<ul class="nav nav-pills">
<li class="nav-item">
<router-link to="/dashboard" class="nav-link">
<font-awesome-icon icon="tachometer-alt" /> Dashboard
</router-link>
</li>
<li class="nav-item">
<router-link to="/settings" class="nav-link">
<font-awesome-icon icon="cog" /> Settings
</router-link>
</li>
</ul>
</header>
<!-- Mobile header -->
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
<object class="bi" width="40" height="40" data="/icon.svg" />
<span class="fs-4 title ms-2">Uptime Kuma</span>
</router-link>
</header>
<main>
<!-- Add :key to disable vue router re-use the same component -->
<router-view v-if="$root.loggedIn" :key="$route.fullPath" />
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
</main>
<footer>
<div class="container-fluid">
Uptime Kuma -
Version: {{ $root.info.version }} -
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">Check Update On GitHub</a>
</div>
</footer>
<!-- Mobile Only -->
<div 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">
<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">
<div><font-awesome-icon icon="list" /></div>
List
</a>
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="plus" /></div>
Add
</router-link>
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="cog" /></div>
Settings
</router-link>
</nav>
</div> </div>
<!-- Desktop header -->
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" alt="Logo" />
<span class="fs-4 title">Uptime Kuma</span>
</router-link>
<ul class="nav nav-pills">
<li class="nav-item">
<router-link to="/dashboard" class="nav-link">
<font-awesome-icon icon="tachometer-alt" /> Dashboard
</router-link>
</li>
<li class="nav-item">
<router-link to="/settings" class="nav-link">
<font-awesome-icon icon="cog" /> Settings
</router-link>
</li>
</ul>
</header>
<!-- Mobile header -->
<header v-else class="d-flex flex-wrap justify-content-center mt-3 mb-3">
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
<object class="bi" width="40" height="40" data="/icon.svg" />
<span class="fs-4 title ms-2">Uptime Kuma</span>
</router-link>
</header>
<main>
<!-- Add :key to disable vue router re-use the same component -->
<router-view v-if="$root.loggedIn" :key="$route.fullPath" />
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
</main>
<footer>
<div class="container-fluid">
Uptime Kuma -
Version: {{ $root.info.version }} -
<a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">Check Update On GitHub</a>
</div>
</footer>
<!-- Mobile Only -->
<div 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">
<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">
<div><font-awesome-icon icon="list" /></div>
List
</a>
<router-link to="/add" class="nav-link" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="plus" /></div>
Add
</router-link>
<router-link to="/settings" class="nav-link" @click="$root.cancelActiveList">
<div><font-awesome-icon icon="cog" /></div>
Settings
</router-link>
</nav>
</template> </template>
<script> <script>
import Login from "../components/Login.vue"; import Login from "../components/Login.vue";
export default { export default {
components: { components: {
Login, Login,
}, },
data() { data() {
return {} return {}
}, },
computed: {},
computed: {
// Theme or Mobile
classes() {
const classes = {};
classes[this.$root.theme] = true;
classes["mobile"] = this.$root.isMobile;
return classes;
}
},
watch: { watch: {
$route (to, from) { $route (to, from) {
this.init(); this.init();
}, },
}, },
mounted() { mounted() {
this.init(); this.init();
}, },
methods: { methods: {
init() { init() {
if (this.$route.name === "root") { if (this.$route.name === "root") {
this.$router.push("/dashboard") this.$router.push("/dashboard")
} }
}, },
}, },
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "../assets/vars.scss"; @import "../assets/vars.scss";
.bottom-nav { .bottom-nav {
@@ -141,6 +159,10 @@ export default {
} }
} }
main {
min-height: calc(100vh - 160px)
}
.title { .title {
font-weight: bold; font-weight: bold;
} }
@@ -159,9 +181,24 @@ footer {
color: #AAA; color: #AAA;
font-size: 13px; font-size: 13px;
margin-top: 10px; margin-top: 10px;
margin-bottom: 30px; padding-bottom: 30px;
margin-left: 10px; margin-left: 10px;
text-align: center; text-align: center;
} }
.dark {
header {
background-color: #161B22;
border-bottom-color: #161B22 !important;
span {
color: #F0F6FC;
}
}
.bottom-nav {
background-color: $dark-bg;
}
}
</style> </style>

View File

@@ -9,12 +9,15 @@ import { FontAwesomeIcon } from "./icon.js";
import EmptyLayout from "./layouts/EmptyLayout.vue"; import EmptyLayout from "./layouts/EmptyLayout.vue";
import Layout from "./layouts/Layout.vue"; import Layout from "./layouts/Layout.vue";
import socket from "./mixins/socket"; import socket from "./mixins/socket";
import theme from "./mixins/theme";
import mobile from "./mixins/mobile";
import Dashboard from "./pages/Dashboard.vue"; import Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue"; import DashboardHome from "./pages/DashboardHome.vue";
import Details from "./pages/Details.vue"; import Details from "./pages/Details.vue";
import EditMonitor from "./pages/EditMonitor.vue"; import EditMonitor from "./pages/EditMonitor.vue";
import Settings from "./pages/Settings.vue"; import Settings from "./pages/Settings.vue";
import Setup from "./pages/Setup.vue"; import Setup from "./pages/Setup.vue";
import { appName } from "./util.ts";
const routes = [ const routes = [
{ {
@@ -76,7 +79,14 @@ const router = createRouter({
const app = createApp({ const app = createApp({
mixins: [ mixins: [
socket, socket,
theme,
mobile
], ],
data() {
return {
appName: appName
}
},
render: () => h(App), render: () => h(App),
}) })

25
src/mixins/mobile.js Normal file
View File

@@ -0,0 +1,25 @@
export default {
data() {
return {
windowWidth: window.innerWidth,
}
},
created() {
window.addEventListener("resize", this.onResize);
},
methods: {
onResize() {
this.windowWidth = window.innerWidth;
},
},
computed: {
isMobile() {
return this.windowWidth <= 767.98;
},
}
}

View File

@@ -27,9 +27,8 @@ export default {
uptimeList: { }, uptimeList: { },
certInfoList: {}, certInfoList: {},
notificationList: [], notificationList: [],
windowWidth: window.innerWidth,
showListMobile: false, showListMobile: false,
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting..." connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
} }
}, },
@@ -193,10 +192,6 @@ export default {
this.$root.showListMobile = false; this.$root.showListMobile = false;
}, },
onResize() {
this.windowWidth = window.innerWidth;
},
storage() { storage() {
return (this.remember) ? localStorage : sessionStorage; return (this.remember) ? localStorage : sessionStorage;
}, },
@@ -270,10 +265,6 @@ export default {
computed: { computed: {
isMobile() {
return this.windowWidth <= 767.98;
},
timezone() { timezone() {
if (this.userTimezone === "auto") { if (this.userTimezone === "auto") {

51
src/mixins/theme.js Normal file
View File

@@ -0,0 +1,51 @@
export default {
data() {
return {
system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
userTheme: localStorage.theme,
};
},
mounted() {
// Default Light
if (! this.userTheme) {
this.userTheme = "light";
}
document.body.classList.add(this.theme);
this.updateThemeColorMeta();
},
computed: {
theme() {
if (this.userTheme === "auto") {
return this.system;
}
return this.userTheme;
}
},
watch: {
userTheme(to, from) {
localStorage.theme = to;
},
theme(to, from) {
document.body.classList.remove(from);
document.body.classList.add(this.theme);
this.updateThemeColorMeta();
}
},
methods: {
updateThemeColorMeta() {
if (this.theme === "dark") {
document.querySelector("#theme-color").setAttribute("content", "#161B22");
} else {
document.querySelector("#theme-color").setAttribute("content", "#5cdd8b");
}
}
}
}

View File

@@ -3,7 +3,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-5 col-xl-4"> <div class="col-12 col-md-5 col-xl-4">
<div v-if="! $root.isMobile"> <div v-if="! $root.isMobile">
<router-link to="/add" class="btn btn-primary"><font-awesome-icon icon="plus" /> Add New Monitor</router-link> <router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> Add New Monitor</router-link>
</div> </div>
<div v-if="showList" class="shadow-box list mb-4"> <div v-if="showList" class="shadow-box list mb-4">
@@ -89,7 +89,7 @@ export default {
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "../assets/vars.scss"; @import "../assets/vars.scss";
.container-fluid { .container-fluid {
@@ -97,9 +97,8 @@ export default {
} }
.list { .list {
margin-top: 25px;
height: auto; height: auto;
min-height: calc(100vh - 200px); min-height: calc(100vh - 240px);
.item { .item {
display: block; display: block;
@@ -133,4 +132,18 @@ export default {
padding-right: 5px !important; padding-right: 5px !important;
} }
.dark {
.list {
.item {
&:hover {
background-color: $dark-bg2;
}
&.active {
background-color: $dark-bg2;
}
}
}
}
</style> </style>

View File

@@ -169,7 +169,7 @@ export default {
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
@import "../assets/vars"; @import "../assets/vars";
.num { .num {

View File

@@ -6,7 +6,7 @@
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span> <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'"> <span v-if="monitor.type === 'keyword'">
<br> <br>
<span>Keyword:</span> <span style="color: black">{{ monitor.keyword }}</span> <span>Keyword:</span> <span class="keyword">{{ monitor.keyword }}</span>
</span> </span>
</p> </p>
@@ -42,7 +42,11 @@
<div class="col"> <div class="col">
<h4>{{ pingTitle }}</h4> <h4>{{ pingTitle }}</h4>
<p>(Current)</p> <p>(Current)</p>
<span class="num"><CountUp :value="ping" /></span> <span class="num">
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
<CountUp :value="ping" />
</a>
</span>
</div> </div>
<div class="col"> <div class="col">
<h4>Avg. {{ pingTitle }}</h4> <h4>Avg. {{ pingTitle }}</h4>
@@ -70,6 +74,14 @@
</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>
</div>
</div>
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center"> <div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@@ -155,6 +167,7 @@
</template> </template>
<script> <script>
import { defineAsyncComponent } from "vue";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification"
const toast = useToast() const toast = useToast()
import Confirm from "../components/Confirm.vue"; import Confirm from "../components/Confirm.vue";
@@ -164,6 +177,7 @@ import Datetime from "../components/Datetime.vue";
import CountUp from "../components/CountUp.vue"; import CountUp from "../components/CountUp.vue";
import Uptime from "../components/Uptime.vue"; import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3"; import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
export default { export default {
components: { components: {
@@ -174,6 +188,7 @@ export default {
Confirm, Confirm,
Status, Status,
Pagination, Pagination,
PingChart,
}, },
data() { data() {
return { return {
@@ -181,6 +196,7 @@ export default {
perPage: 25, perPage: 25,
heartBeatList: [], heartBeatList: [],
toggleCertInfoBox: false, toggleCertInfoBox: false,
showPingChartBox: true,
} }
}, },
computed: { computed: {
@@ -306,6 +322,31 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../assets/vars.scss"; @import "../assets/vars.scss";
@media (max-width: 550px) {
.functions {
text-align: center;
}
button, a {
margin-left: 10px !important;
margin-right: 10px !important;
}
}
@media (max-width: 400px) {
.btn {
display: inline-flex;
flex-direction: column;
align-items: center;
padding-top: 10px;
}
a.btn {
padding-left: 25px;
padding-right: 25px;
}
}
.url { .url {
color: $primary; color: $primary;
margin-bottom: 20px; margin-bottom: 20px;
@@ -352,4 +393,14 @@ table {
margin: 20px 0; margin: 20px 0;
} }
} }
.keyword {
color: black;
}
.dark {
.keyword {
color: $dark-font-color;
}
}
</style> </style>

View File

@@ -1,14 +1,12 @@
<template> <template>
<h1 class="mb-3"> <h1 class="mb-3">{{ pageName }}</h1>
{{ pageName }}
</h1>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div class="shadow-box"> <div class="shadow-box">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h2>General</h2> <h2 class="mb-2">General</h2>
<div class="mb-3"> <div class="my-3">
<label for="type" class="form-label">Monitor Type</label> <label for="type" class="form-label">Monitor Type</label>
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example"> <select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<option value="http"> <option value="http">
@@ -26,17 +24,17 @@
</select> </select>
</div> </div>
<div class="mb-3"> <div class="my-3">
<label for="name" class="form-label">Friendly Name</label> <label for="name" class="form-label">Friendly Name</label>
<input id="name" v-model="monitor.name" type="text" class="form-control" required> <input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div> </div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="url" class="form-label">URL</label> <label for="url" class="form-label">URL</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required> <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div> </div>
<div v-if="monitor.type === 'keyword' " class="mb-3"> <div v-if="monitor.type === 'keyword' " class="my-3">
<label for="keyword" class="form-label">Keyword</label> <label for="keyword" class="form-label">Keyword</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required> <input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text"> <div class="form-text">
@@ -44,22 +42,22 @@
</div> </div>
</div> </div>
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="mb-3"> <div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="my-3">
<label for="hostname" class="form-label">Hostname</label> <label for="hostname" class="form-label">Hostname</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div> </div>
<div v-if="monitor.type === 'port' " class="mb-3"> <div v-if="monitor.type === 'port' " class="my-3">
<label for="port" class="form-label">Port</label> <label for="port" class="form-label">Port</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1"> <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
<div class="mb-3"> <div class="my-3">
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label> <label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1"> <input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
</div> </div>
<div class="mb-3"> <div class="my-3">
<label for="maxRetries" class="form-label">Retries</label> <label for="maxRetries" class="form-label">Retries</label>
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1"> <input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
<div class="form-text"> <div class="form-text">
@@ -67,16 +65,16 @@
</div> </div>
</div> </div>
<h2>Advanced</h2> <h2 class="mt-5 mb-2">Advanced</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3 form-check"> <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=""> <input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls"> <label class="form-check-label" for="ignore-tls">
Ignore TLS/SSL error for HTTPS websites Ignore TLS/SSL error for HTTPS websites
</label> </label>
</div> </div>
<div class="mb-3 form-check"> <div class="my-3 form-check">
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox"> <input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
<label class="form-check-label" for="upside-down"> <label class="form-check-label" for="upside-down">
Upside Down Mode Upside Down Mode
@@ -86,22 +84,50 @@
</div> </div>
</div> </div>
<div> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<button class="btn btn-primary" type="submit" :disabled="processing"> <label for="maxRedirects" class="form-label">Max. Redirects</label>
Save <input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
</button> <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>
<div class="col-md-6"> <div class="col-md-6">
<div v-if="$root.isMobile" class="mt-3" /> <div v-if="$root.isMobile" class="mt-3" />
<h2>Notifications</h2> <h2 class="mb-2">Notifications</h2>
<p v-if="$root.notificationList.length === 0"> <p v-if="$root.notificationList.length === 0">
Not available, please setup. Not available, please setup.
</p> </p>
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch mb-3"> <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"> <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"> <label class="form-check-label" :for=" 'notification' + notification.id">
@@ -124,20 +150,25 @@
<script> <script>
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification"
import VueMultiselect from "vue-multiselect"
const toast = useToast() const toast = useToast()
export default { export default {
components: { components: {
NotificationDialog, NotificationDialog,
VueMultiselect,
}, },
data() { data() {
return { return {
processing: false, processing: false,
monitor: { monitor: {
notificationIDList: {}, notificationIDList: {},
}, },
acceptedStatusCodeOptions: [],
} }
}, },
computed: { computed: {
pageName() { pageName() {
return (this.isAdd) ? "Add New Monitor" : "Edit" return (this.isAdd) ? "Add New Monitor" : "Edit"
@@ -156,6 +187,20 @@ export default {
}, },
mounted() { mounted() {
this.init(); this.init();
let acceptedStatusCodeOptions = [
"100-199",
"200-299",
"300-399",
"400-499",
"500-599",
];
for (let i = 100; i <= 999; i++) {
acceptedStatusCodeOptions.push(i.toString());
}
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
}, },
methods: { methods: {
init() { init() {
@@ -170,6 +215,8 @@ export default {
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,
upsideDown: false, upsideDown: false,
maxredirects: 10,
accepted_statuscodes: ["200-299"],
} }
} else if (this.isEdit) { } else if (this.isEdit) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
@@ -209,6 +256,40 @@ export default {
} }
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
@import "../assets/vars.scss";
.multiselect__tags {
border-radius: 1.5rem;
border: 1px solid #ced4da;
}
.multiselect--active .multiselect__tags {
border-radius: 1rem;
}
.multiselect__option--highlight {
background: $primary !important;
}
.multiselect__option--highlight::after {
background: $primary !important;
}
.multiselect__tag {
border-radius: 50rem;
background: $primary !important;
}
.dark {
.multiselect__tag {
color: $dark-font-color2;
}
}
</style>
<style scoped> <style scoped>
.shadow-box { .shadow-box {
padding: 20px; padding: 20px;

View File

@@ -6,8 +6,26 @@
<div class="shadow-box"> <div class="shadow-box">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h2>General</h2> <h2 class="mb-2">General</h2>
<form class="mb-3" @submit.prevent="saveGeneral"> <form class="mb-3" @submit.prevent="saveGeneral">
<div class="mb-3">
<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"> <div class="mb-3">
<label for="timezone" class="form-label">Timezone</label> <label for="timezone" class="form-label">Timezone</label>
<select id="timezone" v-model="$root.userTimezone" class="form-select"> <select id="timezone" v-model="$root.userTimezone" class="form-select">
@@ -20,6 +38,23 @@
</select> </select>
</div> </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> <div>
<button class="btn btn-primary" type="submit"> <button class="btn btn-primary" type="submit">
Save Save
@@ -29,7 +64,7 @@
<template v-if="loaded"> <template v-if="loaded">
<template v-if="! settings.disableAuth"> <template v-if="! settings.disableAuth">
<h2>Change Password</h2> <h2 class="mt-5 mb-2">Change Password</h2>
<form class="mb-3" @submit.prevent="savePassword"> <form class="mb-3" @submit.prevent="savePassword">
<div class="mb-3"> <div class="mb-3">
<label for="current-password" class="form-label">Current Password</label> <label for="current-password" class="form-label">Current Password</label>
@@ -57,7 +92,7 @@
</form> </form>
</template> </template>
<h2>Advanced</h2> <h2 class="mt-5 mb-2">Advanced</h2>
<div class="mb-3"> <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-outline-primary me-1" @click="enableAuth">Enable Auth</button>
@@ -67,7 +102,7 @@
</template> </template>
</div> </div>
<div class="col-md-6"> <div class="notification-list col-md-6">
<div v-if="$root.isMobile" class="mt-3" /> <div v-if="$root.isMobile" class="mt-3" />
<h2>Notifications</h2> <h2>Notifications</h2>
@@ -150,7 +185,7 @@ export default {
saveGeneral() { saveGeneral() {
localStorage.timezone = this.$root.userTimezone; localStorage.timezone = this.$root.userTimezone;
toast.success("Saved.") this.saveSettings();
}, },
savePassword() { savePassword() {
@@ -171,6 +206,11 @@ export default {
loadSettings() { loadSettings() {
this.$root.getSocket().emit("getSettings", (res) => { this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data; this.settings = res.data;
if (this.settings.searchEngineIndex === undefined) {
this.settings.searchEngineIndex = false;
}
this.loaded = true; this.loaded = true;
}) })
}, },
@@ -201,8 +241,29 @@ export default {
} }
</script> </script>
<style scoped> <style lang="scss" scoped>
.shadow-box { @import "../assets/vars.scss";
padding: 20px;
.shadow-box {
padding: 20px;
}
.btn-check:active + .btn-outline-primary,
.btn-check:checked + .btn-outline-primary,
.btn-check:hover + .btn-outline-primary {
color: #fff;
}
.dark {
.list-group-item {
background-color: $dark-bg2;
color: $dark-font-color;
} }
.btn-check:active + .btn-outline-primary,
.btn-check:checked + .btn-outline-primary,
.btn-check:hover + .btn-outline-primary {
color: #000;
}
}
</style> </style>

View File

@@ -1,6 +1,7 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = void 0; exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.PENDING = exports.UP = exports.DOWN = exports.appName = void 0;
exports.appName = "Uptime Kuma";
exports.DOWN = 0; exports.DOWN = 0;
exports.UP = 1; exports.UP = 1;
exports.PENDING = 2; exports.PENDING = 2;
@@ -32,3 +33,14 @@ function debug(msg) {
} }
} }
exports.debug = debug; exports.debug = debug;
function polyfill() {
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str, newStr) {
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
return this.replace(str, newStr);
}
return this.replace(new RegExp(str, "g"), newStr);
};
}
}
exports.polyfill = polyfill;

View File

@@ -1,8 +1,10 @@
// @ts-nocheck
// Common Util for frontend and backend // Common Util for frontend and backend
// Backend uses the compiled file util.js // Backend uses the compiled file util.js
// Frontend uses util.ts // Frontend uses util.ts
// Need to run "tsc" to compile if there are any changes. // Need to run "tsc" to compile if there are any changes.
export const appName = "Uptime Kuma";
export const DOWN = 0; export const DOWN = 0;
export const UP = 1; export const UP = 1;
export const PENDING = 2; export const PENDING = 2;
@@ -38,6 +40,28 @@ export function ucfirst(str) {
export function debug(msg) { export function debug(msg) {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
console.log(msg) console.log(msg);
}
}
export function polyfill() {
/**
* String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi
* @license MIT
*/
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function(str, newStr) {
// If a regex pattern
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
return this.replace(str, newStr);
}
// If a string
return this.replace(new RegExp(str, "g"), newStr);
};
} }
} }

View File

@@ -1,8 +1,11 @@
{ {
"compileOnSave": true, "compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"target": "ES2018", "target": "es2018",
"module": "commonjs", "module": "commonjs",
"lib": [
"es2020"
],
"removeComments": true, "removeComments": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"sourceMap": false, "sourceMap": false,

View File

@@ -1,14 +1,14 @@
import { defineConfig } from 'vite' import legacy from "@vitejs/plugin-legacy"
import vue from '@vitejs/plugin-vue' import vue from "@vitejs/plugin-vue"
import legacy from '@vitejs/plugin-legacy' import { defineConfig } from "vite"
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
legacy({ legacy({
targets: ['ie > 11'], targets: ["ie > 11"],
additionalLegacyPolyfills: ['regenerator-runtime/runtime'] additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
}) })
] ]
}) })