Compare commits

..

572 Commits
1.0.8 ... 1.6.2

Author SHA1 Message Date
LouisLam
35fe9690e8 update to 1.6.2 2021-09-14 19:34:54 +08:00
LouisLam
ad2062713c fix pi4 on docker 2021-09-14 19:04:56 +08:00
LouisLam
d953ba7c60 update to 1.6.1 2021-09-11 03:12:25 +08:00
LouisLam
ef1604675b change to node:14-bullseye-slim, and reduce the docker image size 2021-09-11 02:50:29 +08:00
LouisLam
201bba63d7 add a healthcheck comment 2021-09-10 21:17:20 +08:00
LouisLam
31b4a5c33e build-docker, debian first 2021-09-10 18:08:23 +08:00
LouisLam
a49df29a87 update to 1.6.0 2021-09-10 17:56:33 +08:00
LouisLam
08de0090dc add a better approach for patching db, change setting.value from varchar to TEXT, restore Database.close() to 1.2.0 2021-09-10 17:23:45 +08:00
Louis Lam
97df200d6c Merge pull request #365 from Ponkhy/master
Minor translation fixes
2021-09-10 11:46:20 +08:00
Louis Lam
d5b17a3778 Merge pull request #367 from xinac721/master
update Simplified Chinese language(更新简体中文语言)
2021-09-10 11:45:57 +08:00
新逸Cary
2f0d994c83 update Simplified Chinese language(更新简体中文语言) 2021-09-10 09:26:01 +08:00
新逸Cary
e683033f4e Merge pull request #2 from louislam/master
update
2021-09-10 08:52:38 +08:00
Ponkhy
c62732fa9c Minor translation fixes 2021-09-10 01:24:21 +02:00
Louis Lam
2583dbe0e0 Update README.md 2021-09-09 22:58:31 +08:00
LouisLam
ca479673e7 avoid Chrome duplicate id warning 2021-09-09 21:28:27 +08:00
LouisLam
573c7faddd switch on the notification, if it is added in EditMonitor.vue 2021-09-09 21:24:29 +08:00
LouisLam
7a4432de1e better switch button color in dark mode 2021-09-09 20:45:58 +08:00
LouisLam
94a6c1a344 update language files 2021-09-09 20:38:19 +08:00
Louis Lam
ee9e48fe53 Merge pull request #361 from iomataani/italian-translation-update
Italian translation update
2021-09-09 20:26:07 +08:00
LouisLam
a4aee49b03 Merge remote-tracking branch 'origin/master' 2021-09-09 20:22:44 +08:00
LouisLam
e330875c80 fix restore fail if no isDefault 2021-09-09 20:22:32 +08:00
Paride Barison
44eba70b78 altri errorini sistemati 2021-09-09 14:19:15 +02:00
Paride Barison
c6dab944d1 altra correzione 2021-09-09 14:12:30 +02:00
Ioma Taani
0f4bc5850b Merge branch 'louislam:master' into italian-translation-update 2021-09-09 14:05:00 +02:00
Louis Lam
f37190a73d Update README.md 2021-09-09 18:02:58 +08:00
Louis Lam
29f96fae93 Update README.md 2021-09-09 18:01:15 +08:00
Louis Lam
331ae5ec20 Merge pull request #306 from Ponkhy/import-export
Added import and export function
2021-09-09 16:15:48 +08:00
LouisLam
8ee34c7904 also handle the first beat problem in uptime 2021-09-09 15:55:34 +08:00
LouisLam
4f07c2ea9a uptime calculation fully sum in sql 2021-09-09 15:46:28 +08:00
LouisLam
0b1d0d22ff add indexes, improve queries 2021-09-09 12:43:18 +08:00
LouisLam
24facc79d7 sendHeartbeatList use raw data 2021-09-09 11:57:23 +08:00
LouisLam
9f9c1007d7 increase sqlite cache size from 2MB to 12MB 2021-09-09 11:56:42 +08:00
LouisLam
70a6f0313c update .dockerignore 2021-09-09 11:41:43 +08:00
LouisLam
1d05df6ec9 [docker healthcheck] use retries instead, longer start-period causes starting problem in Docker Swarm mode 2021-09-09 11:41:28 +08:00
Ioma Taani
b54bbc302d migliorata la traduzione 2021-09-08 21:34:48 +02:00
LouisLam
dd283423ab improve smtp with cc, bbc and ignore tls 2021-09-09 01:13:09 +08:00
LouisLam
6006038689 fix monitor.stop() in some cases 2021-09-08 20:00:16 +08:00
LouisLam
a7b50c3630 Revert "add user-monitor-list"
This reverts commit 230a9bfaf9.
2021-09-08 19:54:38 +08:00
LouisLam
0ddbac5109 Revert "move userMonitorList out of server.js"
This reverts commit a7cf14c663.
2021-09-08 19:54:38 +08:00
LouisLam
0f440596c8 Revert "cache last heartbeat list in memory"
This reverts commit 87678ea92d.
2021-09-08 19:54:37 +08:00
LouisLam
5a0fcebd6e update link style 2021-09-08 18:59:30 +08:00
LouisLam
87678ea92d cache last heartbeat list in memory 2021-09-08 18:58:02 +08:00
LouisLam
0bf0cfa104 Merge remote-tracking branch 'origin/master' 2021-09-08 18:56:43 +08:00
LouisLam
a7cf14c663 move userMonitorList out of server.js 2021-09-08 16:27:37 +08:00
Louis Lam
325ce72a71 Merge pull request #352 from iomataani/italian-language
Italian language
2021-09-08 16:12:44 +08:00
Louis Lam
9d00bd9029 Merge pull request #357 from Saibamen/nav_from_dashHome
Route to monitor on `DashboardHome`
2021-09-08 16:11:39 +08:00
Adam Stachowicz
f6a168282e Route to monitor on DashboardHome 2021-09-08 09:55:08 +02:00
Ponkhy
acf6f1883b Fixed missing spaces in backup file 2021-09-08 09:02:24 +02:00
Paride Barison
f44f951e40 Tradotte le ultime cose come richiesto 2021-09-08 08:41:19 +02:00
Ioma Taani
dea9b05d49 Merge branch 'louislam:master' into italian-language 2021-09-08 08:30:10 +02:00
LouisLam
230a9bfaf9 add user-monitor-list 2021-09-08 14:16:30 +08:00
Louis Lam
d761d54d0e Merge pull request #340 from Ponkhy/default-notification
Added the option for default notifications
2021-09-08 12:49:14 +08:00
LouisLam
e37621853e Merge branch 'estonian'
# Conflicts:
#	src/pages/Settings.vue
2021-09-08 11:00:19 +08:00
LouisLam
f2cec60481 minor 2021-09-08 10:48:03 +08:00
LouisLam
2212cf9c08 fix confirmDisableAuth no msg if not tranlated 2021-09-08 10:28:40 +08:00
LouisLam
f7562e00c1 improve notification component, no double template 2021-09-08 10:25:03 +08:00
Ponkhy
678e248966 Added indicator while changing database records 2021-09-08 01:10:19 +02:00
Ponkhy
c1aebef005 Fixed SQL error due notification setup 2021-09-08 01:03:24 +02:00
Ponkhy
1ef4562905 Paused monitors stay paused after import 2021-09-07 23:32:25 +02:00
Ponkhy
69a7c4dab4 Improved importBackup handling 2021-09-07 23:14:44 +02:00
jtagcat
2c3880a326 Added Estonian.
- "Avg.": using ≈, as it's commonly enough known.
   There is no known shorthand for 'average' that I know of.

