Compare commits

...

348 Commits

Author SHA1 Message Date
Louis Lam
6ccf741bc4
Merge pull request #2632 from chakflying/fix/docker-timeout
Fix: Use default timeout & CachebleDnsHttpsAgent for docker monitor
2023-01-19 19:26:40 +08:00
Louis Lam
0a58069742
Merge pull request #2641 from louislam/1.19.X
Merge 1.19.6 to 1.20.X
2023-01-19 14:27:45 +08:00
Louis Lam
2b57b3e863 Update to 1.19.6 2023-01-19 02:17:17 +08:00
Louis Lam
6cd6a2edf0 Fix ping issue on Windows #2636 2023-01-19 02:16:07 +08:00
Nelson Chan
6961b1bdd2 Fix: Use default timeout & CachebleDnsHttpsAgent 2023-01-18 09:53:04 +08:00
Louis Lam
54d4c4d3f7 Merge package-lock.json 2023-01-17 21:18:13 +08:00
Louis Lam
c47b6c5995 Merge remote-tracking branch 'origin/1.19.X'
# Conflicts:
#	package-lock.json
#	package.json
#	src/util-frontend.js
2023-01-17 21:17:04 +08:00
Louis Lam
7ef404ccc1 Update to 1.19.5 2023-01-17 20:32:44 +08:00
Louis Lam
a5ff27da7a Drop the property monitor.maintenance, use lastHeartBeat.status to check status instead 2023-01-17 17:34:47 +08:00
Louis Lam
7bb12a7e00 Fix #2608 2023-01-17 17:25:35 +08:00
Louis Lam
27585d0812 Fix #2618 2023-01-17 01:21:01 +08:00
Louis Lam
e675316635
Merge pull request #2586 from PopcornPanda/fix-2544
Fix: Allow long sms in PromoSMS
2023-01-16 13:21:56 +08:00
Louis Lam
b073ec2287 Add Help button which links to wiki 2023-01-16 12:39:24 +08:00
Louis Lam
31f45dcfc9
Merge pull request #2540 from twiggotronix/add-mqtt-schemes
Add mqtt, mqtts, ws and wss protocols to the mqtt monitor
2023-01-15 20:14:11 +08:00
Louis Lam
49ac71e25c
Merge pull request #2549 from Computroniks/docs/update-jsdoc-2023-01-05
Added missing JSDoc comments
2023-01-15 13:10:17 +08:00
Louis Lam
1a9b013fc2
Merge pull request #2328 from rmarops/mongodb-ping
added MongoDB ping monitor
2023-01-15 01:43:43 +08:00
Louis Lam
1326761a8a Update mongodb and simplify the logic of mongodbPing 2023-01-15 01:36:49 +08:00
Louis Lam
e48a987b9c Merge remote-tracking branch 'origin/master' into mongodb-ping
# Conflicts:
#	server/model/monitor.js
#	server/util-server.js
#	src/pages/EditMonitor.vue
2023-01-15 01:13:11 +08:00
Louis Lam
712a3c29d4 Fix Postgres monitor do not handle some error cases correctly 2023-01-14 21:06:10 +08:00
Louis Lam
e9497ac1ab Fix knex.js issue 2023-01-14 20:49:34 +08:00
Louis Lam
6437ef198f
Merge pull request #2541 from long2ice/master
feat: support redis monitor
2023-01-14 20:16:53 +08:00
Louis Lam
973d5692d0
Merge pull request #2604 from black23/patch-1
Update cs-CZ.js
2023-01-14 13:04:41 +08:00
Louis Lam
468cb004d6
Merge pull request #1690 from chakflying/feat/tags-manager
Feat: Tags Manager
2023-01-13 23:29:01 +08:00
Louis Lam
f7d41a30fa
Update src/components/TagEditDialog.vue
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-01-13 23:15:41 +08:00
black23
0ef686ac2f
Update cs-CZ.js
new string
2023-01-13 14:52:03 +01:00
long2ice
3b5893ea60 fix: add preserve line in redisPingAsync 2023-01-13 21:30:10 +08:00
long2ice
21cd4d64c3 fix: redisPingAsync 2023-01-13 19:10:07 +08:00
long2ice
db757123ba refactor: reuse databaseConnectionString 2023-01-13 16:32:49 +08:00
Louis Lam
4dcf31621e
Update README.md 2023-01-13 15:34:48 +08:00
Nelson Chan
9c1ba97e7d Chore: Fix typo 2023-01-12 21:25:33 +08:00
Nelson Chan
e9564619f1 Feat: Implement tags manager in settings
Fix: Remove unused color options

Chore: Fix typo
2023-01-12 21:25:33 +08:00
Łukasz Szczepański
8433bceb32 Trim message to maximum allowed length 2023-01-12 08:14:31 +01:00
Louis Lam
98d001b38b
Merge pull request #2575 from Joseph-Irving/victorops_notifications
Add Splunk Notification Provider
2023-01-12 14:15:36 +08:00
Louis Lam
d9f12a6376 Fallback to /bin/ping if ping is not found 2023-01-12 01:05:16 +08:00
Łukasz Szczepański
56ba133a1f Missing semicolon 2023-01-11 14:36:33 +01:00
Łukasz Szczepański
ec30147a7f Add option for allowing long sms in PromoSMS 2023-01-11 14:32:57 +01:00
David Twigger
2bc165379a Add unit test for hostNameRegexPattern 2023-01-11 13:28:30 +01:00
Luke
2172112144
Setting for allowing long sms 2023-01-11 12:13:47 +01:00
Luke
ecd661c801
Allow long sms in PromoSMS 2023-01-11 12:06:09 +01:00
twiggotronix
8fab7112a1
Merge branch 'louislam:master' into add-mqtt-schemes 2023-01-11 10:34:24 +01:00
Louis Lam
cc4ed308b0
Merge pull request #2581 from twiggotronix/add-frontend-unit-tests
Add frontend unit tests
2023-01-11 13:36:20 +08:00
Louis Lam
362280af14
Merge pull request #2578 from DimitriDR/master
Updating French localization
2023-01-10 22:48:20 +08:00
David Twigger
636fc8fcfc Fix workflow 2023-01-10 08:43:39 +01:00
David Twigger
1c05ba09dc Add cypress unit tests to workflow 2023-01-10 08:24:15 +01:00
David Twigger
1565da87cf Implement cypress unit testing 2023-01-10 08:18:48 +01:00
DimitriDR
890e3abf58 Updating French localization
Signed-off-by: DimitriDR <dimitridroeck@gmail.com>
2023-01-09 21:09:57 +01:00
Joseph Irving
33355c51b7 Add Splunk Notifications 2023-01-09 13:33:10 +00:00
Louis Lam
5f5c2d7c46 Update to 1.19.4 2023-01-09 21:02:57 +08:00
Louis Lam
71f4ab0aa6 Improve dockerfile 2023-01-09 21:01:45 +08:00
Louis Lam
24d1dd4c34 [auto-test] Drop Node.js 17, add 19 2023-01-09 20:24:20 +08:00
Louis Lam
c00abac834 Separate golang build layer 2023-01-09 13:43:08 +08:00
Louis Lam
439f963749
Merge pull request #2569 from Computroniks/bug/2565-negative-retention-value
Fixed negative retention time values
2023-01-09 13:08:35 +08:00
Louis Lam
f15c6470af
Merge pull request #2543 from SlothCroissant/master
Removed redundant title in Pushover notification
2023-01-09 12:50:55 +08:00
Matthew Nickson
32f7a0084a
Fixed negative retention time values
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-01-08 19:09:06 +00:00
SlothCroissant
f8658d6160 Removed redundant title in Pushover notification 2023-01-08 10:32:16 -06:00
Louis Lam
d596f8f7eb
Merge pull request #2560 from DimitriDR/master
Adding translations for Kook & ZohoCliq
2023-01-08 14:03:35 +08:00
Louis Lam
23a525e36a
Update CONTRIBUTING.md 2023-01-08 00:42:15 +08:00
Louis Lam
221d1d40f5
Update CONTRIBUTING.md 2023-01-08 00:32:13 +08:00
Louis Lam
60ec87941e
Merge pull request #2552 from MrEddX/bulgarian
Update bg-BG.js
2023-01-08 00:10:18 +08:00
DimitriDR
e8e4361e09 Adding translations for Kook & ZohoCliq 2023-01-07 13:10:25 +01:00
Matthew Nickson
675806829c
Changed wording for safeDelete function JSDoc 2023-01-06 17:17:37 +00:00
Louis Lam
21c1921867
Update server/uptime-kuma-server.js
Co-authored-by: 琚致远 / Zhiyuan Ju <juzhiyuan@apache.org>
2023-01-06 23:04:02 +08:00
David Twigger
e490ec6d29 move hostname regex pattern function to frontend-utils 2023-01-06 11:00:20 +01:00
MrEddX
7ad4392529
Update bg-BG.js
Translation Updated
2023-01-06 09:00:45 +02:00
Louis Lam
f3d3e064f8
Merge pull request #2538 from deluxghost/master
Updated zh-CN.js
2023-01-06 10:46:37 +08:00
Louis Lam
80c91b8234
Merge pull request #2546 from Computroniks/bug/#2419-padding-around-clear-data-dropdown
Fixed #2419 Styling of clear data dropdown
2023-01-06 10:34:55 +08:00
Matthew Nickson
dc8289df12
Added JSDoc for src/
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-01-05 22:55:51 +00:00
Matthew Nickson
caff9ca736
Added JSDoc for server/
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-01-05 22:19:05 +00:00
Matthew Nickson
c7eb72e73b
JSDoc for extra/
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-01-05 19:57:28 +00:00
Matthew Nickson
fc5ec5f492
Fixed styling of clear data dropdown
Fixed #2419

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-01-05 19:24:29 +00:00
long2ice
40ebc2df79 feat: support redis monitor 2023-01-05 23:02:56 +08:00
David Twigger
abf5e435fe move to utility function 2023-01-05 14:48:12 +01:00
David Twigger
8a372201f1 clean up 2023-01-05 14:23:05 +01:00
twiggotronix
8ec240fe19
Merge branch 'louislam:master' into add-mqtt-schemes 2023-01-05 14:08:05 +01:00
David Twigger
5362aab0e5 specify scheme for mqtt monitor type only 2023-01-05 14:06:13 +01:00
Louis Lam
bc7271b99c
Merge pull request #2372 from SirMorfield/docker-compose
Fix spelling in README
2023-01-05 21:04:01 +08:00
Louis Lam
4ebf5a5f07
Update README.md 2023-01-05 21:03:28 +08:00
Louis Lam
fbceefec36
Merge pull request #2223 from Computroniks/feature/remove-hardcoded-ping-path
feat: Change ping module to danielzzz/node-ping
2023-01-05 20:40:41 +08:00
Louis Lam
0b959514f8 Fix timeout 2023-01-05 20:38:37 +08:00
Louis Lam
4c5e2ea237
Update CONTRIBUTING.md 2023-01-05 20:03:28 +08:00
Louis Lam
7d92351568 Match previous settings 2023-01-05 19:30:55 +08:00
Louis Lam
494c53971c Convert to UTF8 on Windows only 2023-01-05 19:22:15 +08:00
David Twigger
fc1914bccd Fix lint 2023-01-05 11:42:19 +01:00
Louis Lam
4239cf4255 Pin dependency of ping 2023-01-05 17:11:37 +08:00
David Twigger
c196c34840 Add mqtt, mqtts, ws and wss protocols to the mqtt monitor 2023-01-05 08:57:48 +01:00
DX
554402b484 Updated zh-CN.js 2023-01-05 15:38:51 +08:00
Louis Lam
b049e4e1b4
Merge pull request #2534 from furkanipek/tr-lang-update
Update tr-TR.js
2023-01-05 14:21:30 +08:00
Matthew Nickson
90a2668272
Restructured condition + ensure data is UTF-8
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-01-04 17:32:27 +00:00
Louis Lam
49b5de7d40 Update Apprise to 1.2.1 2023-01-05 00:48:49 +08:00
Matthew Nickson
69e1880cd3
Added not active condition to prevent false error
Added a check to see if the host is alive. This prevents failiures when
the user specifies a hostname of `unknown`.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-01-04 16:46:36 +00:00
Furkan İpek
610b6248aa Update tr-TR.js 2023-01-04 16:41:00 +03:00
Furkan İpek
e591647b60 Update tr-TR.js 2023-01-04 16:35:48 +03:00
Louis Lam
da99833d4c
Merge pull request #2528 from chakflying/fix/tag-validation
Fix: Fix incorrect add tag form validation
2023-01-04 16:11:05 +08:00
Louis Lam
4bf23fdd1a Update jsonwebtoken from ~8 to ~9 2023-01-04 15:55:36 +08:00
Louis Lam
f99a64da67 Run npm update 2023-01-04 15:48:09 +08:00
Louis Lam
2e022789f6
Merge pull request #2527 from chakflying/fix/docker-down
Fix: Fix incorrect handling for container down
2023-01-04 15:33:59 +08:00
Matthew Nickson
73835f3328
Changed from ping-lite to ping module
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>