- Leaved some strings empty, as repeating the same words is useless, just noise.
2021-09-07 23:31:48 +03:00
Ponkhy
f4e885982d Added export/import description for de-DE 2021-09-07 21:10:47 +02:00
Ioma Taani
dcf6dd8b32 Ultimi aggiustamenti 2021-09-07 19:16:29 +02:00
Ioma Taani
093cfec8dd Typo. 2021-09-07 19:13:04 +02:00
Ioma Taani
f13dcb3c5b Update it-IT.js
Completed.
2021-09-07 19:07:50 +02:00
LouisLam
1a5ffd4755 add description for export/import 2021-09-08 00:46:48 +08:00
Ioma Taani
a2f9e9e9c4 Creata lingua italiana 2021-09-07 18:25:24 +02:00
LouisLam
62712f5cc4 Merge branch 'master' into import-export 2021-09-08 00:18:43 +08:00
LouisLam
f0f1847ad8 update language files 2021-09-07 23:53:20 +08:00
LouisLam
0aeaf87f5b Merge branch 'master' into default-notification
# Conflicts:
#	server/notification.js
#	src/components/NotificationDialog.vue
2021-09-07 23:40:42 +08:00
LouisLam
5ca0dd628d remove debug msg 2021-09-07 23:36:55 +08:00
LouisLam
4a106431f3 convert Telegram into a vue components 2021-09-07 23:32:05 +08:00
LouisLam
d164b6ccce prevent Chrome ask for saving password for notification settings (change to one-time-code to solve it) 2021-09-07 23:06:49 +08:00
LouisLam
da74391c3e convert notifications into modules 2021-09-07 22:42:46 +08:00
Louis Lam
850563442a Merge pull request #349 from Ponkhy/redirect-after-setup
Auto login after setup
2021-09-07 21:16:54 +08:00
Ponkhy
242e494cb5 Merge branch 'master' into import-export 2021-09-07 14:12:53 +02:00
Ponkhy
4faa409027 Merge branch 'master' into default-notification 2021-09-07 14:07:50 +02:00
Louis Lam
f842fb409e Update README.md 2021-09-07 17:44:51 +08:00
Louis Lam
72dd894856 Update README.md 2021-09-07 17:44:39 +08:00
LouisLam
0b0417e064 Merge remote-tracking branch 'origin/master' 2021-09-07 17:40:36 +08:00
LouisLam
fe35d95441 merge language files 2021-09-07 17:38:04 +08:00
LouisLam
da131a5156 Merge branch 'master' into clear-monitor-data
# Conflicts:
#	src/languages/da-DK.js
#	src/languages/en.js
#	src/languages/es-ES.js
#	src/languages/fr-FR.js
#	src/languages/ja.js
#	src/languages/ko-KR.js
#	src/languages/nl-NL.js
#	src/languages/ru-RU.js
#	src/languages/sr-latn.js
#	src/languages/sr.js
#	src/languages/sv-SE.js
#	src/languages/zh-CN.js
#	src/languages/zh-HK.js
2021-09-07 17:36:37 +08:00
Louis Lam
926c15ea40 Update healthcheck.js 2021-09-07 17:30:50 +08:00
LouisLam
87e874406a Merge branch 'Ponkhy_master_hidden' 2021-09-07 17:09:24 +08:00
LouisLam
0e288ea92d disable chrome save password dialog for autocompelete = off only 2021-09-07 17:06:33 +08:00
LouisLam
8a4a87716f disable chrome save password dialog 2021-09-07 16:46:42 +08:00
Ponkhy
d01fa9bcfc Auto login after setup 2021-09-07 10:25:25 +02:00
LouisLam
ec8c1cad31 update healthcheck.js for changing ssl cert / hostname / port 2021-09-07 16:06:28 +08:00
LouisLam
fb0fa2a843 add sqlite cli to docker 2021-09-07 15:36:29 +08:00
LouisLam
f5c6d422d5 switch debian to be the main docker base 2021-09-07 15:31:32 +08:00
Louis Lam
ea4240455f Merge pull request #347 from gaby/refactor-docker
Debian Dockerfile refactoring and improvements
2021-09-07 15:21:19 +08:00
Juan Calderon-Perez
ce30ee74e9 Revert changes breaking ARM releases 2021-09-07 01:26:42 -04:00
Juan Calderon-Perez
66b5f157eb Update Dockerfile order to improve layer caching. 2021-09-07 00:13:07 -04:00
Juan Calderon-Perez
5fe8a0917a Refactor Debian Dockerfile 2021-09-06 22:20:12 -04:00
Ponkhy
848296b77a Changed input for sensitive data to HiddenInput 2021-09-07 02:30:53 +02:00
Ponkhy
edfaacbb5f Added component HiddenInput.vue 2021-09-07 02:24:02 +02:00
Ponkhy
bb8385d690 Merge branch 'default-notification' of https://github.com/Ponkhy/uptime-kuma into default-notification 2021-09-06 09:46:36 +02:00
Ponkhy
b95404b6e0 Fixed suggestions 2021-09-06 09:45:52 +02:00
Louis Lam
cc351b2883 Merge pull request #339 from Minvinea/master
Update few words and file name
2021-09-06 13:38:26 +08:00
Louis Lam
3c06f249ca Merge pull request #338 from Saibamen/pl_fixes
[LANG] `pl` fixes
2021-09-06 12:44:56 +08:00
Minvinea
31648dc5e8 Update #2 2021-09-05 23:41:57 +02:00
Minvinea
1197cfa11e Update file name 2021-09-05 23:37:26 +02:00
Ponkhy
fd8c95d64e Merge branch 'master' into default-notification 2021-09-05 23:32:31 +02:00
Ponkhy
58240aceef Added the option for default notifications 2021-09-05 23:23:06 +02:00
Adam Stachowicz
e8b814733d Fix typo 2021-09-05 23:14:56 +02:00
Minvinea
dcc7799108 Update few words and name of file
https://prnt.sc/1rbosiy
2021-09-05 21:34:32 +02:00
Adam Stachowicz
424f20f8e1 Better upsideDownModeDescription 2021-09-05 20:59:04 +02:00
Adam Stachowicz
d4ff5d8b32 [LANG] pl fixes 2021-09-05 20:55:25 +02:00
LouisLam
899b33b3a9 add language missing keys 2021-09-06 01:31:05 +08:00
Ponkhy
1b8b33c4c3 Added set language to the Setup.vue 2021-09-05 15:40:35 +02:00
Louis Lam
d34ed00841 Merge pull request #333 from Misly16/master
Add Polish Locale
2021-09-05 12:03:07 +08:00
Louis Lam
f9c177b150 Merge pull request #315 from tgxn/patch-1
Notifications Content Improvements: SMTP, LunaSea, PushBullet
2021-09-05 11:52:50 +08:00
Misly
9952463350 Add Polish Locale 2021-09-04 20:38:53 +00:00
LouisLam
5837c353b7 change the default theme to auto from light 2021-09-05 02:47:31 +08:00
LouisLam
d5b32ffbb8 add language missing keys 2021-09-05 02:25:40 +08:00
LouisLam
61936ae5eb Merge remote-tracking branch 'Ponkhy/clear-monitor-data' into clear-monitor-data 2021-09-05 02:04:48 +08:00
LouisLam
299506ce45 reset the heartbeat list instead of reload the page after cleared events or heartbeats 2021-09-05 02:03:40 +08:00
LouisLam
ca8d4ab61b Merge branch 'master' into clear-monitor-data 2021-09-05 00:24:09 +08:00
LouisLam
ffbdf97478 improve heartbeat bar rendering in different dpi 2021-09-05 00:08:24 +08:00
LouisLam
cc25787878 improve heartbeat bar rendering in different dpi 2021-09-04 23:32:46 +08:00
Louis Lam
17453a8812 Merge pull request #332 from LeviSnoot/master
Update & Improve Swedish Translation
2021-09-04 23:16:01 +08:00
Levi
f1a151b4a1 Update & Improve Swedish Translation 2021-09-04 16:03:34 +02:00
Levi
d35b205fcc Update & Improve Swedish Translation 2021-09-04 16:01:41 +02:00
Levi
2a34e41d8c Improved Swedish translation 2021-09-04 15:59:49 +02:00
Domenic Horner
41d32bb9dd Undo parm changes 2021-09-04 20:08:18 +08:00
Ponkhy
0b9e410ea5 Added i18n key to en.js 2021-09-04 13:36:08 +02:00
Ponkhy
904ed326ca Merge branch 'louislam:master' into clear-monitor-data 2021-09-04 13:34:29 +02:00
LouisLam
778995a4fb add dockerfile-debian 2021-09-04 15:48:25 +08:00
LouisLam
ee60d74910 fix dockerfile path 2021-09-04 15:32:55 +08:00
LouisLam
6a603203cc update to 1.5.3 2021-09-04 15:07:32 +08:00
LouisLam
4b8e7fcffc update "build-docker" to build both platforms 2021-09-04 15:03:26 +08:00
LouisLam
7fd12f5485 rename nl_NL.js to nl-NL.js 2021-09-04 14:42:55 +08:00
LouisLam
0d87a6dfea update vue to 3.2.8 2021-09-04 14:41:45 +08:00
Domenic Horner
b0acda52f9 Add time to smtp body content 2021-09-04 11:27:18 +08:00
Domenic Horner
e9cd9be03a Use constants for UP/DOWN through notifications class 2021-09-04 11:09:34 +08:00
Domenic Horner
6ae279c7f3 Move title generation to notification class 2021-09-04 11:06:06 +08:00
Domenic Horner
9c32adfb55 Update pushbullet down body
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-04 18:26:44 +08:00
Domenic Horner
d346afd33b Update pushbullet up body
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-04 18:26:38 +08:00
Domenic Horner
3bf380c684 Update lunasea "up" body content
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-04 18:26:10 +08:00
Domenic Horner
dca5c59982 Update lunasea body content
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-04 18:24:43 +08:00
Ponkhy
8f9a973ede Merge branch 'master' into import-export 2021-09-04 01:26:48 +02:00
Ponkhy
b98ec0c3d4 Added i18n keys if necessary for some languages 2021-09-04 01:26:06 +02:00
Louis Lam
2082c3f68a Merge pull request #313 from xoniq/lang-nl
Add Dutch/Nederlands language (nl_NL)
2021-09-04 00:05:05 +08:00
Louis Lam
ea9f434553 Merge pull request #328 from Ponkhy/german-language
Small german language adjustments
2021-09-03 23:06:32 +08:00
Louis Lam
fa1216c198 Merge pull request #329 from dhfhfk/New-string
Add missing strings for Korean
2021-09-03 23:05:48 +08:00
dhfhfk
6d8aa20fc6 Add missing string for Korean
confirmDisableAuth, new string
2021-09-03 23:18:18 +09:00
Ponkhy
5430da955a Small german language adjustments 2021-09-03 15:54:14 +02:00
J Posthuma
e6e2b0ebf8 Update nl_NL.js 2021-09-03 13:42:14 +02:00
J Posthuma
445dc486be Merge branch 'master' into lang-nl 2021-09-03 13:41:10 +02:00
Louis Lam
d2151737c1 Update README.md 2021-09-03 17:09:11 +08:00
Louis Lam
78fc6de542 Merge pull request #325 from Saibamen/patch-1
Update `README.md` - Open Collective URL
2021-09-03 15:49:23 +08:00
Adam Stachowicz
c15e6631ae Update README.md
Open Collective URL
2021-09-03 09:16:54 +02:00
LouisLam
ebf362754c Merge branch 'master' into clear-monitor-data 2021-09-03 14:56:43 +08:00
Louis Lam
f84135ca67 Merge pull request #322 from chakflying/patch-6
Fix: Fix Notification "Test" btn styling
2021-09-03 14:34:39 +08:00
LouisLam
80eca886ce Merge branch 'add-serbian-latin'
# Conflicts:
#	src/main.js
#	src/pages/Settings.vue
2021-09-03 14:32:12 +08:00
Louis Lam
274a9050c0 Merge pull request #321 from dusansimic/add-serbian-cyrillic
Add Serbian Cyrillic locale
2021-09-03 14:00:43 +08:00
Louis Lam
1f4ad938b1 Merge pull request #323 from xinac721/master
Update Chinese Translation(更新简体中文语言翻译)
2021-09-03 13:55:30 +08:00
新逸Cary
312aedf391 Update Chinese Translation(更新简体中文语言翻译) 2021-09-03 13:33:26 +08:00
Louis Lam
4f28bfbde3 Merge pull request #309 from Saibamen/updateDependencies
Update dependencies
2021-09-03 12:56:55 +08:00
Louis Lam
ee76ab4ec2 Merge pull request #317 from AtaxyaNetwork/master
change translation FR
2021-09-03 12:56:11 +08:00
Nelson Chan
18616ee590 Fix: Fix Notification "Test" btn styling 2021-09-03 12:05:49 +08:00
Dušan Simić
19a4d570ec Fix word capitalization 2021-09-03 02:24:31 +02:00
Dušan Simić
79fda8f442 Add Serbian Latin locale 2021-09-03 02:20:01 +02:00
Dušan Simić
53a14cf4f5 Add Serbian Cyrilic locale 2021-09-03 02:06:26 +02:00
cecile
2241d8817f change translation FR 2021-09-02 23:41:47 +02:00
Louis Lam
3831dfe0b9 Update README.md 2021-09-03 02:55:36 +08:00
Louis Lam
fdde862549 Merge pull request #316 from aictur/feature/es-ES
Add spanish language and update Readme
2021-09-02 23:00:36 +08:00
LouisLam
e31be8caf5 demo mode 2021-09-02 22:52:20 +08:00
LouisLam
60f2f08cea add demo db 2021-09-02 22:39:04 +08:00
LouisLam
b1647a310e add demo db 2021-09-02 22:37:51 +08:00
Victor M. Vicente Cuevas
23f1a73fc8 Add language to src/main and update README 2021-09-02 16:31:45 +02:00
Victor M. Vicente Cuevas
d2bf2a551d Add spanish language and update Readme 2021-09-02 16:23:07 +02:00
Ponkhy
7d70c4d8cd Code optimizations 2021-09-02 16:13:31 +02:00
Domenic Horner
532ad3044c Add space to pushbullet and lunasea notifications
Start changes regarding standardization of notification messages
2021-09-02 21:55:25 +08:00
LouisLam
f23ecef636 add missing cert parameters 2021-09-02 21:16:04 +08:00
LouisLam
51cf2ff6f9 add missing cert parameters 2021-09-02 21:13:59 +08:00
LouisLam
b30b1d3a52 create data dir before copy 2021-09-02 21:11:20 +08:00
LouisLam
582e14098d create data dir before copy 2021-09-02 21:10:18 +08:00
LouisLam
6e3e2fc85c fix db path 2021-09-02 21:08:00 +08:00
LouisLam
b604807cfe create data dir if not exists 2021-09-02 20:42:55 +08:00
LouisLam
3ee13bddd1 dash style for args 2021-09-02 20:36:52 +08:00
LouisLam
c74986647e allow changing data dir 2021-09-02 20:27:18 +08:00
LouisLam
dac410c850 Merge remote-tracking branch 'origin/master' 2021-09-02 20:20:47 +08:00
LouisLam
b88b357b55 add support for https 2021-09-02 20:18:27 +08:00
Louis Lam
9a5cd5172b Merge pull request #314 from xinac721/master
Chinese Translation (from zh-CN.js)
2021-09-02 20:14:05 +08:00
新逸Cary
941788db49 Chinese Translation (from zh-CN.js) 2021-09-02 18:33:09 +08:00
Jelle Posthuma
99725aabe7 Add Dutch/Nederlands language (nl_NL) 2021-09-02 11:10:54 +02:00
Ponkhy
2dd392e609 Added uptime kuma version to backup file 2021-09-02 10:15:25 +02:00
Louis Lam
9d41da4aa2 Merge pull request #311 from xinac721/master
Chinese Translation (from zh-CN.js)
2021-09-02 15:59:06 +08:00
新逸Cary
a0f372e946 Chinese Translation (from zh-CN.js) 2021-09-02 15:34:46 +08:00
新逸Cary
877ad1438f Merge branch 'louislam:master' into master 2021-09-02 15:25:09 +08:00
LouisLam
9116654a33 update language files and run eslint on these 2021-09-02 15:21:46 +08:00
新逸Cary
dbc70bc5ed Merge pull request #1 from louislam/master
update
2021-09-02 14:33:34 +08:00
Louis Lam
a29b65e801 Merge pull request #308 from DX37/translation-ru
Add Russian translation
2021-09-02 12:56:12 +08:00
Louis Lam
bd9568ce5b Merge pull request #310 from Saibamen/more_i18n
Make `Resp. Time (ms)` and `N/A` i18n
2021-09-02 12:55:11 +08:00
Ponkhy
c13cc62d3d Update server/server.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-09-01 21:32:33 +02:00
Adam Stachowicz
7a109689d9 Make Resp. Time (ms) and N/A i18n 2021-09-01 21:17:50 +02:00
Adam Stachowicz
e7929f461d Update dependencies 2021-09-01 20:53:12 +02:00
DX37
a2cf7f394e Add Russian translation 2021-09-02 01:37:01 +07:00
Louis Lam
e1f378ee6c Update README.md 2021-09-02 02:11:44 +08:00
LouisLam
eeb00a5511 fix data type 2021-09-02 01:56:02 +08:00
LouisLam
8e1e4b9204 override file-selector-button hover color 2021-09-02 01:51:51 +08:00
LouisLam
66f9ad5754 Merge branch 'master' into import-export 2021-09-02 01:35:42 +08:00
Louis Lam
129dc3324b Merge pull request #307 from dhfhfk/master
Added Korean Language
2021-09-02 01:17:54 +08:00
Louis Lam
268d2e2353 Merge branch 'master' into master 2021-09-02 01:17:48 +08:00
Louis Lam
7d2cf0ee57 Merge pull request #304 from LeviSnoot/master
Added Swedish Language
2021-09-02 01:17:10 +08:00
오로라
2d408732db Update main.js
Prevent conflict with #304 (Added Swedish Language)
2021-09-02 01:01:18 +09:00
dhfhfk
0cc5053f14 Update ko-KR.js 2021-09-02 00:35:50 +09:00
dhfhfk
1c4e5b79be Create ko-KR.js 2021-09-02 00:30:08 +09:00
dhfhfk
4aae402b36 Update main.js 2021-09-02 00:29:01 +09:00
Ponkhy
b604910bbb Merge branch 'master' into clear-monitor-data 2021-09-01 17:17:40 +02:00
Ponkhy
2f6c5963c5 Added import and export function 2021-09-01 17:09:32 +02:00
Levi
dce2ba8f9f Update sv-SE.js 2021-09-01 16:41:51 +02:00
Levi
ed5c75282c Update main.js 2021-09-01 16:38:05 +02:00
Levi
00ac560bd6 Create sv-SE.js 2021-09-01 16:30:08 +02:00
Louis Lam
8ea4dec5a0 Merge pull request #303 from Saibamen/patch-1
Update translation docs
2021-09-01 22:08:44 +08:00
Adam Stachowicz
3006d13aee Update translation docs 2021-09-01 15:37:56 +02:00
LouisLam
c7e6cb9f37 update language files 2021-09-01 20:10:54 +08:00
LouisLam
9b82bb248e Merge branch 'master' into kometchtech_patch-1
# Conflicts:
#	src/main.js
2021-09-01 19:46:44 +08:00
Louis Lam
e55cf65f28 Merge pull request #291 from Ponkhy/master
Added translation to Setup.vue
2021-09-01 19:45:20 +08:00
Louis Lam
f24002838c Merge branch 'master' into master 2021-09-01 19:45:06 +08:00
Louis Lam
b4fd4f7d90 Merge pull request #301 from Lrss/master
Added Danish language
2021-09-01 19:44:01 +08:00
kometchtech
3a3f1762ac Update ja.js
* Perform translation for added keys.
2021-09-01 19:04:35 +09:00
Lars Sørensen
6376d4eb92 Translated to Danish 2021-09-01 11:51:22 +02:00
LouisLam
e37cf9b1a7 add missing and add Japanese to the list 2021-09-01 17:32:41 +08:00
LouisLam
3b0191210b Merge branch 'master' into kometchtech_patch-1 2021-09-01 17:28:49 +08:00
LouisLam
923d325b44 add language file preparation script 2021-09-01 17:10:12 +08:00
LouisLam
ad38e61c26 add language file preparation script 2021-09-01 17:08:27 +08:00
LouisLam
22095c09b1 add translation guide 2021-09-01 15:49:35 +08:00
LouisLam
a5e62141d5 update to 1.5.2 2021-09-01 15:04:31 +08:00
LouisLam
e4b76717be revert back to node-sqlite3 2021-09-01 15:02:04 +08:00
LouisLam
bc70ecfba7 fix install script 2021-09-01 14:56:19 +08:00
LouisLam
f1238ab762 update to 1.5.1 2021-09-01 14:41:06 +08:00
LouisLam
cd1a3a2fb9 revert back to node-sqlite3, as better-sqlite3 causes a lot of installation problems 2021-09-01 14:33:00 +08:00
kometchtech
25db162721 Create ja.js
* Created a Japanese language file.
2021-09-01 13:23:44 +09:00
Ponkhy
7b92166d18 Added clear all db statistics function 2021-09-01 00:36:24 +02:00
Ponkhy
1341d220ed Merge branch 'louislam:master' into clear-monitor-data 2021-08-31 23:22:45 +02:00
LouisLam
3e7bb355fd update to 1.5.0 2021-08-31 23:10:12 +08:00
LouisLam
697fa6bdfd fix discord notification appended port unexpectedly 2021-08-31 22:15:02 +08:00
LouisLam
527e0c3444 raise the ping timeout from 2s to 10s (avoid #294) 2021-08-31 22:14:33 +08:00
LouisLam
a41534ca60 no declare vars with comma, one line only one statement 2021-08-31 22:08:05 +08:00
LouisLam
fa549cb80e fix npm7 broken dependencies 2021-08-31 20:36:17 +08:00
LouisLam
0127d5102e Merge remote-tracking branch 'origin/master' 2021-08-31 20:03:24 +08:00
LouisLam
ec731d174d Merge branch 'MichelBaie_master' 2021-08-31 20:02:39 +08:00
LouisLam
0d65918a6a change bcrypt to bcryptjs, use my own prebuilt better-sqlite3, supports more prebuilt 2021-08-31 19:56:44 +08:00
Louis Lam
21db31bb30 Update README.md 2021-08-30 21:56:33 +08:00
Louis Lam
3ee0addb1f Merge pull request #281 from MichelBaie/master
Traduction pour le language France (FR Translation)
2021-08-30 20:09:55 +08:00
LouisLam
8c5d1945be add fr to the list 2021-08-30 19:46:10 +08:00
Tristan
bb799163e8 French Translation (from zh-HK.js) 2021-08-30 11:11:22 +02:00
LouisLam
bf29f28726 send stats only if there is at least one client in the room 2021-08-30 14:55:33 +08:00
LouisLam
22db9c9b8a Merge remote-tracking branch 'origin/master' into scroll-into-view 2021-08-30 14:00:34 +08:00
LouisLam
96ff70faaa reduce shifting when click the monitor list item (still shift in some cases, but acceptable) 2021-08-30 13:09:24 +08:00
henrygd
2776f942ab fix monitor list jumping to top on route change 2021-08-29 15:28:43 -07:00
Ponkhy
14b9afb400 Added translation to Setup.vue 2021-08-29 23:07:58 +02:00
LouisLam
3ad736692f improve monitor list 2021-08-30 02:22:49 +08:00
Ponkhy
1952e34110 Added the possibility to clear monitor data 2021-08-29 18:47:01 +02:00
LouisLam
e644a1e36f Merge branch 'master' into scroll-into-view 2021-08-30 00:46:26 +08:00
Louis Lam
78b7e36a38 Update domain name 2021-08-29 19:21:13 +08:00
Louis Lam
f30232c35d Create CNAME 2021-08-29 19:15:40 +08:00
Louis Lam
257a4ee994 Update patch7.sql 2021-08-29 18:58:05 +08:00
Louis Lam
fd1ba46d3d Merge pull request #287 from Ponkhy/dns-monitor
[Before 1.5.0] Show latest dns result in Details.vue
2021-08-29 11:17:35 +08:00
LouisLam
ada6606217 move the new sql to patch8.sql 2021-08-29 11:16:06 +08:00
Ponkhy
dd877cfc70 Added translation to pause monitor confirmation 2021-08-29 04:03:55 +02:00
Ponkhy
93edb8817d More uniform look 2021-08-29 03:57:26 +02:00
Ponkhy
858affa808 Removed useless database query 2021-08-28 21:29:24 +02:00
Louis Lam
2bb182b2b4 Create FUNDING.yml 2021-08-29 03:28:40 +08:00
Ponkhy
303adbf9b1 Show latest dns result in Details.vue 2021-08-28 21:20:25 +02:00
Louis Lam
712da02324 Merge pull request #286 from arnishb/fix-readme
fix-readme
2021-08-28 19:32:01 +08:00
Arnish Baruah
27a4dbb722 fix-readme 2021-08-28 16:40:32 +05:30
Louis Lam
0e676060f2 Update CONTRIBUTING.md 2021-08-28 13:46:47 +08:00
Louis Lam
a14c40f9bb move k8s to advanced installation section 2021-08-28 12:33:10 +08:00
Louis Lam
309fbfa094 Update README.md 2021-08-28 12:32:35 +08:00
Louis Lam
46dcd31142 Update README.md 2021-08-28 12:29:57 +08:00
LouisLam
59e9315647 show dns type and hostname in Details.vue 2021-08-28 11:48:34 +08:00
LouisLam
91e82bd12c set default value for dns resolve type and code refactor 2021-08-28 11:46:26 +08:00
LouisLam
d4dd650bfe translate for zh-hk 2021-08-28 11:20:02 +08:00
Louis Lam
354ee1a58c Merge pull request #283 from Ponkhy/german-language
Added German language for DNS Monitor
2021-08-28 11:10:39 +08:00
henrygd
2901a38628 add position: sticky to monitor details 2021-08-27 15:28:01 -07:00
Ponkhy
6464207f4b Added German language for DNS Monitor 2021-08-27 18:41:58 +02:00
Louis Lam
7652b4849a Merge pull request #238 from Ponkhy/dns-monitor
Added DNS Monitor Type
2021-08-28 00:21:10 +08:00
LouisLam
03b4086372 add CAA test and remove some files added by mistake 2021-08-27 23:57:42 +08:00
Ponkhy
acd5cf63fd Removed wrong PTR answer 2021-08-27 17:41:22 +02:00
Ponkhy
177af2d588 Added more dns types to simple-dns-server 2021-08-27 17:32:50 +02:00
Tristan
b46b214175 Traduction pour le language France (FR Translation) 2021-08-27 11:12:22 +02:00
Louis Lam
d2f0a15076 Merge pull request #264 from antiseptikk/master
feat: add rocket.chat notification
2021-08-26 23:34:07 +08:00
LouisLam
2d50d24276 more translation for zh-HK 2021-08-26 23:32:25 +08:00
Louis Lam
34b6976a03 Merge pull request #269 from Ponkhy/german-language
Added German language and new translations
2021-08-26 23:14:05 +08:00
Louis Lam
d60c11e845 Merge pull request #260 from Thiesjoo/discord-notification
Discord notification updates
2021-08-26 23:03:32 +08:00
LouisLam
890aea8756 Merge branch 'duzun_patch-1' 2021-08-26 23:02:10 +08:00
LouisLam
c55f2b93f7 change to pm2 start server/server.js directly due to (#273) 2021-08-26 22:55:44 +08:00
Louis Lam
d4cf838af7 Merge pull request #273 from duzun/patch-1
Dockerfile: Avoid keeping npm in RAM
2021-08-26 22:45:59 +08:00
LouisLam
508586fcfd add codesandbox config for demo 2021-08-26 20:36:58 +08:00
Dumitru Uzun
feb0feda76 Dockerfile: Avoid keeping npm in RAM
By running node directly, we save some RAM. In my case npm consumes 300MB and does nothing, just waits for the node process to exit.
On small VPSes 300MB is a lot!
2021-08-26 12:53:57 +03:00
Ponkhy
34ca5c5c15 Merge branch 'master' of https://github.com/louislam/uptime-kuma 2021-08-26 02:43:35 +02:00
Ponkhy
2b8c5e2e65 Added German Language 2021-08-26 02:43:26 +02:00
Thomas Ferney
44d9967cfb feat: add rocket.chat notification 2021-08-25 21:01:29 +02:00
LouisLam
8318c2e8ff add a simple dns server for testing, and disable ipRegex for dev only (need to input port) 2021-08-26 01:50:27 +08:00
LouisLam
46ac753c46 Merge branch 'master' into dns-monitor 2021-08-26 01:05:46 +08:00
Thies
72740ba477 Update the styling to better match existing styles
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-25 14:08:41 +02:00
Thies Nieborg
5d438ca2b6 Discord notification URL now also represents non http services 2021-08-25 13:07:52 +02:00
Ponkhy
02a12e68b8 Used ALTER TABLE instead of rebuilding the table 2021-08-25 12:45:47 +02:00
Louis Lam
a7cd70f7de Merge pull request #253 from chakflying/chart-fix
Fix: More Chart improvements
2021-08-25 17:35:25 +08:00
Louis Lam
26db0471da Merge pull request #254 from RasHas/master
added mattermost notification support
2021-08-25 17:06:07 +08:00
Ponkhy
d313a06d5c Optimizations for output handling 2021-08-25 09:31:42 +02:00
LouisLam
4c1f2f85f8 manual fix stylelint 2021-08-25 12:25:19 +08:00
Rashad
23851ef539 added mattermost notification support 2021-08-24 21:19:21 +03:00
LouisLam
397fd12081 remove unused import 2021-08-25 01:26:10 +08:00
LouisLam
564bc96735 eslint: camelcase rule do not check properties, because it could be database field name 2021-08-25 01:25:57 +08:00
LouisLam
682e4d45e2 eslint for notification.js 2021-08-25 01:21:06 +08:00
LouisLam
f96d792fa1 fix patch database using better-sqlite3 2021-08-25 01:11:19 +08:00
Nelson Chan
2d20634738 Chore: Add comments, improve performance & styling 2021-08-25 01:03:20 +08:00
Nelson Chan
9442c3fa05 Fix: Fix chart bars to be full width 2021-08-25 01:02:58 +08:00
LouisLam
302d2665d2 run stylelint for the project 2021-08-24 23:38:25 +08:00
LouisLam
2b68be52b0 Merge branch 'clean-mobile-table' 2021-08-24 23:22:33 +08:00
LouisLam
393c4fb1a7 update stylelint 2021-08-24 23:22:04 +08:00
Louis Lam
13f6c79e79 Merge pull request #236 from Ponkhy/clean-mobile-table
Added clean monitor table for smaller screens
2021-08-24 23:21:22 +08:00
LouisLam
ca69d06e0d update border color 2021-08-24 22:27:26 +08:00
LouisLam
d8d428907e Merge branch 'master' into clean-mobile-table 2021-08-24 21:55:16 +08:00
Ponkhy
a17c14ea1c Centered title, badge and datetime 2021-08-24 13:24:35 +02:00
LouisLam
28a51d806b translate to Traditional Chinese (Hong Kong) 2021-08-24 18:26:44 +08:00
Ponkhy
6eead46fa6 Merge branch 'dns-monitor' of https://github.com/Ponkhy/uptime-kuma into dns-monitor 2021-08-24 11:47:24 +02:00
Ponkhy
44d9fa63f0 Adjusted the output for A and AAAA records 2021-08-24 11:47:12 +02:00
LouisLam
dd4c00eed3 add vue i18n 2021-08-24 16:44:58 +08:00
LouisLam
752ac05149 Merge remote-tracking branch 'origin/master' 2021-08-24 15:46:48 +08:00
LouisLam
14652c9b5f Remove unused variables 2021-08-24 15:46:22 +08:00
Louis Lam
054186370e add source of ipRegex 2021-08-24 15:00:30 +08:00
Louis Lam
40d80dfdfd clarify pull request rule 2021-08-24 14:42:35 +08:00
Louis Lam
b9684e32d3 Update CONTRIBUTING.md 2021-08-24 14:06:23 +08:00
LouisLam
8e726da82a eslint: add camelcase rule 2021-08-24 13:58:52 +08:00
Louis Lam
df41c40cc2 add mit license badge in readme 2021-08-24 13:38:21 +08:00
Ponkhy
5dc834794c Row spacing reduced and badge centered 2021-08-24 03:14:33 +02:00
LouisLam
36ace3e56c naming convention and wrap all styles inside .table-shadow-box to avoid unexpected style in the future 2021-08-24 02:02:38 +08:00
LouisLam
3f12afce28 Merge branch 'master' into clean-mobile-table 2021-08-24 01:53:13 +08:00
Louis Lam
833b4733a8 Merge pull request #246 from chakflying/patch-4
Proj: Add npm lint commands
2021-08-24 01:15:49 +08:00
Nelson Chan
066b67dfce Proj: Add lint commands 2021-08-23 23:54:15 +08:00
Ponkhy
b2041cb36b Fixed ESLint Errors 2021-08-23 16:30:11 +02:00
LouisLam
aa2233eb2d log notification error 2021-08-23 20:57:42 +08:00
Ponkhy
e5981b10ce Replaced var with let and removed re-declaration 2021-08-23 13:08:22 +02:00
LouisLam
46cb955172 afterLogin change to non blocking 2021-08-23 18:52:55 +08:00
LouisLam
50f300dd28 heartbeat interval change to use setTimeout() 2021-08-23 18:52:24 +08:00
LouisLam
2f50fc4c00 plan to switch to better-sqlite3, drop node-sqlite3 2021-08-23 17:27:03 +08:00
LouisLam
a02edf1b4f fix detail page empty if the monitor list is not yet ready 2021-08-23 16:59:27 +08:00
LouisLam
a61a65e5ae Merge remote-tracking branch 'origin/master' 2021-08-23 15:50:00 +08:00
Louis Lam
ffaaeae794 Merge pull request #239 from chakflying/fix-ts
Fix: fix typescript errors
2021-08-23 15:49:53 +08:00
Nelson Chan
ee3bf2961c Fix: fix typescript errors 2021-08-23 11:33:24 +08:00
Ponkhy
ce79f8bfc7 CSS optimizations 2021-08-23 01:22:55 +02:00
Ponkhy
c79be19ec3 Added DNS Monitor Type 2021-08-23 00:05:48 +02:00
LouisLam
b892a92fc8 retry if acquire error 2021-08-22 23:35:24 +08:00
Louis Lam
2912ca1248 Merge pull request #237 from Ponkhy/master
Added animation for list page change
2021-08-22 15:11:51 +08:00
LouisLam
2bff1ebe0f update to 1.3.2 2021-08-22 14:44:08 +08:00
LouisLam
ec0dbf3cbe probably still memory leak over time, not sure what happen, change back to singal pool. 2021-08-22 14:43:26 +08:00
LouisLam
210a0d414c fix check update interval too short 2021-08-22 14:07:56 +08:00
Ponkhy
ca38cc91e9 Use bootstrap integraded class instead of new ones 2021-08-22 03:15:25 +02:00
Ponkhy
c90d17bd66 Added animation for list page change 2021-08-21 23:01:43 +02:00
Ponkhy
dbd3f48f68 Added clean monitor table for smaller screens 2021-08-21 21:12:44 +02:00
LouisLam
49ba5fb1b2 update to 1.3.1 2021-08-22 02:13:00 +08:00
LouisLam
d27789a8ae Revert "update to 1.3.1"
This reverts commit b53582a812.
2021-08-22 02:11:40 +08:00
LouisLam
b53582a812 update to 1.3.1 2021-08-22 02:09:16 +08:00
LouisLam
05680472a7 fix high memory usage 2021-08-22 02:07:10 +08:00
LouisLam
ca3b0a0f19 fix setInterval 2021-08-22 00:39:29 +08:00
LouisLam
4571a9b8c1 check update 2021-08-21 19:50:22 +08:00
LouisLam
362eabab8d allow empty block for catch 2021-08-21 19:45:29 +08:00
Louis Lam
a22d0f6951 Merge pull request #231 from ClemontX/master
added K8s-Deployment and edited README
2021-08-21 02:00:33 +08:00
LouisLam
b1168d4cdb update to 1.3.0 2021-08-21 01:44:43 +08:00
Carl Sander
6fcc2253ec changed domain names to example.com 2021-08-20 06:21:59 +00:00
LouisLam
f21937b197 add animation for page change 2021-08-20 02:37:59 +08:00
Louis Lam
1e7623c459 Update README.md 2021-08-19 21:22:16 +08:00
Carl Sander
22047fe932 removed doubled names of ressources 2021-08-19 11:50:05 +00:00
LouisLam
209e44c2e1 prevent all monitors making requests at the same moment when start the server 2021-08-19 18:41:31 +08:00
LouisLam
30b8d3d0ab prevent all monitors making requests at the same moment when start the server 2021-08-19 18:33:52 +08:00
LouisLam
64498163e1 add /list for mobile 2021-08-19 18:12:52 +08:00
LouisLam
4f70a70dda splite the left monitor list into a component 2021-08-19 18:05:14 +08:00
Carl Sander
5b5a32967c fixed README 2021-08-19 09:55:43 +00:00
Carl Sander
ae8b5eea5a Merge branch 'master' into master 2021-08-19 11:50:27 +02:00
LouisLam
b761aaffdf Merge remote-tracking branch 'origin/master' 2021-08-19 17:49:27 +08:00
LouisLam
7ffdb2eb80 also backup sqlite shm, val file 2021-08-19 17:49:19 +08:00
Louis Lam
2d36f4cd4a Create SECURITY.md 2021-08-19 17:32:57 +08:00
Louis Lam
2339405f90 Update README.md 2021-08-19 14:13:30 +08:00
LouisLam
8f5e5ad944 install.sh - check docker is running 2021-08-19 12:47:11 +08:00
LouisLam
575c3ee182 install.sh - check docker is running 2021-08-19 12:39:51 +08:00
LouisLam
c9aa110f6c install.sh - check docker is running 2021-08-19 12:15:07 +08:00
LouisLam
bb0af35d47 wip: implementing install script 2021-08-19 02:38:45 +08:00
LouisLam
61944d642e wip: implementing install script 2021-08-19 02:04:49 +08:00
Carl Dominik Sander
21640e1bbe changed to tag 1 2021-08-18 17:58:17 +02:00
Louis Lam
de4515ea6e Merge pull request #230 from chakflying/patch-3
Fix: Improve chart styling on mobile
2021-08-18 23:21:18 +08:00
Carl Dominik Sander
b41799f801 reolved suggestions from @srgvg and @louislam 2021-08-18 16:09:06 +02:00
Nelson Chan
d8bcfcaaa2 Fix: Reduce chart padding on mobile 2021-08-18 17:04:13 +08:00
Nelson Chan
cf5168a4e6 Fix: Resize chart on screen breakpoints 2021-08-18 17:03:25 +08:00
Carl Sander
60b0ee2959 added K8s-Deployment and edited README 2021-08-18 08:38:05 +00:00
LouisLam
f1e5e53e8f Merge branch 'patch-2' 2021-08-18 15:32:43 +08:00
LouisLam
432388a905 Merge branch 'Ponkhy_master' 2021-08-18 15:09:56 +08:00
LouisLam
746c1b6acc Merge remote-tracking branch 'origin/master' 2021-08-18 14:55:27 +08:00
LouisLam
269ac2410b install.sh add supports for CentOS 2021-08-18 14:55:03 +08:00
Nelson Chan
f72cdcc663 Feat: Add time to beat tooltip, misc. fixes 2021-08-18 11:40:20 +08:00
Louis Lam
6b3fbcd1e7 Merge pull request #228 from Ismaaa/patch-1
Fix typo in README.md
2021-08-18 01:06:36 +08:00
Ismail D
8a48f5dd71 Fix typo in README.md 2021-08-17 17:59:34 +02:00
LouisLam
d218661f3d wip: implementing install script 2021-08-17 23:08:35 +08:00
Ponkhy
77369bd002 Fixed invisible heartbeat bar after page switch 2021-08-17 15:40:22 +02:00
Louis Lam
29a89df524 Merge pull request #227 from Ponkhy/line-messenger
Added Line Messenger Notification Service
2021-08-17 20:25:01 +08:00
Louis Lam
e257fa7b2d Merge pull request #221 from chakflying/patch-1
Fix: Improve Chart axis, use 24Hr format
2021-08-17 20:05:48 +08:00
LouisLam
6980c38a6c fix #226 a workaround fix similar to https://github.com/jvandemo/generator-angular2-library/issues/221#issuecomment-355945207 2021-08-17 20:00:31 +08:00
LouisLam
8d57df7256 fix #226 a workaround fix similar to https://github.com/jvandemo/generator-angular2-library/issues/221#issuecomment-355945207 2021-08-17 19:58:09 +08:00
Ponkhy
64501bf065 Added Line Messenger Notification Service 2021-08-17 13:41:36 +02:00
LouisLam
440c178403 change sqlite to WAL mode 2021-08-17 18:18:41 +08:00
LouisLam
c9c51e47e1 add some comments 2021-08-17 16:43:59 +08:00
LouisLam
5e52f230b1 create datetime mixin 2021-08-17 16:41:12 +08:00
LouisLam
61e758d872 disable pool for sqlite, re-use a connection to improve the performance. 2021-08-17 15:59:23 +08:00
LouisLam
86826fb826 Merge remote-tracking branch 'origin/master' 2021-08-17 15:32:55 +08:00
LouisLam
7a32e5e6ff catch rejection error globally 2021-08-17 15:32:34 +08:00
Louis Lam
610f2f9c47 Merge pull request #225 from AverageHumanoid/master
Add ping support in FreeBSD
2021-08-17 14:03:28 +08:00
AverageHumanoid
01e9c76a6f Use ping in FreeBSD 2021-08-16 19:48:37 -07:00
Ponkhy
5927c2703f Merge branch 'louislam:master' into master 2021-08-17 02:55:41 +02:00
LouisLam
316db89b9a Merge remote-tracking branch 'origin/master' 2021-08-17 02:09:56 +08:00
LouisLam
eed6d3e847 add more query log for dev env 2021-08-17 02:09:40 +08:00
Louis Lam
2a62f6daae Update ask-for-help.md 2021-08-17 01:32:42 +08:00
Louis Lam
e09c296410 Update bug_report.md 2021-08-17 01:32:21 +08:00
Louis Lam
d7f660ec57 Update bug_report.md 2021-08-17 01:29:34 +08:00
LouisLam
798f39acf0 Merge remote-tracking branch 'origin/master' 2021-08-17 01:26:35 +08:00
LouisLam
31d5b4fd3d do not pass smtp user/pass to nodemailer if both are empty 2021-08-17 01:26:21 +08:00
LouisLam
fc76c2836b increase the query timeout 2021-08-17 01:22:22 +08:00
Nelson Chan
0b30bfff87 Fix: Improve Chart axis, use 24Hr format 2021-08-16 23:58:02 +08:00
Ponkhy
72f0724b9a Update src/mixins/theme.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-16 17:14:21 +02:00
Ponkhy
35176a614f Update src/mixins/theme.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-16 17:14:13 +02:00
Ponkhy
8e883c9c6a Update src/mixins/theme.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-08-16 17:14:05 +02:00
Louis Lam
2f89ee4937 Update README.md 2021-08-16 21:31:05 +08:00
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
Ponkhy
233c5661af Added user choice heartbeat bar 2021-08-15 20:46:21 +02:00
Ponkhy
91d4c15b4d Merge branch 'louislam:master' into master 2021-08-15 20:28:30 +02: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
Ponkhy
e1a38f64f8 Merge branch 'louislam:master' into master 2021-08-11 21:19:33 +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
Ponkhy
45c162583b Added more space over the badge on mobile screens 2021-08-11 11:16:53 +02: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
134 changed files with 21687 additions and 8745 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,13 @@ README.md
package-lock.json
yarn.lock
app.json
CODE_OF_CONDUCT.md
CONTRIBUTING.md
CNAME
install.sh
SECURITY.md
tsconfig.json
### .gitignore content (commented rules are duplicated)

View File

@@ -16,6 +16,10 @@ module.exports = {
requireConfigFile: false,
},
rules: {
"camelcase": ["warn", {
"properties": "never",
"ignoreImports": true
}],
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
"no-unused-vars": "warn",
@@ -36,6 +40,11 @@ module.exports = {
"no-multi-spaces": ["error", {
ignoreEOLComments: true,
}],
"space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}],
"curly": "error",
"object-curly-spacing": ["error", "always"],
"object-curly-newline": "off",
@@ -62,12 +71,15 @@ module.exports = {
exceptAfterSingleLine: true,
}],
"no-unneeded-ternary": "error",
"no-else-return": ["error", {
"allowElseIf": false,
}],
"array-bracket-newline": ["error", "consistent"],
"eol-last": ["error", "always"],
//'prefer-template': 'error',
"comma-dangle": ["warn", "only-multiline"],
"no-empty": ["error", {
"allowEmptyCatch": true
}],
"no-control-regex": "off",
"one-var": ["error", "never"],
"max-statements-per-line": ["error", { "max": 1 }]
},
}

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
#patreon: # Replace with a single Patreon username
open_collective: uptime-kuma # Replace with a single Open Collective username
#ko_fi: # Replace with a single Ko-fi username
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
#liberapay: # Replace with a single Liberapay username
#issuehunt: # Replace with a single IssueHunt username
#otechie: # Replace with a single Otechie username
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -6,5 +6,12 @@ labels: help
assignees: ''
---
**Is it a duplicate question?**
Please search in Issues without filters: https://github.com/louislam/uptime-kuma/issues?q=
**Info**
Uptime Kuma Version:
Using Docker?: Yes/No
OS:
Browser:

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**
A clear and concise description of what the bug is.
@@ -20,15 +23,16 @@ Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Info**
- Uptime Kuma Version:
- Using Docker?: Yes/No
- OS:
- Browser:
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- Uptime Kuma Version:
- Using Docker?: Yes/No
- OS:
- Browser:
**Error Log**
It is easier for us to find out the problem.
**Additional context**
Add any other context about the problem here.

View File

@@ -6,6 +6,8 @@ labels: enhancement
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.**
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,9 @@
{
"extends": "stylelint-config-recommended",
"extends": "stylelint-config-standard",
"rules": {
"indentation": 4,
"no-descending-specificity": null,
"selector-list-comma-newline-after": null,
"declaration-empty-line-before": null
}
}

1
CNAME Normal file
View File

@@ -0,0 +1 @@
git.kuma.pet

View File

@@ -6,7 +6,38 @@ The project was created with vite.js (vue3). Then I created a sub-directory call
The frontend code build into "dist" directory. The server uses "dist" as root. This is how production is working.
Your IDE should follow the config in ".editorconfig". The most special thing is I set it to 4 spaces indentation. I know 2 spaces indentation became a kind of standard nowadays for js, but my eyes is not so comfortable for this. In my opinion, there is no callback-hell nowadays, it is good to go back 4 spaces world again.
# Can I create a pull request for Uptime Kuma?
Generally, if the pull request is working fine and it do not affect any existing logic, workflow and perfomance, I will merge to the master branch once it is tested.
If you are not sure, feel free to create an empty pull request draft first.
## Pull Request Examples
### ✅ High - Medium Priority
- Add a new notification
- Add a chart
- Fix a bug
### *️⃣ Requires one more reviewer
I do not have such knowledge to test it.
- Add k8s supports
### *️⃣ Low Priority
It changed my current workflow and require further studies.
- Change my release approach
### ❌ Won't Merge
- Duplicated pull request
- Buggy
- Existing logic is completely modified or deleted
- A function that is completely out of scope
# Project Styles
@@ -19,16 +50,33 @@ For example, recently, because I am not a python expert, I spent a 2 hours to re
- All settings in frontend.
- Easy to use
# Coding Styles
- Follow .editorconfig
- Follow eslint
## Name convention
- Javascript/Typescript: camelCaseType
- SQLite: underscore_type
- CSS/SCSS: dash-type
# Tools
- Node.js >= 14
- Git
- IDE that supports .editorconfig (I am using Intellji Idea)
- IDE that supports .editorconfig and eslint (I am using Intellji Idea)
- A SQLite tool (I am using SQLite Expert Personal)
# Prepare the dev
# Install dependencies
```bash
npm install
npm install --dev
```
For npm@7, you need --legacy-peer-deps
```
npm install --legacy-peer-deps --dev
```
# Backend Dev
@@ -39,7 +87,6 @@ npm run start-server
# Or
node server/server.js
```
It binds to 0.0.0.0:3001 by default.
@@ -92,7 +139,8 @@ The data and socket logic in "src/mixins/socket.js"
# Database Migration
TODO
1. create `patch{num}.sql` in `./db/`
1. update `latestVersion` in `./server/database.js`
# Unit Test

120
README.md
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>
<div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" />
</div>
@@ -11,34 +10,37 @@ It is a self-hosted monitoring tool like "Uptime Robot".
<img src="https://louislam.net/uptimekuma/1.jpg" width="512" alt="" />
# Features
## 🥔 Live Demo
* Monitoring uptime for HTTP(s) / TCP / Ping.
Try it!
https://demo.uptime.kuma.pet
It is a 5 minutes live demo, all data will be deleted after that. The server is located at Tokyo, if you live far away from here, it may affact your experience. I suggest that you should install to try it.
VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollective.com/uptime-kuma)! Thank you so much!
## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / Ping / DNS Record.
* Fancy, Reactive, Fast UI/UX.
* Notifications via Webhook, Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP) and more by Apprise.
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [70+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/issues/284).
* 20 seconds interval.
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
# How to Use
## 🔧 How to Install
## Docker
### 🐳 Docker
```bash
# Create a volume
docker volume create uptime-kuma
# Start the container
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
Browse to http://localhost:3001 after started.
Change Port and Volume
```bash
docker run -d --restart=always -p <YOUR_PORT>:3001 -v <YOUR_DIR OR VOLUME>:/app/data --name uptime-kuma louislam/uptime-kuma:1
```
## Without Docker
### 💪🏻 Without Docker
Required Tools: Node.js >= 14, git and pm2.
@@ -48,63 +50,43 @@ cd uptime-kuma
npm run setup
# Option 1. Try it
npm run start-server
node server/server.js
# (Recommended)
# Option 2. Run in background using PM2
# (Recommended) Option 2. Run in background using PM2
# Install PM2 if you don't have: npm install pm2 -g
pm2 start npm --name uptime-kuma -- run start-server
# Listen to different port or hostname
pm2 start npm --name uptime-kuma -- run start-server -- --port=80 --hostname=0.0.0.0
pm2 start server/server.js --name uptime-kuma
```
Browse to http://localhost:3001 after started.
### Advanced Installation
## (Optional) One more step for Reverse Proxy
If you need more options or need to browse via a reserve proxy, please read:
This is optional for someone who want to do reverse proxy.
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
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
## 🆙 How to Update
## One-click Deploy
Please read:
<!---
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)
-->
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
[![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.
PS: For every new release, it takes some time to build the docker image, please be patient if it is not available yet.
### Without Docker
```bash
git fetch --all
git checkout 1.0.8 --force
npm install
npm run build
pm2 restart uptime-kuma
```
# What's Next?
## 🆕 What's Next?
I will mark requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones
# More Screenshots
Project Plan:
https://github.com/louislam/uptime-kuma/projects/1
## 🖼 More Screenshots
Dark Mode:
<img src="https://user-images.githubusercontent.com/1336778/128710166-908f8d88-9256-43f3-9c49-bfc2c56011d2.png" width="400" alt="" />
Settings Page:
@@ -114,24 +96,34 @@ Telegram Notification Sample:
<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 ones is statping. Unfortunately, it is not stable and unmaintained.
* Want to build a fancy UI.
* Learn Vue 3 and vite.js.
* Show the power of Bootstrap 5.
* Try to use WebSocket with SPA instead of REST API.
* Deploy my first Docker image to Docker Hub.
If you love this project, please consider giving me a ⭐.
# Contribute
## 🗣️ Discussion
If you want to report a bug or request a new feature. Free feel to open a new issue.
You can also discuss or ask for help in [Issues](https://github.com/louislam/uptime-kuma/issues).
If you want to modify Uptime Kuma, this guideline maybe useful for you: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
Alternatively, you can discuss in my original post on reddit: https://www.reddit.com/r/selfhosted/comments/oi7dc7/uptime_kuma_a_fancy_selfhosted_monitoring_tool_an/
I think the real "Discussion" tab is hard to use, as it is reddit-like flow, I always missed new comments.
## Contribute
If you want to report a bug or request a new feature. Free feel to open a [new issue](https://github.com/louislam/uptime-kuma/issues).
If you want to translate Uptime Kuma into your langauge, please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
If you want to modify Uptime Kuma, this guideline may be 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.

14
SECURITY.md Normal file
View File

@@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 1.x.x | :white_check_mark: |
## Reporting a Vulnerability
https://github.com/louislam/uptime-kuma/issues

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"]
}

BIN
db/demo_kuma.db Normal file

Binary file not shown.

View File

@@ -0,0 +1,10 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- For sendHeartbeatList
CREATE INDEX monitor_time_index ON heartbeat (monitor_id, time);
-- For sendImportantHeartbeatList
CREATE INDEX monitor_important_time_index ON heartbeat (monitor_id, important,time);
COMMIT;

View File

@@ -0,0 +1,22 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- Generated by Intellij IDEA
create table setting_dg_tmp
(
id INTEGER
primary key autoincrement,
key VARCHAR(200) not null
unique,
value TEXT,
type VARCHAR(20)
);
insert into setting_dg_tmp(id, key, value, type) select id, key, value, type from setting;
drop table setting;
alter table setting_dg_tmp rename to setting;
COMMIT;

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;

10
db/patch7.sql Normal file
View File

@@ -0,0 +1,10 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD dns_resolve_type VARCHAR(5);
ALTER TABLE monitor
ADD dns_resolve_server VARCHAR(255);
COMMIT;

7
db/patch8.sql Normal file
View File

@@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD dns_last_result VARCHAR(255);
COMMIT;

7
db/patch9.sql Normal file
View File

@@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE notification
ADD is_default BOOLEAN default 0 NOT NULL;
COMMIT;

View File

@@ -1,32 +1,38 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 AS release
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too
FROM node:14-buster-slim AS build
WORKDIR /app
# split the sqlite install here, so that it can caches the arm prebuilt
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \
# do not modify it, since we don't want to re-compile the arm prebuilt again
RUN apt update && \
apt --yes install python3 python3-pip python3-dev git g++ make && \
ln -s /usr/bin/python3 /usr/bin/python && \
npm install sqlite3@5.0.2 bcrypt@5.0.1 && \
apk del .build-deps
# Touching above code may causes sqlite3 re-compile again, painful slow.
# Install apprise
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 && \
rm -rf /root/.cache
RUN apprise --version
# New things add here
npm install mapbox/node-sqlite3#593c9d --build-from-source
COPY . .
RUN npm install && \
npm run build && \
npm prune
RUN npm install --legacy-peer-deps && npm run build && npm prune --production
FROM node:14-buster-slim AS release
WORKDIR /app
# Install Apprise,
# add sqlite3 cli for debugging in the future
# iputils-ping for ping
RUN apt update && \
apt --yes install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 \
iputils-ping && \
pip3 --no-cache-dir install apprise && \
rm -rf /var/lib/apt/lists/*
# Copy app files from build layer
COPY --from=build /app /app
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js
CMD ["npm", "run", "start-server"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly

33
dockerfile-alpine Normal file
View File

@@ -0,0 +1,33 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:14-alpine3.12 AS build
WORKDIR /app
# split the sqlite install here, so that it can caches the arm prebuilt
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev git && \
ln -s /usr/bin/python3 /usr/bin/python && \
npm install mapbox/node-sqlite3#593c9d && \
apk del .build-deps && \
rm -f /usr/bin/python
COPY . .
RUN npm install --legacy-peer-deps && npm run build && npm prune --production
FROM node:14-alpine3.12 AS release
WORKDIR /app
# Install apprise
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
pip3 --no-cache-dir install apprise && \
rm -rf /root/.cache
# Copy app files from build layer
COPY --from=build /app /app
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly

View File

@@ -0,0 +1,2 @@
# Must enable File Sharing in Docker Desktop
docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh

View File

@@ -1,19 +1,34 @@
var http = require("http");
var options = {
host: "localhost",
port: "3001",
timeout: 2000,
/*
* This script should be run after a period of time (180s), because the server may need some time to prepare.
*/
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
let client;
if (process.env.SSL_KEY && process.env.SSL_CERT) {
client = require("https");
} else {
client = require("http");
}
let options = {
host: process.env.HOST || "127.0.0.1",
port: parseInt(process.env.PORT) || 3001,
timeout: 28 * 1000,
};
var request = http.request(options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);
} else {
process.exit(1);
}
let request = client.request(options, (res) => {
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
if (res.statusCode === 200) {
process.exit(0);
} else {
process.exit(1);
}
});
request.on("error", function (err) {
console.log("ERROR");
process.exit(1);
console.error("Health Check ERROR");
process.exit(1);
});
request.end();

245
extra/install.batsh Normal file
View File

@@ -0,0 +1,245 @@
// install.sh is generated by ./extra/install.batsh, do not modify it directly.
// "npm run compile-install-script" to compile install.sh
// The command is working on Windows PowerShell and Docker for Windows only.
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
println("=====================");
println("Uptime Kuma Installer");
println("=====================");
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
println("---------------------------------------");
println("This script is designed for Linux and basic usage.");
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
println("---------------------------------------");
println("");
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
println("Docker - Install Uptime Kuma Docker container");
println("");
if ("$1" != "") {
type = "$1";
} else {
call("read", "-p", "Which installation method do you prefer? [DOCKER/local]: ", "type");
}
defaultPort = "3001";
function checkNode() {
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
println("Node Version: " ++ nodeVersion);
if (nodeVersion < "12") {
println("Error: Required Node.js 14");
call("exit", "1");
}
if (nodeVersion == "12") {
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
}
}
function deb() {
bash("nodeCheck=$(node -v)");
bash("apt --yes update");
if (nodeCheck != "") {
checkNode();
} else {
// Old nodejs binary name is "nodejs"
bash("check=$(nodejs --version)");
if (check != "") {
println("Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old.");
bash("exit 1");
}
bash("curlCheck=$(curl --version)");
if (curlCheck == "") {
println("Installing Curl");
bash("apt --yes install curl");
}
println("Installing Node.js 14");
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
bash("apt --yes install nodejs");
bash("node -v");
bash("nodeCheckAgain=$(node -v)");
if (nodeCheckAgain == "") {
println("Error during Node.js installation");
bash("exit 1");
}
}
bash("check=$(git --version)");
if (check == "") {
println("Installing Git");
bash("apt --yes install git");
}
}
if (type == "local") {
defaultInstallPath = "/opt/uptime-kuma";
if (exists("/etc/redhat-release")) {
os = call("cat", "/etc/redhat-release");
distribution = "rhel";
} else if (exists("/etc/issue")) {
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
if (os == "Ubuntu") {
distribution = "ubuntu";
}
if (os == "Debian") {
distribution = "debian";
}
}
bash("arch=$(uname -i)");
println("Your OS: " ++ os);
println("Distribution: " ++ distribution);
println("Arch: " ++ arch);
if ("$3" != "") {
port = "$3";
} else {
call("read", "-p", "Listening Port [$defaultPort]: ", "port");
if (port == "") {
port = defaultPort;
}
}
if ("$2" != "") {
installPath = "$2";
} else {
call("read", "-p", "Installation Path [$defaultInstallPath]: ", "installPath");
if (installPath == "") {
installPath = defaultInstallPath;
}
}
// CentOS
if (distribution == "rhel") {
bash("nodeCheck=$(node -v)");
if (nodeCheck != "") {
checkNode();
} else {
bash("curlCheck=$(curl --version)");
if (curlCheck == "") {
println("Installing Curl");
bash("yum -y -q install curl");
}
println("Installing Node.js 14");
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
bash("yum install -y -q nodejs");
bash("node -v");
bash("nodeCheckAgain=$(node -v)");
if (nodeCheckAgain == "") {
println("Error during Node.js installation");
bash("exit 1");
}
}
bash("check=$(git --version)");
if (check == "") {
println("Installing Git");
bash("yum -y -q install git");
}
// Ubuntu
} else if (distribution == "ubuntu") {
deb();
// Debian
} else if (distribution == "debian") {
deb();
} else {
// Unknown distribution
error = 0;
bash("check=$(git --version)");
if (check == "") {
error = 1;
println("Error: git is missing");
}
bash("check=$(node -v)");
if (check == "") {
error = 1;
println("Error: node is missing");
}
if (error > 0) {
println("Please install above missing software");
bash("exit 1");
}
}
bash("check=$(pm2 --version)");
if (check == "") {
println("Installing PM2");
bash("npm install pm2 -g");
bash("pm2 startup");
}
bash("mkdir -p $installPath");
bash("cd $installPath");
bash("git clone https://github.com/louislam/uptime-kuma.git .");
bash("npm run setup");
bash("pm2 start server/server.js --name uptime-kuma -- --port=$port");
} else {
defaultVolume = "uptime-kuma";
bash("check=$(docker -v)");
if (check == "") {
println("Error: docker is not found!");
bash("exit 1");
}
bash("check=$(docker info)");
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then
\"echo\" \"Error: docker is not running\"
\"exit\" \"1\"
fi");
if ("$3" != "") {
port = "$3";
} else {
call("read", "-p", "Expose Port [$defaultPort]: ", "port");
if (port == "") {
port = defaultPort;
}
}
if ("$2" != "") {
volume = "$2";
} else {
call("read", "-p", "Volume Name [$defaultVolume]: ", "volume");
if (volume == "") {
volume = defaultVolume;
}
}
println("Port: $port");
println("Volume: $volume");
bash("docker volume create $volume");
bash("docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1");
}
println("http://localhost:$port");

View File

@@ -1,25 +1,9 @@
/**
* 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 pkg = require("../package.json");
const fs = require("fs");
const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version
const newVersion = oldVersion + "-nightly"
@@ -35,6 +19,6 @@ if (newVersion) {
// Process 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);
})
});
}

144
extra/simple-dns-server.js Normal file
View File

@@ -0,0 +1,144 @@
/*
* Simple DNS Server
* For testing DNS monitoring type, dev only
*/
const dns2 = require("dns2");
const { Packet } = dns2;
const server = dns2.createServer({
udp: true
});
server.on("request", (request, send, rinfo) => {
for (let question of request.questions) {
console.log(question.name, type(question.type), question.class);
const response = Packet.createResponseFromRequest(request);
if (question.name === "existing.com") {
if (question.type === Packet.TYPE.A) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
address: "1.2.3.4"
});
} if (question.type === Packet.TYPE.AAAA) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
address: "fe80::::1234:5678:abcd:ef00",
});
} else if (question.type === Packet.TYPE.CNAME) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
domain: "cname1.existing.com",
});
} else if (question.type === Packet.TYPE.MX) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
exchange: "mx1.existing.com",
priority: 5
});
} else if (question.type === Packet.TYPE.NS) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
ns: "ns1.existing.com",
});
} else if (question.type === Packet.TYPE.SOA) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
primary: "existing.com",
admin: "admin@existing.com",
serial: 2021082701,
refresh: 300,
retry: 3,
expiration: 10,
minimum: 10,
});
} else if (question.type === Packet.TYPE.SRV) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
priority: 5,
weight: 5,
port: 8080,
target: "srv1.existing.com",
});
} else if (question.type === Packet.TYPE.TXT) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
data: "#v=spf1 include:_spf.existing.com ~all",
});
} else if (question.type === Packet.TYPE.CAA) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
flags: 0,
tag: "issue",
value: "ca.existing.com",
});
}
}
if (question.name === "4.3.2.1.in-addr.arpa") {
if (question.type === Packet.TYPE.PTR) {
response.answers.push({
name: question.name,
type: question.type,
class: question.class,
ttl: 300,
domain: "ptr1.existing.com",
});
}
}
send(response);
}
});
server.on("listening", () => {
console.log("Listening");
console.log(server.addresses());
});
server.on("close", () => {
console.log("server closed");
});
server.listen({
udp: 5300
});
function type(code) {
for (let name in Packet.TYPE) {
if (Packet.TYPE[name] === code) {
return name;
}
}
}

View File

@@ -0,0 +1,3 @@
package-lock.json
test.js
languages/

View File

@@ -0,0 +1,78 @@
// Need to use es6 to read language files
import fs from "fs";
import path from "path";
import util from "util";
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
/**
* Look ma, it's cp -R.
* @param {string} src The path to the thing to copy.
* @param {string} dest The path to the new copy.
*/
const copyRecursiveSync = function (src, dest) {
let exists = fs.existsSync(src);
let stats = exists && fs.statSync(src);
let isDirectory = exists && stats.isDirectory();
if (isDirectory) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName),
path.join(dest, childItemName));
});
} else {
fs.copyFileSync(src, dest);
}
};
console.log(process.argv)
const baseLangCode = process.argv[2] || "zh-HK";
console.log("Base Lang: " + baseLangCode);
fs.rmdirSync("./languages", { recursive: true });
copyRecursiveSync("../../src/languages", "./languages");
const en = (await import("./languages/en.js")).default;
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
const files = fs.readdirSync("./languages");
console.log(files);
for (const file of files) {
if (file.endsWith(".js")) {
console.log("Processing " + file);
const lang = await import("./languages/" + file);
let obj;
if (lang.default) {
console.log("is js module");
obj = lang.default;
} else {
console.log("empty file");
obj = {
languageName: "<Your Language name in your language (not in English)>"
};
}
// En first
for (const key in en) {
if (! obj[key]) {
obj[key] = en[key];
}
}
// Base second
for (const key in baseLang) {
if (! obj[key]) {
obj[key] = key;
}
}
const code = "export default " + util.inspect(obj, {
depth: null,
});
fs.writeFileSync(`../../src/languages/${file}`, code);
}
}
fs.rmdirSync("./languages", { recursive: true });
console.log("Done, fix the format by eslint now");

View File

@@ -0,0 +1,12 @@
{
"name": "update-language-files",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

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

@@ -0,0 +1,61 @@
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);
pkg.scripts["build-docker-alpine"] = pkg.scripts["build-docker-alpine"].replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker-debian"] = pkg.scripts["build-docker-debian"].replaceAll(oldVersion, newVersion);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
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>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#5cdd8b" />
<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" />
<title>Uptime Kuma</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

203
install.sh Normal file
View File

@@ -0,0 +1,203 @@
# install.sh is generated by ./extra/install.batsh, do not modify it directly.
# "npm run compile-install-script" to compile install.sh
# The command is working on Windows PowerShell and Docker for Windows only.
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
"echo" "-e" "====================="
"echo" "-e" "Uptime Kuma Installer"
"echo" "-e" "====================="
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
"echo" "-e" "---------------------------------------"
"echo" "-e" "This script is designed for Linux and basic usage."
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
"echo" "-e" "---------------------------------------"
"echo" "-e" ""
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
"echo" "-e" ""
if [ "$1" != "" ]; then
type="$1"
else
"read" "-p" "Which installation method do you prefer? [DOCKER/local]: " "type"
fi
defaultPort="3001"
function checkNode {
local _0
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
"echo" "-e" "Node Version: ""$nodeVersion"
_0="12"
if [ $(($nodeVersion < $_0)) == 1 ]; then
"echo" "-e" "Error: Required Node.js 14"
"exit" "1"
fi
if [ "$nodeVersion" == "12" ]; then
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
fi
}
function deb {
nodeCheck=$(node -v)
apt --yes update
if [ "$nodeCheck" != "" ]; then
"checkNode"
else
# Old nodejs binary name is "nodejs"
check=$(nodejs --version)
if [ "$check" != "" ]; then
"echo" "-e" "Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old."
exit 1
fi
curlCheck=$(curl --version)
if [ "$curlCheck" == "" ]; then
"echo" "-e" "Installing Curl"
apt --yes install curl
fi
"echo" "-e" "Installing Node.js 14"
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
apt --yes install nodejs
node -v
nodeCheckAgain=$(node -v)
if [ "$nodeCheckAgain" == "" ]; then
"echo" "-e" "Error during Node.js installation"
exit 1
fi
fi
check=$(git --version)
if [ "$check" == "" ]; then
"echo" "-e" "Installing Git"
apt --yes install git
fi
}
if [ "$type" == "local" ]; then
defaultInstallPath="/opt/uptime-kuma"
if [ -e "/etc/redhat-release" ]; then
os=$("cat" "/etc/redhat-release")
distribution="rhel"
else
if [ -e "/etc/issue" ]; then
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
if [ "$os" == "Ubuntu" ]; then
distribution="ubuntu"
fi
if [ "$os" == "Debian" ]; then
distribution="debian"
fi
fi
fi
arch=$(uname -i)
"echo" "-e" "Your OS: ""$os"
"echo" "-e" "Distribution: ""$distribution"
"echo" "-e" "Arch: ""$arch"
if [ "$3" != "" ]; then
port="$3"
else
"read" "-p" "Listening Port [$defaultPort]: " "port"
if [ "$port" == "" ]; then
port="$defaultPort"
fi
fi
if [ "$2" != "" ]; then
installPath="$2"
else
"read" "-p" "Installation Path [$defaultInstallPath]: " "installPath"
if [ "$installPath" == "" ]; then
installPath="$defaultInstallPath"
fi
fi
# CentOS
if [ "$distribution" == "rhel" ]; then
nodeCheck=$(node -v)
if [ "$nodeCheck" != "" ]; then
"checkNode"
else
curlCheck=$(curl --version)
if [ "$curlCheck" == "" ]; then
"echo" "-e" "Installing Curl"
yum -y -q install curl
fi
"echo" "-e" "Installing Node.js 14"
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
yum install -y -q nodejs
node -v
nodeCheckAgain=$(node -v)
if [ "$nodeCheckAgain" == "" ]; then
"echo" "-e" "Error during Node.js installation"
exit 1
fi
fi
check=$(git --version)
if [ "$check" == "" ]; then
"echo" "-e" "Installing Git"
yum -y -q install git
fi
# Ubuntu
else
if [ "$distribution" == "ubuntu" ]; then
"deb"
# Debian
else
if [ "$distribution" == "debian" ]; then
"deb"
else
# Unknown distribution
error=$((0))
check=$(git --version)
if [ "$check" == "" ]; then
error=$((1))
"echo" "-e" "Error: git is missing"
fi
check=$(node -v)
if [ "$check" == "" ]; then
error=$((1))
"echo" "-e" "Error: node is missing"
fi
if [ $(($error > 0)) == 1 ]; then
"echo" "-e" "Please install above missing software"
exit 1
fi
fi
fi
fi
check=$(pm2 --version)
if [ "$check" == "" ]; then
"echo" "-e" "Installing PM2"
npm install pm2 -g
pm2 startup
fi
mkdir -p $installPath
cd $installPath
git clone https://github.com/louislam/uptime-kuma.git .
npm run setup
pm2 start server/server.js --name uptime-kuma -- --port=$port
else
defaultVolume="uptime-kuma"
check=$(docker -v)
if [ "$check" == "" ]; then
"echo" "-e" "Error: docker is not found!"
exit 1
fi
check=$(docker info)
if [[ "$check" == *"Is the docker daemon running"* ]]; then
"echo" "Error: docker is not running"
"exit" "1"
fi
if [ "$3" != "" ]; then
port="$3"
else
"read" "-p" "Expose Port [$defaultPort]: " "port"
if [ "$port" == "" ]; then
port="$defaultPort"
fi
fi
if [ "$2" != "" ]; then
volume="$2"
else
"read" "-p" "Volume Name [$defaultVolume]: " "volume"
if [ "$volume" == "" ]; then
volume="$defaultVolume"
fi
fi
"echo" "-e" "Port: $port"
"echo" "-e" "Volume: $volume"
docker volume create $volume
docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1
fi
"echo" "-e" "http://localhost:$port"

28
kubernetes/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Uptime-Kuma K8s Deployment
## How does it work?
Kustomize is a tool which builds a complete deployment file for all config elements.
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing.
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like.
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service
## What do i have to edit?
You have to edit the ```ingressroute.yml``` to your needs.
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/).
- host
- secrets and secret names
- (Cluster)Issuer (optional)
- the Version in the Deployment-File
- update:
- change to newer version and run the above commands, it will update the pods one after another
## How To use:
- install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/)
- Edit files mentioned above to your needs
- run ```kustomize build > apply.yml```
- run ```kubectl apply -f apply.yml```
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address.

View File

@@ -0,0 +1,10 @@
namespace: uptime-kuma
namePrefix: uptime-kuma-
commonLabels:
app: uptime-kuma
bases:
- uptime-kuma

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
component: uptime-kuma
name: deployment
spec:
selector:
matchLabels:
component: uptime-kuma
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
component: uptime-kuma
spec:
containers:
- name: app
image: louislam/uptime-kuma:1
ports:
- containerPort: 3001
volumeMounts:
- mountPath: /app/data
name: storage
livenessProbe:
exec:
command:
- node
- extra/healthcheck.js
readinessProbe:
httpGet:
path: /
port: 3001
scheme: HTTP
volumes:
- name: storage
persistentVolumeClaim:
claimName: pvc

View File

@@ -0,0 +1,39 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/server-snippets: |
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_cache_bypass $http_upgrade;
}
name: ingress
spec:
tls:
- hosts:
- example.com
secretName: example-com-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service
port:
number: 3001

View File

@@ -0,0 +1,5 @@
resources:
- deployment.yml
- service.yml
- ingressroute.yml
- pvc.yml

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 4Gi

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: service
spec:
selector:
component: uptime-kuma
type: ClusterIP
ports:
- name: http
port: 3001
targetPort: 3001
protocol: TCP

20312
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "1.0.8",
"version": "1.6.2",
"license": "MIT",
"repository": {
"type": "git",
@@ -10,65 +10,83 @@
"node": "14.*"
},
"scripts": {
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style",
"dev": "vite --host",
"start": "npm run start-server",
"start-server": "node server/server.js",
"update": "",
"build": "vite build",
"vite-preview-dist": "vite preview --host",
"build-docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:1.0.8 --target release . --push",
"build-docker": "npm run build-docker-debian && npm run build-docker-alpine",
"build-docker-alpine": "docker buildx build -f dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:1.6.2-alpine --target release . --push",
"build-docker-debian": "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.6.2 -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:1.6.2-debian --target release . --push",
"build-docker-nightly": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push",
"setup": "git checkout 1.0.8 && npm install && npm run build",
"version-global-replace": "node extra/version-global-replace.js",
"mark-as-nightly": "node extra/mark-as-nightly.js"
"build-docker-nightly-amd64": "docker buildx build --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"setup": "git checkout 1.6.2 && npm install --legacy-peer-deps && node node_modules/esbuild/install.js && npm run build && npm prune",
"update-version": "node extra/update-version.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js",
"update-language-files": "cd extra/update-language-files && node index.js %npm_config_base_lang% && eslint ../../src/languages/**.js --fix"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-4",
"@popperjs/core": "^2.9.2",
"@popperjs/core": "^2.9.3",
"args-parser": "^1.3.0",
"axios": "^0.21.1",
"bcrypt": "^5.0.1",
"bootstrap": "^5.0.2",
"bcryptjs": "^2.4.3",
"bootstrap": "^5.1.0",
"chart.js": "^3.5.1",
"chartjs-adapter-dayjs": "^1.0.0",
"command-exists": "^1.2.9",
"compare-versions": "^3.6.0",
"dayjs": "^1.10.6",
"express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"form-data": "^4.0.0",
"http-graceful-shutdown": "^3.1.2",
"http-graceful-shutdown": "^3.1.4",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.6.3",
"password-hash": "^1.2.2",
"prom-client": "^13.1.0",
"prom-client": "^13.2.0",
"prometheus-api-metrics": "^3.2.0",
"redbean-node": "0.0.20",
"socket.io": "^4.1.3",
"socket.io-client": "^4.1.3",
"sqlite3": "^5.0.2",
"redbean-node": "0.1.2",
"socket.io": "^4.2.0",
"socket.io-client": "^4.2.0",
"sqlite3": "github:mapbox/node-sqlite3#593c9d",
"tcp-ping": "^0.1.1",
"v-pagination-3": "^0.1.6",
"vue": "^3.0.5",
"vue": "^3.2.8",
"vue-chart-3": "^0.5.7",
"vue-confirm-dialog": "^1.0.2",
"vue-router": "^4.0.10",
"vue-i18n": "^9.1.7",
"vue-multiselect": "^3.0.0-alpha.2",
"vue-router": "^4.0.11",
"vue-toastification": "^2.0.0-rc.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.13.10",
"@types/bootstrap": "^5.0.17",
"@vitejs/plugin-legacy": "^1.5.0",
"@vitejs/plugin-vue": "^1.3.0",
"@vue/compiler-sfc": "^3.1.5",
"core-js": "^3.15.2",
"eslint": "^7.31.0",
"eslint-plugin-vue": "^7.14.0",
"sass": "^1.36.0",
"@babel/eslint-parser": "^7.15.0",
"@types/bootstrap": "^5.1.2",
"@vitejs/plugin-legacy": "^1.5.2",
"@vitejs/plugin-vue": "^1.6.0",
"@vue/compiler-sfc": "^3.2.6",
"core-js": "^3.17.0",
"dns2": "^2.0.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.17.0",
"sass": "^1.38.2",
"stylelint": "^13.13.1",
"stylelint-config-recommended": "^5.0.0",
"stylelint-config-standard": "^22.0.0",
"typescript": "^4.3.5",
"vite": "^2.4.4"
"typescript": "^4.4.2",
"vite": "^2.5.3"
}
}

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:

44
server/check-version.js Normal file
View File

@@ -0,0 +1,44 @@
const { setSetting } = require("./util-server");
const axios = require("axios");
const { isDev } = require("../src/util");
exports.version = require("../package.json").version;
exports.latestVersion = null;
let interval;
exports.startInterval = () => {
let check = async () => {
try {
const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json");
if (typeof res.data === "string") {
res.data = JSON.parse(res.data);
}
// For debug
if (process.env.TEST_CHECK_VERSION === "1") {
res.data.version = "1000.0.0"
}
exports.latestVersion = res.data.version;
console.log("Latest Version: " + exports.latestVersion);
} catch (_) { }
};
check();
interval = setInterval(check, 3600 * 1000 * 48);
};
exports.enableCheckUpdate = async (value) => {
await setSetting("checkUpdate", value);
clearInterval(interval);
if (value) {
exports.startInterval();
}
};
exports.socket = null;

88
server/client.js Normal file
View File

@@ -0,0 +1,88 @@
/*
* For Client Socket
*/
const { TimeLogger } = require("../src/util");
const { R } = require("redbean-node");
const { io } = require("./server");
async function sendNotificationList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("notification", " user_id = ? ", [
socket.userID,
]);
for (let bean of list) {
result.push(bean.export())
}
io.to(socket.userID).emit("notificationList", result)
timeLogger.print("Send Notification List");
return list;
}
/**
* Send Heartbeat History list to socket
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
* @param overwrite Overwrite client-side's heartbeat list
*/
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
const timeLogger = new TimeLogger();
let list = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 100
`, [
monitorID,
])
let result = list.reverse();
if (toUser) {
io.to(socket.userID).emit("heartbeatList", monitorID, result, overwrite);
} else {
socket.emit("heartbeatList", monitorID, result, overwrite);
}
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`);
}
/**
* Important Heart beat list (aka event list)
* @param socket
* @param monitorID
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only
* @param overwrite Overwrite client-side's heartbeat list
*/
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
const timeLogger = new TimeLogger();
let list = await R.find("heartbeat", `
monitor_id = ?
AND important = 1
ORDER BY time DESC
LIMIT 500
`, [
monitorID,
])
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
if (toUser) {
io.to(socket.userID).emit("importantHeartbeatList", monitorID, list, overwrite);
} else {
socket.emit("importantHeartbeatList", monitorID, list, overwrite);
}
}
module.exports = {
sendNotificationList,
sendImportantHeartbeatList,
sendHeartbeatList,
}

View File

@@ -1,17 +1,77 @@
const fs = require("fs");
const { sleep } = require("../src/util");
const { R } = require("redbean-node");
const {
setSetting, setting,
} = require("./util-server");
const { setSetting, setting } = require("./util-server");
const { debug, sleep } = require("../src/util");
const dayjs = require("dayjs");
class Database {
static templatePath = "./db/kuma.db"
static path = "./data/kuma.db";
static latestVersion = 4;
static templatePath = "./db/kuma.db";
static dataDir;
static path;
/**
* @type {boolean}
*/
static patched = false;
/**
* For Backup only
*/
static backupPath = null;
/**
* Add patch filename in key
* Values:
* true: Add it regardless of order
* false: Do nothing
* { parents: []}: Need parents before add it
*/
static patchList = {
"patch-setting-value-type.sql": true,
"patch-improve-performance.sql": true,
}
/**
* The finally version should be 10 after merged tag feature
* @deprecated Use patchList for any new feature
*/
static latestVersion = 9;
static noReject = true;
static async connect() {
const acquireConnectionTimeout = 120 * 1000;
R.setup("sqlite", {
filename: Database.path,
useNullAsDefault: true,
acquireConnectionTimeout: acquireConnectionTimeout,
}, {
min: 1,
max: 1,
idleTimeoutMillis: 120 * 1000,
propagateCreateError: false,
acquireTimeoutMillis: acquireConnectionTimeout,
});
if (process.env.SQL_LOG === "1") {
R.debug(true);
}
// Auto map the model to a bean object
R.freeze(true)
await R.autoloadModels("./server/model");
// Change to WAL
await R.exec("PRAGMA journal_mode = WAL");
await R.exec("PRAGMA cache_size = -12000");
console.log("SQLite config:");
console.log(await R.getAll("PRAGMA journal_mode"));
console.log(await R.getAll("PRAGMA cache_size"));
}
static async patch() {
let version = parseInt(await setting("database_version"));
@@ -24,12 +84,12 @@ class Database {
if (version === this.latestVersion) {
console.info("Database no need to patch");
} else if (version > this.latestVersion) {
console.info("Warning: Database version is newer than expected");
} else {
console.info("Database patch is needed")
console.info("Backup the db")
const backupPath = "./data/kuma.db.bak" + version;
fs.copyFileSync(Database.path, backupPath);
this.backup(version);
// Try catch anything here, if gone wrong, restore the backup
try {
@@ -40,18 +100,92 @@ class Database {
console.info(`Patched ${sqlFile}`);
await setSetting("database_version", i);
}
console.log("Database Patched Successfully");
} catch (ex) {
await Database.close();
console.error("Patch db failed!!! Restoring the backup")
fs.copyFileSync(backupPath, Database.path);
console.error(ex)
this.restore();
console.error(ex)
console.error("Start Uptime-Kuma failed due to patch db failed")
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues")
process.exit(1);
}
}
await this.patch2();
}
/**
* Call it from patch() only
* @returns {Promise<void>}
*/
static async patch2() {
console.log("Database Patch 2.0 Process");
let databasePatchedFiles = await setting("databasePatchedFiles");
if (! databasePatchedFiles) {
databasePatchedFiles = {};
}
debug("Patched files:");
debug(databasePatchedFiles);
try {
for (let sqlFilename in this.patchList) {
await this.patch2Recursion(sqlFilename, databasePatchedFiles)
}
if (this.patched) {
console.log("Database Patched Successfully");
}
} catch (ex) {
await Database.close();
this.restore();
console.error(ex)
console.error("Start Uptime-Kuma failed due to patch db failed");
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
process.exit(1);
}
await setSetting("databasePatchedFiles", databasePatchedFiles);
}
/**
* Used it patch2() only
* @param sqlFilename
* @param databasePatchedFiles
*/
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
let value = this.patchList[sqlFilename];
if (! value) {
console.log(sqlFilename + " skip");
return;
}
// Check if patched
if (! databasePatchedFiles[sqlFilename]) {
console.log(sqlFilename + " is not patched");
if (value.parents) {
console.log(sqlFilename + " need parents");
for (let parentSQLFilename of value.parents) {
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
}
}
this.backup(dayjs().format("YYYYMMDDHHmmss"));
console.log(sqlFilename + " is patching");
this.patched = true;
await this.importSQLFile("./db/" + sqlFilename);
databasePatchedFiles[sqlFilename] = true;
console.log(sqlFilename + " is patched successfully");
} else {
console.log(sqlFilename + " is already patched, skip");
}
}
/**
@@ -88,6 +222,10 @@ class Database {
}
}
static getBetterSQLite3Database() {
return R.knex.client.acquireConnection();
}
/**
* Special handle, because tarn.js throw a promise reject that cannot be caught
* @returns {Promise<void>}
@@ -98,23 +236,92 @@ class Database {
};
process.addListener("unhandledRejection", listener);
console.log("Closing DB")
console.log("Closing DB");
while (true) {
Database.noReject = true;
await R.close()
await sleep(2000)
await R.close();
await sleep(2000);
if (Database.noReject) {
break;
} else {
console.log("Waiting to close the db")
console.log("Waiting to close the db");
}
}
console.log("SQLite closed")
console.log("SQLite closed");
process.removeListener("unhandledRejection", listener);
}
/**
* One backup one time in this process.
* Reset this.backupPath if you want to backup again
* @param version
*/
static backup(version) {
if (! this.backupPath) {
console.info("Backup the db")
this.backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, this.backupPath);
const shmPath = Database.path + "-shm";
if (fs.existsSync(shmPath)) {
this.backupShmPath = shmPath + ".bak" + version;
fs.copyFileSync(shmPath, this.backupShmPath);
}
const walPath = Database.path + "-wal";
if (fs.existsSync(walPath)) {
this.backupWalPath = walPath + ".bak" + version;
fs.copyFileSync(walPath, this.backupWalPath);
}
}
}
/**
*
*/
static restore() {
if (this.backupPath) {
console.error("Patch db failed!!! Restoring the backup");
const shmPath = Database.path + "-shm";
const walPath = Database.path + "-wal";
// Delete patch failed db
try {
if (fs.existsSync(Database.path)) {
fs.unlinkSync(Database.path);
}
if (fs.existsSync(shmPath)) {
fs.unlinkSync(shmPath);
}
if (fs.existsSync(walPath)) {
fs.unlinkSync(walPath);
}
} catch (e) {
console.log("Restore failed, you may need to restore the backup manually");
process.exit(1);
}
// Restore backup
fs.copyFileSync(this.backupPath, Database.path);
if (this.backupShmPath) {
fs.copyFileSync(this.backupShmPath, shmPath);
}
if (this.backupWalPath) {
fs.copyFileSync(this.backupWalPath, walPath);
}
} else {
console.log("Nothing to restore");
}
}
}
module.exports = Database;

View File

@@ -6,11 +6,12 @@ dayjs.extend(utc)
dayjs.extend(timezone)
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util");
const { tcping, ping, checkCertificate } = require("../util-server");
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification")
const version = require("../../package.json").version;
/**
* status:
@@ -45,6 +46,11 @@ class Monitor extends BeanModel {
keyword: this.keyword,
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(),
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
notificationIDList,
};
}
@@ -65,6 +71,10 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown);
}
getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json);
}
start(io) {
let previousBeat = null;
let retries = 0;
@@ -73,6 +83,10 @@ class Monitor extends BeanModel {
const beat = async () => {
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined;
if (! previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id,
@@ -99,30 +113,36 @@ class Monitor extends BeanModel {
try {
if (this.type === "http" || this.type === "keyword") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();
// Use Custom agent to disable session reuse
// https://github.com/nodejs/node/issues/3940
let res = await axios.get(this.url, {
timeout: this.interval * 1000 * 0.8,
headers: {
"User-Agent": "Uptime-Kuma",
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
maxCachedSessions: 0,
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(),
}),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
});
bean.msg = `${res.status} - ${res.statusText}`
bean.ping = dayjs().valueOf() - startTime;
// Check certificate if https is used
let certInfoStartTime = dayjs().valueOf();
if (this.getUrl()?.protocol === "https:") {
try {
await this.updateTlsInfo(checkCertificate(res));
tlsInfo = await this.updateTlsInfo(checkCertificate(res));
} catch (e) {
console.error(e.message)
if (e.message !== "No TLS certificate in response") {
console.error(e.message)
}
}
}
@@ -157,6 +177,46 @@ class Monitor extends BeanModel {
bean.ping = await ping(this.hostname);
bean.msg = ""
bean.status = UP;
} else if (this.type === "dns") {
let startTime = dayjs().valueOf();
let dnsMessage = "";
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.dns_resolve_type);
bean.ping = dayjs().valueOf() - startTime;
if (this.dns_resolve_type == "A" || this.dns_resolve_type == "AAAA" || this.dns_resolve_type == "TXT") {
dnsMessage += "Records: ";
dnsMessage += dnsRes.join(" | ");
} else if (this.dns_resolve_type == "CNAME" || this.dns_resolve_type == "PTR") {
dnsMessage = dnsRes[0];
} else if (this.dns_resolve_type == "CAA") {
dnsMessage = dnsRes[0].issue;
} else if (this.dns_resolve_type == "MX") {
dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
});
dnsMessage = dnsMessage.slice(0, -2)
} else if (this.dns_resolve_type == "NS") {
dnsMessage += "Servers: ";
dnsMessage += dnsRes.join(" | ");
} else if (this.dns_resolve_type == "SOA") {
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
} else if (this.dns_resolve_type == "SRV") {
dnsRes.forEach(record => {
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
});
dnsMessage = dnsMessage.slice(0, -2)
}
if (this.dnsLastResult !== dnsMessage) {
R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [
dnsMessage,
this.id
]);
}
bean.msg = dnsMessage;
bean.status = UP;
}
if (this.isUpsideDown()) {
@@ -223,7 +283,8 @@ class Monitor extends BeanModel {
try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
} catch (e) {
console.error("Cannot send notification to " + notification.name)
console.error("Cannot send notification to " + notification.name);
console.log(e);
}
}
}
@@ -240,22 +301,26 @@ class Monitor extends BeanModel {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
}
prometheus.update(bean)
io.to(this.user_id).emit("heartbeat", bean.toJSON());
await R.store(bean)
Monitor.sendStats(io, this.id, this.user_id)
await R.store(bean);
prometheus.update(bean, tlsInfo);
previousBeat = bean;
if (! this.isStop) {
this.heartbeatInterval = setTimeout(beat, this.interval * 1000);
}
}
beat();
this.heartbeatInterval = setInterval(beat, this.interval * 1000);
}
stop() {
clearInterval(this.heartbeatInterval)
clearTimeout(this.heartbeatInterval);
this.isStop = true;
}
/**
@@ -275,7 +340,7 @@ class Monitor extends BeanModel {
/**
* Store TLS info to database
* @param checkCertificateResult
* @returns {Promise<void>}
* @returns {Promise<object>}
*/
async updateTlsInfo(checkCertificateResult) {
let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
@@ -287,13 +352,21 @@ class Monitor extends BeanModel {
}
tls_info_bean.info_json = JSON.stringify(checkCertificateResult);
await R.store(tls_info_bean);
return checkCertificateResult;
}
static async sendStats(io, monitorID, userID) {
Monitor.sendAvgPing(24, io, monitorID, userID);
Monitor.sendUptime(24, io, monitorID, userID);
Monitor.sendUptime(24 * 30, io, monitorID, userID);
Monitor.sendCertInfo(io, monitorID, userID);
const hasClients = getTotalClientInRoom(io, userID) > 0;
if (hasClients) {
await Monitor.sendAvgPing(24, io, monitorID, userID);
await Monitor.sendUptime(24, io, monitorID, userID);
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
await Monitor.sendCertInfo(io, monitorID, userID);
} else {
debug("No clients in the room, no need to send stats");
}
}
/**
@@ -301,6 +374,8 @@ class Monitor extends BeanModel {
* @param duration : int Hours
*/
static async sendAvgPing(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger();
let avgPing = parseInt(await R.getCell(`
SELECT AVG(ping)
FROM heartbeat
@@ -311,6 +386,8 @@ class Monitor extends BeanModel {
monitorID,
]));
timeLogger.print(`[Monitor: ${monitorID}] avgPing`);
io.to(userID).emit("avgPing", monitorID, avgPing);
}
@@ -330,56 +407,61 @@ class Monitor extends BeanModel {
* @param duration : int Hours
*/
static async sendUptime(duration, io, monitorID, userID) {
let sec = duration * 3600;
const timeLogger = new TimeLogger();
let heartbeatList = await R.getAll(`
SELECT duration, time, status
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
// Handle if heartbeat duration longer than the target duration
// e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL)
let result = await R.getRow(`
SELECT
-- SUM all duration, also trim off the beat out of time window
SUM(
CASE
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
ELSE duration
END
) AS total_duration,
-- SUM all uptime duration, also trim off the beat out of time window
SUM(
CASE
WHEN (status = 1)
THEN
CASE
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
ELSE duration
END
END
) AS uptime_duration
FROM heartbeat
WHERE time > DATETIME('now', ? || ' hours')
AND monitor_id = ? `, [
-duration,
WHERE time > ?
AND monitor_id = ?
`, [
startTime, startTime, startTime, startTime, startTime,
monitorID,
]);
let downtime = 0;
let total = 0;
let uptime;
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
// Special handle for the first heartbeat only
if (heartbeatList.length === 1) {
let totalDuration = result.total_duration;
let uptimeDuration = result.uptime_duration;
let uptime = 0;
if (heartbeatList[0].status === 1) {
uptime = 1;
} else {
if (totalDuration > 0) {
uptime = uptimeDuration / totalDuration;
if (uptime < 0) {
uptime = 0;
}
} else {
for (let row of heartbeatList) {
let value = parseInt(row.duration)
let time = row.time
// Handle if heartbeat duration longer than the target duration
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
if (value > sec) {
let trim = dayjs.utc().diff(dayjs(time), "second");
value = sec - trim;
if (value < 0) {
value = 0;
}
}
total += value;
if (row.status === 0 || row.status === 2) {
downtime += value;
}
}
uptime = (total - downtime) / total;
if (uptime < 0) {
uptime = 0;
// Handle new monitor with only one beat, because the beat's duration = 0
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
console.log("here???" + status);
if (status === UP) {
uptime = 1;
}
}

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

@@ -0,0 +1,26 @@
const NotificationProvider = require("./notification-provider");
const child_process = require("child_process");
class Apprise extends NotificationProvider {
name = "apprise";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
if (output) {
if (! output.includes("ERROR")) {
return "Sent Successfully";
}
throw new Error(output)
} else {
return "No output from apprise";
}
}
}
module.exports = Apprise;

View File

@@ -0,0 +1,105 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Discord extends NotificationProvider {
name = "discord";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let discordtestdata = {
username: discordDisplayName,
content: msg,
}
await axios.post(notification.discordWebhookUrl, discordtestdata)
return okMsg;
}
let url;
if (monitorJSON["type"] === "port") {
url = monitorJSON["hostname"];
if (monitorJSON["port"]) {
url += ":" + monitorJSON["port"];
}
} else {
url = monitorJSON["url"];
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] == DOWN) {
let discorddowndata = {
username: discordDisplayName,
embeds: [{
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌",
color: 16711680,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: "Service URL",
value: url,
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Error",
value: heartbeatJSON["msg"],
},
],
}],
}
await axios.post(notification.discordWebhookUrl, discorddowndata)
return okMsg;
} else if (heartbeatJSON["status"] == UP) {
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: url.startsWith("http") ? "[Visit Service](" + url + ")" : url,
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Ping",
value: heartbeatJSON["ping"] + "ms",
},
],
}],
}
await axios.post(notification.discordWebhookUrl, discordupdata)
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Discord;