#Fixes 2126
2023-01-03 20:03:36 +00:00
Matthew Nickson
4819112e25
Merge remote-tracking branch 'origin/master' into feature/remove-hardcoded-ping-path 2023-01-03 19:10:17 +00:00
Joppe Koers
2d3fd738e4 Fix small spelling mistake 2023-01-03 15:37:32 +01:00
Nelson Chan
edd8fe2e22 Fix: Fix incorrect tag form validation 2023-01-03 22:18:45 +08:00
Nelson Chan
204792dd2d Fix: Fix incorrect handling for container down 2023-01-03 22:07:14 +08:00
Louis Lam
b8e8c1b9db Update to 1.19.3 2023-01-03 18:05:19 +08:00
Louis Lam
0cead83705 Fix #2516 2023-01-03 14:50:41 +08:00
Louis Lam
7b0093b8b3
Merge pull request #2514 from lionep/patch-1
Improve french translation
2023-01-03 13:14:25 +08:00
lionep
cd7e362b81
Improve french translation
Set "Not available, please setup" translation more accurate, because it's in use in notifications and proxies page.
2023-01-02 08:46:42 +01:00
Arniwatt Chonkiattipoom
a8af2a418e
Slack notification block not working (#1958)
* [empty commit] pull request for slack notification

* Add attachments block for slack notification

* chore: update action button in new attachment block

* chore: loop in attachments to push blocks

* chore: missing semicolon

Co-authored-by: pruekanw <arniwatt.c@linecorp.com>
2023-01-02 15:01:50 +08:00
Louis Lam
39ac9b887e Fix #2504 2023-01-01 22:27:14 +08:00
Louis Lam
28c3291020
Merge pull request #2513 from louislam/revert-2433-mathias/Auth-case-insensitive-login
Revert "Auth: Case insensitive login check on username"
2023-01-01 22:19:27 +08:00
Louis Lam
50711391d1
Revert "Auth: Case insensitive login check on username" 2023-01-01 22:19:00 +08:00
Louis Lam
e88e10cc8e Fix #2494 2023-01-01 21:43:54 +08:00
Louis Lam
27146ffeef
Merge pull request #2433 from mathiash98/mathias/Auth-case-insensitive-login
Auth: Case insensitive login check on username
2023-01-01 14:16:59 +08:00
Louis Lam
41a9f2ff8a
Merge pull request #2495 from minhhoangvn/fix/update-service-name-grpc
Bug fix: gRPC check throws errors when response data size > 50 chars
2023-01-01 13:57:10 +08:00
Louis Lam
cd7a6e4019
Merge pull request #2478 from YehowahLiu/master
Add Kook notification provider
2023-01-01 02:53:03 +08:00
Louis Lam
8bb064c6fa
Merge pull request #2157 from Mikkel-T/fix-discord-embed
Improve the URL field in Discord embeds
2022-12-31 22:41:40 +08:00
Louis Lam
1006fbd873 A possible fix for #2447 2022-12-30 13:46:53 +08:00
Louis Lam
5554432b31
Merge pull request #2377 (Zoho Cliq Notification Provider)
Zoho Cliq Notification Provider
2022-12-29 19:00:50 +08:00
minhhoang
d111db0321 fix: add accurate error message when user input invalid service name or method name 2022-12-29 08:10:58 +07:00
minhhoang
4147a4c404 fix: #2480 2022-12-28 22:31:33 +07:00
Louis Lam
8c684e9293
Update SECURITY.md 2022-12-28 19:59:33 +08:00
Louis Lam
f025de6eaf
Merge pull request #2490 from 5idereal/patch-1
update zh-TW.js
2022-12-28 18:17:10 +08:00
5idereal
7f394d0630
update zh-TW.js 2022-12-28 17:29:46 +08:00
Louis Lam
9fe9e235ca
Merge pull request #2236 from mishankov/fix/stats-30-days
Simple fix for Uptime component
2022-12-28 15:37:46 +08:00
401Unauthorized
50b84f5f45
fix code style: add missing semicolon 2022-12-27 14:12:22 +08:00
401Unauthorized
c60b741406
Add kook notification provider 2022-12-27 14:12:22 +08:00
thefourCraft
f6ea1fe9a5
he-IL (#2460) 2022-12-27 14:04:53 +08:00
Louis Lam
b7e3ec2372
Merge pull request #2476 from DimitriDR/master
Huge improvement for French localization.
2022-12-27 14:02:25 +08:00
DimitriDR
625fd7c2aa Huge improvement for French localization. 2022-12-27 01:02:43 +01:00
Louis Lam
aec80b53d5 Update to 1.19.2 2022-12-27 00:22:52 +08:00
Louis Lam
06852bbf0d Fix the UI broken after removed a monitor 2022-12-27 00:22:09 +08:00
Louis Lam
056d957c1e Update to 1.19.1 2022-12-26 23:49:20 +08:00
Louis Lam
e12225e595 Fix #2475 #2468 #2455, add Accept-Encoding only if encountered the abort error 2022-12-26 21:00:46 +08:00
Louis Lam
1b6c587cc9 Fix #2472 2022-12-26 14:46:29 +08:00
Louis Lam
4a1db336df
Merge pull request #2465 from augustin64/patch-1
(fr) Fix typo
2022-12-25 19:18:46 +08:00
Augustin
9e9c5cd1d2
(fr) Fix typo 2022-12-24 19:41:55 +01:00
Louis Lam
1e689d99b4
Merge pull request #2393 from zImPatrick/discord-docker-fix
Fix discord notification not sending when docker container goes down
2022-12-24 14:26:53 +08:00
Louis Lam
14fffcf06b Globally fix if heartbeatJSON["msg"] is undefined 2022-12-24 14:23:50 +08:00
Louis Lam
6962e056ce Update to 1.19.0 2022-12-23 22:44:49 +08:00
Louis Lam
b7a898326e
Merge pull request #2437 from MrEddX/bulgarian
Update bg-BG.js
2022-12-19 20:22:47 +08:00
MrEddX
a89be0e6d4
Update bg-BG.js
Translation Update
2022-12-19 13:37:42 +02:00
Mathias Haugsbø
b3ac7c3d43 Username case insensitive, patch db instead of using LIKE 2022-12-19 12:18:33 +01:00
Mathias Haugsbø
c79b2913a2 Auth: Case insensitive login check on username
Allows users to add users with capital letters and then login with just lowercase letters.

We accidentally capitalized the first letter of our username so the other people using it frequently thinks they wrote the wrong password.
2022-12-18 17:16:19 +01:00
Louis Lam
b58b83541b
Merge pull request #2428 from rbunpat/master
Update th-TH.js
2022-12-18 15:40:40 +08:00
Cyril59310
df4f91c20d
Update EN language (#2429)
* Update EN language
2022-12-18 15:39:54 +08:00
Rachatat Bunpat
fc6b040a4e
Update th-TH.js 2022-12-17 17:34:03 +07:00
Louis Lam
9838f36b50
Merge pull request #2418 from Johno95CZ/patch-1
Update cs-CZ.js
2022-12-17 17:44:55 +08:00
Louis Lam
a60072adbc
Merge pull request #2423 from rbunpat/master
Update th-TH.js
2022-12-17 17:43:51 +08:00
Louis Lam
e7aeb6f6bf
Merge pull request #2422 from Justman10000/master
Update
2022-12-17 17:43:25 +08:00
Rachatat Bunpat
06e570c52d
Update th-TH.js 2022-12-16 19:15:14 +07:00
Justman10000
890b8f8333 Updated German 2022-12-16 10:42:41 +00:00
Louis Lam
df21f7da76 Check login for initServerTimezone 2022-12-16 12:56:40 +08:00
Joppe Koers
3ea6711053 Remove docker compose again 2022-12-15 23:03:06 +01:00
JohnoCZ
20c37a70f7
Update cs-CZ.js 2022-12-15 14:35:59 +01:00
Louis Lam
b75db27658 Fix lint 2022-12-15 13:57:28 +08:00
Louis Lam
765d8e1297 Fix #2318 2022-12-15 13:39:48 +08:00
Louis Lam
9dc2cc1f0d Copy timezone.js from dayjs 2022-12-15 13:05:04 +08:00
Louis Lam
2532becf61 Update to 1.19.0-beta.2 2022-12-14 23:37:00 +08:00
Louis Lam
6154776b34
Merge pull request #2409 from Justman10000/master
Update
2022-12-14 22:39:00 +08:00
Louis Lam
7e6b92203d
Merge pull request #2406 from MrEddX/bulgarian
Bulgarian
2022-12-14 22:38:13 +08:00
Louis Lam
1da00d19fd Try to fix incorrect header check 2022-12-14 21:34:13 +08:00
Justman10000
da4bdab4f6 Updated german 2022-12-14 13:16:32 +00:00
MrEddX
86ab97ef56
Update bg-BG.js
Translation Update
2022-12-14 09:56:28 +02:00
MrEddX
345b0c1829
Update bg-BG.js
Fixed Style
2022-12-14 09:49:31 +02:00
MrEddX
2ac87fcea7
Update bg-BG.js
Fixed Typos
2022-12-14 09:26:43 +02:00
Cyril59310
4862bec965
Update Fr language + added variable for missing translation (#2395)
* Update FR language
2022-12-13 22:00:54 +08:00
Louis Lam
aa784fb3b2 Fix #2394 2022-12-13 16:48:23 +08:00
Louis Lam
466b403a96 Handle unexpected error of checkCertificate 2022-12-13 02:21:12 +08:00
zImPatrick
f32441e2f6 fix discord notification not sending when docker container is down 2022-12-12 18:05:10 +01:00
Louis Lam
39987ba9ac Init server timezone 2022-12-12 22:57:57 +08:00
Louis Lam
3b87209e26 Add configurable dns cache 2022-12-12 17:19:22 +08:00
Louis Lam
e6dc0a0293 Slightly improve maintenance page's css on mobile 2022-12-12 16:06:17 +08:00
Louis Lam
5c5a339a36 Add links for status pages and maintenance for mobile (Fix #2257) 2022-12-12 15:54:46 +08:00
Louis Lam
3040bd41d9 Speed up armv7 build time of healthcheck by using go compiler cross-build feature in the host 2022-12-12 15:42:00 +08:00
Louis Lam
3b58fd3b3c Cache uptime 2022-12-11 21:33:26 +08:00
Louis Lam
bc86f8bb5f Reset busy_timeout to default 2022-12-11 20:25:15 +08:00
Louis Lam
ab5f6dc82c Fix css 2022-12-11 18:52:24 +08:00
Louis Lam
5176fd02c1 Fix healthcheck do not check https 2022-12-10 23:30:32 +08:00
Louis Lam
02b5cae577 Fix #2371 by left join maintenance_timeslot 2022-12-09 21:03:12 +08:00
Louis Lam
54aa7d5dca Merge remote-tracking branch 'origin/master' 2022-12-08 23:22:09 +08:00
Louis Lam
4cd5b5563f Fix #1145 2022-12-08 23:21:55 +08:00
Louis Lam
f1c30204b6
Merge pull request #2373 from saw303/master
Fixed some typos in the German translations 🇩🇪🇨🇭
2022-12-08 23:01:48 +08:00
panos
9da28fbbc7 zoho cliq code style 2022-12-08 13:56:02 +02:00
panos
851a04b082 zoho cliq code style 2022-12-08 13:53:02 +02:00
panos
68bc7ac421 zoho cliq code style 2022-12-08 13:41:05 +02:00
panos
73bfdb9ef9 zoho cliq notification provider 2022-12-08 13:32:10 +02:00
Louis Lam
e478084ff9 Fix Uptime Kuma cannot be stopped 2022-12-08 19:13:47 +08:00
Louis Lam
2dff7dd380 Make dockerfile slightly clear and improve the build flow 2022-12-08 18:29:17 +08:00
Louis Lam
9bfa43100b Compile healthcheck.go 2022-12-08 18:29:17 +08:00
Louis Lam
ad5e1957b1 Deprecate healthcheck.js 2022-12-08 18:29:17 +08:00
Louis Lam
cc68ebca39 Convert healthcheck.js into go-lang 2022-12-08 18:29:17 +08:00
Silvio Wangler
aa27d976c2
Fixed some typos in the German translations 🇩🇪🇨🇭 2022-12-07 09:10:05 +01:00
Louis Lam
ecbc0f0477
Merge pull request #2369 from saw303/master
Added new language German (Switzerland)
2022-12-07 15:14:52 +08:00
Joppe Koers
f8c7da7995 Add docker-compose to README 2022-12-06 18:44:16 +01:00
Joppe Koers
f3660a0cec Fix spelling in README 2022-12-06 18:43:28 +01:00
Silvio Wangler
92caec95fe
Added new language German (Switzerland) 2022-12-06 13:43:37 +01:00
Louis Lam
2c3abdc146
[stale-bot] Do not close pr 2022-12-05 20:11:28 +08:00
Louis Lam
b1170211b7 Update to 1.19.0-beta.1 2022-12-05 19:24:04 +08:00
Louis Lam
eadf2c810a Fix check version 2022-12-05 19:17:24 +08:00
Louis Lam
8aa97635ec Improve the clear filter button 2022-12-05 18:21:16 +08:00
Louis Lam
ee1a56caae Update /test-webhook and reevaluate sensitive fields 2022-12-05 18:18:19 +08:00
Louis Lam
e886df4788 Fix typo 2022-12-05 17:55:45 +08:00
Louis Lam
5196abfd36 Merge remote-tracking branch 'origin/master' into feat/add-auth-header-to-webhook-notification-#1919 2022-12-05 17:52:02 +08:00
Louis Lam
3e68cf2a1c Specify Accept-Encoding for axios request (Fix #2253) 2022-12-04 22:55:05 +08:00
Louis Lam
0ab82e6de3 Generate random nightly version 2022-12-04 22:44:50 +08:00
Louis Lam
8cdbe37f6f Update core-js 2022-12-04 21:41:08 +08:00
Louis Lam
28d13e198c
Merge pull request #2350 from MrEddX/bulgarian
Bulgarian
2022-11-26 20:52:33 +08:00
MrEddX
14a062804e
Update bg-BG.js
- Translation fixes
2022-11-25 22:00:52 +02:00
MrEddX
cf3e03ab40
Update bg-BG.js
- Added new  fields
- Translated new fields
- Fixed some typos
2022-11-25 21:56:00 +02:00
Louis Lam
191f3ad53b
Merge pull request #2339 from jbrunner/fix-2296
Add socks5h support
2022-11-25 16:24:21 +08:00
Louis Lam
370d522920 Pin dependency of axios-ntlm to 1.3.0. As 1.3.1 causes error 2022-11-25 14:00:33 +08:00
Louis Lam
e0a1ad8a1c Update dependencies and drop start-server-watch-dev as it is unstable 2022-11-25 01:32:33 +08:00
Louis Lam
9720006934
Merge pull request #2151 from Computroniks/feature/#1817-add-mysql-monitor
Feat  Add MySQL/MariaDB monitor #1817
2022-11-25 01:27:40 +08:00
Joshua Brunner
cd270bd8b5 Add socks5h support
Add socks5h support as an extra option to not break previous socks5 implementation.
Allows to toggle between socks5 and socks5h explicit.

Fixes #2296
2022-11-22 11:18:16 +01:00
Louis Lam
bc3229828e Merge remote-tracking branch 'origin/master' 2022-11-20 00:11:38 +08:00
Louis Lam
a5f23b9839 Update Apprise from 1.0 to 1.2 2022-11-20 00:07:19 +08:00
Matthew Nickson
2052fa175f
Merge branch 'master' into feature/#1817-add-mysql-monitor
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-11-17 19:04:14 +00:00
Matthew Nickson
15b63c82c3
Merge remote-tracking branch 'upstream/master' into feature/#1817-add-mysql-monitor
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-11-17 18:46:58 +00:00
Matthew Nickson
b053bc61ce
Fixed MySQL monitor to close connection
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-11-17 18:34:02 +00:00
rmarops
0e30843a75 fixed lint check missing semicolon 2022-11-16 22:27:18 -05:00
rmarops
2103edb604 moved client close out of finally block and fixed linting errors 2022-11-16 22:21:15 -05:00
rmarops
b059a36e66 added MongoDB ping monitor 2022-11-16 20:50:34 -05:00
Jan Hartje
258ff56962
Merge branch 'louislam:master' into feat/add-auth-header-to-webhook-notification-#1919 2022-11-14 20:17:36 +01:00
Louis Lam
cb4e512dc6
Merge pull request #2316 from Dafnik/patch-fix-link-preview-description
Fix 'undefined' in link preview generation
2022-11-15 02:37:28 +08:00
Dafnik
4042c26390 Fix 'undefined' in link preview generation 2022-11-14 18:05:52 +01:00
Louis Lam
5da2315534
Merge pull request #2310 from Ealrang/patch-1
Update zh-CN.js
2022-11-14 22:02:43 +08:00
Ealrang
204015f1f5
Update zh-CN.js
Correct typos
2022-11-12 22:15:04 +08:00
Louis Lam
cc6d17d2e0
Merge pull request #1964 from minhhoangvn/feat/add-gRPC-protocol
Feat/add gRPC protocol
2022-11-11 12:34:35 +08:00
Louis Lam
68862c0b3f Fix Pushbullet do not handle general message correctly and fix name convention (Close #1890) 2022-11-01 20:27:40 +08:00
Louis Lam
fd15e7c2dc Merge remote-tracking branch 'origin/master' into ntfy-icon
# Conflicts:
#	server/notification-providers/ntfy.js
#	src/components/notifications/Ntfy.vue
#	src/languages/en.js
2022-10-31 17:10:20 +08:00
Louis Lam
5c4cf68937
Merge pull request #2260 from m-kiszka/smseagle
Added support for SMSEagle device API notifications
2022-10-31 17:03:34 +08:00
Louis Lam
214ddc264d Fix mistake 2022-10-29 23:40:09 +08:00
Louis Lam
2ea71839d1 Add npm run start-server-watch-dev for watching server code changes and restart (Node.js 19 only) 2022-10-29 23:37:05 +08:00
Louis Lam
54efde8185 Update socket.io and remove an useless event listener 2022-10-29 23:29:33 +08:00
Louis Lam
705124d4ac
Merge pull request #2274 from 5idereal/patch-1
update zh-tw translation
2022-10-28 16:50:24 +08:00
5idereal
1cb6940590
update zh-tw translation 2022-10-28 15:50:00 +08:00
Louis Lam
0f8ad288f3
Merge pull request #2255 from b-reich/update-german-translation
Add german translations
2022-10-27 16:47:11 +08:00
Adam Stachowicz
434174d350
I18n PL update (#2264)
* [PL] Only formatting by ESLint for now

* Translate new i18n keys to polish + small grammar/typo fixes

* ESLint again after npm install...
2022-10-27 16:46:32 +08:00
minhhn3
3d1237ed53 fix: resolve conflict 2022-10-26 20:50:34 +07:00
minhhn3
b459408b10 fix: resolve conflict 2022-10-26 20:41:21 +07:00
Benjamin Reich
f04fe4d230 add new german translations 2022-10-25 10:50:58 +02:00
Louis Lam
e579610426
Merge pull request #2265 from Saibamen/patch-1
Add info about `npm install` to translators
2022-10-25 14:32:43 +08:00
Louis Lam
b115d3f8b9
Merge pull request #2266 from Saibamen/fix_linters
Fix 'dayjs' is never used warning
2022-10-25 14:23:59 +08:00
Adam Stachowicz
134b3b8ac1 Fix 'dayjs' is never used warning 2022-10-25 01:27:25 +02:00
Adam Stachowicz
e7e7751e7b
Correct order 2022-10-24 23:38:38 +02:00
Adam Stachowicz
5cd58e6fa3
Add info about npm install to translators
Without this, you can have wrong indentation from ESLint
2022-10-24 23:36:21 +02:00
Marcin Kiszka
08763b700a Added support for SMSEagle device API notifications 2022-10-24 12:45:56 +02:00
Marcin Kiszka
781f855921 [empty commit] pull request for Added support for SMSEagle device API notifications 2022-10-24 12:44:29 +02:00
Louis Lam
9e81fe120f
Merge pull request #2235 from dave9123/patch-3
Update id-ID.js
2022-10-20 15:57:39 +08:00
Denis Mishankov
7313aa6563 fix spaces 2022-10-17 20:36:52 +03:00
Denis Mishankov
c7871427c3 compute title value 2022-10-17 20:20:51 +03:00
Dave
c0e67b6de9
Update id-ID.js 2022-10-17 20:09:25 +07:00
Louis Lam
a17084f75d
Merge pull request #2229 from falentio/fix-id-lang
fix typos in id lang
2022-10-16 16:25:35 +08:00
Louis Lam
e4fe7b802a
Update bg-BG.js #2228 2022-10-16 16:24:39 +08:00
falentio
5761bc9b90 fix typos in id lang 2022-10-16 13:49:25 +07:00
MrEddX
92ea019fd4
Update bg-BG.js
- Added new  fields
- Translated new fields
2022-10-16 07:21:23 +03:00
Cyril59310
a774b37369
Update FR language + fixed daytime error (#2226)
* Update FR language

* fix a daytime error + add for translation

* Update language file FR + fixed daytime error
2022-10-16 01:42:28 +08:00
Louis Lam
06755f249d Update to 1.19.0-beta.0 2022-10-15 21:02:56 +08:00
Louis Lam
64f84eb118 Update Details.vue's button styles 2022-10-15 21:01:48 +08:00
Louis Lam
afe12ccf24
A complete maintenance planning system (#1213)
A complete maintenance planning system from karelkryda/master
2022-10-15 20:54:28 +08:00
Louis Lam
6f4424de28 Remove unused language keys 2022-10-15 20:44:02 +08:00
Louis Lam
24cb212a37 Fix recurring 2022-10-15 20:15:50 +08:00
Louis Lam
d8a676abb6 Implement recurring day of month and day of week 2022-10-15 18:49:09 +08:00
Louis Lam
0b8d4cdaac Generate Next Timeslot for recurring interval 2022-10-15 17:17:26 +08:00
Louis Lam
268cbdbf8d Merge remote-tracking branch 'origin/master' into maintenance
# Conflicts:
#	server/server.js
#	src/components/settings/General.vue
2022-10-15 15:57:39 +08:00
Louis Lam
b60dde0b2d Update SQLite 2022-10-15 15:18:54 +08:00
Louis Lam
aecf95864e Add index for maintenance tables 2022-10-14 13:26:41 +08:00
Louis Lam
c662d259b0
Firefox Better Support #2206 2022-10-13 19:28:02 +08:00
Matthew Nickson
6b47ad07ca
[empty commit] pull request for #2126 remove hardcoded ping 2022-10-12 22:03:05 +01:00
Matthew Nickson
f459ea845c
Added #2182 Add support for custom radius ports (#2197)
This commit adds support for the port to be specified when using the
radius monitor type. A check has been implemented to ensure that a null
value is not passed to the radius check function as could occur with
monitors that were created before this change was introduced. The
default port of 1812 is displayed when the user selects the radius
monitor in much the same way as the DNS port is handled. The port was
not included in the hostname in the form hostname:port in order to avoid
issues with IPv6 addresses and monitors that had been created before
this change was implemented.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-10-13 00:32:05 +08:00
Louis Lam
b24c75eec5
Merge pull request #2162 from UltraWelfare/fix-entry-page-redirect
Fixed entry route not redirecting correctly.
2022-10-13 00:28:34 +08:00
MagicFun1241
cb5f90aa89
Update Russian locale (#2218)
* Update ru-RU.js

* Remove duplicates

* Remove duplicates x2

* Revert to previous version for one translation

* Removed conflicting lines

* Remove conflicting 'Reverse Proxy' key
2022-10-13 00:27:50 +08:00
Louis Lam
edacff123b Add UTC in the serverTimezone dropdown 2022-10-12 22:13:07 +08:00
Louis Lam
2faf866e9e Implement generateTimeslot() for recurring interval type 2022-10-12 17:02:16 +08:00
Louis Lam
8cc3e4b7c1 Revert 2022-10-11 22:28:00 +08:00
Louis Lam
7b9766091e Revert testing 2022-10-11 22:18:09 +08:00
Louis Lam
d95e722658 Init dayjs for backend.spec.js 2022-10-11 22:09:18 +08:00
Louis Lam
39b6725163 Update maintenance tables 2022-10-11 21:48:43 +08:00
Louis Lam
dfb75c8afb Update status page's maintenance message 2022-10-11 20:56:48 +08:00
Louis Lam
e07aa982c3 WIP 2022-10-11 18:23:17 +08:00
Christian Meis
1e8a16504b Make icon optional for ntfy notificaation provider. Add Icon header to ntfy request only, if icon is actually defined. 2022-10-11 11:15:33 +02:00
Alexander Borzov
180d881ac1
Update ru-RU.js (#2217) 2022-10-11 15:35:08 +08:00
Louis Lam
2271ac4a5a Add info.serverTimezoneOffset and improve some styles 2022-10-11 14:52:47 +08:00
Louis Lam
f6bbd1ca67 Merge remote-tracking branch 'origin/master' into maintenance 2022-10-11 14:13:08 +08:00
Louis Lam
d5c02fc627 Update Maintenance list order by status 2022-10-11 01:59:47 +08:00
Louis Lam
c84de4d259 WIP: Add maintenance status 2022-10-11 01:45:30 +08:00
Louis Lam
c1ccaa7a9f WIP 2022-10-10 20:48:11 +08:00
Louis Lam
539683f8e9 Merge remote-tracking branch 'origin/master' into maintenance 2022-10-10 16:50:25 +08:00
Louis Lam
a577fba848 Change DateTime Range using serverTimezone 2022-10-10 02:28:03 +08:00
Louis Lam
a36f24d827 Add configurable server timezone 2022-10-09 20:59:58 +08:00
Louis Lam
b007681e67 Merge remote-tracking branch 'origin/master' into karelkryda_master
# Conflicts:
#	server/model/monitor.js
#	server/model/status_page.js
#	src/languages/en.js
2022-10-09 19:26:00 +08:00
Louis Lam
ad1a7c255f Drop exports.entryPage fully 2022-10-08 23:56:58 +08:00
janhartje
b879428a03
feat(notification): add additional Header to webhook 2022-10-05 17:48:07 +02:00
janhartje
3c5de1c889 Merge branch 'master' of https://github.com/louislam/uptime-kuma into feat/add-auth-header-to-webhook-notification-#1919 2022-10-05 16:44:13 +02:00
George Tsomlektsis
3e699f8ac3 Fix linting errors. 2022-10-03 18:01:52 +03:00
George Tsomlektsis
b0d6b5b13d Fixed entry route not redirecting correctly when the status entry page changes slug. 2022-10-03 17:48:34 +03:00
Mikkel-T
a42f7416b5 Improve the URL field in Discord embeds
Instead of having two different ways of showing the URL field in Discord embeds, always show the raw address.
2022-10-02 19:29:33 +02:00
Matthew Nickson
f9be918246
Add support for MySQL/MariaDB databases #1817
This commit adds support for monitoring MySQL and MariaDB database
servers. The mysql2 package was choosen over mysql as it provides a
promise wrapper and is reportedly faster than the original mysql package
whilst still maintaining the same API.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-10-02 01:52:53 +01:00
Matthew Nickson
314ae38f91
Changed name of SQL Server to avoid confusion
It appears that SQL Server causes some confusion among users as they
believe that it means any SQL database, not the Microsoft product SQL
Server. To avoid this issue, the display value has been changed to
Microsoft SQL Server. No backend changes have been made and it is still
stored as sqlserver in the database. This is only a frontent change.

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-10-01 21:35:33 +01:00
Christian Meis
c03d911657
Update src/languages/en.js
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-09-28 11:39:13 +02:00
Christian Meis
e12642cf21 Fix double quotes in fallback for no icon url in ntfy notification provider settings 2022-09-28 10:24:56 +02:00
Christian Meis
618d904001 [empty commit] pull request for icon support in ntfy notification provider (fixes #2135) 2022-09-28 10:17:02 +02:00
Christian Meis
6f86236b63 Add support for icon to ntfy notification provider (requires minimum ntfy server version 1.28.0 and Android app 1.14.0, no iOS support as of today) 2022-09-28 10:13:18 +02:00
Louis Lam
204339fbed Make two functions to convert ISO 8601 <=> YYYY-MM-DD hh:mm:ss 2022-09-28 00:48:15 +08:00
Louis Lam
b1465c0282 - Maintenance standardize datetime format to YYYY-MM-DD hh:mm:ss
- Import dayjs extensions one time only
- Maintenance activeCondition centralize
2022-09-28 00:20:17 +08:00
Louis Lam
4002b9f577 [WIP] Checking maintenance time using maintenance_timeslot table 2022-09-27 20:44:44 +08:00
Louis Lam
3f63cb246b [WIP] Handle timezone offset for timeRange 2022-09-25 19:38:28 +08:00
Louis Lam
f11dfc8f43 [WIP] Add/Edit Maintenance with new UI and recurring 2022-09-24 19:18:24 +08:00
Louis Lam
9d99c39f30 Update Maintenance UI for recurring 2022-09-24 02:33:29 +08:00
Louis Lam
617ba49e6c Fix race condition of selectedStatusPagesOptions 2022-09-18 22:40:53 +08:00
Louis Lam
7853c2cc38 Update Maintenance UI 2022-09-18 22:34:05 +08:00
Louis Lam
f61c1c47aa Update Maintenance UI 2022-09-18 02:13:29 +08:00
Louis Lam
9fe07742ea Linting 2022-09-18 02:07:32 +08:00
Louis Lam
a29eae3213 Update Maintenance UI 2022-09-18 02:02:18 +08:00
Louis Lam
80698a58b8 Tidy up 2022-09-17 22:09:09 +08:00
Louis Lam
bb883e6fa0 Move maintenance under /maintenance 2022-09-17 22:00:11 +08:00
Louis Lam
120e578398 Move maintenance code to maintenance-socket-handler.js 2022-09-17 16:58:08 +08:00
Louis Lam
7017c2e625 Move maintenance code to maintenance-socket-handler.js 2022-09-17 16:54:21 +08:00
Louis Lam
2f67d26702 Merge manually, as this part had been moved 2022-09-17 16:20:10 +08:00
Louis Lam
90761cf831 Merge remote-tracking branch 'origin/master' into karelkryda_master
# Conflicts:
#	server/database.js
#	server/model/monitor.js
#	server/routers/api-router.js
#	server/server.js
#	src/components/HeartbeatBar.vue
#	src/components/MonitorList.vue
#	src/icon.js
#	src/layouts/Layout.vue
#	src/mixins/datetime.js
#	src/mixins/socket.js
#	src/router.js
#	src/util.js
2022-09-17 16:12:57 +08:00
Louis Lam
68875c3091 Fix merging issue 2022-09-13 22:22:01 +08:00
Louis Lam
f35d7c0a1a Merge remote-tracking branch 'origin/master' into feat/add-gRPC-protocol
# Conflicts:
#	package-lock.json
2022-09-13 22:19:41 +08:00
minhhn3
3a90d246a4 fix: wrong type 2022-08-20 22:45:11 +07:00
minhhn3
6bb79597e8 fix: resolve merge conflict 2022-08-13 13:26:05 +07:00
minhhn3
34ab6142db fix: remove new space line 2022-08-08 19:38:43 +07:00
minhhn3
2232236a7a [empty commit] pull request for add gRPC protocol 2022-08-03 13:39:31 +07:00
Minh Hoàng
dcecd10c88
Feat/add gRPC protocol (#1)
* feat: added monitor with gRPC

Co-authored-by: minhhn3 <minhhn3@vng.com.vn>
2022-08-03 12:00:39 +07:00
Jan Hartje
af07c7f050 feat(notification): add Authorization Header option to backend 2022-07-18 16:04:27 +00:00
Jan Hartje
95dba6dcaf feat(notification): add Authorization Header option to frontend 2022-07-18 16:04:18 +00:00
Jan Hartje
90c2bf7c94 [empty commit] pull request for #1919 2022-07-18 15:56:53 +00:00
Karel Krýda
fa777c5bc0
Update server/server.js
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-05-30 15:32:42 +02:00
Karel Krýda
6d0683b055
Update server/routers/api-router.js
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-05-30 15:32:19 +02:00
Karel Krýda
25262cfb91
Update server/model/monitor.js
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2022-05-30 15:31:45 +02:00
Louis Lam
7a46b44d25 Merge remote-tracking branch 'origin/master' into karelkryda_master
# Conflicts:
#	src/components/HeartbeatBar.vue
2022-05-18 19:49:54 +08:00
Karel Krýda
42f931f6cf
Merge branch 'master' into master 2022-05-09 10:28:14 +02:00
Karel Krýda
2fe5c090aa small fixes 2022-05-08 20:50:08 +02:00
Karel Krýda
ed218e73bb UI improvements 2022-05-08 20:03:24 +02:00
Karel Krýda
9a35386841
Merge branch 'master' into master 2022-05-06 11:24:21 +02:00
Karel Krýda
2b14bdae62
Merge branch 'master' into master 2022-05-01 12:40:34 +02:00
Karel Krýda
31b90d12a4 Added the ability to choose on which status pages maintenance information should be displayed 2022-04-30 17:17:22 +02:00
Karel Krýda
b4ffcc5555 Added JSDoc 2022-04-30 15:50:05 +02:00
Karel Krýda
57368c8c6c More modern look of maintenance information on status page (same design as for the new incident system) 2022-04-30 15:32:56 +02:00
Karel Krýda
11ef22edec Fixed remaining lint errors 2022-04-30 15:13:13 +02:00
Karel Krýda
f78d01d770 Resolve lint errors 2022-04-30 14:57:08 +02:00
Karel Krýda
7532acc95d Resolve conflicts 2022-04-30 14:33:54 +02:00
Karel Krýda
ed84e56a85 Merge remote-tracking branch 'origin_kuma/master'
# Conflicts:
#	package-lock.json
#	server/database.js
#	server/model/monitor.js
#	server/routers/api-router.js
#	server/server.js
#	src/components/MonitorList.vue
#	src/components/PingChart.vue
#	src/icon.js
#	src/pages/DashboardHome.vue
#	src/pages/StatusPage.vue
#	src/router.js
#	src/util.js
2022-04-30 13:40:34 +02:00
Karel Krýda
b49e5d5c39 The SQL query to determine if the monitor is under maintenance is now in its own method. 2022-01-25 19:07:27 +01:00
Karel Krýda
e7b2832967 The start and end dates of the maintenance are now stored in UTC, which allows it to be converted between time zones 2022-01-24 22:33:15 +01:00
Karel Krýda
5fda1f0f59 minor fixes (missing commas, spaces, translations) 2022-01-23 20:33:39 +01:00
Karel Krýda
0d3414c6d6 A complete maintenance planning system has been created 2022-01-23 15:22:00 +01:00
160 changed files with 14403 additions and 3682 deletions

View File

@ -31,6 +31,9 @@ tsconfig.json
/tmp
/babel.config.js
/ecosystem.config.js
/extra/healthcheck.exe
/extra/healthcheck
### .gitignore content (commented rules are duplicated)

View File

@ -19,3 +19,6 @@ indent_size = 2
[*.vue]
trim_trailing_whitespace = false
[*.go]
indent_style = tab

View File

@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
node: [ 14, 16, 17, 18 ]
node: [ 14, 16, 18, 19 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@ -66,3 +66,19 @@ jobs:
- run: npm install
- run: npm run build
- run: npm run cy:test
frontend-unit-tests:
needs: [ check-linters ]
runs-on: ubuntu-latest
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- name: Use Node.js 14
uses: actions/setup-node@v3
with:
node-version: 14
cache: 'npm'
- run: npm install
- run: npm run build
- run: npm run cy:run:unit

View File

@ -12,13 +12,11 @@ jobs:
- uses: actions/stale@v5
with:
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for 2 days with no activity.'
days-before-stale: 90
days-before-close: 2
days-before-pr-stale: 999999999
days-before-pr-close: 1
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,feature-request'
exempt-issue-assignees: 'louislam'
exempt-pr-assignees: 'louislam'
operations-per-run: 200

4
.gitignore vendored
View File

@ -16,3 +16,7 @@ dist-ssr
cypress/videos
cypress/screenshots
/extra/healthcheck.exe
/extra/healthcheck
/extra/healthcheck-armv7

View File

@ -1,6 +1,6 @@
# Project Info
First of all, thank you everyone who made pull requests for Uptime Kuma, I never thought GitHub Community can be that nice! And also because of this, I also never thought other people actually read my code and edit my code. It is not structured and commented so well, lol. Sorry about that.
First of all, I want to thank everyone who made pull requests for Uptime Kuma. I never thought the GitHub Community would be so nice! Because of this, I also never thought that other people would actually read and edit my code. It is not very well structured or commented, sorry about that.
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for server part. Both frontend and backend share the same package.json.
@ -27,7 +27,7 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
## Can I create a pull request for Uptime Kuma?
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can discuss first**. Especially for a large pull request or you don't know it will be merged or not.
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**. Especially for a large pull request or you don't know it will be merged or not.
Here are some references:
@ -48,8 +48,13 @@ Here are some references:
- UI/UX is not close to Uptime Kuma
- Existing logic is completely modified or deleted for no reason
- A function that is completely out of scope
- Convert existing code into other programming languages
- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
The above cases cannot cover all situations.
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
@ -72,9 +77,9 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
## Project Styles
I personally do not like something need to learn so much and need to config so much before you can finally start the app.
I personally do not like it when something requires so much learning and configuration before you can finally start the app.
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort to get it run
- Easy to install for non-Docker users, no native build dependency is needed (at least for x86_64), no extra config, no extra effort required to get it running
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`.
- Easy to use
@ -172,15 +177,11 @@ The data and socket logic are in `src/mixins/socket.js`.
## Unit Test
It is an end-to-end testing. It is using Jest and Puppeteer.
```bash
npm run build
npm test
```
By default, the Chromium window will be shown up during the test. Specifying `HEADLESS_TEST=1` for terminal environments.
## Dependencies
Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not used in the production environment, because it is usually also baked into dist files. So:

View File

@ -7,9 +7,9 @@
<img src="./public/icon.svg" width="128" alt="" />
</div>
It is a self-hosted monitoring tool like "Uptime Robot".
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
<img src="https://uptime.kuma.pet/img/dark.jpg" width="700" alt="" />
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
## 🥔 Live Demo
@ -22,17 +22,17 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
* Fancy, Reactive, Fast UI/UX.
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
* 20 second intervals.
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers
* Fancy, Reactive, Fast UI/UX
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
* 20 second intervals
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/languages)
* Multiple Status Pages
* Map Status Page to Domain
* Ping Chart
* Certificate Info
* Proxy Support
* 2FA available
* Multiple status pages
* Map status pages to specific domains
* Ping chart
* Certificate info
* Proxy support
* 2FA support
## 🔧 How to Install
@ -44,14 +44,14 @@ docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name upti
⚠️ Please use a **local volume** only. Other types such as NFS are not supported.
Browse to http://localhost:3001 after starting.
Uptime Kuma is now running on http://localhost:3001
### 💪🏻 Non-Docker
Required Tools:
- [Node.js](https://nodejs.org/en/download/) >= 14
- [Git](https://git-scm.com/downloads)
- [pm2](https://pm2.keymetrics.io/) - For run in background
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
```bash
# Update your npm to the latest version
@ -73,7 +73,7 @@ pm2 start server/server.js --name uptime-kuma
```
Browse to http://localhost:3001 after starting.
Uptime Kuma is now running on http://localhost:3001
More useful PM2 Commands

View File

@ -2,9 +2,9 @@
## Reporting a Vulnerability
Please report security issues to uptime@kuma.pet.
Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
Do not use the issue tracker or discuss it in the public as it will cause more damage.
Do not use the public issue tracker or discuss it in the public as it will cause more damage.
## Supported Versions

View File

@ -0,0 +1,10 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
supportFile: false,
specPattern: [
"test/cypress/unit/**/*.js"
],
}
});

25
db/patch-grpc-monitor.sql Normal file
View File

@ -0,0 +1,25 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD grpc_url VARCHAR(255) default null;
ALTER TABLE monitor
ADD grpc_protobuf TEXT default null;
ALTER TABLE monitor
ADD grpc_body TEXT default null;
ALTER TABLE monitor
ADD grpc_metadata TEXT default null;
ALTER TABLE monitor
ADD grpc_method VARCHAR(255) default null;
ALTER TABLE monitor
ADD grpc_service_name VARCHAR(255) default null;
ALTER TABLE monitor
ADD grpc_enable_tls BOOLEAN default 0 not null;
COMMIT;

View File

@ -0,0 +1,83 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- Just for someone who tested maintenance before (patch-maintenance-table.sql)
DROP TABLE IF EXISTS maintenance_status_page;
DROP TABLE IF EXISTS monitor_maintenance;
DROP TABLE IF EXISTS maintenance;
DROP TABLE IF EXISTS maintenance_timeslot;
-- maintenance
CREATE TABLE [maintenance] (
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[title] VARCHAR(150) NOT NULL,
[description] TEXT NOT NULL,
[user_id] INTEGER REFERENCES [user]([id]) ON DELETE SET NULL ON UPDATE CASCADE,
[active] BOOLEAN NOT NULL DEFAULT 1,
[strategy] VARCHAR(50) NOT NULL DEFAULT 'single',
[start_date] DATETIME,
[end_date] DATETIME,
[start_time] TIME,
[end_time] TIME,
[weekdays] VARCHAR2(250) DEFAULT '[]',
[days_of_month] TEXT DEFAULT '[]',
[interval_day] INTEGER
);
CREATE INDEX [manual_active] ON [maintenance] (
[strategy],
[active]
);
CREATE INDEX [active] ON [maintenance] ([active]);
CREATE INDEX [maintenance_user_id] ON [maintenance] ([user_id]);
-- maintenance_status_page
CREATE TABLE maintenance_status_page (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
status_page_id INTEGER NOT NULL,
maintenance_id INTEGER NOT NULL,
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT FK_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX [status_page_id_index]
ON [maintenance_status_page]([status_page_id]);
CREATE INDEX [maintenance_id_index]
ON [maintenance_status_page]([maintenance_id]);
-- maintenance_timeslot
CREATE TABLE [maintenance_timeslot] (
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[maintenance_id] INTEGER NOT NULL CONSTRAINT [FK_maintenance] REFERENCES [maintenance]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
[start_date] DATETIME NOT NULL,
[end_date] DATETIME,
[generated_next] BOOLEAN DEFAULT 0
);
CREATE INDEX [maintenance_id] ON [maintenance_timeslot] ([maintenance_id] DESC);
CREATE INDEX [active_timeslot_index] ON [maintenance_timeslot] (
[maintenance_id] DESC,
[start_date] DESC,
[end_date] DESC
);
CREATE INDEX [generated_next_index] ON [maintenance_timeslot] ([generated_next]);
-- monitor_maintenance
CREATE TABLE monitor_maintenance (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
monitor_id INTEGER NOT NULL,
maintenance_id INTEGER NOT NULL,
CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX [maintenance_id_index2] ON [monitor_maintenance]([maintenance_id]);
CREATE INDEX [monitor_id_index] ON [monitor_maintenance]([monitor_id]);
COMMIT;

View File

@ -4,5 +4,5 @@ WORKDIR /app
# Install apprise, iputils for non-root ping, setpriv
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \
pip3 --no-cache-dir install apprise==1.0.0 && \
pip3 --no-cache-dir install apprise==1.2.1 && \
rm -rf /root/.cache

View File

@ -0,0 +1,16 @@
############################################
# Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
############################################
FROM golang:1.19.4-buster
WORKDIR /app
ARG TARGETPLATFORM
COPY ./extra/ ./extra/
# Compile healthcheck.go
RUN apt update && \
apt --yes --no-install-recommends install curl && \
curl -sL https://deb.nodesource.com/setup_18.x | bash && \
apt --yes --no-install-recommends install nodejs && \
node ./extra/build-healthcheck.js $TARGETPLATFORM && \
apt --yes remove nodejs

View File

@ -11,7 +11,7 @@ WORKDIR /app
RUN apt update && \
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise==1.0.0 && \
pip3 --no-cache-dir install apprise==1.2.1 && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove

View File

@ -1,30 +1,50 @@
############################################
# Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
# Check file: builder-go.dockerfile
############################################
FROM louislam/uptime-kuma:builder-go AS build_healthcheck
############################################
# Build in Node.js
############################################
FROM louislam/uptime-kuma:base-debian AS build
WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
COPY .npmrc .npmrc
COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm ci --omit=dev
COPY . .
RUN npm ci --production && \
chmod +x /app/extra/entrypoint.sh
COPY --from=build_healthcheck /app/extra/healthcheck /app/extra/healthcheck
RUN chmod +x /app/extra/entrypoint.sh
############################################
# ⭐ Main Image
############################################
FROM louislam/uptime-kuma:base-debian AS release
WORKDIR /app
# 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
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
CMD ["node", "server/server.js"]
############################################
# Mark as Nightly
############################################
FROM release AS nightly
RUN npm run mark-as-nightly
############################################
# Build an image for testing pr
############################################
FROM louislam/uptime-kuma:base-debian AS pr-test
WORKDIR /app
@ -54,8 +74,9 @@ VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
CMD ["npm", "run", "start-pr-test"]
############################################
# Upload the artifact to Github
############################################
FROM louislam/uptime-kuma:base-debian AS upload-artifact
WORKDIR /
RUN apt update && \

View File

@ -3,10 +3,12 @@ WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
COPY .npmrc .npmrc
COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm ci --omit=dev
COPY . .
RUN npm ci --production && \
chmod +x /app/extra/entrypoint.sh
RUN chmod +x /app/extra/entrypoint.sh
FROM louislam/uptime-kuma:base-alpine AS release
WORKDIR /app

View File

@ -32,6 +32,10 @@ if (! exists) {
process.exit(1);
}
/**
* Commit updated files
* @param {string} version Version to update to
*/
function commit(version) {
let msg = "Update to " + version;
@ -47,6 +51,10 @@ function commit(version) {
console.log(res.stdout.toString().trim());
}
/**
* Create a tag with the specified version
* @param {string} version Tag to create
*/
function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim());
@ -55,6 +63,11 @@ function tag(version) {
console.log(res.stdout.toString().trim());
}
/**
* Check if a tag exists for the specified version
* @param {string} version Version to check
* @returns {boolean} Does the tag already exist
*/
function tagExists(version) {
if (! version) {
throw new Error("invalid version");

View File

@ -0,0 +1,27 @@
const childProcess = require("child_process");
const fs = require("fs");
const platform = process.argv[2];
if (!platform) {
console.error("No platform??");
process.exit(1);
}
if (platform === "linux/arm/v7") {
console.log("Arch: armv7");
if (fs.existsSync("./extra/healthcheck-armv7")) {
fs.renameSync("./extra/healthcheck-armv7", "./extra/healthcheck");
console.log("Already built in the host, skip.");
process.exit(0);
} else {
console.log("prebuilt not found, it will be slow! You should execute `npm run build-healthcheck-armv7` before build.");
}
} else {
if (fs.existsSync("./extra/healthcheck-armv7")) {
fs.rmSync("./extra/healthcheck-armv7");
}
}
const output = childProcess.execSync("go build -x -o ./extra/healthcheck ./extra/healthcheck.go").toString("utf8");
console.log(output);

View File

@ -25,6 +25,10 @@ if (platform === "linux/amd64") {
const file = fs.createWriteStream("cloudflared.deb");
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
/**
* Download specified file
* @param {string} url URL to request
*/
function get(url) {
http.get(url, function (res) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {

81
extra/healthcheck.go Normal file
View File

@ -0,0 +1,81 @@
/*
* If changed, have to run `npm run build-docker-builder-go`.
* This script should be run after a period of time (180s), because the server may need some time to prepare.
*/
package main
import (
"crypto/tls"
"io/ioutil"
"log"
"net/http"
"os"
"runtime"
"time"
)
func main() {
isFreeBSD := runtime.GOOS == "freebsd"
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
client := http.Client{
Timeout: 28 * time.Second,
}
sslKey := os.Getenv("UPTIME_KUMA_SSL_KEY")
if len(sslKey) == 0 {
sslKey = os.Getenv("SSL_KEY")
}
sslCert := os.Getenv("UPTIME_KUMA_SSL_CERT")
if len(sslCert) == 0 {
sslCert = os.Getenv("SSL_CERT")
}
hostname := os.Getenv("UPTIME_KUMA_HOST")
if len(hostname) == 0 && !isFreeBSD {
hostname = os.Getenv("HOST")
}
if len(hostname) == 0 {
hostname = "127.0.0.1"
}
port := os.Getenv("UPTIME_KUMA_PORT")
if len(port) == 0 {
port = os.Getenv("PORT")
}
if len(port) == 0 {
port = "3001"
}
protocol := ""
if len(sslKey) != 0 && len(sslCert) != 0 {
protocol = "https"
} else {
protocol = "http"
}
url := protocol + "://" + hostname + ":" + port
log.Println("Checking " + url)
resp, err := client.Get(url)
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
}
log.Printf("Health Check OK [Res Code: %d]\n", resp.StatusCode)
}

View File

@ -1,4 +1,5 @@
/*
* Deprecated: Changed to healthcheck.go, it will be deleted in the future.
* This script should be run after a period of time (180s), because the server may need some time to prepare.
*/
const { FBSD } = require("../server/util-server");

View File

@ -5,7 +5,7 @@ const util = require("../src/util");
util.polyfill();
const oldVersion = pkg.version;
const newVersion = oldVersion + "-nightly";
const newVersion = oldVersion + "-nightly-" + util.genSecret(8);
console.log("Old Version: " + oldVersion);
console.log("New Version: " + newVersion);

View File

@ -43,6 +43,11 @@ const main = async () => {
console.log("Finished.");
};
/**
* Ask question of user
* @param {string} question Question to ask
* @returns {Promise<string>} Users response
*/
function question(question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {

View File

@ -53,6 +53,11 @@ const main = async () => {
console.log("Finished.");
};
/**
* Ask question of user
* @param {string} question Question to ask
* @returns {Promise<string>} Users response
*/
function question(question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {

View File

@ -135,6 +135,11 @@ server.listen({
udp: 5300
});
/**
* Get human readable request type from request code
* @param {number} code Request code to translate
* @returns {string} Human readable request type
*/
function type(code) {
for (let name in Packet.TYPE) {
if (Packet.TYPE[name] === code) {

View File

@ -11,6 +11,7 @@ class SimpleMqttServer {
this.port = port;
}
/** Start the MQTT server */
start() {
this.server.listen(this.port, () => {
console.log("server started and listening on port ", this.port);

View File

@ -36,10 +36,8 @@ if (! exists) {
}
/**
* Updates the version number in package.json and commits it to git.
* @param {string} version - The new version number
*
* Generated by Trelent
* Commit updated files
* @param {string} version Version to update to
*/
function commit(version) {
let msg = "Update to " + version;
@ -53,16 +51,19 @@ function commit(version) {
}
}
/**
* Create a tag with the specified version
* @param {string} version Tag to create
*/
function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim());
}
/**
* Checks if a given version is already tagged in the git repository.
* @param {string} version - The version to check for.
*
* Generated by Trelent
* Check if a tag exists for the specified version
* @param {string} version Version to check
* @returns {boolean} Does the tag already exist
*/
function tagExists(version) {
if (! version) {

View File

@ -10,6 +10,10 @@ if (!newVersion) {
updateWiki(newVersion);
/**
* Update the wiki with new version number
* @param {string} newVersion Version to update to
*/
function updateWiki(newVersion) {
const wikiDir = "./tmp/wiki";
const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
@ -39,6 +43,10 @@ function updateWiki(newVersion) {
safeDelete(wikiDir);
}
/**
* Check if a directory exists and then delete it
* @param {string} dir Directory to delete
*/
function safeDelete(dir) {
if (fs.existsSync(dir)) {
fs.rm(dir, {

8606
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.18.5",
"version": "1.19.6",
"license": "MIT",
"repository": {
"type": "git",
@ -31,6 +31,7 @@
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine",
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push",
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push",
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/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:$VERSION-alpine --target release . --push",
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
@ -38,7 +39,7 @@
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.18.5 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.19.6 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@ -60,13 +61,17 @@
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
"cy:test": "node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/ --e2e",
"cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\""
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js",
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go"
},
"dependencies": {
"@louislam/sqlite3": "~15.0.6",
"@grpc/grpc-js": "~1.7.3",
"@louislam/ping": "~0.4.2-mod.1",
"@louislam/sqlite3": "15.1.2",
"args-parser": "~1.3.0",
"axios": "~0.27.0",
"axios-ntlm": "~1.3.0",
"axios-ntlm": "1.3.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
"bree": "~7.1.5",
@ -88,11 +93,13 @@
"https-proxy-agent": "~5.0.1",
"iconv-lite": "~0.6.3",
"jsesc": "~3.0.2",
"jsonwebtoken": "~8.5.1",
"jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2",
"limiter": "~2.1.0",
"mongodb": "~4.13.0",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mysql2": "~2.3.3",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
@ -102,9 +109,11 @@
"pg-connection-string": "~2.5.0",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"redbean-node": "0.1.4",
"socket.io": "~4.4.1",
"socket.io-client": "~4.4.1",
"protobufjs": "~7.1.1",
"redbean-node": "~0.2.0",
"redis": "~4.5.1",
"socket.io": "~4.5.3",
"socket.io-client": "~4.5.3",
"socks-proxy-agent": "6.1.1",
"tar": "~6.1.11",
"tcp-ping": "~0.1.1",
@ -123,13 +132,14 @@
"@vitejs/plugin-legacy": "~2.1.0",
"@vitejs/plugin-vue": "~3.1.0",
"@vue/compiler-sfc": "~3.2.36",
"@vuepic/vue-datepicker": "~3.4.8",
"aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3",
"chart.js": "~3.6.2",
"chartjs-adapter-dayjs": "~1.0.0",
"concurrently": "^7.1.0",
"core-js": "~3.18.3",
"core-js": "~3.26.1",
"cross-env": "~7.0.3",
"cypress": "^10.1.0",
"delay": "^5.0.0",

View File

@ -63,6 +63,12 @@ function myAuthorizer(username, password, callback) {
});
}
/**
* Use basic auth if auth is not disabled
* @param {express.Request} req Express request object
* @param {express.Response} res Express response object
* @param {express.NextFunction} next
*/
exports.basicAuth = async function (req, res, next) {
const middleware = basicAuth({
authorizer: myAuthorizer,

View File

@ -1,6 +1,8 @@
const https = require("https");
const http = require("http");
const CacheableLookup = require("cacheable-lookup");
const { Settings } = require("./settings");
const { log } = require("../src/util");
class CacheableDnsHttpAgent {
@ -9,14 +11,36 @@ class CacheableDnsHttpAgent {
static httpAgentList = {};
static httpsAgentList = {};
static enable = false;
/**
* Register cacheable to global agents
* Register/Disable cacheable to global agents
*/
static registerGlobalAgent() {
static async update() {
log.debug("CacheableDnsHttpAgent", "update");
let isEnable = await Settings.get("dnsCache");
if (isEnable !== this.enable) {
log.debug("CacheableDnsHttpAgent", "value changed");
if (isEnable) {
log.debug("CacheableDnsHttpAgent", "enable");
this.cacheable.install(http.globalAgent);
this.cacheable.install(https.globalAgent);
} else {
log.debug("CacheableDnsHttpAgent", "disable");
this.cacheable.uninstall(http.globalAgent);
this.cacheable.uninstall(https.globalAgent);
}
}
this.enable = isEnable;
}
/**
* Attach cacheable to HTTP agent
* @param {http.Agent} agent Agent to install
*/
static install(agent) {
this.cacheable.install(agent);
}
@ -26,6 +50,10 @@ class CacheableDnsHttpAgent {
* @return {https.Agent}
*/
static getHttpsAgent(agentOptions) {
if (!this.enable) {
return new https.Agent(agentOptions);
}
let key = JSON.stringify(agentOptions);
if (!(key in this.httpsAgentList)) {
this.httpsAgentList[key] = new https.Agent(agentOptions);
@ -39,6 +67,10 @@ class CacheableDnsHttpAgent {
* @return {https.Agents}
*/
static getHttpAgent(agentOptions) {
if (!this.enable) {
return new http.Agent(agentOptions);
}
let key = JSON.stringify(agentOptions);
if (!(key in this.httpAgentList)) {
this.httpAgentList[key] = new http.Agent(agentOptions);

View File

@ -25,7 +25,7 @@ exports.startInterval = () => {
let checkBeta = await setting("checkBeta");
if (checkBeta && res.data.beta) {
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
if (compareVersions.compare(res.data.beta, res.data.slow, ">")) {
exports.latestVersion = res.data.beta;
return;
}

View File

@ -4,7 +4,8 @@
const { TimeLogger } = require("../src/util");
const { R } = require("redbean-node");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const io = UptimeKumaServer.getInstance().io;
const server = UptimeKumaServer.getInstance();
const io = server.io;
const { setting } = require("./util-server");
const checkVersion = require("./check-version");
@ -121,7 +122,9 @@ async function sendInfo(socket) {
socket.emit("info", {
version: checkVersion.version,
latestVersion: checkVersion.latestVersion,
primaryBaseURL: await setting("primaryBaseURL")
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),
});
}

View File

@ -62,8 +62,10 @@ class Database {
"patch-add-clickable-status-page-link.sql": true,
"patch-add-sqlserver-monitor.sql": true,
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
"patch-grpc-monitor.sql": true,
"patch-add-radius-monitor.sql": true,
"patch-monitor-add-resend-interval.sql": true,
"patch-maintenance-table2.sql": true,
};
/**
@ -150,9 +152,6 @@ class Database {
await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = FULL");
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
await R.exec("PRAGMA busy_timeout = 5000");
// This ensures that an operating system crash or power failure will not corrupt the database.
// FULL synchronous is very safe, but it is also slower.
// Read more: https://sqlite.org/pragma.html#pragma_synchronous

View File

@ -32,6 +32,7 @@ const initBackgroundJobs = function (args) {
return bree;
};
/** Stop all background jobs if running */
const stopBackgroundJobs = function () {
if (bree) {
bree.stop();

View File

@ -25,6 +25,10 @@ const DEFAULT_KEEP_PERIOD = 180;
parsedPeriod = DEFAULT_KEEP_PERIOD;
}
if (parsedPeriod < 1) {
log(`Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
} else {
log(`Clearing Data older than ${parsedPeriod} days...`);
try {
@ -35,6 +39,7 @@ const DEFAULT_KEEP_PERIOD = 180;
} catch (e) {
log(`Failed to clear old data: ${e.message}`);
}
}
exit();
})();

View File

@ -1,8 +1,3 @@
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc");
let timezone = require("dayjs/plugin/timezone");
dayjs.extend(utc);
dayjs.extend(timezone);
const { BeanModel } = require("redbean-node/dist/bean-model");
/**
@ -10,6 +5,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
* 0 = DOWN
* 1 = UP
* 2 = PENDING
* 3 = MAINTENANCE
*/
class Heartbeat extends BeanModel {

240
server/model/maintenance.js Normal file
View File

@ -0,0 +1,240 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
class Maintenance extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON() {
let dateRange = [];
if (this.start_date) {
dateRange.push(utcToLocal(this.start_date));
if (this.end_date) {
dateRange.push(utcToLocal(this.end_date));
}
}
let timeRange = [];
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
timeRange.push(startTime);
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
timeRange.push(endTime);
let obj = {
id: this.id,
title: this.title,
description: this.description,
strategy: this.strategy,
intervalDay: this.interval_day,
active: !!this.active,
dateRange: dateRange,
timeRange: timeRange,
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
timeslotList: [],
};
const timeslotList = await this.getTimeslotList();
for (let timeslot of timeslotList) {
obj.timeslotList.push(await timeslot.toPublicJSON());
}
if (!Array.isArray(obj.weekdays)) {
obj.weekdays = [];
}
if (!Array.isArray(obj.daysOfMonth)) {
obj.daysOfMonth = [];
}
// Maintenance Status
if (!obj.active) {
obj.status = "inactive";
} else if (obj.strategy === "manual") {
obj.status = "under-maintenance";
} else if (obj.timeslotList.length > 0) {
let currentTimestamp = dayjs().unix();
for (let timeslot of obj.timeslotList) {
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
obj.status = "under-maintenance";
break;
}
}
if (!obj.status) {
obj.status = "scheduled";
}
} else if (obj.timeslotList.length === 0) {
obj.status = "ended";
} else {
obj.status = "unknown";
}
return obj;
}
/**
* Only get future or current timeslots only
* @returns {Promise<[]>}
*/
async getTimeslotList() {
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
SELECT maintenance_timeslot.*
FROM maintenance_timeslot, maintenance
WHERE maintenance_timeslot.maintenance_id = maintenance.id
AND maintenance.id = ?
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
`, [
this.id
]));
}
/**
* Return an object that ready to parse to JSON
* @param {string} timezone If not specified, the timeRange will be in UTC
* @returns {Object}
*/
async toJSON(timezone = null) {
return this.toPublicJSON(timezone);
}
/**
* Get a list of weekdays that the maintenance is active for
* Monday=1, Tuesday=2 etc.
* @returns {number[]} Array of active weekdays
*/
getDayOfWeekList() {
log.debug("timeslot", "List: " + this.weekdays);
return JSON.parse(this.weekdays).sort(function (a, b) {
return a - b;
});
}
/**
* Get a list of days in month that maintenance is active for
* @returns {number[]} Array of active days in month
*/
getDayOfMonthList() {
return JSON.parse(this.days_of_month).sort(function (a, b) {
return a - b;
});
}
/**
* Get the start date and time for maintenance
* @returns {dayjs.Dayjs} Start date and time
*/
getStartDateTime() {
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
// Start Time
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
log.debug("timeslot", "startTime: " + startTimeSecond);
// Bake StartDate + StartTime = Start DateTime
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
}
/**
* Get the duraction of maintenance in seconds
* @returns {number} Duration of maintenance
*/
getDuration() {
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
// Add 24hours if it is across day
if (duration < 0) {
duration += 24 * 3600;
}
return duration;
}
/**
* Convert data from socket to bean
* @param {Bean} bean Bean to fill in
* @param {Object} obj Data to fill bean with
* @returns {Bean} Filled bean
*/
static jsonToBean(bean, obj) {
if (obj.id) {
bean.id = obj.id;
}
// Apply timezone offset to timeRange, as it cannot apply automatically.
if (obj.timeRange[0]) {
timeObjectToUTC(obj.timeRange[0]);
if (obj.timeRange[1]) {
timeObjectToUTC(obj.timeRange[1]);
}
}
bean.title = obj.title;
bean.description = obj.description;
bean.strategy = obj.strategy;
bean.interval_day = obj.intervalDay;
bean.active = obj.active;
if (obj.dateRange[0]) {
bean.start_date = localToUTC(obj.dateRange[0]);
if (obj.dateRange[1]) {
bean.end_date = localToUTC(obj.dateRange[1]);
}
}
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
bean.weekdays = JSON.stringify(obj.weekdays);
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
return bean;
}
/**
* SQL conditions for active maintenance
* @returns {string}
*/
static getActiveMaintenanceSQLCondition() {
return `
(
(maintenance_timeslot.start_date <= DATETIME('now')
AND maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1)
)
`;
}
/**
* SQL conditions for active and future maintenance
* @returns {string}
*/
static getActiveAndFutureMaintenanceSQLCondition() {
return `
(
((maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1))
)
`;
}
}
module.exports = Maintenance;

View File

@ -0,0 +1,198 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class MaintenanceTimeslot extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON() {
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
const obj = {
id: this.id,
startDate: this.start_date,
endDate: this.end_date,
startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
serverTimezoneOffset,
};
return obj;
}
/**
* Return an object that ready to parse to JSON
* @returns {Object}
*/
async toJSON() {
return await this.toPublicJSON();
}
/**
* @param {Maintenance} maintenance
* @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
* @param {boolean} removeExist Remove existing timeslot before create
* @returns {Promise<MaintenanceTimeslot>}
*/
static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
if (removeExist) {
await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
maintenance.id
]);
}
if (maintenance.strategy === "manual") {
log.debug("maintenance", "No need to generate timeslot for manual type");
} else if (maintenance.strategy === "single") {
let bean = R.dispense("maintenance_timeslot");
bean.maintenance_id = maintenance.id;
bean.start_date = maintenance.start_date;
bean.end_date = maintenance.end_date;
bean.generated_next = true;
return await R.store(bean);
} else if (maintenance.strategy === "recurring-interval") {
// Prevent dead loop, in case interval_day is not set
if (!maintenance.interval_day || maintenance.interval_day <= 0) {
maintenance.interval_day = 1;
}
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
return startDateTime.add(maintenance.interval_day, "day");
}, () => {
return true;
});
} else if (maintenance.strategy === "recurring-weekday") {
let dayOfWeekList = maintenance.getDayOfWeekList();
log.debug("timeslot", dayOfWeekList);
if (dayOfWeekList.length <= 0) {
log.debug("timeslot", "No weekdays selected?");
return null;
}
const isValid = (startDateTime) => {
log.debug("timeslot", "nextDateTime: " + startDateTime);
let day = startDateTime.local().day();
log.debug("timeslot", "nextDateTime.day(): " + day);
return dayOfWeekList.includes(day);
};
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
while (true) {
startDateTime = startDateTime.add(1, "day");
if (isValid(startDateTime)) {
return startDateTime;
}
}
}, isValid);
} else if (maintenance.strategy === "recurring-day-of-month") {
let dayOfMonthList = maintenance.getDayOfMonthList();
if (dayOfMonthList.length <= 0) {
log.debug("timeslot", "No day selected?");
return null;
}
const isValid = (startDateTime) => {
let day = parseInt(startDateTime.local().format("D"));
log.debug("timeslot", "day: " + day);
// Check 1-31
if (dayOfMonthList.includes(day)) {
return startDateTime;
}
// Check "lastDay1","lastDay2"...
let daysInMonth = startDateTime.daysInMonth();
let lastDayList = [];
// Small first, e.g. 28 > 29 > 30 > 31
for (let i = 4; i >= 1; i--) {
if (dayOfMonthList.includes("lastDay" + i)) {
lastDayList.push(daysInMonth - i + 1);
}
}
log.debug("timeslot", lastDayList);
return lastDayList.includes(day);
};
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
while (true) {
startDateTime = startDateTime.add(1, "day");
if (isValid(startDateTime)) {
return startDateTime;
}
}
}, isValid);
} else {
throw new Error("Unknown maintenance strategy");
}
}
/**
* Generate a next timeslot for all recurring types
* @param maintenance
* @param minDate
* @param {function} nextDayCallback The logic how to get the next possible day
* @param {function} isValidCallback Check the day whether is matched the current strategy
* @returns {Promise<null|MaintenanceTimeslot>}
*/
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
let bean = R.dispense("maintenance_timeslot");
let duration = maintenance.getDuration();
let startDateTime = maintenance.getStartDateTime();
let endDateTime;
// Keep generating from the first possible date, until it is ok
while (true) {
log.debug("timeslot", "startDateTime: " + startDateTime.format());
// Handling out of effective date range
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
log.debug("timeslot", "Out of effective date range");
return null;
}
endDateTime = startDateTime.add(duration, "second");
// If endDateTime is out of effective date range, use the end datetime from effective date range
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
endDateTime = dayjs.utc(maintenance.end_date);
}
// If minDate is set, the endDateTime must be bigger than it.
// And the endDateTime must be bigger current time
// Is valid under current recurring strategy
if (
(!minDate || endDateTime.diff(minDate) > 0) &&
endDateTime.diff(dayjs()) > 0 &&
isValidCallback(startDateTime)
) {
break;
}
startDateTime = nextDayCallback(startDateTime);
}
bean.maintenance_id = maintenance.id;
bean.start_date = localToUTC(startDateTime);
bean.end_date = localToUTC(endDateTime);
bean.generated_next = false;
return await R.store(bean);
}
}
module.exports = MaintenanceTimeslot;

View File

@ -1,13 +1,11 @@
const https = require("https");
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc");
let timezone = require("dayjs/plugin/timezone");
dayjs.extend(utc);
dayjs.extend(timezone);
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing,
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification");
@ -18,12 +16,15 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker");
const Maintenance = require("./maintenance");
const { UptimeCacheList } = require("../uptime-cache-list");
/**
* status:
* 0 = DOWN
* 1 = UP
* 2 = PENDING
* 3 = MAINTENANCE
*/
class Monitor extends BeanModel {
@ -90,26 +91,23 @@ class Monitor extends BeanModel {
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
pushToken: this.pushToken,
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList,
tags: tags,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
maintenance: await Monitor.isUnderMaintenance(this.id),
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
databaseConnectionString: this.databaseConnectionString,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
radiusUsername: this.radiusUsername,
radiusPassword: this.radiusPassword,
grpcUrl: this.grpcUrl,
grpcProtobuf: this.grpcProtobuf,
grpcMethod: this.grpcMethod,
grpcServiceName: this.grpcServiceName,
grpcEnableTls: this.getGrpcEnableTls(),
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
radiusSecret: this.radiusSecret,
};
if (includeSensitiveData) {
@ -117,12 +115,23 @@ class Monitor extends BeanModel {
...data,
headers: this.headers,
body: this.body,
grpcBody: this.grpcBody,
grpcMetadata: this.grpcMetadata,
basic_auth_user: this.basic_auth_user,
basic_auth_pass: this.basic_auth_pass,
pushToken: this.pushToken,
databaseConnectionString: this.databaseConnectionString,
radiusUsername: this.radiusUsername,
radiusPassword: this.radiusPassword,
radiusSecret: this.radiusSecret,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
};
}
data.includeSensitiveData = includeSensitiveData;
return data;
}
@ -167,6 +176,14 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown);
}
/**
* Parse to boolean
* @returns {boolean}
*/
getGrpcEnableTls() {
return Boolean(this.grpcEnableTls);
}
/**
* Get accepted status codes
* @returns {Object}
@ -230,7 +247,10 @@ class Monitor extends BeanModel {
}
try {
if (this.type === "http" || this.type === "keyword") {
if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance";
bean.status = MAINTENANCE;
} else if (this.type === "http" || this.type === "keyword") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();
@ -249,6 +269,7 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
// Axios Options
const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
@ -287,20 +308,8 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);
let res;
if (this.auth_method === "ntlm") {
options.httpsAgent.keepAlive = true;
res = await httpNtlm(options, {
username: this.basic_auth_user,
password: this.basic_auth_pass,
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axios.request(options);
}
// Make Request
let res = await this.makeAxiosRequest(options);
bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime;
@ -486,14 +495,18 @@ class Monitor extends BeanModel {
const options = {
url: `/containers/${this.docker_container}/json`,
timeout: this.interval * 1000 * 0.8,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}),
};
if (dockerHost._dockerType === "socket") {
@ -506,7 +519,9 @@ class Monitor extends BeanModel {
let res = await axios.request(options);
if (res.data.State.Running) {
bean.status = UP;
bean.msg = "";
bean.msg = res.data.State.Status;
} else {
throw Error("Container State is " + res.data.State.Status);
}
} else if (this.type === "mqtt") {
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
@ -524,6 +539,37 @@ class Monitor extends BeanModel {
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "grpc-keyword") {
let startTime = dayjs().valueOf();
const options = {
grpcUrl: this.grpcUrl,
grpcProtobufData: this.grpcProtobuf,
grpcServiceName: this.grpcServiceName,
grpcEnableTls: this.grpcEnableTls,
grpcMethod: this.grpcMethod,
grpcBody: this.grpcBody,
keyword: this.keyword
};
const response = await grpcQuery(options);
bean.ping = dayjs().valueOf() - startTime;
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
let responseData = response.data;
if (responseData.length > 50) {
responseData = responseData.toString().substring(0, 47) + "...";
}
if (response.code !== 1) {
bean.status = DOWN;
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
} else {
if (response.data.toString().includes(this.keyword)) {
bean.status = UP;
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
} else {
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
bean.status = DOWN;
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
}
}
} else if (this.type === "postgres") {
let startTime = dayjs().valueOf();
@ -532,8 +578,36 @@ class Monitor extends BeanModel {
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mysql") {
let startTime = dayjs().valueOf();
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mongodb") {
let startTime = dayjs().valueOf();
await mongodbPing(this.databaseConnectionString);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "radius") {
let startTime = dayjs().valueOf();
// Handle monitors that were created before the
// update and as such don't have a value for
// this.port.
let port;
if (this.port == null) {
port = 1812;
} else {
port = this.port;
}
try {
const resp = await radius(
this.hostname,
@ -541,7 +615,8 @@ class Monitor extends BeanModel {
this.radiusPassword,
this.radiusCalledStationId,
this.radiusCallingStationId,
this.radiusSecret
this.radiusSecret,
port
);
if (resp.code) {
bean.msg = resp.code;
@ -556,6 +631,12 @@ class Monitor extends BeanModel {
}
}
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "redis") {
let startTime = dayjs().valueOf();
bean.msg = await redisPingAsync(this.databaseConnectionString);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;
@ -594,8 +675,12 @@ class Monitor extends BeanModel {
if (isImportant) {
bean.important = true;
if (Monitor.isImportantForNotification(isFirstBeat, previousBeat?.status, bean.status)) {
log.debug("monitor", `[${this.name}] sendNotification`);
await Monitor.sendNotification(isFirstBeat, this, bean);
} else {
log.debug("monitor", `[${this.name}] will not sendNotification because it is (or was) under maintenance`);
}
// Reset down count
bean.downCount = 0;
@ -604,6 +689,8 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] apicache clear`);
apicache.clear();
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
} else {
bean.important = false;
@ -627,11 +714,14 @@ class Monitor extends BeanModel {
beatInterval = this.retryInterval;
}
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else if (bean.status === MAINTENANCE) {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
} else {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
}
log.debug("monitor", `[${this.name}] Send to socket`);
UptimeCacheList.clearCache(this.id);
io.to(this.user_id).emit("heartbeat", bean.toJSON());
Monitor.sendStats(io, this.id, this.user_id);
@ -678,6 +768,47 @@ class Monitor extends BeanModel {
}
}
/**
* Make a request using axios
* @param {Object} options Options for Axios
* @param {boolean} finalCall Should this be the final call i.e
* don't retry on faliure
* @returns {Object} Axios response
*/
async makeAxiosRequest(options, finalCall = false) {
try {
let res;
if (this.auth_method === "ntlm") {
options.httpsAgent.keepAlive = true;
res = await httpNtlm(options, {
username: this.basic_auth_user,
password: this.basic_auth_pass,
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axios.request(options);
}
return res;
} catch (e) {
// Fix #2253
// Read more: https://stackoverflow.com/questions/1759956/curl-error-18-transfer-closed-with-outstanding-read-data-remaining
if (!finalCall && typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
log.debug("monitor", "makeAxiosRequest with gzip");
options.headers["Accept-Encoding"] = "gzip, deflate";
return this.makeAxiosRequest(options, true);
} else {
if (typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
e.message = "response timeout: incomplete response within a interval";
}
throw e;
}
}
}
/** Stop monitor */
stop() {
clearTimeout(this.heartbeatInterval);
@ -816,7 +947,15 @@ class Monitor extends BeanModel {
* @param {number} duration Hours
* @param {number} monitorID ID of monitor to calculate
*/
static async calcUptime(duration, monitorID) {
static async calcUptime(duration, monitorID, forceNoCache = false) {
if (!forceNoCache) {
let cachedUptime = UptimeCacheList.getUptime(monitorID, duration);
if (cachedUptime != null) {
return cachedUptime;
}
}
const timeLogger = new TimeLogger();
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
@ -837,7 +976,7 @@ class Monitor extends BeanModel {
-- SUM all uptime duration, also trim off the beat out of time window
SUM(
CASE
WHEN (status = 1)
WHEN (status = 1 OR status = 3)
THEN
CASE
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
@ -875,6 +1014,9 @@ class Monitor extends BeanModel {
}
}
// Cache
UptimeCacheList.addUptime(monitorID, duration, uptime);
return uptime;
}
@ -908,11 +1050,49 @@ class Monitor extends BeanModel {
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
let isImportant = isFirstBeat ||
// MAINTENANCE -> MAINTENANCE = not important
// * MAINTENANCE -> UP = important
// * MAINTENANCE -> DOWN = important
// * DOWN -> MAINTENANCE = important
// * UP -> MAINTENANCE = important
return isFirstBeat ||
(previousBeatStatus === DOWN && currentBeatStatus === MAINTENANCE) ||
(previousBeatStatus === UP && currentBeatStatus === MAINTENANCE) ||
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
(previousBeatStatus === MAINTENANCE && currentBeatStatus === UP) ||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
}
/**
* Is this beat important for notifications?
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
* @param {const} previousBeatStatus Status of the previous beat
* @param {const} currentBeatStatus Status of the current beat
* @returns {boolean} True if is an important beat else false
*/
static isImportantForNotification(isFirstBeat, previousBeatStatus, currentBeatStatus) {
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
// MAINTENANCE -> MAINTENANCE = not important
// MAINTENANCE -> UP = not important
// * MAINTENANCE -> DOWN = important
// DOWN -> MAINTENANCE = not important
// UP -> MAINTENANCE = not important
return isFirstBeat ||
(previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN) ||
(previousBeatStatus === UP && currentBeatStatus === DOWN) ||
(previousBeatStatus === DOWN && currentBeatStatus === UP) ||
(previousBeatStatus === PENDING && currentBeatStatus === DOWN);
return isImportant;
}
/**
@ -936,7 +1116,13 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), bean.toJSON());
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
const heartbeatJSON = bean.toJSON();
if (!heartbeatJSON["msg"]) {
heartbeatJSON["msg"] = "N/A";
}
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e);
@ -1049,6 +1235,36 @@ class Monitor extends BeanModel {
monitorID
]);
}
/**
* Check if monitor is under maintenance
* @param {number} monitorID ID of monitor to check
* @returns {Promise<boolean>}
*/
static async isUnderMaintenance(monitorID) {
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
const maintenance = await R.getRow(`
SELECT COUNT(*) AS count
FROM monitor_maintenance mm
JOIN maintenance
ON mm.maintenance_id = maintenance.id
AND mm.monitor_id = ?
LEFT JOIN maintenance_timeslot
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
LIMIT 1`, [ monitorID ]);
return maintenance.count !== 0;
}
/** Make sure monitor interval is between bounds */
validate() {
if (this.interval > MAX_INTERVAL_SECOND) {
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);
}
if (this.interval < MIN_INTERVAL_SECOND) {
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
}
}
}
module.exports = Monitor;

View File

@ -3,6 +3,7 @@ const { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const jsesc = require("jsesc");
const Maintenance = require("./maintenance");
class StatusPage extends BeanModel {
@ -37,7 +38,7 @@ class StatusPage extends BeanModel {
*/
static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML);
const description155 = statusPage.description?.substring(0, 155);
const description155 = statusPage.description?.substring(0, 155) ?? "";
$("title").text(statusPage.title);
$("meta[name=description]").attr("content", description155);
@ -90,6 +91,8 @@ class StatusPage extends BeanModel {
incident = incident.toPublicJSON();
}
let maintenanceList = await StatusPage.getMaintenanceList(statusPage.id);
// Public Group List
const publicGroupList = [];
const showTags = !!statusPage.show_tags;
@ -107,7 +110,8 @@ class StatusPage extends BeanModel {
return {
config: await statusPage.toPublicJSON(),
incident,
publicGroupList
publicGroupList,
maintenanceList,
};
}
@ -266,6 +270,38 @@ class StatusPage extends BeanModel {
}
}
/**
* Get list of maintenances
* @param {number} statusPageId ID of status page to get maintenance for
* @returns {Object} Object representing maintenances sanitized for public
*/
static async getMaintenanceList(statusPageId) {
try {
const publicMaintenanceList = [];
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
SELECT DISTINCT maintenance.*
FROM maintenance
JOIN maintenance_status_page
ON maintenance_status_page.maintenance_id = maintenance.id
AND maintenance_status_page.status_page_id = ?
LEFT JOIN maintenance_timeslot
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
ORDER BY maintenance.end_date
`, [ statusPageId ]));
for (const bean of maintenanceBeanList) {
publicMaintenanceList.push(await bean.toPublicJSON());
}
return publicMaintenanceList;
} catch (error) {
return [];
}
}
}
module.exports = StatusPage;

View File

@ -0,0 +1,20 @@
import { PluginFunc, ConfigType } from 'dayjs'
declare const plugin: PluginFunc
export = plugin
declare module 'dayjs' {
interface Dayjs {
tz(timezone?: string, keepLocalTime?: boolean): Dayjs
offsetName(type?: 'short' | 'long'): string | undefined
}
interface DayjsTimezone {
(date: ConfigType, timezone?: string): Dayjs
(date: ConfigType, format: string, timezone?: string): Dayjs
guess(): string
setDefault(timezone?: string): void
}
const tz: DayjsTimezone
}

View File

@ -0,0 +1,115 @@
/**
* Copy from node_modules/dayjs/plugin/timezone.js
* Try to fix https://github.com/louislam/uptime-kuma/issues/2318
* Source: https://github.com/iamkun/dayjs/tree/dev/src/plugin/utc
* License: MIT
*/
!function (t, e) {
// eslint-disable-next-line no-undef
typeof exports == "object" && typeof module != "undefined" ? module.exports = e() : typeof define == "function" && define.amd ? define(e) : (t = typeof globalThis != "undefined" ? globalThis : t || self).dayjs_plugin_timezone = e();
}(this, (function () {
"use strict";
let t = {
year: 0,
month: 1,
day: 2,
hour: 3,
minute: 4,
second: 5
};
let e = {};
return function (n, i, o) {
let r;
let a = function (t, n, i) {
void 0 === i && (i = {});
let o = new Date(t);
let r = function (t, n) {
void 0 === n && (n = {});
let i = n.timeZoneName || "short";
let o = t + "|" + i;
let r = e[o];
return r || (r = new Intl.DateTimeFormat("en-US", {
hour12: !1,
timeZone: t,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: i
}), e[o] = r), r;
}(n, i);
return r.formatToParts(o);
};
let u = function (e, n) {
let i = a(e, n);
let r = [];
let u = 0;
for (; u < i.length; u += 1) {
let f = i[u];
let s = f.type;
let m = f.value;
let c = t[s];
c >= 0 && (r[c] = parseInt(m, 10));
}
let d = r[3];
let l = d === 24 ? 0 : d;
let v = r[0] + "-" + r[1] + "-" + r[2] + " " + l + ":" + r[4] + ":" + r[5] + ":000";
let h = +e;
return (o.utc(v).valueOf() - (h -= h % 1e3)) / 6e4;
};
let f = i.prototype;
f.tz = function (t, e) {
void 0 === t && (t = r);
let n = this.utcOffset();
let i = this.toDate();
let a = i.toLocaleString("en-US", { timeZone: t }).replace("\u202f", " ");
let u = Math.round((i - new Date(a)) / 1e3 / 60);
let f = o(a).$set("millisecond", this.$ms).utcOffset(15 * -Math.round(i.getTimezoneOffset() / 15) - u, !0);
if (e) {
let s = f.utcOffset();
f = f.add(n - s, "minute");
}
return f.$x.$timezone = t, f;
}, f.offsetName = function (t) {
let e = this.$x.$timezone || o.tz.guess();
let n = a(this.valueOf(), e, { timeZoneName: t }).find((function (t) {
return t.type.toLowerCase() === "timezonename";
}));
return n && n.value;
};
let s = f.startOf;
f.startOf = function (t, e) {
if (!this.$x || !this.$x.$timezone) {
return s.call(this, t, e);
}
let n = o(this.format("YYYY-MM-DD HH:mm:ss:SSS"));
return s.call(n, t, e).tz(this.$x.$timezone, !0);
}, o.tz = function (t, e, n) {
let i = n && e;
let a = n || e || r;
let f = u(+o(), a);
if (typeof t != "string") {
return o(t).tz(a);
}
let s = function (t, e, n) {
let i = t - 60 * e * 1e3;
let o = u(i, n);
if (e === o) {
return [ i, e ];
}
let r = u(i -= 60 * (o - e) * 1e3, n);
return o === r ? [ i, o ] : [ t - 60 * Math.min(o, r) * 1e3, Math.max(o, r) ];
}(o.utc(t, i).valueOf(), f, a);
let m = s[0];
let c = s[1];
let d = o(m).utcOffset(c);
return d.$x.$timezone = a, d;
}, o.tz.guess = function () {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}, o.tz.setDefault = function (t) {
r = t;
};
};
}));

View File

@ -64,7 +64,7 @@ class Discord extends NotificationProvider {
},
{
name: "Error",
value: heartbeatJSON["msg"],
value: heartbeatJSON["msg"] == null ? "N/A" : heartbeatJSON["msg"],
},
],
}],
@ -91,7 +91,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
},
{
name: "Time (UTC)",

View File

@ -0,0 +1,31 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Kook extends NotificationProvider {
name = "Kook";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let url = "https://www.kookapp.cn/api/v3/message/create";
let data = {
target_id: notification.kookGuildID,
content: msg,
};
let config = {
headers: {
"Authorization": "Bot " + notification.kookBotToken,
"Content-Type": "application/json",
},
};
try {
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Kook;

View File

@ -20,6 +20,11 @@ class Ntfy extends NotificationProvider {
"priority": notification.ntfyPriority || 4,
"title": "Uptime-Kuma",
};
if (notification.ntfyIcon) {
data.icon = notification.ntfyIcon;
}
await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
return okMsg;

View File

@ -8,6 +8,14 @@ class PromoSMS extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
if (notification.promosmsAllowLongSMS === undefined) {
notification.promosmsAllowLongSMS = false;
}
//TODO: Add option for enabling special characters. It will decrese message max length from 160 to 70 chars.
//Lets remove non ascii char
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "");
try {
let config = {
headers: {
@ -18,8 +26,9 @@ class PromoSMS extends NotificationProvider {
};
let data = {
"recipients": [ notification.promosmsPhoneNumber ],
//Lets remove non ascii char
"text": msg.replace(/[^\x00-\x7F]/g, ""),
//Trim message to maximum length of 1 SMS or 4 if we allowed long messages
"text": notification.promosmsAllowLongSMS ? cleanMsg.substring(0, 639) : cleanMsg.substring(0, 159),
"long-sms": notification.promosmsAllowLongSMS,
"type": Number(notification.promosmsSMSType),
"sender": notification.promosmsSenderName
};

View File

@ -19,26 +19,26 @@ class Pushbullet extends NotificationProvider {
}
};
if (heartbeatJSON == null) {
let testdata = {
let data = {
"type": "note",
"title": "Uptime Kuma Alert",
"body": "Testing Successful.",
"body": msg,
};
await axios.post(pushbulletUrl, testdata, config);
await axios.post(pushbulletUrl, data, config);
} else if (heartbeatJSON["status"] === DOWN) {
let downdata = {
let downData = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
};
await axios.post(pushbulletUrl, downdata, config);
await axios.post(pushbulletUrl, downData, config);
} else if (heartbeatJSON["status"] === UP) {
let updata = {
let upData = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
};
await axios.post(pushbulletUrl, updata, config);
await axios.post(pushbulletUrl, upData, config);
}
return okMsg;
} catch (error) {

View File

@ -10,7 +10,7 @@ class Pushover extends NotificationProvider {
let pushoverlink = "https://api.pushover.net/1/messages.json";
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
"message": "<b>Message</b>:" + msg,
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,

View File

@ -21,6 +21,12 @@ class ServerChan extends NotificationProvider {
}
}
/**
* Get the formatted title for message
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {string} Formatted title
*/
checkStatus(heartbeatJSON, monitorJSON) {
let title = "UptimeKuma Message";
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {

View File

@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setSettings, setting } = require("../util-server");
const { getMonitorRelativeURL } = require("../../src/util");
const { getMonitorRelativeURL, UP } = require("../../src/util");
class Slack extends NotificationProvider {
@ -46,7 +46,11 @@ class Slack extends NotificationProvider {
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"blocks": [{
"attachments": [
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
@ -63,7 +67,10 @@ class Slack extends NotificationProvider {
"type": "mrkdwn",
"text": "*Time (UTC)*\n" + time,
}],
}],
}
],
}
]
};
if (notification.slackbutton) {
@ -74,7 +81,8 @@ class Slack extends NotificationProvider {
// Button
if (baseURL) {
data.blocks.push({
data.attachments.forEach(element => {
element.blocks.push({
"type": "actions",
"elements": [{
"type": "button",
@ -86,6 +94,7 @@ class Slack extends NotificationProvider {
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
}],
});
});
}
await axios.post(notification.slackwebhookURL, data);

View File

@ -0,0 +1,71 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SMSEagle extends NotificationProvider {
name = "SMSEagle";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
}
};
let postData;
let sendMethod;
let recipientType;
let encoding = (notification.smseagleEncoding) ? "1" : "0";
let priority = (notification.smseaglePriority) ? notification.smseaglePriority : "0";
if (notification.smseagleRecipientType === "smseagle-contact") {
recipientType = "contactname";
sendMethod = "sms.send_tocontact";
}
if (notification.smseagleRecipientType === "smseagle-group") {
recipientType = "groupname";
sendMethod = "sms.send_togroup";
}
if (notification.smseagleRecipientType === "smseagle-to") {
recipientType = "to";
sendMethod = "sms.send_sms";
}
let params = {
access_token: notification.smseagleToken,
[recipientType]: notification.smseagleRecipient,
message: msg,
responsetype: "extended",
unicode: encoding,
highpriority: priority
};
postData = {
method: sendMethod,
params: params
};
let resp = await axios.post(notification.smseagleUrl + "/jsonrpc/sms", postData, config);
if ((JSON.stringify(resp.data)).indexOf("message_id") === -1) {
let error = "";
if (resp.data.result && resp.data.result.error_text) {
error = `SMSEagle API returned error: ${JSON.stringify(resp.data.result.error_text)}`;
} else {
error = "SMSEagle API returned an unexpected response";
}
throw new Error(error);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SMSEagle;

View File

@ -0,0 +1,113 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
let successMessage = "Sent Successfully.";
class Splunk extends NotificationProvider {
name = "Splunk";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
try {
if (heartbeatJSON == null) {
const title = "Uptime Kuma Alert";
const monitor = {
type: "ping",
url: "Uptime Kuma Test Button",
};
return this.postNotification(notification, title, msg, monitor, "trigger");
}
if (heartbeatJSON.status === UP) {
const title = "Uptime Kuma Monitor ✅ Up";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "recovery");
}
if (heartbeatJSON.status === DOWN) {
const title = "Uptime Kuma Monitor 🔴 Down";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "trigger");
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Check if result is successful, result code should be in range 2xx
* @param {Object} result Axios response object
* @throws {Error} The status code is not in range 2xx
*/
checkResult(result) {
if (result.status == null) {
throw new Error("Splunk notification failed with invalid response!");
}
if (result.status < 200 || result.status >= 300) {
throw new Error("Splunk notification failed with status code " + result.status);
}
}
/**
* Send the message
* @param {BeanModel} notification Message title
* @param {string} title Message title
* @param {string} body Message
* @param {Object} monitorInfo Monitor details (For Up/Down only)
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
* @returns {string}
*/
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
let monitorUrl;
if (monitorInfo.type === "port") {
monitorUrl = monitorInfo.hostname;
if (monitorInfo.port) {
monitorUrl += ":" + monitorInfo.port;
}
} else if (monitorInfo.hostname != null) {
monitorUrl = monitorInfo.hostname;
} else {
monitorUrl = monitorInfo.url;
}
if (eventAction === "recovery") {
if (notification.splunkAutoResolve === "0") {
return "No action required";
}
eventAction = notification.splunkAutoResolve;
} else {
eventAction = notification.splunkSeverity;
}
const options = {
method: "POST",
url: notification.splunkRestURL,
headers: { "Content-Type": "application/json" },
data: {
message_type: eventAction,
state_message: `[${title}] [${monitorUrl}] ${body}`,
entity_display_name: "Uptime Kuma Alert: " + monitorInfo.name,
routing_key: notification.pagerdutyIntegrationKey,
entity_id: "Uptime Kuma/" + monitorInfo.id,
}
};
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
}
let result = await axios.request(options);
this.checkResult(result);
if (result.statusText != null) {
return "Splunk notification succeed: " + result.statusText;
}
return successMessage;
}
}
module.exports = Splunk;

View File

@ -16,20 +16,29 @@ class Webhook extends NotificationProvider {
msg,
};
let finalData;
let config = {};
let config = {
headers: {}
};
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append("data", JSON.stringify(data));
config = {
headers: finalData.getHeaders(),
};
config.headers = finalData.getHeaders();
} else {
finalData = data;
}
if (notification.webhookAdditionalHeaders) {
try {
config.headers = {
...config.headers,
...JSON.parse(notification.webhookAdditionalHeaders)
};
} catch (err) {
throw "Additional Headers is not a valid JSON";
}
}
await axios.post(notification.webhookURL, finalData, config);
return okMsg;

View File

@ -0,0 +1,116 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class ZohoCliq extends NotificationProvider {
name = "ZohoCliq";
/**
* Generate the message to send
* @param {const} status The status constant
* @param {string} monitorName Name of monitor
* @returns {string}
*/
_statusMessageFactory = (status, monitorName) => {
if (status === DOWN) {
return `🔴 Application [${monitorName}] went down\n`;
} else if (status === UP) {
return `✅ Application [${monitorName}] is back online\n`;
}
return "Notification\n";
};
/**
* Send the notification
* @param {string} webhookUrl URL to send the request to
* @param {Array} payload Payload generated by _notificationPayloadFactory
*/
_sendNotification = async (webhookUrl, payload) => {
await axios.post(webhookUrl, { text: payload.join("\n") });
};
/**
* Generate payload for notification
* @param {const} status The status of the monitor
* @param {string} monitorMessage Message to send
* @param {string} monitorName Name of monitor affected
* @param {string} monitorUrl URL of monitor affected
* @returns {Array}
*/
_notificationPayloadFactory = ({
status,
monitorMessage,
monitorName,
monitorUrl,
}) => {
const payload = [];
payload.push("### Uptime Kuma\n");
payload.push(this._statusMessageFactory(status, monitorName));
payload.push(`*Description:* ${monitorMessage}`);
if (monitorName) {
payload.push(`*Monitor:* ${monitorName}`);
}
if (monitorUrl && monitorUrl !== "https://") {
payload.push(`*URL:* [${monitorUrl}](${monitorUrl})`);
}
return payload;
};
/**
* Send a general notification
* @param {string} webhookUrl URL to send request to
* @param {string} msg Message to send
* @returns {Promise<void>}
*/
_handleGeneralNotification = (webhookUrl, msg) => {
const payload = this._notificationPayloadFactory({
monitorMessage: msg
});
return this._sendNotification(webhookUrl, payload);
};
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
if (heartbeatJSON == null) {
await this._handleGeneralNotification(notification.webhookUrl, msg);
return okMsg;
}
let url;
switch (monitorJSON["type"]) {
case "http":
case "keywork":
url = monitorJSON["url"];
break;
case "docker":
url = monitorJSON["docker_host"];
break;
default:
url = monitorJSON["hostname"];
break;
}
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: url,
status: heartbeatJSON.status
});
await this._sendNotification(notification.webhookUrl, payload);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = ZohoCliq;

View File

@ -14,6 +14,7 @@ const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Gotify = require("./notification-providers/gotify");
const HomeAssistant = require("./notification-providers/home-assistant");
const Kook = require("./notification-providers/kook");
const Line = require("./notification-providers/line");
const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea");
@ -32,17 +33,20 @@ const RocketChat = require("./notification-providers/rocket-chat");
const SerwerSMS = require("./notification-providers/serwersms");
const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack");
const SMSEagle = require("./notification-providers/smseagle");
const SMTP = require("./notification-providers/smtp");
const Squadcast = require("./notification-providers/squadcast");
const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push");
const Telegram = require("./notification-providers/telegram");
const Splunk = require("./notification-providers/splunk");
const Webhook = require("./notification-providers/webhook");
const WeCom = require("./notification-providers/wecom");
const GoAlert = require("./notification-providers/goalert");
const SMSManager = require("./notification-providers/smsmanager");
const ServerChan = require("./notification-providers/serverchan");
const ZohoCliq = require("./notification-providers/zoho-cliq");
class Notification {
@ -69,6 +73,7 @@ class Notification {
new Gorush(),
new Gotify(),
new HomeAssistant(),
new Kook(),
new Line(),
new LineNotify(),
new LunaSea(),
@ -89,15 +94,18 @@ class Notification {
new Signal(),
new SMSManager(),
new Slack(),
new SMSEagle(),
new SMTP(),
new Squadcast(),
new Stackfield(),
new Teams(),
new TechulusPush(),
new Telegram(),
new Splunk(),
new Webhook(),
new WeCom(),
new GoAlert(),
new ZohoCliq()
];
for (let item of list) {

View File

@ -1,199 +0,0 @@
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
// Fixed on Windows
const net = require("net");
const spawn = require("child_process").spawn;
const events = require("events");
const fs = require("fs");
const util = require("./util-server");
module.exports = Ping;
/**
* Constructor for ping class
* @param {string} host Host to ping
* @param {object} [options] Options for the ping command
* @param {array|string} [options.args] - Arguments to pass to the ping command
*/
function Ping(host, options) {
if (!host) {
throw new Error("You must specify a host to ping!");
}
this._host = host;
this._options = options = (options || {});
events.EventEmitter.call(this);
const timeout = 10;
if (util.WIN) {
this._bin = "c:/windows/system32/ping.exe";
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
this._regmatch = /[><=]([0-9.]+?)ms/;
} else if (util.LIN) {
this._bin = "/bin/ping";
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 (util.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 (util.BSD) {
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.");
}
if (!fs.existsSync(this._bin)) {
throw new Error("Could not detect " + this._bin + " on your system");
}
this._i = 0;
return this;
}
Ping.prototype.__proto__ = events.EventEmitter.prototype;
/**
* Callback for send
* @callback pingCB
* @param {any} err Any error encountered
* @param {number} ms Ping time in ms
*/
/**
* Send a ping
* @param {pingCB} callback Callback to call with results
*/
Ping.prototype.send = function (callback) {
let self = this;
callback = callback || function (err, ms) {
if (err) {
return self.emit("error", err);
}
return self.emit("result", ms);
};
let _ended;
let _exited;
let _errored;
this._ping = spawn(this._bin, this._args, { windowsHide: true }); // spawn the binary
this._ping.on("error", function (err) { // handle binary errors
_errored = true;
callback(err);
});
this._ping.stdout.on("data", function (data) { // log stdout
if (util.WIN) {
data = convertOutput(data);
}
this._stdout = (this._stdout || "") + data;
});
this._ping.stdout.on("end", function () {
_ended = true;
if (_exited && !_errored) {
onEnd.call(self._ping);
}
});
this._ping.stderr.on("data", function (data) { // log stderr
if (util.WIN) {
data = convertOutput(data);
}
this._stderr = (this._stderr || "") + data;
});
this._ping.on("exit", function (code) { // handle complete
_exited = true;
if (_ended && !_errored) {
onEnd.call(self._ping);
}
});
/**
* @param {Function} callback
*
* Generated by Trelent
*/
function onEnd() {
let stdout = this.stdout._stdout;
let stderr = this.stderr._stderr;
let ms;
if (stderr) {
return callback(new Error(stderr));
}
if (!stdout) {
return callback(new Error("No stdout detected"));
}
ms = stdout.match(self._regmatch); // parse out the ##ms response
ms = (ms && ms[1]) ? Number(ms[1]) : ms;
callback(null, ms, stdout);
}
};
/**
* Ping every interval
* @param {pingCB} callback Callback to call with results
*/
Ping.prototype.start = function (callback) {
let self = this;
this._i = setInterval(function () {
self.send(callback);
}, (self._options.interval || 5000));
self.send(callback);
};
/** Stop sending pings */
Ping.prototype.stop = function () {
clearInterval(this._i);
};
/**
* Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
* Thank @pemassi
* https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
* @param {any} data
* @returns {string}
*/
function convertOutput(data) {
if (util.WIN) {
if (data) {
return util.convertToUTF8(data);
}
}
return data;
}

View File

@ -99,6 +99,7 @@ class Prometheus {
}
}
/** Remove monitor from prometheus */
remove() {
try {
monitorCertDaysRemaining.remove(this.monitorLabelValues);

View File

@ -7,7 +7,7 @@ const { UptimeKumaServer } = require("./uptime-kuma-server");
class Proxy {
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks4" ];
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks5h", "socks4" ];
/**
* Saves and updates given proxy entity
@ -126,6 +126,7 @@ class Proxy {
break;
case "socks":
case "socks5":
case "socks5h":
case "socks4":
agent = new SocksProxyAgent({
...httpAgentOptions,

View File

@ -4,7 +4,7 @@ const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, DOWN, flipStatus, log } = require("../../src/util");
const { UP, MAINTENANCE, DOWN, flipStatus, log } = require("../../src/util");
const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { makeBadge } = require("badge-maker");
@ -67,6 +67,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
}
if (await Monitor.isUnderMaintenance(monitor.id)) {
msg = "Monitor under maintenance";
status = MAINTENANCE;
}
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
log.debug("router", "PreviousStatus: " + previousStatus);
log.debug("router", "Current Status: " + status);
@ -87,7 +92,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
ok: true,
});
if (bean.important) {
if (Monitor.isImportantForNotification(isFirstBeat, previousStatus, status)) {
await Monitor.sendNotification(isFirstBeat, monitor, bean);
}

View File

@ -5,6 +5,12 @@
*/
console.log("Welcome to Uptime Kuma");
// As the log function need to use dayjs, it should be very top
const dayjs = require("dayjs");
dayjs.extend(require("dayjs/plugin/utc"));
dayjs.extend(require("./modules/dayjs/plugin/timezone"));
dayjs.extend(require("dayjs/plugin/customParseFormat"));
// Check Node.js Version
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
const requiredVersion = 14;
@ -33,6 +39,7 @@ log.info("server", "Importing Node libraries");
const fs = require("fs");
log.info("server", "Importing 3rd-party libraries");
log.debug("server", "Importing express");
const express = require("express");
const expressStaticGzip = require("express-static-gzip");
@ -127,6 +134,10 @@ const StatusPage = require("./model/status_page");
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler");
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
const { Settings } = require("./settings");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
app.use(express.json());
@ -154,8 +165,9 @@ let needSetup = false;
(async () => {
Database.init(args);
await initDatabase(testMode);
await server.initAfterDatabaseReady();
exports.entryPage = await setting("entryPage");
server.entryPage = await Settings.get("entryPage");
await StatusPage.loadDomainMappingList();
log.info("server", "Adding route");
@ -176,14 +188,15 @@ let needSetup = false;
log.debug("entry", `Request Domain: ${hostname}`);
const uptimeKumaEntryPage = server.entryPage;
if (hostname in StatusPage.domainMappingList) {
log.debug("entry", "This is a status page domain");
let slug = StatusPage.domainMappingList[hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
} else if (uptimeKumaEntryPage && uptimeKumaEntryPage.startsWith("statusPage-")) {
response.redirect("/status/" + uptimeKumaEntryPage.replace("statusPage-", ""));
} else {
response.redirect("/dashboard");
@ -192,6 +205,7 @@ let needSetup = false;
if (isDev) {
app.post("/test-webhook", async (request, response) => {
log.debug("test", request.headers);
log.debug("test", request.body);
response.send("OK");
});
@ -620,6 +634,9 @@ let needSetup = false;
bean.import(monitor);
bean.user_id = socket.userID;
bean.validate();
await R.store(bean);
await updateMonitorNotification(bean.id, notificationIDList);
@ -695,12 +712,21 @@ let needSetup = false;
bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain;
bean.grpcUrl = monitor.grpcUrl;
bean.grpcProtobuf = monitor.grpcProtobuf;
bean.grpcServiceName = monitor.grpcServiceName;
bean.grpcMethod = monitor.grpcMethod;
bean.grpcBody = monitor.grpcBody;
bean.grpcMetadata = monitor.grpcMetadata;
bean.grpcEnableTls = monitor.grpcEnableTls;
bean.radiusUsername = monitor.radiusUsername;
bean.radiusPassword = monitor.radiusPassword;
bean.radiusCalledStationId = monitor.radiusCalledStationId;
bean.radiusCallingStationId = monitor.radiusCallingStationId;
bean.radiusSecret = monitor.radiusSecret;
bean.validate();
await R.store(bean);
await updateMonitorNotification(bean.id, monitor.notificationIDList);
@ -915,13 +941,21 @@ let needSetup = false;
try {
checkLogin(socket);
let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]);
let bean = await R.findOne("tag", " id = ? ", [ tag.id ]);
if (bean == null) {
callback({
ok: false,
msg: "Tag not found",
});
return;
}
bean.name = tag.name;
bean.color = tag.color;
await R.store(bean);
callback({
ok: true,
msg: "Saved",
tag: await bean.toJSON(),
});
@ -1055,10 +1089,15 @@ let needSetup = false;
socket.on("getSettings", async (callback) => {
try {
checkLogin(socket);
const data = await getSettings("general");
if (!data.serverTimezone) {
data.serverTimezone = await server.getTimezone();
}
callback({
ok: true,
data: await getSettings("general"),
data: data,
});
} catch (e) {
@ -1084,7 +1123,14 @@ let needSetup = false;
}
await setSettings("general", data);
exports.entryPage = data.entryPage;
server.entryPage = data.entryPage;
await CacheableDnsHttpAgent.update();
// Also need to apply timezone globally
if (data.serverTimezone) {
await server.setTimezone(data.serverTimezone);
}
callback({
ok: true,
@ -1092,6 +1138,7 @@ let needSetup = false;
});
sendInfo(socket);
server.sendMaintenanceList(socket);
} catch (e) {
callback({
@ -1450,6 +1497,8 @@ let needSetup = false;
databaseSocketHandler(socket);
proxySocketHandler(socket);
dockerSocketHandler(socket);
maintenanceSocketHandler(socket);
generalSocketHandler(socket, server);
log.debug("server", "added all socket handlers");
@ -1552,6 +1601,7 @@ async function afterLogin(socket, user) {
socket.join(user.id);
let monitorList = await server.sendMonitorList(socket);
server.sendMaintenanceList(socket);
sendNotificationList(socket);
sendProxyList(socket);
sendDockerHostList(socket);
@ -1571,6 +1621,13 @@ async function afterLogin(socket, user) {
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id);
}
// Set server timezone from client browser if not set
// It should be run once only
if (! await Settings.get("initServerTimezone")) {
log.debug("server", "emit initServerTimezone");
socket.emit("initServerTimezone");
}
}
/**
@ -1697,6 +1754,8 @@ async function shutdownFunction(signal) {
log.info("server", "Shutdown requested");
log.info("server", "Called signal: " + signal);
await server.stop();
log.info("server", "Stopping all monitors");
for (let id in server.monitorList) {
let monitor = server.monitorList[id];
@ -1707,6 +1766,7 @@ async function shutdownFunction(signal) {
stopBackgroundJobs();
await cloudflaredStop();
Settings.stopCacheCleaner();
}
/** Final function called before application exits */

View File

@ -158,6 +158,13 @@ class Settings {
delete Settings.cacheList[key];
}
}
static stopCacheCleaner() {
if (Settings.cacheCleaner) {
clearInterval(Settings.cacheCleaner);
Settings.cacheCleaner = null;
}
}
}
module.exports = {

View File

@ -1,6 +1,7 @@
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { log } = require("../../src/util");
const io = UptimeKumaServer.getInstance().io;
const prefix = "cloudflared_";
@ -107,7 +108,7 @@ module.exports.autoStart = async (token) => {
/** Stop cloudflared */
module.exports.stop = async () => {
console.log("Stop cloudflared");
log.info("cloudflared", "Stop cloudflared");
if (cloudflared) {
cloudflared.stop();
}

View File

@ -0,0 +1,20 @@
const { log } = require("../../src/util");
const { Settings } = require("../settings");
const { sendInfo } = require("../client");
const { checkLogin } = require("../util-server");
module.exports.generalSocketHandler = (socket, server) => {
socket.on("initServerTimezone", async (timezone) => {
try {
checkLogin(socket);
log.debug("generalSocketHandler", "Timezone: " + timezone);
await Settings.set("initServerTimezone", true);
await server.setTimezone(timezone);
await sendInfo(socket);
} catch (e) {
log.warn("initServerTimezone", e.message);
}
});
};

View File

@ -0,0 +1,317 @@
const { checkLogin } = require("../util-server");
const { log } = require("../../src/util");
const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const Maintenance = require("../model/maintenance");
const server = UptimeKumaServer.getInstance();
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
/**
* Handlers for Maintenance
* @param {Socket} socket Socket.io instance
*/
module.exports.maintenanceSocketHandler = (socket) => {
// Add a new maintenance
socket.on("addMaintenance", async (maintenance, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", maintenance);
let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
bean.user_id = socket.userID;
let maintenanceID = await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean);
await server.sendMaintenanceList(socket);
callback({
ok: true,
msg: "Added Successfully.",
maintenanceID,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
// Edit a maintenance
socket.on("editMaintenance", async (maintenance, callback) => {
try {
checkLogin(socket);
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
if (bean.user_id !== socket.userID) {
throw new Error("Permission denied.");
}
Maintenance.jsonToBean(bean, maintenance);
await R.store(bean);
await MaintenanceTimeslot.generateTimeslot(bean, null, true);
await server.sendMaintenanceList(socket);
callback({
ok: true,
msg: "Saved.",
maintenanceID: bean.id,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
// Add a new monitor_maintenance
socket.on("addMonitorMaintenance", async (maintenanceID, monitors, callback) => {
try {
checkLogin(socket);
await R.exec("DELETE FROM monitor_maintenance WHERE maintenance_id = ?", [
maintenanceID
]);
for await (const monitor of monitors) {
let bean = R.dispense("monitor_maintenance");
bean.import({
monitor_id: monitor.id,
maintenance_id: maintenanceID
});
await R.store(bean);
}
apicache.clear();
callback({
ok: true,
msg: "Added Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
// Add a new monitor_maintenance
socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => {
try {
checkLogin(socket);
await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [
maintenanceID
]);
for await (const statusPage of statusPages) {
let bean = R.dispense("maintenance_status_page");
bean.import({
status_page_id: statusPage.id,
maintenance_id: maintenanceID
});
await R.store(bean);
}
apicache.clear();
callback({
ok: true,
msg: "Added Successfully.",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [
maintenanceID,
socket.userID,
]);
callback({
ok: true,
maintenance: await bean.toJSON(),
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMaintenanceList", async (callback) => {
try {
checkLogin(socket);
await server.sendMaintenanceList(socket);
callback({
ok: true,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMonitorMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
maintenanceID,
]);
callback({
ok: true,
monitors,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
maintenanceID,
]);
callback({
ok: true,
statusPages,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("deleteMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
if (maintenanceID in server.maintenanceList) {
delete server.maintenanceList[maintenanceID];
}
await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [
maintenanceID,
socket.userID,
]);
apicache.clear();
callback({
ok: true,
msg: "Deleted Successfully.",
});
await server.sendMaintenanceList(socket);
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("pauseMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
maintenanceID,
]);
apicache.clear();
callback({
ok: true,
msg: "Paused Successfully.",
});
await server.sendMaintenanceList(socket);
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("resumeMaintenance", async (maintenanceID, callback) => {
try {
checkLogin(socket);
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
maintenanceID,
]);
apicache.clear();
callback({
ok: true,
msg: "Resume Successfully",
});
await server.sendMaintenanceList(socket);
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
};

View File

@ -0,0 +1,49 @@
const { log } = require("../src/util");
class UptimeCacheList {
/**
* list[monitorID][duration]
*/
static list = {};
/**
* Get the uptime for a specific period
* @param {number} monitorID
* @param {number} duration
* @return {number}
*/
static getUptime(monitorID, duration) {
if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) {
log.debug("UptimeCacheList", "getUptime: " + monitorID + " " + duration);
return UptimeCacheList.list[monitorID][duration];
} else {
return null;
}
}
/**
* Add uptime for specified monitor
* @param {number} monitorID
* @param {number} duration
* @param {number} uptime Uptime to add
*/
static addUptime(monitorID, duration, uptime) {
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
if (!UptimeCacheList.list[monitorID]) {
UptimeCacheList.list[monitorID] = {};
}
UptimeCacheList.list[monitorID][duration] = uptime;
}
/**
* Clear cache for specified monitor
* @param {number} monitorID
*/
static clearCache(monitorID) {
log.debug("UptimeCacheList", "clearCache: " + monitorID);
delete UptimeCacheList.list[monitorID];
}
}
module.exports = {
UptimeCacheList,
};

View File

@ -9,6 +9,8 @@ const Database = require("./database");
const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@ -26,6 +28,13 @@ class UptimeKumaServer {
* @type {{}}
*/
monitorList = {};
/**
* Main maintenance list
* @type {{}}
*/
maintenanceList = {};
entryPage = "dashboard";
app = undefined;
httpServer = undefined;
@ -37,6 +46,8 @@ class UptimeKumaServer {
*/
indexHTML = "";
generateMaintenanceTimeslotsInterval = undefined;
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
@ -72,11 +83,27 @@ class UptimeKumaServer {
}
}
CacheableDnsHttpAgent.registerGlobalAgent();
this.io = new Server(this.httpServer);
}
/** Initialise app after the database has been set up */
async initAfterDatabaseReady() {
await CacheableDnsHttpAgent.update();
process.env.TZ = await this.getTimezone();
dayjs.tz.setDefault(process.env.TZ);
log.debug("DEBUG", "Timezone: " + process.env.TZ);
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
await this.generateMaintenanceTimeslots();
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
}
/**
* Send list of monitors to client
* @param {Socket} socket
* @returns {Object} List of monitors
*/
async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID);
this.io.to(socket.userID).emit("monitorList", list);
@ -104,6 +131,45 @@ class UptimeKumaServer {
return result;
}
/**
* Send maintenance list to client
* @param {Socket} socket Socket.io instance to send to
* @returns {Object}
*/
async sendMaintenanceList(socket) {
return await this.sendMaintenanceListByUserID(socket.userID);
}
/**
* Send list of maintenances to user
* @param {number} userID
* @returns {Object}
*/
async sendMaintenanceListByUserID(userID) {
let list = await this.getMaintenanceJSONList(userID);
this.io.to(userID).emit("maintenanceList", list);
return list;
}
/**
* Get a list of maintenances for the given user.
* @param {string} userID - The ID of the user to get maintenances for.
* @returns {Promise<Object>} A promise that resolves to an object with maintenance IDs as keys and maintenances objects as values.
*/
async getMaintenanceJSONList(userID) {
let result = {};
let maintenanceList = await R.find("maintenance", " user_id = ? ORDER BY end_date DESC, title", [
userID,
]);
for (let maintenance of maintenanceList) {
result[maintenance.id] = await maintenance.toJSON();
}
return result;
}
/**
* Write error to log file
* @param {any} error The error to write
@ -130,6 +196,11 @@ class UptimeKumaServer {
errorLogStream.end();
}
/**
* Get the IP of the client connected to the socket
* @param {Socket} socket
* @returns {string}
*/
async getClientIP(socket) {
let clientIP = socket.client.conn.remoteAddress;
@ -147,8 +218,65 @@ class UptimeKumaServer {
return clientIP.replace(/^.*:/, "");
}
}
/**
* Attempt to get the current server timezone
* If this fails, fall back to environment variables and then make a
* guess.
* @returns {string}
*/
async getTimezone() {
let timezone = await Settings.get("serverTimezone");
if (timezone) {
return timezone;
} else if (process.env.TZ) {
return process.env.TZ;
} else {
return dayjs.tz.guess();
}
}
/**
* Get the current offset
* @returns {string}
*/
getTimezoneOffset() {
return dayjs().format("Z");
}
/**
* Set the current server timezone and environment variables
* @param {string} timezone
*/
async setTimezone(timezone) {
await Settings.set("serverTimezone", timezone, "general");
process.env.TZ = timezone;
dayjs.tz.setDefault(timezone);
}
/** Load the timeslots for maintenance */
async generateMaintenanceTimeslots() {
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
for (let maintenanceTimeslot of list) {
let maintenance = await maintenanceTimeslot.maintenance;
await MaintenanceTimeslot.generateTimeslot(maintenance, maintenanceTimeslot.end_date, false);
maintenanceTimeslot.generated_next = true;
await R.store(maintenanceTimeslot);
}
}
/** Stop the server */
async stop() {
clearTimeout(this.generateMaintenanceTimeslotsInterval);
}
}
module.exports = {
UptimeKumaServer
};
// Must be at the end
const MaintenanceTimeslot = require("./model/maintenance_timeslot");

View File

@ -1,5 +1,5 @@
const tcpp = require("tcp-ping");
const Ping = require("./ping-lite");
const ping = require("@louislam/ping");
const { R } = require("redbean-node");
const { log, genSecret } = require("../src/util");
const passwordHash = require("./password-hash");
@ -13,21 +13,22 @@ const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2");
const { MongoClient } = require("mongodb");
const { NtlmClient } = require("axios-ntlm");
const { Settings } = require("./settings");
const grpc = require("@grpc/grpc-js");
const protojs = require("protobufjs");
const radiusClient = require("node-radius-client");
const redis = require("redis");
const {
dictionaries: {
rfc2865: { file, attributes },
},
} = require("node-radius-utils");
const dayjs = require("dayjs");
// From ping-lite
exports.WIN = /^win/.test(process.platform);
exports.LIN = /^linux/.test(process.platform);
exports.MAC = /^darwin/.test(process.platform);
exports.FBSD = /^freebsd/.test(process.platform);
exports.BSD = /bsd$/.test(process.platform);
const isWindows = process.platform === /^win/.test(process.platform);
/**
* Init or reset JWT secret
@ -101,18 +102,23 @@ exports.ping = async (hostname) => {
*/
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);
} else if (ms === null) {
reject(new Error(stdout));
ping.promise.probe(hostname, {
v6: ipv6,
min_reply: 1,
deadline: 10,
}).then((res) => {
// If ping failed, it will set field to unknown
if (res.alive) {
resolve(res.time);
} else {
resolve(Math.round(ms));
if (isWindows) {
reject(new Error(exports.convertToUTF8(res.output)));
} else {
reject(new Error(res.output));
}
}
}).catch((err) => {
reject(err);
});
});
};
@ -131,7 +137,7 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
const { port, username, password, interval = 20 } = options;
// Adds MQTT protocol to the hostname if not already present
if (!/^(?:http|mqtt)s?:\/\//.test(hostname)) {
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
hostname = "mqtt://" + hostname;
}
@ -141,10 +147,11 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
reject(new Error("Timeout"));
}, interval * 1000 * 0.8);
log.debug("mqtt", "MQTT connecting");
const mqttUrl = `${hostname}:${port}`;
let client = mqtt.connect(hostname, {
port,
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
let client = mqtt.connect(mqttUrl, {
username,
password
});
@ -244,19 +251,19 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mssqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
mssql.connect(connectionString).then(pool => {
return pool.request()
.query(query);
}).then(result => {
resolve(result);
}).catch(err => {
reject(err);
}).finally(() => {
mssql.close();
});
});
exports.mssqlQuery = async function (connectionString, query) {
let pool;
try {
pool = new mssql.ConnectionPool(connectionString);
await pool.connect();
await pool.request().query(query);
pool.close();
} catch (e) {
if (pool) {
pool.close();
}
throw e;
}
};
/**
@ -276,9 +283,36 @@ exports.postgresQuery = function (connectionString, query) {
const client = new Client({ connectionString });
client.connect();
client.connect((err) => {
if (err) {
reject(err);
client.end();
} else {
// Connected here
client.query(query, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
client.end();
});
}
});
return client.query(query)
});
};
/**
* Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mysqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const connection = mysql.createConnection(connectionString);
connection.promise().query(query)
.then(res => {
resolve(res);
})
@ -286,11 +320,39 @@ exports.postgresQuery = function (connectionString, query) {
reject(err);
})
.finally(() => {
client.end();
connection.end();
});
});
};
/**
* Connect to and Ping a MongoDB database
* @param {string} connectionString The database connection string
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mongodbPing = async function (connectionString) {
let client = await MongoClient.connect(connectionString);
let dbPing = await client.db().command({ ping: 1 });
await client.close();
if (dbPing["ok"] === 1) {
return "UP";
} else {
throw Error("failed");
}
};
/**
* Query radius server
* @param {string} hostname Hostname of radius server
* @param {string} username Username to use
* @param {string} password Password to use
* @param {string} calledStationId ID of called station
* @param {string} callingStationId ID of calling station
* @param {string} secret Secret to use
* @param {number} [port=1812] Port to contact radius server on
* @returns {Promise<any>}
*/
exports.radius = function (
hostname,
username,
@ -298,9 +360,11 @@ exports.radius = function (
calledStationId,
callingStationId,
secret,
port = 1812,
) {
const client = new radiusClient({
host: hostname,
hostPort: port,
dictionaries: [ file ],
});
@ -315,6 +379,30 @@ exports.radius = function (
});
};
/**
* Redis server ping
* @param {string} dsn The redis connection string
*/
exports.redisPingAsync = function (dsn) {
return new Promise((resolve, reject) => {
const client = redis.createClient({
url: dsn,
});
client.on("error", (err) => {
reject(err);
});
client.connect().then(() => {
client.ping().then((res, err) => {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
});
};
/**
* Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve
@ -431,6 +519,10 @@ const parseCertificateInfo = function (info) {
* @returns {Object} Object containing certificate information
*/
exports.checkCertificate = function (res) {
if (!res.request.res.socket) {
throw new Error("No socket found");
}
const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false;
@ -645,3 +737,121 @@ module.exports.send403 = (res, msg = "") => {
"msg": msg,
});
};
function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
let offsetString;
if (timezone) {
offsetString = dayjs().tz(timezone).format("Z");
} else {
offsetString = dayjs().format("Z");
}
let hours = parseInt(offsetString.substring(1, 3));
let minutes = parseInt(offsetString.substring(4, 6));
if (
(timeObjectToUTC && offsetString.startsWith("+")) ||
(!timeObjectToUTC && offsetString.startsWith("-"))
) {
hours *= -1;
minutes *= -1;
}
obj.hours += hours;
obj.minutes += minutes;
// Handle out of bound
if (obj.minutes < 0) {
obj.minutes += 60;
obj.hours--;
} else if (obj.minutes > 60) {
obj.minutes -= 60;
obj.hours++;
}
if (obj.hours < 0) {
obj.hours += 24;
} else if (obj.hours > 24) {
obj.hours -= 24;
}
return obj;
}
/**
*
* @param {object} obj
* @param {string} timezone
* @returns {object}
*/
module.exports.timeObjectToUTC = (obj, timezone = undefined) => {
return timeObjectConvertTimezone(obj, timezone, true);
};
/**
*
* @param {object} obj
* @param {string} timezone
* @returns {object}
*/
module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
return timeObjectConvertTimezone(obj, timezone, false);
};
/**
* Create gRPC client stib
* @param {Object} options from gRPC client
*/
module.exports.grpcQuery = async (options) => {
const { grpcUrl, grpcProtobufData, grpcServiceName, grpcEnableTls, grpcMethod, grpcBody } = options;
const protocObject = protojs.parse(grpcProtobufData);
const protoServiceObject = protocObject.root.lookupService(grpcServiceName);
const Client = grpc.makeGenericClientConstructor({});
const credentials = grpcEnableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure();
const client = new Client(
grpcUrl,
credentials
);
const grpcService = protoServiceObject.create(function (method, requestData, cb) {
const fullServiceName = method.fullName;
const serviceFQDN = fullServiceName.split(".");
const serviceMethod = serviceFQDN.pop();
const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`;
log.debug("monitor", `gRPC method ${serviceMethodClientImpl}`);
client.makeUnaryRequest(
serviceMethodClientImpl,
arg => arg,
arg => arg,
requestData,
cb);
}, false, false);
return new Promise((resolve, _) => {
try {
return grpcService[`${grpcMethod}`](JSON.parse(grpcBody), function (err, response) {
const responseData = JSON.stringify(response);
if (err) {
return resolve({
code: err.code,
errorMessage: err.details,
data: ""
});
} else {
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
return resolve({
code: 1,
errorMessage: "",
data: responseData
});
}
});
} catch (err) {
return resolve({
code: -1,
errorMessage: `Error ${err}. Please review your gRPC configuration option. The service name must not include package name value, and the method name must follow camelCase format`,
data: ""
});
}
});
};

View File

@ -22,6 +22,19 @@ textarea.form-control {
width: 10px;
}
.bg-maintenance {
color: white !important;
background-color: $maintenance !important;
}
.bg-dark {
color: white;
}
.text-maintenance {
color: $maintenance !important;
}
.list-group {
border-radius: 0.75rem;
@ -107,6 +120,19 @@ optgroup {
}
}
.btn-normal {
$bg-color: #F5F5F5;
background-color: $bg-color;
border-color: $bg-color;
&:hover {
$hover-color: darken($bg-color, 3%);
background-color: $hover-color;
border-color: $hover-color;
}
}
.btn-warning {
color: white;
@ -256,6 +282,20 @@ optgroup {
color: white;
}
.btn-normal {
$bg-color: $dark-header-bg;
color: $dark-font-color;
background-color: $bg-color;
border-color: $bg-color;
&:hover {
$hover-color: darken($bg-color, 3%);
background-color: $hover-color;
border-color: $hover-color;
}
}
.btn-warning {
color: $dark-font-color2;
@ -323,6 +363,7 @@ optgroup {
&.bg-info,
&.bg-warning,
&.bg-danger,
&.bg-maintenance,
&.bg-light {
color: $dark-font-color2;
}

View File

@ -3,3 +3,7 @@ html[lang='fa'] {
font-family: 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', 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;
}
}
ul.multiselect__content {
padding-left: 0 !important;
}

View File

@ -1,6 +1,7 @@
$primary: #5cdd8b;
$danger: #dc3545;
$warning: #f8a306;
$maintenance: #1747f5;
$link-color: #111;
$border-radius: 50rem;

View File

@ -0,0 +1,39 @@
@import "@vuepic/vue-datepicker/dist/main.css";
@import "vars.scss";
// Must use #{ }
// Remark: https://stackoverflow.com/questions/50202991/unable-to-set-scss-variable-to-css-variable
.dp__theme_dark {
--dp-background-color: #{$dark-bg2};
--dp-text-color: #{$dark-font-color};
--dp-hover-color: #484848;
--dp-hover-text-color: #ffffff;
--dp-hover-icon-color: #959595;
--dp-primary-color: #{#5cdd8b};
--dp-primary-text-color: #ffffff;
--dp-secondary-color: #494949;
--dp-border-color: #{$dark-border-color};
--dp-menu-border-color: #2d2d2d;
--dp-border-color-hover: #{$dark-border-color};
--dp-disabled-color: #212121;
--dp-scroll-bar-background: #212121;
--dp-scroll-bar-color: #484848;
--dp-success-color: #{$primary};
--dp-success-color-disabled: #428f59;
--dp-icon-color: #959595;
--dp-danger-color: #e53935;
--dp-highlight-color: rgba(0, 92, 178, 0.2);
}
.dp__input {
border-radius: $border-radius;
}
// Fix: Full width of text input when using "inline textInput inlineWithInput" mode
.dp__main > div[aria-label="Datepicker input"] {
width: 100%;
}
.dp__main > div[aria-label="Datepicker menu"]:nth-child(2) {
margin-top: 20px;
}

View File

@ -3,14 +3,6 @@
</template>
<script>
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
export default {
props: {
/** Value of date time */

View File

@ -73,7 +73,7 @@ export default {
emits: [ "added" ],
data() {
return {
model: null,
modal: null,
processing: false,
id: null,
connectionTypes: [ "socket", "tcp" ],
@ -91,11 +91,16 @@ export default {
},
methods: {
/** Confirm deletion of docker host */
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
/**
* Show specified docker host
* @param {number} dockerHostID
*/
show(dockerHostID) {
if (dockerHostID) {
let found = false;
@ -126,6 +131,7 @@ export default {
this.modal.show();
},
/** Add docker host */
submit() {
this.processing = true;
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
@ -144,6 +150,7 @@ export default {
});
},
/** Test the docker host */
test() {
this.processing = true;
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
@ -152,6 +159,7 @@ export default {
});
},
/** Delete this docker host */
deleteDockerHost() {
this.processing = true;
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {

View File

@ -5,7 +5,7 @@
v-for="(beat, index) in shortBeatList"
:key="index"
class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2) }"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
:style="beatStyle"
:title="getBeatTitle(beat)"
/>
@ -211,6 +211,10 @@ export default {
background-color: $warning;
}
&.maintenance {
background-color: $maintenance;
}
&:not(.empty):hover {
transition: all ease-in-out 0.15s;
opacity: 0.8;

View File

@ -42,7 +42,7 @@ export default {
/** Should the field auto complete */
autocomplete: {
type: String,
default: undefined,
default: "new-password",
},
/** Is the input required? */
required: {

View File

@ -0,0 +1,44 @@
<template>
<div>
<div v-if="maintenance.strategy === 'manual'" class="timeslot">
{{ $t("Manual") }}
</div>
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
{{ maintenance.timeslotList[0].startDateServerTimezone }}
<span class="to">-</span>
{{ maintenance.timeslotList[0].endDateServerTimezone }}
(UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }})
</div>
</div>
</template>
<script>
export default {
props: {
maintenance: {
type: Object,
required: true
},
},
};
</script>
<style lang="scss">
.timeslot {
margin-top: 5px;
display: inline-block;
font-size: 14px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 20px;
padding: 0 10px;
.to {
margin: 0 6px;
}
.dark & {
color: white;
background-color: rgba(255, 255, 255, 0.1);
}
}
</style>

View File

@ -206,6 +206,16 @@ export default {
.search-icon {
padding: 10px;
color: #c0c0c0;
// Clear filter button (X)
svg[data-icon="times"] {
cursor: pointer;
transition: all ease-in-out 0.1s;
&:hover {
opacity: 0.5;
}
}
}
.search-input {

View File

@ -16,18 +16,14 @@
</div>
</template>
<script lang="ts">
<script lang="js">
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import "chartjs-adapter-dayjs";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { LineChart } from "vue-chart-3";
import { useToast } from "vue-toastification";
import { DOWN, log } from "../util.ts";
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
dayjs.extend(utc);
dayjs.extend(timezone);
const toast = useToast();
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
@ -163,7 +159,8 @@ export default {
},
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
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down (red color), under maintenance (blue color) or pending (orange color), 0 if target is up
let colorData = []; // Color Data for Bar Chart
let heartbeatList = this.heartbeatList ||
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
@ -185,8 +182,9 @@ export default {
});
downData.push({
x,
y: beat.status === DOWN ? 1 : 0,
y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
});
colorData.push((beat.status === MAINTENANCE) ? "rgba(23,71,245,0.41)" : ((beat.status === PENDING) ? "rgba(245,182,23,0.41)" : "#DC354568"));
});
return {
@ -205,7 +203,7 @@ export default {
type: "bar",
data: downData,
borderColor: "#00000000",
backgroundColor: "#DC354568",
backgroundColor: colorData,
yAxisID: "y1",
barThickness: "flex",
barPercentage: 1,

View File

@ -17,6 +17,7 @@
<option value="http">HTTP</option>
<option value="socks">SOCKS</option>
<option value="socks5">SOCKS v5</option>
<option value="socks5h">SOCKS v5 (+DNS)</option>
<option value="socks4">SOCKS v4</option>
</select>
</div>

View File

@ -225,4 +225,8 @@ export default {
}
}
.bg-maintenance {
background-color: $maintenance;
}
</style>

View File

@ -26,6 +26,10 @@ export default {
return "warning";
}
if (this.status === 3) {
return "maintenance";
}
return "secondary";
},
@ -42,6 +46,10 @@ export default {
return this.$t("Pending");
}
if (this.status === 3) {
return this.$t("statusMaintenance");
}
return this.$t("Unknown");
},
},

View File

@ -41,7 +41,7 @@ export default {
},
computed: {
displayText() {
if (this.item.value === "") {
if (this.item.value === "" || this.item.value === undefined) {
return this.item.name;
} else {
return `${this.item.name}: ${this.item.value}`;

View File

@ -0,0 +1,376 @@
<template>
<form @submit.prevent="submit">
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title">
{{ $t("Edit Tag") }}
</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="tag-name" class="form-label">{{ $t("Name") }}</label>
<input id="tag-name" v-model="tag.name" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="tag-color" class="form-label">{{ $t("Color") }}</label>
<div class="d-flex">
<div class="col-8 pe-1">
<vue-multiselect
v-model="selectedColor"
:options="colorOptions"
:multiple="false"
:searchable="true"
:placeholder="$t('color')"
track-by="color"
label="name"
select-label=""
deselect-label=""
>
<template #option="{ option }">
<div
class="mx-2 py-1 px-3 rounded d-inline-flex"
style="height: 24px; color: white;"
:style="{ backgroundColor: option.color + ' !important' }"
>
<span>{{ option.name }}</span>
</div>
</template>
<template #singleLabel="{ option }">
<div
class="py-1 px-3 rounded d-inline-flex"
style="height: 24px; color: white;"
:style="{ backgroundColor: option.color + ' !important' }"
>
<span>{{ option.name }}</span>
</div>
</template>
</vue-multiselect>
</div>
<div class="col-4 ps-1">
<input id="tag-color-hex" v-model="tag.color" type="text" class="form-control">
</div>
</div>
</div>
<div class="mb-3">
<label for="tag-monitors" class="form-label">{{ $tc("Monitor", selectedMonitors.length) }}</label>
<div class="tag-monitors-list">
<router-link v-for="monitor in selectedMonitors" :key="monitor.id" class="d-flex align-items-center justify-content-between text-decoration-none tag-monitors-list-row py-2 px-3" :to="monitorURL(monitor.id)" @click="modal.hide()">
<span>{{ monitor.name }}</span>
<button type="button" class="btn-rm-monitor btn btn-outline-danger ms-2 py-1" @click.stop.prevent="removeMonitor(monitor.id)">
<font-awesome-icon class="" icon="times" />
</button>
</router-link>
</div>
<div v-if="allMonitorList.length > 0" class="pt-3 px-3">
<label class="form-label">{{ $t("Add a monitor") }}:</label>
<select v-model="selectedAddMonitor" class="form-control">
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button v-if="tag" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
{{ $t("Delete") }}
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Save") }}
</button>
</div>
</div>
</div>
</div>
</form>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteTag">
{{ $t("confirmDeleteTagMsg") }}
</Confirm>
</template>
<script>
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
import VueMultiselect from "vue-multiselect";
import { colorOptions } from "../util-frontend";
import { useToast } from "vue-toastification";
import { getMonitorRelativeURL } from "../util.ts";
const toast = useToast();
export default {
components: {
VueMultiselect,
Confirm,
},
props: {
updated: {
type: Function,
default: () => {},
}
},
data() {
return {
modal: null,
processing: false,
selectedColor: {
name: null,
color: null,
},
tag: {
id: null,
name: "",
color: "",
// Do not set default value here, please scroll to show()
},
monitors: [],
removingMonitor: [],
addingMonitor: [],
selectedAddMonitor: null,
};
},
computed: {
colorOptions() {
if (!colorOptions(this).find(option => option.color === this.tag.color)) {
return colorOptions(this).concat(
{
name: "custom",
color: this.tag.color
});
} else {
return colorOptions(this);
}
},
selectedMonitors() {
return this.monitors
.concat(Object.values(this.$root.monitorList).filter(monitor => this.addingMonitor.includes(monitor.id)))
.filter(monitor => !this.removingMonitor.includes(monitor.id));
},
allMonitorList() {
return Object.values(this.$root.monitorList).filter(monitor => !this.selectedMonitors.includes(monitor));
},
},
watch: {
// Set color option to "Custom" when a unknown color is entered
"tag.color"(to, from) {
if (colorOptions(this).find(x => x.color === to) == null) {
this.selectedColor.name = this.$t("Custom");
this.selectedColor.color = to;
}
},
selectedColor(to, from) {
if (to != null) {
this.tag.color = to.color;
}
},
/**
* Selected a monitor and add to the list.
*/
selectedAddMonitor(monitor) {
if (monitor) {
if (this.removingMonitor.includes(monitor.id)) {
this.removingMonitor = this.removingMonitor.filter(id => id !== monitor.id);
} else {
this.addingMonitor.push(monitor.id);
}
this.selectedAddMonitor = null;
}
},
},
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
/**
* Show confirmation for deleting a tag
*/
deleteConfirm() {
this.$refs.confirmDelete.show();
},
/**
* Load tag information for display in the edit dialog
* @param {Object} tag tag object to edit
* @returns {void}
*/
show(tag) {
if (tag) {
this.selectedColor = this.colorOptions.find(x => x.color === tag.color) ?? {
name: this.$t("Custom"),
color: tag.color
};
this.tag.id = tag.id;
this.tag.name = tag.name;
this.tag.color = tag.color;
this.monitors = this.monitorsByTag(tag.id);
this.removingMonitor = [];
this.addingMonitor = [];
this.selectedAddMonitor = null;
}
this.modal.show();
},
/**
* Submit tag and monitorTag changes to server
* @returns {void}
*/
async submit() {
this.processing = true;
let editResult = true;
for (let addId of this.addingMonitor) {
await this.addMonitorTagAsync(this.tag.id, addId, "").then((res) => {
if (!res.ok) {
toast.error(res.msg);
editResult = false;
}
});
}
for (let removeId of this.removingMonitor) {
this.monitors.find(monitor => monitor.id === removeId)?.tags.forEach(async (monitorTag) => {
await this.deleteMonitorTagAsync(this.tag.id, removeId, monitorTag.value).then((res) => {
if (!res.ok) {
toast.error(res.msg);
editResult = false;
}
});
});
}
this.$root.getSocket().emit("editTag", this.tag, (res) => {
this.$root.toastRes(res);
this.processing = false;
if (res.ok && editResult) {
this.updated();
this.modal.hide();
}
});
},
/**
* Delete the editing tag from server
* @returns {void}
*/
deleteTag() {
this.processing = true;
this.$root.getSocket().emit("deleteTag", this.tag.id, (res) => {
this.$root.toastRes(res);
this.processing = false;
if (res.ok) {
this.updated();
this.modal.hide();
}
});
},
/**
* Remove a monitor from the monitors list locally
* @param {number} id id of the tag to remove
* @returns {void}
*/
removeMonitor(id) {
if (this.addingMonitor.includes(id)) {
this.addingMonitor = this.addingMonitor.filter(x => x !== id);
} else {
this.removingMonitor.push(id);
}
},
/**
* Get monitors which has a specific tag locally
* @param {number} tagId id of the tag to filter
* @returns {Object[]} list of monitors which has a specific tag
*/
monitorsByTag(tagId) {
return Object.values(this.$root.monitorList).filter((monitor) => {
return monitor.tags.find(monitorTag => monitorTag.tag_id === tagId);
});
},
/**
* Get URL of monitor
* @param {number} id ID of monitor
* @returns {string} Relative URL of monitor
*/
monitorURL(id) {
return getMonitorRelativeURL(id);
},
/**
* Add a tag to a monitor asynchronously
* @param {number} tagId ID of tag to add
* @param {number} monitorId ID of monitor to add tag to
* @param {string} value Value of tag
* @returns {Promise<void>}
*/
addMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("addMonitorTag", tagId, monitorId, value, resolve);
});
},
/**
* Delete a tag from a monitor asynchronously
* @param {number} tagId ID of tag to remove
* @param {number} monitorId ID of monitor to remove tag from
* @param {string} value Value of tag
* @returns {Promise<void>}
*/
deleteMonitorTagAsync(tagId, monitorId, value) {
return new Promise((resolve) => {
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dark {
.modal-dialog .form-text, .modal-dialog p {
color: $dark-font-color;
}
}
.btn-rm-monitor {
padding-left: 11px;
padding-right: 11px;
}
.tag-monitors-list {
max-height: 40vh;
overflow-y: scroll;
}
.tag-monitors-list .tag-monitors-list-row {
cursor: pointer;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
.dark & {
border-bottom: 1px solid $dark-border-color;
}
&:hover {
background-color: $highlight-white;
}
.dark &:hover {
background-color: $dark-bg2;
}
}
</style>

View File

@ -130,6 +130,7 @@
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification";
import { colorOptions } from "../util-frontend";
import Tag from "../components/Tag.vue";
const toast = useToast();
@ -176,24 +177,7 @@ export default {
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id === tag.id));
},
colorOptions() {
return [
{ name: this.$t("Gray"),
color: "#4B5563" },
{ name: this.$t("Red"),
color: "#DC2626" },
{ name: this.$t("Orange"),
color: "#D97706" },
{ name: this.$t("Green"),
color: "#059669" },
{ name: this.$t("Blue"),
color: "#2563EB" },
{ name: this.$t("Indigo"),
color: "#4F46E5" },
{ name: this.$t("Purple"),
color: "#7C3AED" },
{ name: this.$t("Pink"),
color: "#DB2777" },
];
return colorOptions(this);
},
validateDraftTag() {
let nameInvalid = false;
@ -204,7 +188,7 @@ export default {
nameInvalid = false;
valueInvalid = false;
invalid = false;
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0) {
} else if (this.existingTags.filter(tag => tag.name === this.newDraftTag.name).length > 0 && this.newDraftTag.select == null) {
// Try to create new tag with existing name
nameInvalid = true;
invalid = true;

View File

@ -1,8 +1,10 @@
<template>
<span :class="className" :title="24 + $t('-hour')">{{ uptime }}</span>
<span :class="className" :title="title">{{ uptime }}</span>
</template>
<script>
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
export default {
props: {
/** Monitor this represents */
@ -24,6 +26,9 @@ export default {
computed: {
uptime() {
if (this.type === "maintenance") {
return this.$t("statusMaintenance");
}
let key = this.monitor.id + "_" + this.type;
@ -35,15 +40,19 @@ export default {
},
color() {
if (this.lastHeartBeat.status === 0) {
if (this.lastHeartBeat.status === MAINTENANCE) {
return "maintenance";
}
if (this.lastHeartBeat.status === DOWN) {
return "danger";
}
if (this.lastHeartBeat.status === 1) {
if (this.lastHeartBeat.status === UP) {
return "primary";
}
if (this.lastHeartBeat.status === 2) {
if (this.lastHeartBeat.status === PENDING) {
return "warning";
}
@ -67,6 +76,14 @@ export default {
return "";
},
title() {
if (this.type === "720") {
return `30${this.$t("-day")}`;
}
return `24${this.$t("-hour")}`;
}
},
};
</script>

View File

@ -6,7 +6,7 @@
</i18n-t>
<input id="clicksendsms-login" v-model="$parent.notification.clicksendsmsLogin" type="text" class="form-control" required>
<label for="clicksendsms-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="clicksendsms-key" v-model="$parent.notification.clicksendsmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<div class="form-text">

View File

@ -11,7 +11,7 @@
<div class="mb-3">
<label for="goalert-token" class="form-label">{{ $t("Token") }}</label>
<HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="one-time-code" :required="true"></HiddenInput>
<HiddenInput id="goalert-token" v-model="$parent.notification.goAlertToken" autocomplete="new-password" :required="true"></HiddenInput>
<div class="form-text">
{{ $t("goAlertIntegrationKeyInfo") }}

View File

@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="gotify-application-token" class="form-label">{{ $t("Application Token") }}</label>
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="gotify-application-token" v-model="$parent.notification.gotifyapplicationToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>

View File

@ -0,0 +1,36 @@
<template>
<div class="mb-3">
<label for="kook-bot-token" class="form-label">{{ $t("Bot Token") }}</label>
<HiddenInput id="kook-bot-token" v-model="$parent.notification.kookBotToken" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetKookBotToken" class="form-text">
<a href="https://developer.kookapp.cn/bot" target="_blank">https://developer.kookapp.cn/bot</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="kook-guild-id" class="form-label">{{ $t("Guild ID") }}</label>
<div class="input-group mb-3">
<input id="kook-guild-id" v-model="$parent.notification.kookGuildID" type="text" class="form-control" required>
</div>
<div class="form-text">
<p style="margin-top: 8px;">
{{ $t("wayToGetKookGuildID") }}
</p>
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://developer.kookapp.cn" target="_blank">https://developer.kookapp.cn</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
}
};
</script>

View File

@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
<b>{{ $t("Basic Settings") }}</b>

View File

@ -9,7 +9,7 @@
</div>
<div class="mb-3">
<label for="access-token" class="form-label">{{ $t("Access Token") }}</label><span style="color: red;"><sup>*</sup></span>
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="one-time-code" :maxlength="500"></HiddenInput>
<HiddenInput id="access-token" v-model="$parent.notification.accessToken" :required="true" autocomplete="new-password" :maxlength="500"></HiddenInput>
</div>
<div class="form-text">

View File

@ -27,6 +27,10 @@
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
</div>
</div>
<div class="mb-3">
<label for="ntfy-icon" class="form-label">{{ $t("IconUrl") }}</label>
<input id="ntfy-icon" v-model="$parent.notification.ntfyIcon" type="text" class="form-control">
</div>
</template>
<script>

View File

@ -11,7 +11,7 @@
</div>
<div class="mb-3">
<label for="octopush-key" class="form-label">{{ $t("octopushAPIKey") }}</label>
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="octopush-key" v-model="$parent.notification.octopushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
<label for="octopush-login" class="form-label">{{ $t("octopushLogin") }}</label>
<input id="octopush-login" v-model="$parent.notification.octopushLogin" type="text" class="form-control" required>
</div>

View File

@ -3,7 +3,7 @@
<label for="promosms-login" class="form-label">{{ $t("promosmsLogin") }}</label>
<input id="promosms-login" v-model="$parent.notification.promosmsLogin" type="text" class="form-control" required>
<label for="promosms-key" class="form-label">{{ $t("promosmsPassword") }}</label>
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="promosms-key" v-model="$parent.notification.promosmsPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="promosms-type-sms" class="form-label">{{ $t("SMS Type") }}</label>
@ -26,6 +26,10 @@
<label for="promosms-sender-name" class="form-label">{{ $t("promosmsSMSSender") }}</label>
<input id="promosms-sender-name" v-model="$parent.notification.promosmsSenderName" type="text" minlength="3" maxlength="11" class="form-control">
</div>
<div class="form-check form-switch">
<input id="promosms-allow-long" v-model="$parent.notification.promosmsAllowLongSMS" type="checkbox" class="form-check-input">
<label for="promosms-allow-long" class="form-label">{{ $t("promosmsAllowLongSMS") }}</label>
</div>
</template>
<script>

View File

@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="one-time-code" placeholder="PDUxxxx"></HiddenInput>
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="new-password" placeholder="PDUxxxx"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View File

@ -1,7 +1,7 @@
<template>
<div class="mb-3">
<label for="pushbullet-access-token" class="form-label">{{ $t("Access Token") }}</label>
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushbullet-access-token" v-model="$parent.notification.pushbulletAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View File

@ -1,9 +1,9 @@
<template>
<div class="mb-3">
<label for="pushover-user" class="form-label">{{ $t("User Key") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushover-user" v-model="$parent.notification.pushoveruserkey" :required="true" autocomplete="new-password"></HiddenInput>
<label for="pushover-app-token" class="form-label">{{ $t("Application Token") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushover-app-token" v-model="$parent.notification.pushoverapptoken" :required="true" autocomplete="new-password"></HiddenInput>
<label for="pushover-device" class="form-label">{{ $t("Device") }}</label>
<input id="pushover-device" v-model="$parent.notification.pushoverdevice" type="text" class="form-control">
<label for="pushover-device" class="form-label">{{ $t("Message Title") }}</label>

View File

@ -1,13 +1,13 @@
<template>
<div class="mb-3">
<label for="pushy-app-token" class="form-label">{{ $t("pushyAPIKey") }}</label>
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushy-app-token" v-model="$parent.notification.pushyAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="pushy-user-key" class="form-label">{{ $t("pushyToken") }}</label>
<div class="input-group mb-3">
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="one-time-code"></HiddenInput>
<HiddenInput id="pushy-user-key" v-model="$parent.notification.pushyToken" :required="true" autocomplete="new-password"></HiddenInput>
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

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