View File

@@ -0,0 +1,28 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Gotify extends NotificationProvider {
name = "gotify";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
}
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
"message": msg,
"priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma",
})
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Gotify;

View File

@@ -0,0 +1,60 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Line extends NotificationProvider {
name = "line";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
let config = {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + notification.lineChannelAccessToken
}
};
if (heartbeatJSON == null) {
let testMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "Test Successful!"
}
]
}
await axios.post(lineAPIUrl, testMessage, config)
} else if (heartbeatJSON["status"] == DOWN) {
let downMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, downMessage, config)
} else if (heartbeatJSON["status"] == UP) {
let upMessage = {
"to": notification.lineUserID,
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
}
]
}
await axios.post(lineAPIUrl, upMessage, config)
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Line;

View File

@@ -0,0 +1,48 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class LunaSea extends NotificationProvider {
name = "lunasea";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
try {
if (heartbeatJSON == null) {
let testdata = {
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(lunaseadevice, testdata)
return okMsg;
}
if (heartbeatJSON["status"] == DOWN) {
let downdata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, downdata)
return okMsg;
}
if (heartbeatJSON["status"] == UP) {
let updata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, updata)
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = LunaSea;

View File

@@ -0,0 +1,123 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Mattermost extends NotificationProvider {
name = "mattermost";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let mattermostTestData = {
username: mattermostUserName,
text: msg,
}
await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
return okMsg;
}
const mattermostChannel = notification.mattermostchannel;
const mattermostIconEmoji = notification.mattermosticonemo;
const mattermostIconUrl = notification.mattermosticonurl;
if (heartbeatJSON["status"] == DOWN) {
let mattermostdowndata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went down.",
color: "#FF0000",
title:
"❌ " +
monitorJSON["name"] +
" service went down. ❌",
title_link: monitorJSON["url"],
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
short: false,
title: "Error",
value: heartbeatJSON["msg"],
},
],
},
],
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostdowndata
);
return okMsg;
} else if (heartbeatJSON["status"] == UP) {
let mattermostupdata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went up!",
color: "#32CD32",
title:
"✅ " +
monitorJSON["name"] +
" service went up! ✅",
title_link: monitorJSON["url"],
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
short: false,
title: "Ping",
value: heartbeatJSON["ping"] + "ms",
},
],
},
],
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostupdata
);
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Mattermost;

View File

@@ -0,0 +1,36 @@
class NotificationProvider {
/**
* Notification Provider Name
* @type string
*/
name = undefined;
/**
* @param notification : BeanModel
* @param msg : string General Message
* @param monitorJSON : object Monitor details (For Up/Down only)
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
* @returns {Promise<string>} Return Successful Message
* Throw Error with fail msg
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
throw new Error("Have to override Notification.send(...)");
}
throwGeneralAxiosError(error) {
let msg = "Error: " + error + " ";
if (error.response && error.response.data) {
if (typeof error.response.data === "string") {
msg += error.response.data;
} else {
msg += JSON.stringify(error.response.data)
}
}
throw new Error(msg)
}
}
module.exports = NotificationProvider;

View File

@@ -0,0 +1,40 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Octopush extends NotificationProvider {
name = "octopush";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
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 okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Octopush;

View File

@@ -0,0 +1,50 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Pushbullet extends NotificationProvider {
name = "pushbullet";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
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"] == DOWN) {
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"] == UP) {
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) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Pushbullet;

View File

@@ -0,0 +1,49 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Pushover extends NotificationProvider {
name = "pushover";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
let pushoverlink = "https://api.pushover.net/1/messages.json"
try {
if (heartbeatJSON == null) {
let data = {
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
}
await axios.post(pushoverlink, data)
return okMsg;
}
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
}
await axios.post(pushoverlink, data)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Pushover;

View File

@@ -0,0 +1,30 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Pushy extends NotificationProvider {
name = "pushy";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
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 okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Pushy;

View File

@@ -0,0 +1,46 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class RocketChat extends NotificationProvider {
name = "rocket.chat";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
if (heartbeatJSON == null) {
let data = {
"text": "Uptime Kuma Rocket.chat testing successful.",
"channel": notification.rocketchannel,
"username": notification.rocketusername,
"icon_emoji": notification.rocketiconemo,
}
await axios.post(notification.rocketwebhookURL, data)
return okMsg;
}
const time = heartbeatJSON["time"];
let data = {
"text": "Uptime Kuma Alert",
"channel": notification.rocketchannel,
"username": notification.rocketusername,
"icon_emoji": notification.rocketiconemo,
"attachments": [
{
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
"title_link": notification.rocketbutton,
"text": "*Message*\n" + msg,
"color": "#32cd32"
}
]
}
await axios.post(notification.rocketwebhookURL, data)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = RocketChat;

View File

@@ -0,0 +1,27 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Signal extends NotificationProvider {
name = "signal";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
let data = {
"message": msg,
"number": notification.signalNumber,
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
};
let config = {};
await axios.post(notification.signalURL, data, config)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Signal;

View File

@@ -0,0 +1,70 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Slack extends NotificationProvider {
name = "slack";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
if (heartbeatJSON == null) {
let data = {
"text": "Uptime Kuma Slack testing successful.",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
}
await axios.post(notification.slackwebhookURL, data)
return okMsg;
}
const time = heartbeatJSON["time"];
let data = {
"text": "Uptime Kuma Alert",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"blocks": [{
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
},
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
},
{
"type": "mrkdwn",
"text": "*Time (UTC)*\n" + time,
}],
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
},
"value": "Uptime-Kuma",
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
},
],
}],
}
await axios.post(notification.slackwebhookURL, data)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Slack;

View File

@@ -0,0 +1,48 @@
const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider");
class SMTP extends NotificationProvider {
name = "smtp";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const config = {
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
};
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
if (notification.smtpUsername || notification.smtpPassword) {
config.auth = {
user: notification.smtpUsername,
pass: notification.smtpPassword,
};
}
let transporter = nodemailer.createTransport(config);
let bodyTextContent = msg;
if (heartbeatJSON) {
bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`;
}
// send mail with defined transport object
await transporter.sendMail({
from: notification.smtpFrom,
cc: notification.smtpCC,
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: msg,
text: bodyTextContent,
tls: {
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
},
});
return "Sent Successfully.";
}
}
module.exports = SMTP;

View File

@@ -0,0 +1,27 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Telegram extends NotificationProvider {
name = "telegram";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
params: {
chat_id: notification.telegramChatID,
text: msg,
},
})
return okMsg;
} catch (error) {
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
throw new Error(msg)
}
}
}
module.exports = Telegram;

View File

@@ -0,0 +1,44 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const FormData = require("form-data");
class Webhook extends NotificationProvider {
name = "webhook";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
msg,
};
let finalData;
let config = {};
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append("data", JSON.stringify(data));
config = {
headers: finalData.getHeaders(),
}
} else {
finalData = data;
}
await axios.post(notification.webhookURL, finalData, config)
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error)
}
}
}
module.exports = Webhook;

View File

@@ -1,277 +1,75 @@
const axios = require("axios");
const { R } = require("redbean-node");
const FormData = require("form-data");
const nodemailer = require("nodemailer");
const child_process = require("child_process");
const Apprise = require("./notification-providers/apprise");
const Discord = require("./notification-providers/discord");
const Gotify = require("./notification-providers/gotify");
const Line = require("./notification-providers/line");
const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost");
const Octopush = require("./notification-providers/octopush");
const Pushbullet = require("./notification-providers/pushbullet");
const Pushover = require("./notification-providers/pushover");
const Pushy = require("./notification-providers/pushy");
const RocketChat = require("./notification-providers/rocket-chat");
const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack");
const SMTP = require("./notification-providers/smtp");
const Telegram = require("./notification-providers/telegram");
const Webhook = require("./notification-providers/webhook");
class Notification {
providerList = {};
static init() {
console.log("Prepare Notification Providers");
this.providerList = {};
const list = [
new Apprise(),
new Discord(),
new Gotify(),
new Line(),
new LunaSea(),
new Mattermost(),
new Octopush(),
new Pushbullet(),
new Pushover(),
new Pushy(),
new RocketChat(),
new Signal(),
new Slack(),
new SMTP(),
new Telegram(),
new Webhook(),
];
for (let item of list) {
if (! item.name) {
throw new Error("Notification provider without name");
}
if (this.providerList[item.name]) {
throw new Error("Duplicate notification provider name");
}
this.providerList[item.name] = item;
}
}
/**
*
* @param notification
* @param msg
* @param monitorJSON
* @param heartbeatJSON
* @param notification : BeanModel
* @param msg : string General Message
* @param monitorJSON : object Monitor details (For Up/Down only)
* @param heartbeatJSON : object Heartbeat details (For Up/Down only)
* @returns {Promise<string>} Successful msg
* Throw Error with fail msg
*/
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully. ";
if (notification.type === "telegram") {
try {
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
params: {
chat_id: notification.telegramChatID,
text: msg,
},
})
return okMsg;
} catch (error) {
let msg = (error.response.data.description) ? error.response.data.description : "Error without description"
throw new Error(msg)
}
} else if (notification.type === "gotify") {
try {
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
}
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, {
"message": msg,
"priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma",
})
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "webhook") {
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
msg,
};
let finalData;
let config = {};
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append("data", JSON.stringify(data));
config = {
headers: finalData.getHeaders(),
}
} else {
finalData = data;
}
await axios.post(notification.webhookURL, finalData, config)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "smtp") {
return await Notification.smtp(notification, msg)
} else if (notification.type === "discord") {
try {
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let data = {
username: "Uptime-Kuma",
content: msg,
}
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] == 0) {
var alertColor = "16711680";
} else if (heartbeatJSON["status"] == 1) {
var alertColor = "65280";
}
let data = {
username: "Uptime-Kuma",
embeds: [{
title: "Uptime-Kuma Alert",
color: alertColor,
fields: [
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
name: "Message",
value: msg,
},
],
}],
}
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "signal") {
try {
let data = {
"message": msg,
"number": notification.signalNumber,
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
};
let config = {};
await axios.post(notification.signalURL, data, config)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "slack") {
try {
if (heartbeatJSON == null) {
let data = {
"text": "Uptime Kuma Slack testing successful.",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
}
await axios.post(notification.slackwebhookURL, data)
return okMsg;
}
const time = heartbeatJSON["time"];
let data = {
"text": "Uptime Kuma Alert",
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"blocks": [{
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
},
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
},
{
"type": "mrkdwn",
"text": "*Time (UTC)*\n" + time,
}],
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
},
"value": "Uptime-Kuma",
"url": notification.slackbutton || "https://github.com/louislam/uptime-kuma",
},
],
}],
}
await axios.post(notification.slackwebhookURL, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "pushover") {
let pushoverlink = "https://api.pushover.net/1/messages.json"
try {
if (heartbeatJSON == null) {
let data = {
"message": "<b>Uptime Kuma Pushover testing successful.</b>",
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
}
await axios.post(pushoverlink, data)
return okMsg;
}
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"],
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,
"priority": notification.pushoverpriority,
"title": notification.pushovertitle,
"retry": "30",
"expire": "3600",
"html": 1,
}
await axios.post(pushoverlink, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
}
} else if (notification.type === "apprise") {
return Notification.apprise(notification, msg)
} else if (notification.type === "lunasea") {
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice
try {
if (heartbeatJSON == null) {
let testdata = {
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
}
await axios.post(lunaseadevice, testdata)
return okMsg;
}
if (heartbeatJSON["status"] == 0) {
let downdata = {
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[🔴 Down]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, downdata)
return okMsg;
}
if (heartbeatJSON["status"] == 1) {
let updata = {
"title": "UptimeKuma Alert:" + monitorJSON["name"],
"body": "[✅ Up]" + heartbeatJSON["msg"] + "\nTime (UTC):" + heartbeatJSON["time"],
}
await axios.post(lunaseadevice, updata)
return okMsg;
}
} catch (error) {
throwGeneralAxiosError(error)
}
if (this.providerList[notification.type]) {
return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON);
} else {
throw new Error("Notification type is not supported")
throw new Error("Notification type is not supported");
}
}
@@ -294,8 +92,15 @@ class Notification {
bean.name = notification.name;
bean.user_id = userID;
bean.config = JSON.stringify(notification)
bean.config = JSON.stringify(notification);
bean.is_default = notification.isDefault || false;
await R.store(bean)
if (notification.applyExisting) {
await applyNotificationEveryMonitor(bean.id, userID);
}
return bean;
}
static async delete(notificationID, userID) {
@@ -311,46 +116,6 @@ class Notification {
await R.trash(bean)
}
static async smtp(notification, msg) {
let transporter = nodemailer.createTransport({
host: notification.smtpHost,
port: notification.smtpPort,
secure: notification.smtpSecure,
auth: {
user: notification.smtpUsername,
pass: notification.smtpPassword,
},
});
// send mail with defined transport object
await transporter.sendMail({
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
to: notification.smtpTo,
subject: msg,
text: msg,
});
return "Sent Successfully.";
}
static async apprise(notification, msg) {
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL])
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
if (output) {
if (! output.includes("ERROR")) {
return "Sent Successfully";
}
throw new Error(output)
} else {
return ""
}
}
static checkApprise() {
let commandExistsSync = require("command-exists").sync;
let exists = commandExistsSync("apprise");
@@ -359,18 +124,24 @@ class Notification {
}
function throwGeneralAxiosError(error) {
let msg = "Error: " + error + " ";
async function applyNotificationEveryMonitor(notificationID, userID) {
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
userID
]);
if (error.response && error.response.data) {
if (typeof error.response.data === "string") {
msg += error.response.data;
} else {
msg += JSON.stringify(error.response.data)
for (let i = 0; i < monitors.length; i++) {
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [
monitors[i].id,
notificationID,
])
if (! checkNotification) {
let relation = R.dispense("monitor_notification");
relation.monitor_id = monitors[i].id;
relation.notification_id = notificationID;
await R.store(relation)
}
}
throw new Error(msg)
}
module.exports = {

View File

@@ -1,5 +1,5 @@
const passwordHashOld = require("password-hash");
const bcrypt = require("bcrypt");
const bcrypt = require("bcryptjs");
const saltRounds = 10;
exports.generate = function (password) {

View File

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

View File

@@ -1,22 +1,33 @@
const PrometheusClient = require('prom-client');
const PrometheusClient = require("prom-client");
const commonLabels = [
'monitor_name',
'monitor_type',
'monitor_url',
'monitor_hostname',
'monitor_port',
"monitor_name",
"monitor_type",
"monitor_url",
"monitor_hostname",
"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({
name: 'monitor_response_time',
help: 'Monitor Response Time (ms)',
name: "monitor_response_time",
help: "Monitor Response Time (ms)",
labelNames: commonLabels
});
const monitor_status = new PrometheusClient.Gauge({
name: 'monitor_status',
help: 'Monitor Status (1 = UP, 0= DOWN)',
name: "monitor_status",
help: "Monitor Status (1 = UP, 0= DOWN)",
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 {
monitor_status.set(this.monitorLabelValues, heartbeat.status)
} catch (e) {
@@ -41,7 +72,7 @@ class Prometheus {
}
try {
if (typeof heartbeat.ping === 'number') {
if (typeof heartbeat.ping === "number") {
monitor_response_time.set(this.monitorLabelValues, heartbeat.ping)
} else {
// Is it good?

View File

@@ -1,18 +1,18 @@
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, TimeLogger, getRandomInt } = require("../src/util");
console.log("Importing Node libraries")
const fs = require("fs");
const http = require("http");
const https = require("https");
console.log("Importing 3rd-party libraries")
debug("Importing express");
const express = require("express");
debug("Importing socket.io");
const { Server } = require("socket.io");
debug("Importing dayjs");
const dayjs = require("dayjs");
debug("Importing redbean-node");
const { R } = require("redbean-node");
debug("Importing jsonwebtoken");
@@ -26,9 +26,12 @@ console.log("Importing this project modules");
debug("Importing Monitor");
const Monitor = require("./model/monitor");
debug("Importing Settings");
const { getSettings, setSettings, setting } = require("./util-server");
const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server");
debug("Importing Notification");
const { Notification } = require("./notification");
Notification.init();
debug("Importing Database");
const Database = require("./database");
@@ -38,17 +41,56 @@ const passwordHash = require("./password-hash");
const args = require("args-parser")(process.argv);
const version = require("../package.json").version;
const hostname = process.env.HOST || args.host || "0.0.0.0"
const checkVersion = require("./check-version");
console.info("Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
const hostname = process.env.HOST || args.host;
const port = parseInt(process.env.PORT || args.port || 3001);
console.info("Version: " + version)
// SSL
const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
// Demo Mode?
const demoMode = args["demo"] || false;
if (demoMode) {
console.log("==== Demo Mode ====");
}
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
Database.path = Database.dataDir + "kuma.db";
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });
}
console.log(`Data Dir: ${Database.dataDir}`);
console.log("Creating express and socket.io instance")
const app = express();
const server = http.createServer(app);
let server;
if (sslKey && sslCert) {
console.log("Server Type: HTTPS");
server = https.createServer({
key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert)
}, app);
} else {
console.log("Server Type: HTTP");
server = http.createServer(app);
}
const io = new Server(server);
app.use(express.json())
module.exports.io = io;
// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList } = require("./client");
app.use(express.json());
/**
* Total WebSocket client connected to server currently, no actual use
@@ -87,24 +129,35 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
// 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
// Prometheus API metrics /metrics
// 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
app.get("*", function(request, response, next) {
response.end(indexHTML)
app.get("*", async (_request, response) => {
response.send(indexHTML);
});
console.log("Adding socket handler")
io.on("connection", async (socket) => {
socket.emit("info", {
version,
version: checkVersion.version,
latestVersion: checkVersion.latestVersion,
})
totalClient++;
@@ -114,11 +167,6 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
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", () => {
totalClient--;
});
@@ -139,7 +187,11 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
])
if (user) {
await afterLogin(socket, user)
debug("afterLogin")
afterLogin(socket, user)
debug("afterLogin ok")
callback({
ok: true,
@@ -165,7 +217,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let user = await login(data.username, data.password)
if (user) {
await afterLogin(socket, user)
afterLogin(socket, user)
callback({
ok: true,
@@ -231,6 +283,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
bean.import(monitor)
bean.user_id = socket.userID
await R.store(bean)
@@ -275,6 +330,10 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls;
bean.upsideDown = monitor.upsideDown;
bean.maxredirects = monitor.maxredirects;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
bean.dns_resolve_type = monitor.dns_resolve_type;
bean.dns_resolve_server = monitor.dns_resolve_server;
await R.store(bean)
@@ -409,10 +468,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
if (user && passwordHash.verify(password.currentPassword, user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password.newPassword),
socket.userID,
]);
user.resetPassword(password.newPassword);
callback({
ok: true,
@@ -471,12 +527,13 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
try {
checkLogin(socket)
await Notification.save(notification, notificationID, socket.userID)
let notificationBean = await Notification.save(notification, notificationID, socket.userID)
await sendNotificationList(socket)
callback({
ok: true,
msg: "Saved",
id: notificationBean.id,
});
} catch (e) {
@@ -536,18 +593,191 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
callback(false);
}
});
socket.on("uploadBackup", async (uploadedJSON, callback) => {
try {
checkLogin(socket)
let backupData = JSON.parse(uploadedJSON);
console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`)
let notificationList = backupData.notificationList;
let monitorList = backupData.monitorList;
if (notificationList.length >= 1) {
for (let i = 0; i < notificationList.length; i++) {
let notification = JSON.parse(notificationList[i].config);
await Notification.save(notification, null, socket.userID)
}
}
if (monitorList.length >= 1) {
for (let i = 0; i < monitorList.length; i++) {
let monitor = {
name: monitorList[i].name,
type: monitorList[i].type,
url: monitorList[i].url,
interval: monitorList[i].interval,
hostname: monitorList[i].hostname,
maxretries: monitorList[i].maxretries,
port: monitorList[i].port,
keyword: monitorList[i].keyword,
ignoreTls: monitorList[i].ignoreTls,
upsideDown: monitorList[i].upsideDown,
maxredirects: monitorList[i].maxredirects,
accepted_statuscodes: monitorList[i].accepted_statuscodes,
dns_resolve_type: monitorList[i].dns_resolve_type,
dns_resolve_server: monitorList[i].dns_resolve_server,
notificationIDList: {},
}
let bean = R.dispense("monitor")
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
bean.import(monitor)
bean.user_id = socket.userID
await R.store(bean)
await updateMonitorNotification(bean.id, notificationIDList)
if (monitorList[i].active == 1) {
await startMonitor(socket.userID, bean.id);
} else {
await pauseMonitor(socket.userID, bean.id);
}
}
await sendNotificationList(socket)
await sendMonitorList(socket);
}
callback({
ok: true,
msg: "Backup successfully restored.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("clearEvents", async (monitorID, callback) => {
try {
checkLogin(socket)
console.log(`Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`)
await R.exec("UPDATE heartbeat SET msg = ?, important = ? WHERE monitor_id = ? ", [
"",
"0",
monitorID,
]);
await sendImportantHeartbeatList(socket, monitorID, true, true);
callback({
ok: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("clearHeartbeats", async (monitorID, callback) => {
try {
checkLogin(socket)
console.log(`Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`)
await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [
monitorID
]);
await sendHeartbeatList(socket, monitorID, true, true);
callback({
ok: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("clearStatistics", async (callback) => {
try {
checkLogin(socket)
console.log(`Clear Statistics User ID: ${socket.userID}`)
await R.exec("DELETE FROM heartbeat");
callback({
ok: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
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")
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, () => {
console.log(`Listening on ${hostname}:${port}`);
if (hostname) {
console.log(`Listening on ${hostname}:${port}`);
} else {
console.log(`Listening on ${port}`);
}
startMonitors();
checkVersion.startInterval();
});
})();
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,
])
@@ -578,41 +808,32 @@ async function sendMonitorList(socket) {
return list;
}
async function sendNotificationList(socket) {
let result = [];
let list = await R.find("notification", " user_id = ? ", [
socket.userID,
]);
for (let bean of list) {
result.push(bean.export())
}
io.to(socket.userID).emit("notificationList", result)
return list;
}
async function afterLogin(socket, user) {
socket.userID = user.id;
socket.join(user.id)
let monitorList = await sendMonitorList(socket)
for (let monitorID in monitorList) {
sendHeartbeatList(socket, monitorID);
sendImportantHeartbeatList(socket, monitorID);
Monitor.sendStats(io, monitorID, user.id)
}
sendNotificationList(socket)
socket.emit("autoLogin")
await sleep(500);
for (let monitorID in monitorList) {
await sendHeartbeatList(socket, monitorID);
}
for (let monitorID in monitorList) {
await sendImportantHeartbeatList(socket, monitorID);
}
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id)
}
}
async function getMonitorJSONList(userID) {
let result = {};
let monitorList = await R.find("monitor", " user_id = ? ", [
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
])
@@ -636,32 +857,22 @@ async function initDatabase() {
}
console.log("Connecting to Database")
R.setup("sqlite", {
filename: Database.path,
});
await Database.connect();
console.log("Connected")
// Patch the database
await Database.patch()
// Auto map the model to a bean object
R.freeze(true)
await R.autoloadModels("./server/model");
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret",
]);
if (! jwtSecretBean) {
console.log("JWT secret is not found, generate one.")
jwtSecretBean = R.dispense("setting")
jwtSecretBean.key = "jwtSecret"
jwtSecretBean.value = passwordHash.generate(dayjs() + "")
await R.store(jwtSecretBean)
console.log("Stored JWT secret into database")
console.log("JWT secret is not found, generate one.");
jwtSecretBean = await initJWTSecret();
console.log("Stored JWT secret into database");
} 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
@@ -721,43 +932,14 @@ async function startMonitors() {
let list = await R.find("monitor", " active = 1 ")
for (let monitor of list) {
monitor.start(io)
monitorList[monitor.id] = monitor;
}
}
/**
* Send Heartbeat History list to socket
*/
async function sendHeartbeatList(socket, monitorID) {
let list = await R.find("heartbeat", `
monitor_id = ?
ORDER BY time DESC
LIMIT 100
`, [
monitorID,
])
let result = [];
for (let bean of list) {
result.unshift(bean.toJSON())
for (let monitor of list) {
monitor.start(io);
// Give some delays, so all monitors won't make request at the same moment when just start the server.
await sleep(getRandomInt(300, 1000));
}
socket.emit("heartbeatList", monitorID, result)
}
async function sendImportantHeartbeatList(socket, monitorID) {
let list = await R.find("heartbeat", `
monitor_id = ?
AND important = 1
ORDER BY time DESC
LIMIT 500
`, [
monitorID,
])
socket.emit("importantHeartbeatList", monitorID, list)
}
async function shutdownFunction(signal) {
@@ -771,11 +953,10 @@ async function shutdownFunction(signal) {
}
await sleep(2000);
await Database.close();
console.log("Stopped DB")
}
function finalFunction() {
console.log("Graceful Shutdown Done")
console.log("Graceful shutdown successfully!");
}
gracefulShutdown(server, {
@@ -786,3 +967,9 @@ gracefulShutdown(server, {
onShutdown: shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ...
finally: finalFunction, // finally function (sync) - e.g. for logging
});
// Catch unexpected errors here
process.addListener("unhandledRejection", (error, promise) => {
console.trace(error);
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
});

View File

@@ -2,6 +2,28 @@ const tcpp = require("tcp-ping");
const Ping = require("./ping-lite");
const { R } = require("redbean-node");
const { debug } = require("../src/util");
const passwordHash = require("./password-hash");
const dayjs = require("dayjs");
const { Resolver } = require("dns");
/**
* 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) {
return new Promise((resolve, reject) => {
@@ -9,7 +31,7 @@ exports.tcping = function (hostname, port) {
address: hostname,
port: port,
attempts: 1,
}, function(err, data) {
}, function (err, data) {
if (err) {
reject(err);
@@ -24,15 +46,30 @@ exports.tcping = function (hostname, port) {
});
}
exports.ping = function (hostname) {
return new Promise((resolve, reject) => {
const ping = new Ping(hostname);
exports.ping = async (hostname) => {
try {
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) {
reject(err)
reject(err);
} else if (ms === null) {
reject(new Error("timeout"))
reject(new Error(stdout))
} else {
resolve(Math.round(ms))
}
@@ -40,6 +77,30 @@ exports.ping = function (hostname) {
});
}
exports.dnsResolve = function (hostname, resolver_server, rrtype) {
const resolver = new Resolver();
resolver.setServers([resolver_server]);
return new Promise((resolve, reject) => {
if (rrtype == "PTR") {
resolver.reverse(hostname, (err, records) => {
if (err) {
reject(err);
} else {
resolve(records);
}
});
} else {
resolver.resolve(hostname, rrtype, (err, records) => {
if (err) {
reject(err);
} else {
resolve(records);
}
});
}
})
}
exports.setting = async function (key) {
let value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [
key,
@@ -58,7 +119,7 @@ exports.setSetting = async function (key, value) {
let bean = await R.findOne("setting", " `key` = ? ", [
key,
])
if (! bean) {
if (!bean) {
bean = R.dispense("setting")
bean.key = key;
}
@@ -158,3 +219,55 @@ exports.checkCertificate = function (res) {
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;
}
exports.getTotalClientInRoom = (io, roomName) => {
const sockets = io.sockets;
if (! sockets) {
return 0;
}
const adapter = sockets.adapter;
if (! adapter) {
return 0;
}
const room = adapter.rooms.get(roomName);
if (room) {
return room.size;
} else {
return 0;
}
}

View File

@@ -2,12 +2,48 @@
@import "node_modules/bootstrap/scss/bootstrap";
#app {
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, 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 {
overflow: hidden;
box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
//overflow: hidden; // Forget why add this, but multiple select hide by this
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
padding: 10px;
border-radius: 10px;
@@ -29,10 +65,226 @@
background-color: $highlight;
border-color: $highlight;
}
.dark & {
color: $dark-font-color2;
}
}
.modal-content {
border-radius: 1rem;
backdrop-filter: blur(3px);
.btn-warning {
color: white;
&:hover, &:active, &:focus, &.active {
color: white;
}
}
.btn-info {
color: white;
&:hover, &:active, &:focus, &.active {
color: white;
}
}
@media (max-width: 550px) {
.table-shadow-box {
padding: 10px !important;
thead {
display: none;
}
tbody {
.shadow-box {
background-color: white;
}
}
tr {
margin-top: 0 !important;
padding: 4px 10px !important;
display: block;
margin-bottom: 6px;
td:first-child {
font-weight: bold;
}
td:nth-child(-n+3) {
text-align: center;
}
td:last-child {
text-align: left;
}
td {
border-bottom: 1px solid $dark-font-color;
display: block;
padding: 4px;
.badge {
margin: auto;
display: block;
width: 30%;
}
}
}
}
}
// Dark Theme override here
.dark {
background-color: #090c10;
color: $dark-font-color;
&::-webkit-scrollbar-thumb, ::-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: #232f3b;
}
a,
.table,
.nav-link {
color: $dark-font-color;
&.btn-info {
color: white;
}
}
.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-warning {
color: $dark-font-color2;
&:hover, &:active, &:focus, &.active {
color: $dark-font-color2;
}
}
.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;
}
@media (max-width: 550px) {
.table-shadow-box {
tbody {
.shadow-box {
background-color: $dark-bg2;
td {
border-bottom: 1px solid $dark-border-color;
}
}
}
}
}
}
/*
* Transitions
*/
// page-change
.slide-fade-enter-active {
transition: all 0.2s $easing-in;
}
.slide-fade-leave-active {
transition: all 0.2s $easing-in;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(50px);
opacity: 0;
}

View File

@@ -1,8 +1,20 @@
$primary: #5CDD8B;
$danger: #DC3545;
$primary: #5cdd8b;
$danger: #dc3545;
$warning: #f8a306;
$link-color: #111;
$border-radius: 50rem;
$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;
$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97);
$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86);
$dropdown-border-radius: 0.5rem;

View File

@@ -4,7 +4,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title">
Confirm
{{ $t("Confirm") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
@@ -35,7 +35,7 @@ export default {
},
yesText: {
type: String,
default: "Yes",
default: "Yes", // TODO: No idea what to translate this
},
noText: {
type: String,

View File

@@ -22,15 +22,11 @@ export default {
computed: {
displayText() {
if (this.value !== undefined && this.value !== "") {
let format = "YYYY-MM-DD HH:mm:ss";
if (this.dateOnly) {
format = "YYYY-MM-DD";
}
return dayjs.utc(this.value).tz(this.$root.timezone).format(format);
if (this.dateOnly) {
return this.$root.date(this.value);
} else {
return this.$root.datetime(this.value);
}
return "";
},
},
}

View File

@@ -7,7 +7,7 @@
class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
:style="beatStyle"
:title="beat.msg"
:title="getBeatTitle(beat)"
/>
</div>
</div>
@@ -21,14 +21,17 @@ export default {
type: String,
default: "big",
},
monitorId: Number,
monitorId: {
type: Number,
required: true,
},
},
data() {
return {
beatWidth: 10,
beatHeight: 30,
hoverScale: 1.5,
beatMargin: 3, // Odd number only, even = blurry
beatMargin: 4,
move: false,
maxBeat: -1,
}
@@ -36,14 +39,15 @@ export default {
computed: {
beatList() {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
return this.$root.heartbeatList[this.monitorId]
},
shortBeatList() {
let placeholders = []
if (! this.beatList) {
return [];
}
let placeholders = [];
let start = this.beatList.length - this.maxBeat;
@@ -113,11 +117,30 @@ export default {
unmounted() {
window.removeEventListener("resize", this.resize);
},
beforeMount() {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
},
mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
this.beatMargin = 2.4;
this.beatHeight = 16
this.beatWidth = 5;
this.beatHeight = 16;
this.beatMargin = 2;
}
// Suddenly, have an idea how to handle it universally.
// If the pixel * ratio != Integer, then it causes render issue, round it to solve it!!
const actualWidth = this.beatWidth * window.devicePixelRatio;
const actualMargin = this.beatMargin * window.devicePixelRatio;
if (! Number.isInteger(actualWidth)) {
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
}
if (! Number.isInteger(actualMargin)) {
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
}
window.addEventListener("resize", this.resize);
@@ -129,11 +152,15 @@ export default {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
}
},
getBeatTitle(beat) {
return `${this.$root.datetime(beat.time)} - ${beat.msg}`;
}
},
}
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
@import "../assets/vars.scss";
.wrap {
@@ -168,4 +195,10 @@ export default {
}
}
.dark {
.hp-bar-big .beat.empty {
background-color: #848484;
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div class="input-group mb-3">
<input
ref="input"
v-model="model"
:type="visibility"
class="form-control"
:placeholder="placeholder"
:maxlength="maxlength"
:autocomplete="autocomplete"
:required="required"
:readonly="readonly"
>
<a v-if="visibility == 'password'" class="btn btn-outline-primary" @click="showInput()">
<font-awesome-icon icon="eye" />
</a>
<a v-if="visibility == 'text'" class="btn btn-outline-primary" @click="hideInput()">
<font-awesome-icon icon="eye-slash" />
</a>
</div>
</template>
<script>
export default {
props: {
modelValue: {
type: String,
default: ""
},
placeholder: {
type: String,
default: ""
},
maxlength: {
type: Number,
default: 255
},
autocomplete: {
type: String,
default: undefined,
},
required: {
type: Boolean
},
readonly: {
type: String,
default: undefined,
},
},
data() {
return {
visibility: "password",
}
},
computed: {
model: {
get() {
return this.modelValue
},
set(value) {
this.$emit("update:modelValue", value)
}
}
},
created() {
},
methods: {
showInput() {
this.visibility = "text";
},
hideInput() {
this.visibility = "password";
},
}
}
</script>

View File

@@ -6,12 +6,12 @@
<div class="form-floating">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<label for="floatingInput">Username</label>
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
<div class="form-floating mt-3">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<label for="floatingPassword">Password</label>
<label for="floatingPassword">{{ $t("Password") }}</label>
</div>
<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
@@ -19,12 +19,12 @@
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
<label class="form-check-label" for="remember">
Remember me
{{ $t("Remember me") }}
</label>
</div>
</div>
<button class="w-100 btn btn-primary" type="submit" :disabled="processing">
Login
{{ $t("Login") }}
</button>
<div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">

View File

@@ -0,0 +1,143 @@
<template>
<div class="shadow-box list mb-3" :class="{ scrollbar: scrollbar }">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div>
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
<div class="row">
<div class="col-6 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info">
<Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }}
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
</router-link>
</div>
</template>
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Uptime from "../components/Uptime.vue";
export default {
components: {
Uptime,
HeartbeatBar,
},
props: {
scrollbar: {
type: Boolean,
},
},
computed: {
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.name.localeCompare(m2.name);
})
return result;
},
},
methods: {
monitorURL(id) {
return "/dashboard/" + id;
},
},
}
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.small-padding {
padding-left: 5px !important;
padding-right: 5px !important;
}
.list {
&.scrollbar {
min-height: calc(100vh - 240px);
max-height: calc(100vh - 30px);
overflow-y: auto;
position: sticky;
top: 10px;
}
.item {
display: block;
text-decoration: none;
padding: 13px 15px 10px 15px;
border-radius: 10px;
transition: all ease-in-out 0.15s;
&.disabled {
opacity: 0.3;
}
.info {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
background-color: $highlight-white;
}
&.active {
background-color: #cdf8f4;
}
}
}
.dark {
.list {
.item {
&:hover {
background-color: $dark-bg2;
}
&.active {
background-color: $dark-bg2;
}
}
}
}
.monitorItem {
width: 100%;
}
</style>

View File

@@ -5,70 +5,41 @@
<div class="modal-content">
<div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title">
Setup Notification
{{ $t("Setup Notification") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="mb-3">
<label for="type" class="form-label">Notification Type</label>
<select id="type" v-model="notification.type" class="form-select">
<label for="notification-type" class="form-label">{{ $t("Notification Type") }}</label>
<select id="notification-type" v-model="notification.type" class="form-select">
<option value="telegram">Telegram</option>
<option value="webhook">Webhook</option>
<option value="smtp">Email (SMTP)</option>
<option value="smtp">{{ $t("Email") }} (SMTP)</option>
<option value="discord">Discord</option>
<option value="signal">Signal</option>
<option value="gotify">Gotify</option>
<option value="slack">Slack</option>
<option value="rocket.chat">Rocket.chat</option>
<option value="pushover">Pushover</option>
<option value="pushy">Pushy</option>
<option value="octopush">Octopush</option>
<option value="lunasea">LunaSea</option>
<option value="apprise">Apprise (Support 50+ Notification services)</option>
<option value="pushbullet">Pushbullet</option>
<option value="line">Line Messenger</option>
<option value="mattermost">Mattermost</option>
</select>
</div>
<div class="mb-3">
<label for="name" class="form-label">Friendly Name</label>
<input id="name" v-model="notification.name" type="text" class="form-control" required>
<label for="notification-name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="notification-name" v-model="notification.name" type="text" class="form-control" required>
</div>
<template v-if="notification.type === 'telegram'">
<div class="mb-3">
<label for="telegram-bot-token" class="form-label">Bot Token</label>
<input id="telegram-bot-token" v-model="notification.telegramBotToken" type="text" class="form-control" required>
<div class="form-text">
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
</div>
</div>
<Telegram v-if="notification.type === 'telegram'" />
<div class="mb-3">
<label for="telegram-chat-id" class="form-label">Chat ID</label>
<div class="input-group mb-3">
<input id="telegram-chat-id" v-model="notification.telegramChatID" type="text" class="form-control" required>
<button v-if="notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
Auto Get
</button>
</div>
<div class="form-text">
Support Direct Chat / Group / Channel's Chat ID
<p style="margin-top: 8px;">
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
</p>
<p style="margin-top: 8px;">
<template v-if="notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
</template>
<template v-else>
{{ telegramGetUpdatesURL }}
</template>
</p>
</div>
</div>
</template>
<!-- TODO: Convert all into vue components, but not an easy task. -->
<template v-if="notification.type === 'webhook'">
<div class="mb-3">
@@ -94,49 +65,7 @@
</div>
</template>
<template v-if="notification.type === 'smtp'">
<div class="mb-3">
<label for="hostname" class="form-label">Hostname</label>
<input id="hostname" v-model="notification.smtpHost" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="port" class="form-label">Port</label>
<input id="port" v-model="notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
<div class="mb-3">
<div class="form-check">
<input id="secure" v-model="notification.smtpSecure" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="secure">
Secure
</label>
</div>
<div class="form-text">
Generally, true for 465, false for other ports.
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input id="username" v-model="notification.smtpUsername" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input id="password" v-model="notification.smtpPassword" type="password" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="from-email" class="form-label">From Email</label>
<input id="from-email" v-model="notification.smtpFrom" type="email" class="form-control" required autocomplete="false">
</div>
<div class="mb-3">
<label for="to-email" class="form-label">To Email</label>
<input id="to-email" v-model="notification.smtpTo" type="email" class="form-control" required autocomplete="false">
</div>
</template>
<SMTP v-if="notification.type === 'smtp'" />
<template v-if="notification.type === 'discord'">
<div class="mb-3">
@@ -146,6 +75,11 @@
You can get this by going to Server Settings -> Integrations -> Create Webhook
</div>
</div>
<div class="mb-3">
<label for="discord-username" class="form-label">Bot Display Name</label>
<input id="discord-username" v-model="notification.discordUsername" type="text" class="form-control" autocomplete="false" :placeholder="$root.appName">
</div>
</template>
<template v-if="notification.type === 'signal'">
@@ -184,7 +118,7 @@
<template v-if="notification.type === 'gotify'">
<div class="mb-3">
<label for="gotify-application-token" class="form-label">Application Token</label>
<input id="gotify-application-token" v-model="notification.gotifyapplicationToken" type="text" class="form-control" required>
<HiddenInput id="gotify-application-token" v-model="notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="gotify-server-url" class="form-label">Server URL</label>
@@ -201,7 +135,7 @@
<template v-if="notification.type === 'slack'">
<div class="mb-3">
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
<label for="slack-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
<input id="slack-webhook-url" v-model="notification.slackwebhookURL" type="text" class="form-control" required>
<label for="slack-username" class="form-label">Username</label>
<input id="slack-username" v-model="notification.slackusername" type="text" class="form-control">
@@ -212,7 +146,7 @@
<label for="slack-button-url" class="form-label">Uptime Kuma URL</label>
<input id="slack-button" v-model="notification.slackbutton" type="text" class="form-control">
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://api.slack.com/messaging/webhooks" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p>
@@ -229,12 +163,123 @@
</div>
</template>
<template v-if="notification.type === 'rocket.chat'">
<div class="mb-3">
<label for="rocket-webhook-url" class="form-label">Webhook URL<span style="color: red;"><sup>*</sup></span></label>
<input id="rocket-webhook-url" v-model="notification.rocketwebhookURL" type="text" class="form-control" required>
<label for="rocket-username" class="form-label">Username</label>
<input id="rocket-username" v-model="notification.rocketusername" type="text" class="form-control">
<label for="rocket-iconemo" class="form-label">Icon Emoji</label>
<input id="rocket-iconemo" v-model="notification.rocketiconemo" type="text" class="form-control">
<label for="rocket-channel" class="form-label">Channel Name</label>
<input id="rocket-channel-name" v-model="notification.rocketchannel" type="text" class="form-control">
<label for="rocket-button-url" class="form-label">Uptime Kuma URL</label>
<input id="rocket-button" v-model="notification.rocketbutton" type="text" class="form-control">
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://docs.rocket.chat/guides/administration/administration/integrations" target="_blank">https://api.slack.com/messaging/webhooks</a>
</p>
<p style="margin-top: 8px;">
Enter the channel name on Rocket.chat Channel Name field if you want to bypass the webhook channel. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'mattermost'">
<div class="mb-3">
<label for="mattermost-webhook-url" class="form-label">Webhook URL<span style="color:red;"><sup>*</sup></span></label>
<input id="mattermost-webhook-url" v-model="notification.mattermostWebhookUrl" type="text" class="form-control" required>
<label for="mattermost-username" class="form-label">Username</label>
<input id="mattermost-username" v-model="notification.mattermostusername" type="text" class="form-control">
<label for="mattermost-iconurl" class="form-label">Icon URL</label>
<input id="mattermost-iconurl" v-model="notification.mattermosticonurl" type="text" class="form-control">
<label for="mattermost-iconemo" class="form-label">Icon Emoji</label>
<input id="mattermost-iconemo" v-model="notification.mattermosticonemo" type="text" class="form-control">
<label for="mattermost-channel" class="form-label">Channel Name</label>
<input id="mattermost-channel-name" v-model="notification.mattermostchannel" type="text" class="form-control">
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info about webhooks on: <a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
</p>
<p style="margin-top: 8px;">
You can override the default channel that webhook posts to by entering the channel name into "Channel Name" field. This needs to be enabled in Mattermost webhook settings. Ex: #other-channel
</p>
<p style="margin-top: 8px;">
If you leave the Uptime Kuma URL field blank, it will default to the Project Github page.
</p>
<p style="margin-top: 8px;">
You can provide a link to a picture in "Icon URL" to override the default profile picture. Will not be used if Icon Emoji is set.
</p>
<p style="margin-top: 8px;">
Emoji cheat sheet: <a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a> Note: emoji takes preference over Icon URL.
</p>
</div>
</div>
</template>
<template v-if="notification.type === 'pushy'">
<div class="mb-3">
<label for="pushy-app-token" class="form-label">API_KEY</label>
<HiddenInput id="pushy-app-token" v-model="notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="pushy-user-key" class="form-label">USER_TOKEN</label>
<div class="input-group mb-3">
<HiddenInput id="pushy-user-key" v-model="notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</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>
<HiddenInput id="octopush-key" v-model="notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<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'">
<div class="mb-3">
<label for="pushover-user" class="form-label">User Key<span style="color:red;"><sup>*</sup></span></label>
<input id="pushover-user" v-model="notification.pushoveruserkey" type="text" class="form-control" required>
<label for="pushover-app-token" class="form-label">Application Token<span style="color:red;"><sup>*</sup></span></label>
<input id="pushover-app-token" v-model="notification.pushoverapptoken" type="text" class="form-control" required>
<label for="pushover-user" class="form-label">User Key<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-user" v-model="notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="pushover-app-token" class="form-label">Application Token<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-app-token" v-model="notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
<label for="pushover-device" class="form-label">Device</label>
<input id="pushover-device" v-model="notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">Message Title</label>
@@ -273,7 +318,7 @@
<option>none</option>
</select>
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>Required
<span style="color: red;"><sup>*</sup></span>Required
<p style="margin-top: 8px;">
More info on: <a href="https://pushover.net/api" target="_blank">https://pushover.net/api</a>
</p>
@@ -302,31 +347,84 @@
<p>
Status:
<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>
</div>
</template>
<template v-if="notification.type === 'lunasea'">
<div class="mb-3">
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color:red;"><sup>*</sup></span></label>
<label for="lunasea-device" class="form-label">LunaSea Device ID<span style="color: red;"><sup>*</sup></span></label>
<input id="lunasea-device" v-model="notification.lunaseaDevice" type="text" class="form-control" required>
<div class="form-text">
<p><span style="color:red;"><sup>*</sup></span>Required</p>
<p><span style="color: red;"><sup>*</sup></span>Required</p>
</div>
</div>
</template>
<template v-if="notification.type === 'pushbullet'">
<div class="mb-3">
<label for="pushbullet-access-token" class="form-label">Access Token</label>
<HiddenInput id="pushbullet-access-token" v-model="notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<p style="margin-top: 8px;">
More info on: <a href="https://docs.pushbullet.com" target="_blank">https://docs.pushbullet.com</a>
</p>
</template>
<template v-if="notification.type === 'line'">
<div class="mb-3">
<label for="line-channel-access-token" class="form-label">Channel access token</label>
<HiddenInput id="line-channel-access-token" v-model="notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="form-text">
Line Developers Console - <b>Basic Settings</b>
</div>
<div class="mb-3" style="margin-top: 12px;">
<label for="line-user-id" class="form-label">User ID</label>
<input id="line-user-id" v-model="notification.lineUserID" type="text" class="form-control" required>
</div>
<div class="form-text">
Line Developers Console - <b>Messaging API</b>
</div>
<div class="form-text" style="margin-top: 8px;">
First access the <a href="https://developers.line.biz/console/" target="_blank">Line Developers Console</a>, create a provider and channel (Messaging API), then you can get the channel access token and user id from the above mentioned menu items.
</div>
</template>
<!-- DEPRECATED! Please create vue component in "./src/components/notifications/{notification name}.vue" -->
<div class="mb-3 mt-4">
<hr class="dropdown-divider mb-4">
<div class="form-check form-switch">
<input v-model="notification.isDefault" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("Default enabled") }}</label>
</div>
<div class="form-text">
{{ $t("enableDefaultNotificationDescription") }}
</div>
<br>
<div class="form-check form-switch">
<input v-model="notification.applyExisting" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("Also apply to existing monitors") }}</label>
</div>
</div>
</div>
<div class="modal-footer">
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
Delete
{{ $t("Delete") }}
</button>
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
Test
{{ $t("Test") }}
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
Save
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Save") }}
</button>
</div>
</div>
@@ -334,24 +432,29 @@
</div>
</form>
<Confirm ref="confirmDelete" btn-style="btn-danger" @yes="deleteNotification">
Are you sure want to delete this notification for all monitors?
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteNotification">
{{ $t("deleteNotificationMsg") }}
</Confirm>
</template>
<script lang="ts">
import { Modal } from "bootstrap"
import { ucfirst } from "../util.ts"
import axios from "axios";
import { useToast } from "vue-toastification"
import Confirm from "./Confirm.vue";
const toast = useToast()
import HiddenInput from "./HiddenInput.vue";
import Telegram from "./notifications/Telegram.vue";
import SMTP from "./notifications/SMTP.vue";
export default {
components: {
Confirm,
HiddenInput,
Telegram,
SMTP,
},
props: {},
emits: ["added"],
data() {
return {
model: null,
@@ -360,22 +463,13 @@ export default {
notification: {
name: "",
type: null,
gotifyPriority: 8,
isDefault: false,
// Do not set default value here, please scroll to show()
},
appriseInstalled: false,
}
},
computed: {
telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>"
if (this.notification.telegramBotToken) {
token = this.notification.telegramBotToken;
}
return `https://api.telegram.org/bot${token}/getUpdates`;
},
},
watch: {
"notification.type"(to, from) {
let oldName;
@@ -420,11 +514,13 @@ export default {
this.notification = {
name: "",
type: null,
isDefault: false,
}
// Default set to Telegram
this.notification.type = "telegram"
this.notification.gotifyPriority = 8
// Set Default value here
this.notification.type = "telegram";
this.notification.gotifyPriority = 8;
this.notification.smtpSecure = false;
}
this.modal.show()
@@ -437,7 +533,13 @@ export default {
this.processing = false;
if (res.ok) {
this.modal.hide()
this.modal.hide();
// Emit added event, doesn't emit edit.
if (! this.id) {
this.$emit("added", res.id);
}
}
})
},
@@ -461,32 +563,16 @@ export default {
}
})
},
async autoGetTelegramChatID() {
try {
let res = await axios.get(this.telegramGetUpdatesURL)
if (res.data.result.length >= 1) {
let update = res.data.result[res.data.result.length - 1]
if (update.channel_post) {
this.notification.telegramChatID = update.channel_post.chat.id;
} else if (update.message) {
this.notification.telegramChatID = update.message.chat.id;
} else {
throw new Error("Chat ID is not found, please send a message to this bot first")
}
} else {
throw new Error("Chat ID is not found, please send a message to this bot first")
}
} catch (error) {
toast.error(error.message)
}
},
},
}
</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,176 @@
<template>
<LineChart :chart-data="chartData" :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 {
// Configurable filtering on top of the returned data
chartPeriodHrs: 6,
};
},
computed: {
chartOptions() {
return {
responsive: true,
maintainAspectRatio: false,
onResize: (chart) => {
chart.canvas.parentNode.style.position = "relative";
if (screen.width < 576) {
chart.canvas.parentNode.style.height = "275px";
} else if (screen.width < 768) {
chart.canvas.parentNode.style.height = "320px";
} else if (screen.width < 992) {
chart.canvas.parentNode.style.height = "300px";
} else {
chart.canvas.parentNode.style.height = "250px";
}
},
layout: {
padding: {
left: 10,
right: 30,
top: 30,
bottom: 10,
},
},
elements: {
point: {
// Hide points on chart unless mouse-over
radius: 0,
hitRadius: 100,
},
},
scales: {
x: {
type: "time",
time: {
minUnit: "minute",
round: "second",
tooltipFormat: "YYYY-MM-DD HH:mm:ss",
displayFormats: {
minute: "HH:mm",
hour: "MM-DD HH:mm",
}
},
ticks: {
maxRotation: 0,
autoSkipPadding: 30,
},
grid: {
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
offset: false,
},
},
y: {
title: {
display: true,
text: this.$t("respTime"),
},
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,
backgroundColor: this.$root.theme === "light" ? "rgba(212,232,222,1.0)" : "rgba(32,42,38,1.0)",
bodyColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)",
titleColor: this.$root.theme === "light" ? "rgba(12,12,18,1.0)" : "rgba(220,220,220,1.0)",
filter: function (tooltipItem) {
return tooltipItem.datasetIndex === 0; // Hide tooltip on Bar Chart
},
callbacks: {
label: (context) => {
return ` ${new Intl.NumberFormat().format(context.parsed.y)} ms`
},
}
},
legend: {
display: false,
},
},
}
},
chartData() {
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down, 0 if target is up
if (this.monitorId in this.$root.heartbeatList) {
this.$root.heartbeatList[this.monitorId]
.filter(
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(dayjs().subtract(this.chartPeriodHrs, "hours")))
.map((beat) => {
const x = this.$root.datetime(beat.time);
pingData.push({
x,
y: beat.ping,
});
downData.push({
x,
y: beat.status === 0 ? 1 : 0,
})
});
}
return {
datasets: [
{
// Line Chart
data: pingData,
fill: "origin",
tension: 0.2,
borderColor: "#5CDD8B",
backgroundColor: "#5CDD8B38",
yAxisID: "y",
},
{
// Bar Chart
type: "bar",
data: downData,
borderColor: "#00000000",
backgroundColor: "#DC354568",
yAxisID: "y1",
barThickness: "flex",
barPercentage: 1,
categoryPercentage: 1,
},
],
};
},
},
};
</script>

View File

@@ -27,18 +27,18 @@ export default {
text() {
if (this.status === 0) {
return "Down"
return this.$t("Down");
}
if (this.status === 1) {
return "Up"
return this.$t("Up");
}
if (this.status === 2) {
return "Pending"
return this.$t("Pending");
}
return "Unknown"
return this.$t("Unknown");
},
},
}

View File

@@ -22,7 +22,7 @@ export default {
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
}
return "N/A"
return this.$t("notAvailableShort")
},
color() {

View File

@@ -0,0 +1,75 @@
<template>
<div class="mb-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
<div class="mb-3">
<label for="secure" class="form-label">Secure</label>
<select id="secure" v-model="$parent.notification.smtpSecure" class="form-select">
<option :value="false">None / STARTTLS (25, 587)</option>
<option :value="true">TLS (465)</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input id="ignore-tls-error" v-model="$parent.notification.smtpIgnoreTLSError" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls-error">
Ignore TLS Error
</label>
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">{{ $t("Username") }}</label>
<input id="username" v-model="$parent.notification.smtpUsername" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ $t("Password") }}</label>
<HiddenInput id="password" v-model="$parent.notification.smtpPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
</div>
<div class="mb-3">
<label for="from-email" class="form-label">From Email</label>
<input id="from-email" v-model="$parent.notification.smtpFrom" type="text" class="form-control" required autocomplete="false" placeholder="&quot;Uptime Kuma&quot; &lt;example@kuma.pet&gt;">
<div class="form-text">
</div>
</div>
<div class="mb-3">
<label for="to-email" class="form-label">To Email</label>
<input id="to-email" v-model="$parent.notification.smtpTo" type="text" class="form-control" required autocomplete="false" placeholder="example2@kuma.pet, example3@kuma.pet">
</div>
<div class="mb-3">
<label for="to-cc" class="form-label">CC</label>
<input id="to-cc" v-model="$parent.notification.smtpCC" type="text" class="form-control" autocomplete="false">
</div>
<div class="mb-3">
<label for="to-bcc" class="form-label">BCC</label>
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false">
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
data() {
return {
name: "smtp",
}
},
}
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div class="mb-3">
<label for="telegram-bot-token" class="form-label">Bot Token</label>
<HiddenInput id="telegram-bot-token" v-model="$parent.notification.telegramBotToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<div class="form-text">
You can get a token from <a href="https://t.me/BotFather" target="_blank">https://t.me/BotFather</a>.
</div>
</div>
<div class="mb-3">
<label for="telegram-chat-id" class="form-label">Chat ID</label>
<div class="input-group mb-3">
<input id="telegram-chat-id" v-model="$parent.notification.telegramChatID" type="text" class="form-control" required>
<button v-if="$parent.notification.telegramBotToken" class="btn btn-outline-secondary" type="button" @click="autoGetTelegramChatID">
{{ $t("Auto Get") }}
</button>
</div>
<div class="form-text">
Support Direct Chat / Group / Channel's Chat ID
<p style="margin-top: 8px;">
You can get your chat id by sending message to the bot and go to this url to view the chat_id:
</p>
<p style="margin-top: 8px;">
<template v-if="$parent.notification.telegramBotToken">
<a :href="telegramGetUpdatesURL" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL }}</a>
</template>
<template v-else>
{{ telegramGetUpdatesURL }}
</template>
</p>
</div>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
import axios from "axios";
import { useToast } from "vue-toastification"
const toast = useToast();
export default {
components: {
HiddenInput,
},
data() {
return {
name: "telegram",
}
},
computed: {
telegramGetUpdatesURL() {
let token = "<YOUR BOT TOKEN HERE>"
if (this.$parent.notification.telegramBotToken) {
token = this.$parent.notification.telegramBotToken;
}
return `https://api.telegram.org/bot${token}/getUpdates`;
},
},
mounted() {
},
methods: {
async autoGetTelegramChatID() {
try {
let res = await axios.get(this.telegramGetUpdatesURL)
if (res.data.result.length >= 1) {
let update = res.data.result[res.data.result.length - 1]
if (update.channel_post) {
this.notification.telegramChatID = update.channel_post.chat.id;
} else if (update.message) {
this.notification.telegramChatID = update.message.chat.id;
} else {
throw new Error("Chat ID is not found, please send a message to this bot first")
}
} else {
throw new Error("Chat ID is not found, please send a message to this bot first")
}
} catch (error) {
toast.error(error.message)
}
},
}
}
</script>

View File

@@ -1,10 +1,10 @@
import { library } from "@fortawesome/fontawesome-svg-core"
import { faCog, faEdit, faList, faPause, faPlay, faPlus, faTachometerAlt, faTrash } from "@fortawesome/free-solid-svg-icons"
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"
//import { fa } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
// Add Free Font Awesome Icons here
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
library.add(faCog, faTachometerAlt, faEdit, faPlus, faPause, faPlay, faTrash, faList)
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash);
export { FontAwesomeIcon }

18
src/languages/README.md Normal file
View File

@@ -0,0 +1,18 @@
# How to translate
1. Fork this repo.
2. Create a language file. (e.g. `zh-TW.js`) The filename must be ISO language code: http://www.lingoes.net/en/translator/langcode.htm
3. `npm run update-language-files --base-lang=de-DE`
6. Your language file should be filled in. You can translate now.
7. Translate `src/pages/Settings.vue` (search for a `Confirm` component with `rel="confirmDisableAuth"`).
8. Import your language file in `src/main.js` and add it to `languageList` constant.
9. Make a [pull request](https://github.com/louislam/uptime-kuma/pulls) when you have done.
One of good examples:
https://github.com/louislam/uptime-kuma/pull/316/files
If you do not have programming skills, let me know in [Issues section](https://github.com/louislam/uptime-kuma/issues). I will assist you. 😏

131
src/languages/da-DK.js Normal file
View File

@@ -0,0 +1,131 @@
export default {
languageName: "Danish",
Settings: "Indstillinger",
Dashboard: "Dashboard",
"New Update": "Opdatering tilgængelig",
Language: "Sprog",
Appearance: "Udseende",
Theme: "Tema",
General: "Generelt",
Version: "Version",
"Check Update On GitHub": "Tjek efter opdateringer på Github",
List: "Liste",
Add: "Tilføj",
"Add New Monitor": "Tilføj ny Overvåger",
"Quick Stats": "Oversigt",
Up: "Aktiv",
Down: "Inaktiv",
Pending: "Afventer",
Unknown: "Ukendt",
Pause: "Pause",
pauseDashboardHome: "Pauset",
Name: "Navn",
Status: "Status",
DateTime: "Dato / Tid",
Message: "Beskeder",
"No important events": "Inden vigtige begivenheder",
Resume: "Fortsæt",
Edit: "Rediger",
Delete: "Slet",
Current: "Aktuelt",
Uptime: "Oppetid",
"Cert Exp.": "Certifikatets udløb",
days: "Dage",
day: "Dag",
"-day": "-Dage",
hour: "Timer",
"-hour": "-Timer",
checkEverySecond: "Tjek hvert {0} sekund",
"Avg.": "Gennemsnit",
Response: " Respons",
Ping: "Ping",
"Monitor Type": "Overvåger Type",
Keyword: "Nøgleord",
"Friendly Name": "Visningsnavn",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Taktinterval",
Retries: "Gentagelser",
retriesDescription: "Maksimalt antal gentagelser, før tjenesten markeres som inaktiv og sender en meddelelse.",
Advanced: "Avanceret",
ignoreTLSError: "Ignorere TLS/SSL web fejl",
"Upside Down Mode": "Omvendt tilstand",
upsideDownModeDescription: "Håndter tilstanden omvendt. Hvis tjenesten er tilgængelig, vises den som inaktiv.",
"Max. Redirects": "Maks. Omdirigeringer",
maxRedirectDescription: "Maksimalt antal omdirigeringer, der skal følges. Indstil til 0 for at deaktivere omdirigeringer.",
"Accepted Status Codes": "Tilladte HTTP-Statuskoder",
acceptedStatusCodesDescription: "Vælg de statuskoder, der stadig skal vurderes som vellykkede.",
Save: "Gem",
Notifications: "Underretninger",
"Not available, please setup.": "Ikke tilgængelige, opsæt venligst.",
"Setup Notification": "Opsæt underretninger",
Light: "Lys",
Dark: "Mørk",
Auto: "Auto",
"Theme - Heartbeat Bar": "Tema - Tidslinje",
Normal: "Normal",
Bottom: "Bunden",
None: "Ingen",
Timezone: "Tidszone",
"Search Engine Visibility": "Søgemaskine synlighed",
"Allow indexing": "Tillad indeksering",
"Discourage search engines from indexing site": "Frabed søgemaskiner at indeksere webstedet",
"Change Password": "Ændre adgangskode",
"Current Password": "Nuværende adgangskode",
"New Password": "Ny adgangskode",
"Repeat New Password": "Gentag den nye adgangskode",
passwordNotMatchMsg: "Adgangskoderne er ikke ens.",
"Update Password": "Opdater adgangskode",
"Disable Auth": "Deaktiver autentificering",
"Enable Auth": "Aktiver autentificering",
Logout: "Log ud",
notificationDescription: "Tildel underretninger til Overvåger(e), så denne funktion træder i kraft.",
Leave: "Verlassen",
"I understand, please disable": "Jeg er indforstået, deaktiver venligst",
Confirm: "Bekræft",
Yes: "Ja",
No: "Nej",
Username: "Brugernavn",
Password: "Adgangskode",
"Remember me": "Husk mig",
Login: "Log ind",
"No Monitors, please": "Ingen Overvågere",
"add one": "tilføj en",
"Notification Type": "Underretningstype",
Email: "E-Mail",
Test: "Test",
"Certificate Info": "Certifikatoplysninger",
keywordDescription: "Søg efter et søgeord i almindelig HTML- eller JSON -output. Bemærk, at der skelnes mellem store og små bogstaver.",
deleteMonitorMsg: "Er du sikker på, at du vil slette overvågeren?",
deleteNotificationMsg: "Er du sikker på, at du vil slette denne underretning for alle overvågere? ",
resoverserverDescription: "Cloudflare er standardserveren, den kan til enhver tid ændres.",
"Resolver Server": "Navne-server",
rrtypeDescription: "Vælg den type RR, du vil overvåge.",
"Last Result": "Seneste resultat",
pauseMonitorMsg: "Er du sikker på, at du vil pause Overvågeren?",
"Create your admin account": "Opret din administratorkonto",
"Repeat Password": "Gentag adgangskoden",
"Resource Record Type": "Resource Record Type",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

132
src/languages/de-DE.js Normal file
View File

@@ -0,0 +1,132 @@
export default {
languageName: "German",
Settings: "Einstellungen",
Dashboard: "Dashboard",
"New Update": "Update Verfügbar",
Language: "Sprache",
Appearance: "Erscheinung",
Theme: "Thema",
General: "Allgemein",
Version: "Version",
"Check Update On GitHub": "Überprüfen von Updates auf Github",
List: "Liste",
Add: "Hinzufügen",
"Add New Monitor": "Neuer Monitor",
"Quick Stats": "Übersicht",
Up: "Aktiv",
Down: "Inaktiv",
Pending: "Ausstehend",
Unknown: "Unbekannt",
Pause: "Pausieren",
pauseDashboardHome: "Pausiert",
Name: "Name",
Status: "Status",
DateTime: "Datum / Uhrzeit",
Message: "Nachricht",
"No important events": "Keine wichtigen Ereignisse",
Resume: "Fortsetzen",
Edit: "Bearbeiten",
Delete: "Löschen",
Current: "Aktuell",
Uptime: "Verfügbarkeit",
"Cert Exp.": "Zertifikatsablauf",
days: "Tage",
day: "Tag",
"-day": "-Tage",
hour: "Stunde",
"-hour": "-Stunden",
checkEverySecond: "Überprüfe alle {0} Sekunden",
"Avg.": "Durchschn. ",
Response: " Antwortzeit",
Ping: "Ping",
"Monitor Type": "Monitor Typ",
Keyword: "Schlüsselwort",
"Friendly Name": "Anzeigename",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Taktintervall",
Retries: "Wiederholungen",
retriesDescription: "Maximale Anzahl von Wiederholungen, bevor der Dienst als inaktiv markiert und eine Benachrichtigung gesendet wird.",
Advanced: "Erweitert",
ignoreTLSError: "Ignoriere TLS/SSL Fehler von Webseiten",
"Upside Down Mode": "Umgedrehter Modus",
upsideDownModeDescription: "Drehe den Modus um, ist der Dienst erreichbar, wird er als Inaktiv angezeigt.",
"Max. Redirects": "Max. Weiterleitungen",
maxRedirectDescription: "Maximale Anzahl von Weiterleitungen, denen gefolgt werden soll. Setzte auf 0, um Weiterleitungen zu deaktivieren.",
"Accepted Status Codes": "Erlaubte HTTP-Statuscodes",
acceptedStatusCodesDescription: "Wähle die Statuscodes aus, welche trotzdem als erfolgreich gewertet werden sollen.",
Save: "Speichern",
Notifications: "Benachrichtigungen",
"Not available, please setup.": "Keine verfügbar, bitte einrichten.",
"Setup Notification": "Benachrichtigung einrichten",
Light: "Hell",
Dark: "Dunkel",
Auto: "Auto",
"Theme - Heartbeat Bar": "Thema - Taktleiste",
Normal: "Normal",
Bottom: "Unten",
None: "Keine",
Timezone: "Zeitzone",
"Search Engine Visibility": "Suchmaschinensichtbarkeit",
"Allow indexing": "Indizierung zulassen",
"Discourage search engines from indexing site": "Halte Suchmaschinen von der Indexierung der Seite ab",
"Change Password": "Passwort ändern",
"Current Password": "Dezeitiges Passwort",
"New Password": "Neues Passwort",
"Repeat New Password": "Wiederhole neues Passwort",
passwordNotMatchMsg: "Passwörter stimmen nicht überein. ",
"Update Password": "Ändere Passwort",
"Disable Auth": "Authentifizierung deaktivieren",
"Enable Auth": "Authentifizierung aktivieren",
Logout: "Ausloggen",
notificationDescription: "Weise den Monitor(en) eine Benachrichtigung zu, damit diese Funktion greift.",
Leave: "Verlassen",
"I understand, please disable": "Ich verstehe, bitte deaktivieren",
Confirm: "Bestätige",
Yes: "Ja",
No: "Nein",
Username: "Benutzername",
Password: "Passwort",
"Remember me": "Passwort merken",
Login: "Einloggen",
"No Monitors, please": "Keine Monitore, bitte",
"add one": "hinzufügen",
"Notification Type": "Benachrichtigungs Dienst",
Email: "E-Mail",
Test: "Test",
"Certificate Info": "Zertifikatsinfo",
keywordDescription: "Suche nach einem Schlüsselwort in der HTML oder JSON Ausgabe. Bitte beachte, es wird in der Groß-/Kleinschreibung unterschieden.",
deleteMonitorMsg: "Bist du sicher das du den Monitor löschen möchtest?",
deleteNotificationMsg: "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
resoverserverDescription: "Cloudflare ist als der Standardserver festgelegt, dieser kann jederzeit geändern werden.",
"Resolver Server": "Auflösungsserver",
rrtypeDescription: "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
"Last Result": "Letztes Ergebnis",
pauseMonitorMsg: "Bist du sicher das du den Monitor pausieren möchtest?",
clearEventsMsg: "Bist du sicher das du alle Ereignisse für diesen Monitor löschen möchtest?",
clearHeartbeatsMsg: "Bist du sicher das du alle Statistiken für diesen Monitor löschen möchtest?",
"Clear Data": "Lösche Daten",
Events: "Ereignisse",
Heartbeats: "Statistiken",
confirmClearStatisticsMsg: "Bist du sicher das du ALLE Statistiken löschen möchtest?",
"Create your admin account": "Erstelle dein Admin Konto",
"Repeat Password": "Wiederhole das Passwort",
"Resource Record Type": "Resource Record Type",
"Import/Export Backup": "Import/Export Backup",
"Export": "Export",
"Import": "Import",
respTime: "Antw. Zeit (ms)",
notAvailableShort: "N/A",
"Default enabled": "Standardmäßig aktiviert",
"Also apply to existing monitors": "Auch für alle existierenden Monitore aktivieren",
enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.",
Create: "Erstellen",
"Auto Get": "Auto Get",
backupDescription: "Es können alle Monitore und Benachrichtigungen in einer JSON-Datei gesichert werden.",
backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.",
backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.",
alertNoFile: "Bitte wähle eine Datei zum importieren aus.",
alertWrongFileType: "Bitte wähle eine JSON Datei aus.",
"Clear all statistics": "Lösche alle Statistiken"
}

132
src/languages/en.js Normal file
View File

@@ -0,0 +1,132 @@
export default {
languageName: "English",
checkEverySecond: "Check every {0} seconds.",
"Avg.": "Avg. ",
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
maxRedirectDescription: "Maximum number of redirects to follow. Set to 0 to disable redirects.",
acceptedStatusCodesDescription: "Select status codes which are considered as a successful response.",
passwordNotMatchMsg: "The repeat password does not match.",
notificationDescription: "Please assign a notification to monitor(s) to get it to work.",
keywordDescription: "Search keyword in plain html or JSON response and it is case-sensitive",
pauseDashboardHome: "Pause",
deleteMonitorMsg: "Are you sure want to delete this monitor?",
deleteNotificationMsg: "Are you sure want to delete this notification for all monitors?",
resoverserverDescription: "Cloudflare is the default server, you can change the resolver server anytime.",
rrtypeDescription: "Select the RR-Type you want to monitor",
pauseMonitorMsg: "Are you sure want to pause?",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
Settings: "Settings",
Dashboard: "Dashboard",
"New Update": "New Update",
Language: "Language",
Appearance: "Appearance",
Theme: "Theme",
General: "General",
Version: "Version",
"Check Update On GitHub": "Check Update On GitHub",
List: "List",
Add: "Add",
"Add New Monitor": "Add New Monitor",
"Quick Stats": "Quick Stats",
Up: "Up",
Down: "Down",
Pending: "Pending",
Unknown: "Unknown",
Pause: "Pause",
Name: "Name",
Status: "Status",
DateTime: "DateTime",
Message: "Message",
"No important events": "No important events",
Resume: "Resume",
Edit: "Edit",
Delete: "Delete",
Current: "Current",
Uptime: "Uptime",
"Cert Exp.": "Cert Exp.",
days: "days",
day: "day",
"-day": "-day",
hour: "hour",
"-hour": "-hour",
Response: "Response",
Ping: "Ping",
"Monitor Type": "Monitor Type",
Keyword: "Keyword",
"Friendly Name": "Friendly Name",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Heartbeat Interval",
Retries: "Retries",
Advanced: "Advanced",
"Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "Max. Redirects",
"Accepted Status Codes": "Accepted Status Codes",
Save: "Save",
Notifications: "Notifications",
"Not available, please setup.": "Not available, please setup.",
"Setup Notification": "Setup Notification",
Light: "Light",
Dark: "Dark",
Auto: "Auto",
"Theme - Heartbeat Bar": "Theme - Heartbeat Bar",
Normal: "Normal",
Bottom: "Bottom",
None: "None",
Timezone: "Timezone",
"Search Engine Visibility": "Search Engine Visibility",
"Allow indexing": "Allow indexing",
"Discourage search engines from indexing site": "Discourage search engines from indexing site",
"Change Password": "Change Password",
"Current Password": "Current Password",
"New Password": "New Password",
"Repeat New Password": "Repeat New Password",
"Update Password": "Update Password",
"Disable Auth": "Disable Auth",
"Enable Auth": "Enable Auth",
Logout: "Logout",
Leave: "Leave",
"I understand, please disable": "I understand, please disable",
Confirm: "Confirm",
Yes: "Yes",
No: "No",
Username: "Username",
Password: "Password",
"Remember me": "Remember me",
Login: "Login",
"No Monitors, please": "No Monitors, please",
"add one": "add one",
"Notification Type": "Notification Type",
Email: "Email",
Test: "Test",
"Certificate Info": "Certificate Info",
"Resolver Server": "Resolver Server",
"Resource Record Type": "Resource Record Type",
"Last Result": "Last Result",
"Create your admin account": "Create your admin account",
"Repeat Password": "Repeat Password",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
respTime: "Resp. Time (ms)",
notAvailableShort: "N/A",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Create: "Create",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
"Clear all statistics": "Clear all Statistics"
}

131
src/languages/es-ES.js Normal file
View File

@@ -0,0 +1,131 @@
export default {
languageName: "Español",
checkEverySecond: "Comprobar cada {0} segundos.",
"Avg.": "Media. ",
retriesDescription: "Número máximo de intentos antes de que el servicio se marque como CAÍDO y una notificación sea enviada.",
ignoreTLSError: "Ignorar error TLS/SSL para sitios web HTTPS",
upsideDownModeDescription: "Invertir el estado. Si el servicio es alcanzable, está CAÍDO.",
maxRedirectDescription: "Número máximo de direcciones a seguir. Establecer a 0 para deshabilitar.",
acceptedStatusCodesDescription: "Seleccionar los códigos de estado que se consideran como respuesta exitosa.",
passwordNotMatchMsg: "La contraseña repetida no coincide.",
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).",
keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas",
pauseDashboardHome: "Pausar",
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
resoverserverDescription: "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
rrtypeDescription: "Selecciona el tipo de registro que quieres monitorizar",
pauseMonitorMsg: "¿Seguro que quieres pausar?",
Settings: "Ajustes",
Dashboard: "Panel",
"New Update": "Vueva actualización",
Language: "Idioma",
Appearance: "Apariencia",
Theme: "Tema",
General: "General",
Version: "Versión",
"Check Update On GitHub": "Comprobar actualizaciones en GitHub",
List: "Lista",
Add: "Añadir",
"Add New Monitor": "Añadir nuevo monitor",
"Quick Stats": "Estadísticas rápidas",
Up: "Funcional",
Down: "Caído",
Pending: "Pendiente",
Unknown: "Desconociso",
Pause: "Pausa",
Name: "Nombre",
Status: "Estado",
DateTime: "Fecha y Hora",
Message: "Mensaje",
"No important events": "No hay eventos importantes",
Resume: "Reanudar",
Edit: "Editar",
Delete: "Eliminar",
Current: "Actual",
Uptime: "Tiempo activo",
"Cert Exp.": "Caducidad cert.",
days: "días",
day: "día",
"-day": "-día",
hour: "hora",
"-hour": "-hora",
Response: "Respuesta",
Ping: "Ping",
"Monitor Type": "Tipo de Monitor",
Keyword: "Palabra clave",
"Friendly Name": "Nombre sencillo",
URL: "URL",
Hostname: "Nombre del host",
Port: "Puerto",
"Heartbeat Interval": "Intervalo de latido",
Retries: "Reintentos",
Advanced: "Avanzado",
"Upside Down Mode": "Modo invertido",
"Max. Redirects": "Máx. redirecciones",
"Accepted Status Codes": "Códigos de estado aceptados",
Save: "Guardar",
Notifications: "Notificaciones",
"Not available, please setup.": "No disponible, por favor configurar.",
"Setup Notification": "Configurar notificación",
Light: "Claro",
Dark: "Oscuro",
Auto: "Auto",
"Theme - Heartbeat Bar": "Tema - Barra de intervalo de latido",
Normal: "Normal",
Bottom: "Abajo",
None: "Ninguno",
Timezone: "Zona horaria",
"Search Engine Visibility": "Visibilidad motor de búsqueda",
"Allow indexing": "Permitir indexación",
"Discourage search engines from indexing site": "Disuadir a los motores de búsqueda de indexar el sitio",
"Change Password": "Cambiar contraseña",
"Current Password": "Contraseña actual",
"New Password": "Nueva contraseña",
"Repeat New Password": "Repetir nueva contraseña",
"Update Password": "Actualizar contraseña",
"Disable Auth": "Deshabilitar Autenticación ",
"Enable Auth": "Habilitar Autenticación ",
Logout: "Cerrar sesión",
Leave: "Salir",
"I understand, please disable": "Lo comprendo, por favor deshabilitar",
Confirm: "Confirmar",
Yes: "Sí",
No: "No",
Username: "Usuario",
Password: "Contraseña",
"Remember me": "Recordarme",
Login: "Acceso",
"No Monitors, please": "Sin monitores, por favor",
"add one": "añade uno",
"Notification Type": "Tipo de notificación",
Email: "Email",
Test: "Test",
"Certificate Info": "Información del certificado ",
"Resolver Server": "Servidor de resolución",
"Resource Record Type": "Tipo de Registro",
"Last Result": "Último resultado",
"Create your admin account": "Crea tu cuenta de administrador",
"Repeat Password": "Repetir contraseña",
respTime: "Tiempo de resp. (ms)",
notAvailableShort: "N/A",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

131
src/languages/et-EE.js Normal file
View File

@@ -0,0 +1,131 @@
export default {
languageName: "eesti",
checkEverySecond: "Kontrolli {0} sekundilise vahega.",
"Avg.": "≈ ",
retriesDescription: "Mitu korda tuleb kontrollida, mille järel märkida 'maas' ja saata välja teavitus.",
ignoreTLSError: "Eira TLS/SSL viga HTTPS veebisaitidel.",
upsideDownModeDescription: "Käitle teenuse saadavust rikkena, teenuse kättesaamatust töötavaks.",
maxRedirectDescription: "Suurim arv ümbersuunamisi, millele järgida. 0 ei luba ühtegi ",
acceptedStatusCodesDescription: "Vali välja HTTP koodid, mida arvestada kõlblikuks.",
passwordNotMatchMsg: "Salasõnad ei kattu.",
notificationDescription: "Teavitusmeetodi kasutamiseks seo see seirega.",
keywordDescription: "Jälgi võtmesõna HTML või JSON vastustes. (tõstutundlik)",
pauseDashboardHome: "Seiskamine",
deleteMonitorMsg: "Kas soovid eemaldada seire?",
deleteNotificationMsg: "Kas soovid eemaldada selle teavitusmeetodi kõikidelt seiretelt?",
resoverserverDescription: "Cloudflare on vaikimisi pöördserver.",
rrtypeDescription: "Vali kirje tüüp, mida soovid jälgida.",
pauseMonitorMsg: "Kas soovid peatada seire?",
Settings: "Seaded",
Dashboard: "Töölaud",
"New Update": "Uuem tarkvara versioon on saadaval.",
Language: "Keel",
Appearance: "Välimus",
Theme: "Teema",
General: "Üldine",
Version: "Versioon",
"Check Update On GitHub": "Otsi uuendusi GitHub'ist",
List: "Nimekiri",
Add: "Lisa",
"Add New Monitor": "Seire lisamine",
"Quick Stats": "Ülevaade",
Up: "Töökorras",
Down: "Rikkis",
Pending: "Määramisel",
Unknown: "Teadmata",
Pause: "Seiskamine",
Name: "Nimi",
Status: "Olek",
DateTime: "Kuupäev",
Message: "Tulemus",
"No important events": "Märkimisväärsed juhtumid puuduvad.",
Resume: "Taasta",
Edit: "Muutmine",
Delete: "Eemalda",
Current: "Hetkeseisund",
Uptime: "Eluiga",
"Cert Exp.": "Sert. aegumine",
days: "päeva",
day: "päev",
"-day": "-päev",
hour: "tund",
"-hour": "-tund",
Response: "Vastus",
Ping: "Ping",
"Monitor Type": "Seire tüüp",
Keyword: "Võtmesõna",
"Friendly Name": "Sõbralik nimi",
URL: "URL",
Hostname: "Hostname",
Port: "Port",
"Heartbeat Interval": "Tukse sagedus",
Retries: "Korduskatsed",
Advanced: "Rohkem",
"Upside Down Mode": "Tagurpidi seire",
"Max. Redirects": "Max. ümbersuunamine",
"Accepted Status Codes": "Kõlblikud HTTP koodid",
Save: "Salvesta",
Notifications: "Teavitused",
"Not available, please setup.": "Ühtegi teavitusteenust pole saadaval.",
"Setup Notification": "Lisa teavitusteenus",
Light: "hele",
Dark: "tume",
Auto: "automaatne",
"Theme - Heartbeat Bar": "Teemasäte — tuksete riba",
Normal: "tavaline",
Bottom: "all",
None: "puudub",
Timezone: "Ajatsoon",
"Search Engine Visibility": "Otsimootorite ligipääs",
"Allow indexing": "Luba indekseerimine",
"Discourage search engines from indexing site": "Keela selle saidi indekseerimine otsimootorite poolt",
"Change Password": "Muuda parooli",
"Current Password": "praegune parool",
"New Password": "uus parool",
"Repeat New Password": "korda salasõna",
"Update Password": "Uuenda salasõna",
"Disable Auth": "Lülita autentimine välja",
"Enable Auth": "Lülita autentimine sisse",
Logout: "Logi välja",
Leave: "Lahku",
"I understand, please disable": "Olen tutvunud riskidega, lülita välja",
Confirm: "Kinnita",
Yes: "Jah",
No: "Ei",
Username: "kasutajanimi",
Password: "parool",
"Remember me": "Mäleta mind",
Login: "Logi sisse",
"No Monitors, please": "Seired puuduvad.",
"add one": "Lisa esimene",
"Notification Type": "Teavituse tüüp",
Email: "e-posti aadress",
Test: "Saada prooviteavitus",
"Certificate Info": "Sertifikaadi teave",
"Resolver Server": "Server, mis vastab DNS päringutele.",
"Resource Record Type": "DNS kirje tüüp",
"Last Result": "Viimane",
"Create your admin account": "Admininstraatori konto loomine",
"Repeat Password": "korda salasõna",
respTime: "Reageerimisaeg (ms)",
notAvailableShort: "N/A",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Create: "Create",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

131
src/languages/fr-FR.js Normal file
View File

@@ -0,0 +1,131 @@
export default {
languageName: "Français (France)",
Settings: "Paramètres",
Dashboard: "Tableau de bord",
"New Update": "Mise à jour disponible",
Language: "Langue",
Appearance: "Apparence",
Theme: "Thème",
General: "Général",
Version: "Version",
"Check Update On GitHub": "Consulter les mises à jour sur Github",
List: "Lister",
Add: "Ajouter",
"Add New Monitor": "Ajouter une nouvelle sonde",
"Quick Stats": "Résumé",
Up: "En ligne",
Down: "Hors ligne",
Pending: "En attente",
Unknown: "Inconnu",
Pause: "En Pause",
pauseDashboardHome: "Éléments mis en pause",
Name: "Nom",
Status: "État",
DateTime: "Heure",
Message: "Messages",
"No important events": "Pas d'évènements important",
Resume: "Reprendre",
Edit: "Modifier",
Delete: "Supprimer",
Current: "Actuellement",
Uptime: "Uptime",
"Cert Exp.": "Certificat expiré",
days: "Jours",
day: "Jour",
"-day": "Journée",
hour: "Heure",
"-hour": "Heures",
checkEverySecond: "Vérifier toutes les {0} secondes",
"Avg.": "Moyen",
Response: "Temps de réponse",
Ping: "Ping",
"Monitor Type": "Type de Sonde",
Keyword: "Mot-clé",
"Friendly Name": "Nom d'affichage",
URL: "URL",
Hostname: "Nom d'hôte",
Port: "Port",
"Heartbeat Interval": "Intervale de vérification",
Retries: "Essais",
retriesDescription: "Nombre d'essais avant que le service soit déclaré hors-ligne.",
Advanced: "Avancé",
ignoreTLSError: "Ignorer les erreurs liées au certificat SSL/TLS",
"Upside Down Mode": "Mode inversé",
upsideDownModeDescription: "Si le service est en ligne, il sera alors noté hors-ligne et vice-versa.",
"Max. Redirects": "Nombre maximum de redirections",
maxRedirectDescription: "Nombre maximal de redirections avant que le service soit noté hors-ligne.",
"Accepted Status Codes": "Codes HTTP",
acceptedStatusCodesDescription: "Codes HTTP considérés comme en ligne",
Save: "Sauvegarder",
Notifications: "Notifications",
"Not available, please setup.": "Pas de système de notification disponible, merci de le configurer",
"Setup Notification": "Créer une notification",
Light: "Clair",
Dark: "Sombre",
Auto: "Automatique",
"Theme - Heartbeat Bar": "Voir les services surveillés",
Normal: "Général",
Bottom: "En dessous",
None: "Rien",
Timezone: "Fuseau Horaire",
"Search Engine Visibility": "Visibilité par les moteurs de recherche",
"Allow indexing": "Autoriser l'indexation par des moteurs de recherche",
"Discourage search engines from indexing site": "Refuser l'indexation par des moteurs de recherche",
"Change Password": "Changer le mot de passe",
"Current Password": "Mot de passe actuel",
"New Password": "Nouveau mot de passe",
"Repeat New Password": "Répéter votre nouveau mot de passe",
passwordNotMatchMsg: "Les mots de passe ne correspondent pas",
"Update Password": "Mettre à jour le mot de passe",
"Disable Auth": "Désactiver l'authentification",
"Enable Auth": "Activer l'authentification",
Logout: "Se déconnecter",
notificationDescription: "Une fois ajoutée, vous devez l'activer manuellement dans les paramètres de vos hôtes.",
Leave: "Quitter",
"I understand, please disable": "J'ai compris, désactivez-le",
Confirm: "Confirmer",
Yes: "Oui",
No: "Non",
Username: "Nom d'utilisateur",
Password: "Mot de passe",
"Remember me": "Se souvenir de moi",
Login: "Se connecter",
"No Monitors, please": "Pas de sondes, veuillez ",
"add one": "en ajouter une.",
"Notification Type": "Type de notification",
Email: "Email",
Test: "Tester",
keywordDescription: "Le mot clé sera recherché dans la réponse HTML/JSON reçue du site internet.",
"Certificate Info": "Informations sur le certificat SSL",
deleteMonitorMsg: "Êtes-vous sûr de vouloir supprimer cette sonde ?",
deleteNotificationMsg: "Êtes-vous sûr de vouloir supprimer ce type de notifications ? Une fois désactivée, les services qui l'utilisent ne pourront plus envoyer de notifications.",
"Resolver Server": "Serveur DNS utilisé",
"Resource Record Type": "Type d'enregistrement DNS recherché",
resoverserverDescription: "Le DNS de cloudflare est utilisé par défaut, mais vous pouvez le changer si vous le souhaitez.",
rrtypeDescription: "Veuillez séléctionner un type d'enregistrement DNS",
pauseMonitorMsg: "Etes vous sur de vouloir mettre en pause cette sonde ?",
"Last Result": "Dernier résultat",
"Create your admin account": "Créez votre compte administrateur",
"Repeat Password": "Répéter le mot de passe",
respTime: "Temps de réponse (ms)",
notAvailableShort: "N/A",
Create: "Créer",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
"Import/Export Backup": "Import/Export Backup",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file."
}

Some files were not shown because too many files have changed in this diff Show More