Compare commits

..

378 Commits

Author SHA1 Message Date
Louis Lam
11d01ebc78 Add queue package 2023-07-19 03:14:48 +08:00
Nelson Chan
66cfbd02c3 Fix: Update monitor-list height (#3444) 2023-07-18 14:39:05 +08:00
Louis Lam
688f23035b Update @louislam/ping to 0.4.4-mod.1 (Add back OpenBSD ping support) 2023-07-18 14:36:59 +08:00
Frank Elsinga
7701e2ad36 Update README.md (#3438) 2023-07-18 11:17:20 +08:00
Louis Lam
8e72d6f534 Fix codespace url (#3436)
* Fix codespace url (https://github.com/louislam/uptime-kuma/pull/3432#discussion_r1265120809)
2023-07-17 20:14:05 +08:00
Muhammed Hussein karimi
278b88a9d9 feat: added kafka producer (#3268)
*  feat: added kafka producer

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: eslint warn

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: typings and auth problems

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: better variable name to trrack disconnection

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: grouping Kafka Producer special settings into one template

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

*  feat: add kafka producer translations into `en.json`

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: disable close-on-select on kafka broker picker

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: `en.json` invalid json (conflict resolve)

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* Nostr dm notifications (#3051)

* Add nostr DM notification provider

* require crypto for node 18 compatibility

* remove whitespace

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* move closer to where it is used

* simplify success or failure logic

* don't clobber the non-alert msg

* Update server/notification-providers/nostr.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* polyfills required for node <= 18

* resolve linter warnings

* missing comma

---------

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Drop nostr

* Minor

* Fix a bug of clone

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-07-17 16:15:44 +08:00
Louis Lam
084cf01fcd Add support for Codespaces (#3432)
* Create devcontainer.json

* WIP

* WIP

* WIP

* Create README.md

* Try to fix cypress issue

* Add extensions

* WIP

* Minor
2023-07-17 14:54:40 +08:00
Louis Lam
25c8196641 Support Node.js 20 again (#3431)
* Support >= Node.js 20.4.0

* Improve the Node.js warning, ban 20.0 to 20.3

* Update

* Minor
2023-07-17 13:17:00 +08:00
Frank Elsinga
baf5613dfa Fixed Replit not being mentioned in the help template (#3430) 2023-07-16 22:43:51 +08:00
Louis Lam
695691468c Merge pull request #3428 from chakflying/fix/no-delete-draft-tag
Fix: Hide delete button in Settings -> Create New Tag
2023-07-16 21:39:12 +08:00
Louis Lam
4891ec4527 Merge pull request #3312 from chakflying/feat/monitor-list-improved-filtering
Feat: Improved comprehensive monitor list filtering
2023-07-16 21:27:42 +08:00
Louis Lam
e2a87eb430 Improve the filter translate keys 2023-07-16 21:15:25 +08:00
Louis Lam
80927332cb Merge remote-tracking branch 'origin/master' into feat/monitor-list-improved-filtering 2023-07-16 21:04:46 +08:00
Nelson Chan
a0eb733d54 Fix: Hide the Delete button correctly 2023-07-16 08:10:02 +08:00
Louis Lam
21d556528f Fix #3420 timezone issue (#3425) 2023-07-15 23:23:27 +08:00
Louis Lam
357466cc90 Minor 2023-07-15 21:27:39 +08:00
Louis Lam
b038d09349 Minor 2023-07-15 21:26:41 +08:00
Louis Lam
5dd4231e56 Fix pr-test image 2023-07-15 21:24:33 +08:00
Louis Lam
c6d0c431bd Merge pull request #3080 from duanearnett/feature/add-channel-notification-for-slack
Adds configurable @channel notification for Slack integrations
2023-07-15 18:41:17 +08:00
Louis Lam
d1b7f4c834 Merge pull request #3329 from chakflying/feat/badge-generator-placeholders
Chore: Add value placeholders & preview for badge generator
2023-07-15 01:19:37 +08:00
Louis Lam
5c4180fb45 Merge conflicts 2023-07-15 01:09:09 +08:00
Louis Lam
345e61abca Merge remote-tracking branch 'origin/master' into feat/badge-generator-placeholders
# Conflicts:
#	package-lock.json
#	package.json
2023-07-15 01:05:34 +08:00
Louis Lam
dd1526deff Merge pull request #3421 from louislam/some-update
Some update
2023-07-14 18:07:42 +08:00
Louis Lam
be26bb75d9 Update version handling 2023-07-14 18:02:49 +08:00
Louis Lam
99fb5836e2 Add SMSC (СМСЦентр) provider notification (#3335) By @FlatronBuda
* Add SMSC, code from #3334

Co-authored-by: FlatronBuda <>

* Update server/notification-providers/smsc.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Update server/notification-providers/smsc.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Update server/notification-providers/smsc.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Update according to @FlatronBuda

* Move to the regional list

---------

Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-07-14 14:29:35 +08:00
Louis Lam
2f5a565ce4 Merge pull request #3381 from n-thumann/fix_ipv6_handling
Fix handling of IPv6 addresses in getClientIP
2023-07-14 12:34:59 +08:00
Louis Lam
973db9d4b2 Merge pull request #3330 from chakflying/fix/datetime-wrong-type
Chore: Fix incorrect data type for DateTime component
2023-07-14 12:26:22 +08:00
Muhammed Hussein karimi
6bece8796e feat: json-query monitor added (#3253)
*  feat: json-query monitor added

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: import warning error

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: br tag and remove comment

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: supporting compare string with other types

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: switch to a better lib for json query

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: better description on json query and using `v-html` in jsonQueryDescription element to fix `a` tags

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: result variable in error message

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: typos in json query description

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* 📝 docs: `HTTP(s) Json Query` added to monitor list in `README.md`

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: needed white space in `README.md`

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Nostr dm notifications (#3051)

* Add nostr DM notification provider

* require crypto for node 18 compatibility

* remove whitespace

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* move closer to where it is used

* simplify success or failure logic

* don't clobber the non-alert msg

* Update server/notification-providers/nostr.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* polyfills required for node <= 18

* resolve linter warnings

* missing comma

---------

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Drop nostr

* Rebuild package-lock.json

* Lint

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
Co-authored-by: zappityzap <128872140+zappityzap@users.noreply.github.com>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-07-13 23:37:26 +08:00
Louis Lam
e7d1b4e14a Merge pull request #3174 from chakflying/fix/push-monitor-safe-restart
Fix: Use safeBeat in push monitor
2023-07-13 23:14:28 +08:00
Louis Lam
e5c6783781 Merge pull request #3205 from woj-tek/master
Add option to use ApiKeys in Twilio in addition to main account credentials
2023-07-13 23:11:05 +08:00
Louis Lam
ac68a35d3a Merge pull request #3386 from chakflying/patch-1
Fix: Multiselect blocked by bottom bar
2023-07-10 15:16:02 +08:00
Louis Lam
d825dbf828 Merge pull request #3188 from chakflying/fix/radius-timeout
Fix: Set radius connection timeout to monitor default
2023-07-09 22:47:39 +08:00
Louis Lam
cfb4bbc6cb Merge pull request #3088 from chakflying/feat/webhook-custom-body
Feat: Custom request body for Webhook Notifications
2023-07-09 20:42:50 +08:00
Louis Lam
293015ff35 Parse x-www-form-urlencoded for /test-webhook 2023-07-09 18:53:57 +08:00
Louis Lam
18d8b3a8e0 Merge remote-tracking branch 'origin/master' into feat/webhook-custom-body 2023-07-09 18:20:06 +08:00
nthumann
d55794e1a5 Add test cases for IPv6 addresses in getClientIP 2023-07-08 17:46:26 +02:00
Nelson Chan
3d50572dd7 Fix: Multiselect blocked by bottom bar 2023-07-08 23:33:14 +08:00
Louis Lam
cdb38d49eb Merge pull request #3380 from chakflying/experiment/incremental-vacuum-job
Feat: Run incremental_vacuum and optimize
2023-07-08 21:59:58 +08:00
Louis Lam
80b55786a4 Merge pull request #2574 from sjoukedv/feat/global-status-page-badge
feat(server): add badge for overall status of status-page
2023-07-08 21:55:52 +08:00
Louis Lam
fe40d819bd Update send403 to sendHttpError 2023-07-08 21:34:58 +08:00
Louis Lam
3dbd8277f0 Merge remote-tracking branch 'origin/master' into feat/global-status-page-badge
# Conflicts:
#	.gitignore
2023-07-08 21:28:57 +08:00
Louis Lam
771d21c4ad Update dependencies (#3384) 2023-07-08 17:14:41 +08:00
Louis Lam
ed6b4e5ae5 Merge remote-tracking branch 'origin/master' into miles/invert-keyword
# Conflicts:
#	server/database.js
2023-07-08 16:19:44 +08:00
Louis Lam
3b9c95a8a8 Prevent users from specifying an unexpected executable as Chromium (#3348) 2023-07-08 15:52:09 +08:00
nthumann
cdf6922bdd Fix handling of IPv6 addresses in getClientIP 2023-07-08 00:02:01 +02:00
Nelson Chan
9954ba82e7 Feat: Run incremental_vacuum and optimize 2023-07-08 04:57:53 +08:00
Louis Lam
19873e5b9e Remove npm cache from the auto test workflow (#3359) 2023-07-05 21:03:02 +08:00
Louis Lam
13ae878ee8 Merge pull request #3347 from louislam/1.22.X
1.22.x merge to master
2023-07-05 11:35:36 +08:00
DevMirza
1774bb86dc 🐛 fix lint warning (#3355) 2023-07-04 23:46:36 +08:00
Louis Lam
c583037dff Fix auto test for armv7 2023-07-04 23:13:08 +08:00
Louis Lam
8223121cd8 Update to 1.22.1 2023-07-04 20:41:30 +08:00
Louis Lam
ff22010330 Update dependencies 2023-07-04 16:24:03 +08:00
Louis Lam
a9d691a6a8 Update dependencies 2023-07-03 21:05:40 +08:00
Louis Lam
7c529d8f83 Check docker before build 2023-07-03 20:30:21 +08:00
Louis Lam
4fe0891a60 Merge pull request #3296 from crystalcommunication/pr-custom-domain-auto
Fix auto theme for status pages on custom domains
2023-07-03 15:48:38 +08:00
Francisco Marques
bd5496d267 Fixed update checker making requests to uptime.kuma.pet even when turned off (#2281)
* fix: update checker

- fixed bug where it would make the request to uptime.kuma.pet regardless of the `checkUpdate` config;
- defined constants in the top of the document for easier configuration/documentation;
- removed unnecessary compareVersions: we were comparing the same var on both sides res.data.beta, so it will always be equal.

* improvement: better logging and added doc

* improved UPDATE_CHECKER_INTERVAL_MS const

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-07-03 15:28:03 +08:00
Louis Lam
a0736e04b2 Merge pull request #3346 from louislam/revert-plugin-code
Drop unused code
2023-07-03 15:21:13 +08:00
Louis Lam
df8fcffb19 Drop unused code 2023-07-03 14:50:30 +08:00
Nelson Chan
9da712054a Chore: Fix incorrect id for CopyableInput 2023-07-02 18:57:07 +08:00
Nelson Chan
af78da1dd9 Chore: Add missing translation
Co-authored-by: Yoswaris Lawpaiboon <22832362+kiznick@users.noreply.github.com>
2023-07-02 18:52:56 +08:00
Nelson Chan
9e041f219b Chore: Fix translation string
Co-authored-by: Yoswaris Lawpaiboon <22832362+kiznick@users.noreply.github.com>
2023-07-02 18:52:36 +08:00
Louis Lam
8c60e902e1 Remove an unused variable 2023-07-01 22:44:33 +08:00
Louis Lam
de74efb2e6 Merge pull request #3169 from janow25/docker-health-check
Added Docker Health Status Support
2023-07-01 02:55:05 +08:00
Louis Lam
9ee2780e9e Merge pull request #2871 from pruekk/chore/missing-notificationList
chore: notification toggle missing when import from backup
2023-06-29 22:42:09 +08:00
Nelson Chan
a386f1fc9e [Experiment] Use incremental vacuum to speed up delete? (#2800)
* DB: Use incremental vacuum

* Chore: Add log for delete monitor exec. time

* WIP: Test synchronous NORMAL
2023-06-29 22:41:01 +08:00
Nelson Chan
35154ef9c5 Chore: Translate hours
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-06-29 19:04:34 +08:00
Nelson Chan
1baa592824 Chore: Translate preview alt
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-06-29 19:04:09 +08:00
Nelson Chan
9882fc65b1 Fix: Incorrect label for badge values 2023-06-29 07:12:19 +08:00
Nelson Chan
3e5e7e6e32 Fix: Incorrect options for cert-exp badge 2023-06-29 07:12:19 +08:00
Nelson Chan
0e725569e5 Feat: Add placeholders for badge generator
Chore: Save as dev dep.
2023-06-29 07:11:58 +08:00
Nelson Chan
afcfb7e19c Fix: Incorrect data type for DateTime component 2023-06-29 04:17:47 +08:00
Louis Lam
eaee55fc8f Add arm runners to this repo (#3326) 2023-06-28 21:04:38 +08:00
Louis Lam
affac0a97b Update Windows Portable to 1.0.1 2023-06-28 14:52:53 +08:00
Louis Lam
a12e7eba72 [exe] Try to deal with the false positive issue (#3143)
* [exe] Add more assembly info
2023-06-28 14:52:53 +08:00
Louis Lam
4f6035899d Real browser monitor type (#3308) 2023-06-27 15:54:33 +08:00
Louis Lam
dd77baabe1 Merge pull request #3234 from kefoster951/fix_redis_auth
Fix redis authentication reattempt issue
2023-06-27 15:21:30 +08:00
Louis Lam
820f2eec9f Merge remote-tracking branch 'origin/1.23.X' 2023-06-26 21:38:12 +08:00
Louis Lam
4b913c8b4c Update to 1.22.0 2023-06-26 21:12:11 +08:00
Louis Lam
d01c7c3faa Update package-lock.json 2023-06-26 21:09:21 +08:00
Louis Lam
772a946234 Merge pull request #3242 from UptimeKumaBot/weblate-uptime-kuma-uptime-kuma
Translations Update from Weblate
2023-06-26 21:04:52 +08:00
Nelson Chan
f8c9a20afd Chore: Disable clear filters button 2023-06-26 13:42:46 +08:00
Nelson Chan
cea894cc6d Chore: Fix lint 2023-06-26 13:39:19 +08:00
Nelson Chan
79b38e0e7b Feat: Improve monitorList filtering 2023-06-26 13:23:06 +08:00
Nelson Chan
7cc9783436 Fix: Active needs to return bool instead of 0 2023-06-26 13:21:51 +08:00
Weblate
21405f71b5 Merge remote-tracking branch 'origin/master' 2023-06-26 04:54:08 +00:00
Louis Lam
b4b6e07e6b Merge pull request #3310 from chakflying/chore/auth-logging
Chore: Add logging for failed auth
2023-06-26 12:54:01 +08:00
Tarun Singh
cf4220901b Translated using Weblate (Hindi)
Currently translated at 5.4% (41 of 754 strings)

Co-authored-by: Tarun Singh <tarun7singh7@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hi/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
Buchtič
f3996fdef4 Translated using Weblate (Czech)
Currently translated at 100.0% (754 of 754 strings)

Co-authored-by: Buchtič <martin.buchta@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
CarlosCF
1dfe5227ad Translated using Weblate (Galician)
Currently translated at 2.7% (21 of 754 strings)

Added translation using Weblate (Galician)

Co-authored-by: CarlosCF <carloscaamano@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/gl/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
SEOAlexRamon
4ead0609af Translated using Weblate (Catalan)
Currently translated at 3.4% (26 of 754 strings)

Translated using Weblate (Spanish)

Currently translated at 95.0% (717 of 754 strings)

Added translation using Weblate (Catalan)

Co-authored-by: SEOAlexRamon <seoalexramon@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ca/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
401Unauthorized
a8bf52b1e0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (754 of 754 strings)

Co-authored-by: 401Unauthorized <hi@4o1.to>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
Adam Stachowicz
ede6d90497 Translated using Weblate (Polish)
Currently translated at 96.6% (729 of 754 strings)

Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pl/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
Cyril59310
4b8e86efb7 Translated using Weblate (French)
Currently translated at 100.0% (754 of 754 strings)

Translated using Weblate (French)

Currently translated at 100.0% (753 of 753 strings)

Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
Alex Javadi
5f706e1921 Translated using Weblate (Persian)
Currently translated at 100.0% (754 of 754 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (753 of 753 strings)

Co-authored-by: Alex Javadi <15309978+aljvdi@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
Vincent Houdan
722c64a4d1 Translated using Weblate (French)
Currently translated at 100.0% (752 of 752 strings)

Co-authored-by: Vincent Houdan <vincenthoudan@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
Rachatat Bunpat
23de52ca5a Translated using Weblate (Thai)
Currently translated at 85.3% (642 of 752 strings)

Co-authored-by: Rachatat Bunpat <rbunpat@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/th/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
Yaroslav
3d3fb357f9 Translated using Weblate (Russian)
Currently translated at 100.0% (752 of 752 strings)

Co-authored-by: Yaroslav <ykargin@outlook.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:40 +00:00
Tivin
3b9aa00126 Translated using Weblate (Hebrew (Israel))
Currently translated at 94.4% (710 of 752 strings)

Co-authored-by: Tivin <git@fickle.email>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/he_IL/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
Saurabh
29267e5c2e Translated using Weblate (Hindi)
Currently translated at 1.3% (10 of 752 strings)

Added translation using Weblate (Hindi)

Co-authored-by: Saurabh <saurabhsharma2u@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hi/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
Ray
3e801323b6 Translated using Weblate (Chinese (Traditional))
Currently translated at 95.3% (716 of 751 strings)

Co-authored-by: Ray <ray7496422@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
b80fd81d24 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (751 of 751 strings)

Co-authored-by: 楓 <nitu2003@126.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
kosssi
9cb776405a Translated using Weblate (French)
Currently translated at 100.0% (751 of 751 strings)

Co-authored-by: kosssi <github@fafaru.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
Marco
de7ae3e2db Translated using Weblate (German)
Currently translated at 100.0% (754 of 754 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (754 of 754 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (751 of 751 strings)

Co-authored-by: Marco <marco@nanoweb.ch>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
Michal
e49ced0524 Translated using Weblate (Czech)
Currently translated at 99.7% (752 of 754 strings)

Translated using Weblate (Czech)

Currently translated at 99.7% (750 of 752 strings)

Translated using Weblate (Czech)

Currently translated at 99.3% (746 of 751 strings)

Co-authored-by: Michal <black23@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
AnnAngela
7e782edf44 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (753 of 753 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (752 of 752 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (751 of 751 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (748 of 748 strings)

Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
stanol
43e1e3c272 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (754 of 754 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (753 of 753 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (752 of 752 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (751 of 751 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (748 of 748 strings)

Co-authored-by: stanol <stanol777@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
Ömer Faruk Genç
dc4cf7087f Translated using Weblate (Turkish)
Currently translated at 100.0% (754 of 754 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (753 of 753 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (752 of 752 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (751 of 751 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (748 of 748 strings)

Co-authored-by: Ömer Faruk Genç <omer@farukgenc.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
Oleg Logvinov
65a0a2b2b5 Translated using Weblate (Russian)
Currently translated at 99.5% (745 of 748 strings)

Co-authored-by: Oleg Logvinov <oleglogwinow@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
ITQ
2d269c3639 Translated using Weblate (Russian)
Currently translated at 100.0% (754 of 754 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (752 of 752 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (751 of 751 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (748 of 751 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (745 of 748 strings)

Co-authored-by: ITQ <itq.dev@ya.ru>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
MrEddX
11bad53709 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (754 of 754 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (753 of 753 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (752 of 752 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (751 of 751 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (748 of 748 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.8% (747 of 748 strings)

Co-authored-by: MrEddX <mreddx@chatrix.one>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:39 +00:00
Arin Faraj
9f7782b1c1 Translated using Weblate (Kurdish (Central))
Currently translated at 5.8% (44 of 746 strings)

Co-authored-by: Arin Faraj <arin.abdul99@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ckb/
Translation: Uptime Kuma/Uptime Kuma
2023-06-26 04:52:38 +00:00
Louis Lam
9fb8f94e22 Merge pull request #3311 from tarun7singh/monitor-group-fix
Added fix to remove children when type changed
2023-06-26 12:52:31 +08:00
Tarun Singh
7a34103da6 Added fix to remove children when type changed 2023-06-25 22:44:15 -04:00
Nelson Chan
8955c3816b Chore: Rename select ID & add translation 2023-06-26 05:00:14 +08:00
Nelson Chan
7761e9a05e Chore: Add translations to options text 2023-06-26 04:59:56 +08:00
Nelson Chan
c9d6e576ab Chore: Remove redundant assign
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-06-26 04:59:55 +08:00
Nelson Chan
97d38ee1a8 Feat: Add custom body for Webhook Notif. 2023-06-26 04:59:55 +08:00
Nelson Chan
cc94609423 Chore: Add logging for failed auth 2023-06-26 04:49:49 +08:00
Louis Lam
149f8c3646 Update required node version and update dependencies 2023-06-25 12:41:32 +08:00
Louis Lam
bdcbd6389b Merge pull request #3283 from lassebm/ha-strip-trailing-slashes
Strip trailing slashes to avoid 404 from Home Assistant's API endpoint
2023-06-24 21:11:02 +08:00
Louis Lam
c06b929529 Update dependencies 2023-06-24 20:24:51 +08:00
crystal
d3ecdb8456 Fix auto theme for status pages on custom domains 2023-06-21 09:58:49 -06:00
Cyril59310
4e420ee3ff Missing translation key (#3281)
* missing translation key

* Update src/lang/en.json

Co-authored-by: Frank Elsinga <frank@elsinga.de>

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-06-19 13:17:58 +08:00
Lasse Bang Mikkelsen
a00561ff09 Strip trailing slashes to avoid 404 2023-06-18 18:28:30 +02:00
Louis Lam
6af44e0780 ChatGPT fixed my horrible grammar 2023-06-18 23:45:50 +08:00
Louis Lam
596402e71f Merge pull request #3246 from chakflying/ui/monitor-group-select
UI: Improve no group monitor message
2023-06-17 16:43:03 +08:00
Louis Lam
62bbc1cf55 Merge pull request #3270 from chakflying/fix/clone-group-monitor-fields
Fix: Remove extra fields on clone
2023-06-16 23:20:31 +08:00
Nelson Chan
19fc7d31e6 Fix: Remove extra fields on clone 2023-06-15 00:58:45 +08:00
Kenneth Foster
6708eed121 Fixed error handling if client is closed 2023-06-14 11:49:33 -04:00
kefoster951
3c56a6f395 Merge branch 'louislam:master' into fix_redis_auth 2023-06-14 11:47:44 -04:00
Louis Lam
2b46693995 Merge pull request #3239 from madnight/master
Fix: prometheus monitor_status metric has 4 values
2023-06-13 23:09:29 +08:00
Louis Lam
c61a3d360f Merge pull request #3254 from CommanderStorm/transaltion_home
Chore: Added a translation key for "Home"
2023-06-13 23:05:02 +08:00
Louis Lam
392f95cdd2 Merge pull request #3260 from DevItq/master
Add translation for Not Found page
2023-06-13 23:04:04 +08:00
ITQ
dfc6e5ea5b Update NotFound.vue 2023-06-13 16:10:41 +03:00
Frank Elsinga
ba4d925374 Added a translation key for "Home" 2023-06-12 17:58:54 +02:00
kefoster951
d37c33ad42 Update server/util-server.js
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-06-12 11:06:20 -04:00
Nelson Chan
c04194191f UI: improve disabled select legibility 2023-06-12 19:26:50 +08:00
Nelson Chan
de9ad0fe60 UI: Add help msg for no Group Mon. 2023-06-12 19:26:50 +08:00
Louis Lam
8884c2108b Merge pull request #3249 from CommanderStorm/translation_bugfix
Chore: Added additional translation keys
2023-06-12 14:07:19 +08:00
Frank Elsinga
ac8ca36895 Enabled adding missing keys to the translation database 2023-06-11 22:43:33 +02:00
Louis Lam
71c34694b7 Update to 1.22.0-beta.0 2023-06-11 15:17:26 +08:00
Louis Lam
2128ed5ce3 Update dependencies 2023-06-11 14:44:05 +08:00
Louis Lam
09ab6a015b Merge pull request #3046 from UptimeKumaBot/weblate-uptime-kuma-uptime-kuma
Translations Update from Weblate
2023-06-11 14:36:20 +08:00
Louis Lam
ec858eb67a Merge remote-tracking branch 'origin/master' into weblate-uptime-kuma-uptime-kuma
# Conflicts:
#	src/lang/de-DE.json
2023-06-11 14:35:10 +08:00
Louis Lam
c4c3fc81b2 Merge pull request #2693 from julian-piehl/group-monitors
Group monitors
2023-06-11 14:09:02 +08:00
Arin Faraj
8aa577529f Added translation using Weblate (Kurdish (Central))
Co-authored-by: Arin Faraj <arin.abdul99@gmail.com>
2023-06-10 20:28:20 +00:00
Manh PHam
cdd5067b17 Added translation using Weblate (Xhosa)
Co-authored-by: Manh PHam <manh.pham0997@gmail.com>
2023-06-10 20:28:20 +00:00
Oskar Fagerfjäll
5fdb01308a Translated using Weblate (Swedish)
Currently translated at 18.6% (139 of 746 strings)

Co-authored-by: Oskar Fagerfjäll <oskar.fagerfjall@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/sv/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:20 +00:00
Alexandre
eb1a1d0ac7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 78.4% (585 of 746 strings)

Co-authored-by: Alexandre <alexandre@lopes.eng.br>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
mottcha
a1fc283b3c Translated using Weblate (Japanese)
Currently translated at 69.0% (515 of 746 strings)

Translated using Weblate (Japanese)

Currently translated at 69.0% (515 of 746 strings)

Co-authored-by: mottcha <yuki627f@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
deluxghost
419b684433 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: deluxghost <deluxghost@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
Lê Huy Mạnh Tân
8bc139d8c1 Translated using Weblate (Vietnamese)
Currently translated at 62.1% (464 of 746 strings)

Co-authored-by: Lê Huy Mạnh Tân <tanmanh350@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/vi/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
Artur Wróblewski
91dfd8dfaa Translated using Weblate (Polish)
Currently translated at 96.6% (721 of 746 strings)

Co-authored-by: Artur Wróblewski <krypalkora1984@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pl/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
Marco
1f405cf2a0 Translated using Weblate (German)
Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (746 of 746 strings)

Co-authored-by: Marco <marco@nanoweb.ch>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
Cyril59310
cf61077dd8 Translated using Weblate (French)
Currently translated at 99.8% (745 of 746 strings)

Translated using Weblate (French)

Currently translated at 97.7% (729 of 746 strings)

Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
TKB Studios
4396e0d4d8 Translated using Weblate (French)
Currently translated at 100.0% (722 of 722 strings)

Co-authored-by: TKB Studios <alessio.dambrosio@outlook.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
Henry Wu
240db1d173 Translated using Weblate (Chinese (Traditional))
Currently translated at 94.0% (678 of 721 strings)

Co-authored-by: Henry Wu <me@henry40408.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
AnnAngela
ef06b5376d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (721 of 721 strings)

Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
deluxghost
186d733134 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (722 of 722 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (721 of 721 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (721 of 721 strings)

Co-authored-by: deluxghost <deluxghost@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
R1KO
225ba61e22 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (721 of 721 strings)

Co-authored-by: R1KO <r1kobeats@i.ua>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
Furkan İ
11b32ce553 Translated using Weblate (Turkish)
Currently translated at 100.0% (721 of 721 strings)

Co-authored-by: Furkan İ <developer@furkanipek.com.tr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
Filip#0475
3707919025 Translated using Weblate (Slovak)
Currently translated at 28.1% (203 of 721 strings)

Co-authored-by: Filip#0475 <surinfilip@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/sk/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:19 +00:00
kokofixcomputers
d10e378fb1 Translated using Weblate (French)
Currently translated at 100.0% (721 of 721 strings)

Co-authored-by: kokofixcomputers <koko@trashmail.se>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Alex Javadi
370328c3b8 Translated using Weblate (Persian)
Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (722 of 722 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (721 of 721 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (721 of 721 strings)

Co-authored-by: Alex Javadi <15309978+aljvdi@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Sergio Leon
1d8a82ae3e Translated using Weblate (Spanish)
Currently translated at 99.1% (715 of 721 strings)

Co-authored-by: Sergio Leon <serge@1nationgfx.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
mickeydarrenlau
7da336e975 Translated using Weblate (Malay)
Currently translated at 3.6% (26 of 721 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (721 of 721 strings)

Added translation using Weblate (Malay)

Co-authored-by: mickeydarrenlau <darrenwjlau@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ms/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
@shiroo
b8c12cca2a Translated using Weblate (Arabic)
Currently translated at 94.3% (680 of 721 strings)

Co-authored-by: @shiroo <elrayan202021@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ar/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Cyril59310
88fcfcc6fc Translated using Weblate (French)
Currently translated at 100.0% (721 of 721 strings)

Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Petros Giannhs
fcb22f7d05 Translated using Weblate (Greek)
Currently translated at 92.3% (666 of 721 strings)

Co-authored-by: Petros Giannhs <souvlaki420@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/el/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Windless
5be41990bc Translated using Weblate (Chinese (Traditional))
Currently translated at 92.4% (665 of 719 strings)

Co-authored-by: Windless <eason2008212@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
andershh
09149f50e0 Translated using Weblate (Danish)
Currently translated at 78.1% (562 of 719 strings)

Co-authored-by: andershh <ahh@jlbr.dk>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/da/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Buchtič
8f60274582 Translated using Weblate (Czech)
Currently translated at 100.0% (721 of 721 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Buchtič <martin.buchta@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Tomasz Ad
7dadac3ebe Translated using Weblate (Polish)
Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Tomasz Ad <djtms84@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pl/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Hossein Niyumard
28c29f755d Translated using Weblate (Persian)
Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Hossein Niyumard <niyumard@riseup.net>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Yoswaris Lawpaiboon
b426840b5b Translated using Weblate (Thai)
Currently translated at 86.4% (623 of 721 strings)

Translated using Weblate (Thai)

Currently translated at 85.8% (617 of 719 strings)

Co-authored-by: Yoswaris Lawpaiboon <konglha19@outlook.co.th>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/th/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:18 +00:00
Lance
4f2d39d5fc Translated using Weblate (Chinese (Traditional))
Currently translated at 92.3% (664 of 719 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 92.2% (663 of 719 strings)

Co-authored-by: Lance <2124757129@qq.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:17 +00:00
AnnAngela
4012fc6964 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (721 of 721 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:17 +00:00
Ömer Faruk Genç
d1b52bc098 Translated using Weblate (Turkish)
Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (722 of 722 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (721 of 721 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Ömer Faruk Genç <omer@farukgenc.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:17 +00:00
Michal
c9a32f9dbb Translated using Weblate (Czech)
Currently translated at 99.5% (743 of 746 strings)

Translated using Weblate (Czech)

Currently translated at 98.9% (738 of 746 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (722 of 722 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (722 of 722 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (721 of 721 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Michal <black23@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:17 +00:00
stanol
b884f82de6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (722 of 722 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (721 of 721 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: stanol <stanol777@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:17 +00:00
Thiago Felipe Cruz E Souza
65928a26c7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 75.6% (544 of 719 strings)

Co-authored-by: Thiago Felipe Cruz E Souza <thiago.felipe@tutanota.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:17 +00:00
DoyunShin
abe00efa7f Translated using Weblate (Korean)
Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: DoyunShin <doyun.shin@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ko/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:17 +00:00
Unai Tolosa Pontesta
0c364fc288 Translated using Weblate (Basque)
Currently translated at 75.7% (545 of 719 strings)

Co-authored-by: Unai Tolosa Pontesta <utolosa002@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/eu/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:17 +00:00
MrEddX
0d21529037 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (722 of 722 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (721 of 721 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: MrEddX <mreddx@chatrix.one>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/
Translation: Uptime Kuma/Uptime Kuma
2023-06-10 20:28:17 +00:00
Fabian Beuke
37ae8eb44a Fix: prometheus monitor_status metric has 4 values
The prometheus monitor_status metric has actually 4 values. This can easily be verified by looking up the related source code or by using the metric in grafana an see values like 2 (which indicates timeout).
2023-06-10 20:22:33 +02:00
Kenneth Foster
8897385690 Fixed linting 2023-06-09 16:26:02 -04:00
Kenneth Foster
6132a45c7c fixed when auth is needed but not provided 2023-06-09 16:06:33 -04:00
Kenneth Foster
f68452c47a Added changes to stop auth attempts after an error 2023-06-09 14:54:17 -04:00
Louis Lam
fea8ef8367 Update bug_report.yaml 2023-06-09 20:50:13 +08:00
Kenneth Foster
9d71e34a83 [empty commit] pull request for fixing redis auth 2023-06-08 17:48:18 -04:00
Louis Lam
37031fb9a7 Merge pull request #3222 from chakflying/fix/mysql-aborted-connection
Fix: Try to close mysql connection properly
2023-06-08 00:26:18 +08:00
Nelson Chan
58ec53fb1d Fix: Try to close mysql connection properly 2023-06-06 20:28:51 +08:00
Peace
57190b58c6 fix: dont show ping on details page of groups 2023-06-03 20:55:24 +02:00
Peace
6c2948d2de fix: list collapse storage 2023-06-03 20:54:52 +02:00
Louis Lam
68f389868c Update bug_report.yaml 2023-06-03 16:25:49 +08:00
Louis Lam
af5d7cbb0b Update README.md 2023-06-03 16:22:51 +08:00
duane
1fa8c0f9fe Adds help text for Notify Channel option 2023-06-01 08:40:26 -05:00
duane
9a8bea5761 Changes 'Mention Channel' -> 'Notify Channel'
- Updates variable names
- Updates any Slack mention references
2023-06-01 08:23:13 -05:00
Peace
56f448bfe5 fix: maintenance heredity 2023-05-31 21:29:20 +02:00
Peace
2b46da0f47 style: fix linting 2023-05-31 21:19:46 +02:00
Peace
9bd76c2795 Merge branch 'master' into group-monitors 2023-05-31 20:51:33 +02:00
duane
376d84c742 Merge branch 'master' into feature/add-channel-notification-for-slack 2023-05-31 10:31:33 -05:00
Louis Lam
4b3a2ee71b Merge pull request #3211 from kiznick/badge-generator
Add i18n variable for Badge Generator #2915
2023-05-31 18:00:13 +08:00
Yoswaris Lawpaiboon
1634df5a39 Update en.json 2023-05-31 16:00:38 +07:00
Louis Lam
039fdb0730 Merge pull request #2915 from kiznick/badge-generator
Feat: Badge Generator
2023-05-31 16:09:13 +08:00
Louis Lam
20af2d9d95 Merge pull request #3209 from chakflying/fix/apikey-modal-layout
Fix: Fix incorrect modal layout in generate api-key
2023-05-31 15:09:58 +08:00
Nelson Chan
04806ba4f3 Fix: Fix incorrect modal layout 2023-05-31 09:26:54 +08:00
Yoswaris Lawpaiboon
3ff910a8f8 Fix Modal 2023-05-30 20:06:53 +07:00
Louis Lam
343a1d3344 Merge pull request #3203 from CommanderStorm/applied_timezone_formatting
chore: Made sure that every notification provider uses `timezone`/`localTime`
2023-05-30 20:36:32 +08:00
Louis Lam
f1c184c30c Update README.md 2023-05-30 17:37:53 +08:00
Wojciech Kapcia
f3fe392ec4 Add option to use ApiKeys in Twilio in addition to main account credentials 2023-05-29 19:30:33 -04:00
Frank Elsinga
f3c09f2bbd made every Notification provider supply time like dingding after #3152 2023-05-29 19:24:40 +02:00
Yoswaris Lawpaiboon
85eb084305 Setting Modal 2023-05-29 20:11:06 +07:00
Louis Lam
0735f12d19 Merge pull request #2594 from skaempfe/skaempfe#2593
Improvement: Support TLS Expiry alerts also for CA certs in cert chain
2023-05-28 22:13:48 +08:00
Louis Lam
8ed2b59410 Resolve conflict 2023-05-26 21:38:51 +08:00
Louis Lam
0b8dddba24 Merge remote-tracking branch 'origin/master' into skaempfe#2593
# Conflicts:
#	server/model/monitor.js
#	src/pages/Details.vue
2023-05-26 21:32:58 +08:00
Louis Lam
2114295381 Merge pull request #3052 from chakflying/ui/monitor-page-design-mobile
UI: Improve monitor page layout on mobile
2023-05-26 18:30:43 +08:00
Louis Lam
bc95875aa0 Merge pull request #3156 from maximilian-krauss/feat/add-pushover-ttl
feat: Adds message ttl to pushover notification
2023-05-26 18:18:24 +08:00
Louis Lam
c1efe0f26d Add a warning for Node.js >= 20 2023-05-26 18:09:05 +08:00
Maximilian Krauß
a0d0d5b015 fix: sends pushover ttl only if defined 2023-05-26 07:27:43 +02:00
Maximilian Krauß
8d05d80a5f feat: Adds message ttl to pushover notification 2023-05-26 07:27:43 +02:00
Louis Lam
36942de329 Merge pull request #2916 from lukasbableck/master
Add safe-area-inset-bottom padding to bottom-nav
2023-05-25 17:53:44 +08:00
Louis Lam
771ca09331 npm update (mainly for socket.io) 2023-05-25 13:41:35 +08:00
Louis Lam
3cb287a40e Merge pull request #3009 from chakflying/ui/url-more-monitor-types
UI: Support more monitor types in URL field
2023-05-24 20:46:48 +08:00
duane
9c3bb67b6b Merge branch 'master' into feature/add-channel-notification-for-slack 2023-05-23 10:31:11 -05:00
duane
5200e10aab Removes ternary operator for Slack channel mention 2023-05-23 10:29:18 -05:00
Nelson Chan
f1a396b0f7 Fix: Align radius timeout to default 2023-05-23 18:18:54 +08:00
Nelson Chan
83a59bd984 Fix: Add password filtering 2023-05-22 04:17:45 +08:00
Nelson Chan
446b5fa9e4 UI: Support more monitor types in URL field 2023-05-21 16:03:05 +08:00
Zaid-maker
0d1b5321ad 🚀 Update legacy deps 2023-05-21 12:02:02 +08:00
Louis Lam
1e1cc86a10 Update Apprise to 1.4.0 2023-05-20 01:46:07 +08:00
duane
9825b33ef3 Fixes eslint warnings for Slack notification modal 2023-05-19 11:01:08 -05:00
duane
00f733d352 Adds ability to notify channel when Slack webhook triggered
- Adds field to toggle channel mentions on/off for Slack integration
- Adds special mention for @channel when enabled

Reference:
[Slack docs](https://api.slack.com/reference/surfaces/formatting#special-mentions)
2023-05-19 11:01:08 -05:00
duane
fd10897988 Adds translation for English Slack channel mention label 2023-05-19 11:01:08 -05:00
Nelson Chan
317024ed72 Fix: Use safebeat for push monitor 2023-05-19 18:52:00 +08:00
Janne Nowak
f604d96c5b splited if to inner if 2023-05-18 09:55:33 +02:00
Janne Nowak
f30f00655f small fix for down containers 2023-05-17 23:18:29 +02:00
Janne Nowak
891f09def7 removed log 2023-05-17 19:15:10 +02:00
Janne Nowak
6b5e179bb0 linting 2023-05-17 19:02:34 +02:00
Janne Nowak
f653aba735 added docker health status 2023-05-17 18:52:28 +02:00
Louis Lam
9dc02bb8e2 Update .gitignore 2023-05-16 21:57:08 +08:00
Louis Lam
bb15fa0179 Merge pull request #3154 from chakflying/fix/clear-data-remove-worker-thread
Fix: Remove use of worker threads in clear-old-data
2023-05-16 19:51:20 +08:00
Yoswaris Lawpaiboon
966066b897 Update src/components/BadgeGeneratorDialog.vue
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-05-15 13:56:59 +07:00
Louis Lam
8d24891b8e Merge pull request #3054 from TechWilk/keyword-not-found-whitespace
Trim before truncating "keword not found" message
2023-05-13 18:36:04 +08:00
Louis Lam
ba7de3fd37 Merge pull request #3152 from AnnAngela/patch-1
feat: show time as server timezone in dingding notification
2023-05-13 18:15:05 +08:00
Nelson Chan
80c8fd7372 Chore: Remove util-worker 2023-05-13 01:51:23 +08:00
Nelson Chan
a27386bb92 Fix: Use croner for clear-old-data 2023-05-13 00:59:58 +08:00
AnnAngela
ce70b3fc62 feat: add a space to separate the words 2023-05-12 22:14:59 +08:00
AnnAngela
06fba5b55a feat: show time as server timezone in dingding notification 2023-05-12 22:04:44 +08:00
Louis Lam
f2c294e9e5 Merge pull request #3150 from theitguycj/master
Update README.md
2023-05-12 13:56:03 +08:00
The IT Guy CJ
332e54937e Update README.md 2023-05-11 23:19:09 -05:00
Louis Lam
a1adc30a89 Fix: Add back PagerTree 2023-05-11 14:56:42 +08:00
Louis Lam
6ce882ad4a Update README.md 2023-05-10 22:51:44 +08:00
Louis Lam
e392d12585 Mention in the README that Node.js 20 is not supported due to a weird issue 2023-05-09 23:37:51 +08:00
Louis Lam
253214ad2b Merge pull request #3024 from chakflying/feat/edit-tag-multiselect
UI: Use vue-multiselect in Edit Tag & Styling Fixes
2023-05-09 20:43:30 +08:00
Louis Lam
33de7bdb1c Merge conflict 2023-05-09 00:45:31 +08:00
Louis Lam
7f5d0e5490 Merge remote-tracking branch 'origin/1.21.X'
# Conflicts:
#	package-lock.json
2023-05-09 00:42:11 +08:00
Louis Lam
1a344c1371 Update to 1.21.3 2023-05-09 00:28:29 +08:00
Louis Lam
28b0f8fc00 Update dependencies 2023-05-08 22:52:57 +08:00
Louis Lam
0eaaa8b6fa Minor 2023-05-08 22:52:41 +08:00
Louis Lam
5cd506e340 Minor 2023-05-08 22:39:32 +08:00
Louis Lam
f0beccf6bf Fix Same As Server Timezone do not save correctly 2023-05-08 22:14:58 +08:00
Louis Lam
72c16c3aa2 Fix eslint warnings 2023-05-08 04:26:11 +08:00
Louis Lam
aa8454b73f Slightly improve error check on maintenance edit page 2023-05-08 04:14:24 +08:00
Louis Lam
d23cb0b382 Fix maintenance do not start after 1.21.2 2023-05-08 04:08:30 +08:00
Nelson Chan
9975050872 Chore: Fix line break 2023-05-07 23:20:28 +08:00
Nelson Chan
f8c2909576 UI: Improve styling 2023-05-07 23:20:28 +08:00
Nelson Chan
fcfe13e52d Feat: Use vue-multiselect in Edit Tag 2023-05-07 23:20:28 +08:00
Nelson Chan
9f51115a19 Chore: Fix lint 2023-05-07 23:20:03 +08:00
Nelson Chan
4057ca6e72 UI: Improve monitor page on mobile 2023-05-07 23:20:03 +08:00
Louis Lam
8a3bce44ef Update dependenices 2023-05-02 16:22:00 +08:00
Louis Lam
dfe6f52f6a Add test for Node.js 20, drop 19 2023-05-02 16:17:37 +08:00
Louis Lam
333a631389 Merge pull request #3106 from shihaamabr/master
Fix typo in dashboard TCP Port items
2023-04-29 12:44:31 +08:00
Shihaam Abdul Rahman
eaa948579b Fix typo in dashboard TCP Port times 2023-04-27 12:34:01 +05:00
Louis Lam
74dd07c3ca Merge pull request #3101 from stumpylog/feature/cloudflare-pkgs
Install cloudflared via Cloudflare Package Repository
2023-04-25 21:10:25 +08:00
Louis Lam
f75cf3a186 Merge pull request #2905 from Sharknoon/ntfy-bearer-authorization
Added option for notification provider ntfy to use access tokens
2023-04-25 18:24:44 +08:00
Louis Lam
a3e31b22bc Minor 2023-04-25 18:22:17 +08:00
Louis Lam
078d1f96a5 Better handling for old added ntfy notifications 2023-04-25 18:17:32 +08:00
Louis Lam
8207f16396 Merge remote-tracking branch 'origin/master' into ntfy-bearer-authorization 2023-04-25 18:07:52 +08:00
Trenton Holmes
ba82abe5f3 Updates the install of cloudflared to utilize the Cloudflare Package Repository 2023-04-24 06:47:44 -07:00
Louis Lam
eb9c748071 Merge pull request #3006 from chakflying/ui/tags-settings-mobile
UI: Improve Tags settings design on mobile
2023-04-17 16:58:57 +08:00
Yoswaris Lawpaiboon
3579520575 Fix Eslint
Co-authored-by: Nelson Chan <3271800+chakflying@users.noreply.github.com>
2023-04-12 20:09:04 +07:00
Yoswaris Lawpaiboon
030faddd1c Merge branch 'louislam:master' into badge-generator 2023-04-12 12:02:36 +07:00
Christopher Wilkinson
0e516a42e5 Trim before truncating "keword not found" message 2023-04-11 16:57:30 +01:00
Louis Lam
680dccefea Merge pull request #2868 from chakflying/update-chartjs
Chore: Update chart.js & improve performance
2023-04-11 19:01:47 +08:00
Louis Lam
8c9423f4de Merge conflicts manually 2023-04-11 19:01:17 +08:00
Louis Lam
f433f33418 Merge remote-tracking branch 'origin/master' into update-chartjs
# Conflicts:
#	package-lock.json
#	package.json
2023-04-11 18:58:20 +08:00
Louis Lam
d4a31cf02a Merge pull request #2867 from Small6oy/grpc-patch
Resolved issue with using IP Address as GRPC URL
2023-04-11 18:55:43 +08:00
Louis Lam
a7588adc52 Merge branch 'master' into grpc-patch 2023-04-11 18:55:27 +08:00
Louis Lam
6356b1e50a Merge pull request #2961 from chakflying/feat/flush-wal
Chore: Flush WAL on shutdown
2023-04-11 18:53:58 +08:00
Louis Lam
af6e01ee3a Merge pull request #3010 from chakflying/fix/grpc-invalid-regex
Fix: Remove invalid gRPC url regex
2023-04-11 18:43:51 +08:00
Josua Frank
11f4cb8725 Merge branch 'louislam:master' into ntfy-bearer-authorization 2023-04-10 16:06:53 +02:00
Louis Lam
1bf97e701d Merge pull request #2837 from dsb3/hostname-regex
Feature: hostnames may be specified with a trailing dot
2023-04-09 16:27:16 +08:00
Louis Lam
4c1ac5e870 Merge pull request #2863 from mtelgkamp/ntfy-notification-improvements
Improve ntfy notifications
2023-04-09 16:08:12 +08:00
Louis Lam
9e320dc5fb Expose timezone and local datetime to notification providers 2023-04-09 16:01:27 +08:00
Louis Lam
2f3f929fbd Merge pull request #2831 from mtelgkamp/mattermost-notification-improvements
Improve mattermost notifications
2023-04-09 15:42:28 +08:00
Louis Lam
b776e88b26 Merge pull request #3042 from Zaid-maker/master
🐛 fix(package.json): correct typo in deploy-demo-server script name
2023-04-07 16:32:23 +08:00
Zaid-maker
49741bbef2 🐛 fix(package.json): correct typo in deploy-demo-server script name 2023-04-07 02:09:13 +05:00
Miles Steele
682f8e52a8 lint 2023-04-06 15:30:38 -05:00
Miles Steele
171aff1226 add invert keyword feature 2023-04-06 15:25:25 -05:00
Josua Frank
1f7f1f70bf Merge branch 'louislam:master' into ntfy-bearer-authorization 2023-04-06 10:55:28 +02:00
Louis Lam
be7d3f6142 Merge pull request #2752 from titanventura/show-tags-in-status-page-monitor-select-list
show tags in monitor select list under status page
2023-04-04 16:13:26 +08:00
Louis Lam
7706c29564 Minor 2023-04-04 15:42:37 +08:00
Louis Lam
9dd1b1ca0f Merge remote-tracking branch 'origin/master' into show-tags-in-status-page-monitor-select-list
# Conflicts:
#	src/pages/StatusPage.vue
2023-04-04 15:35:20 +08:00
Louis Lam
21ad715e6a Merge pull request #3021 from louislam/1.22.X
1.22.x -> master
2023-04-04 14:58:34 +08:00
Josua Frank
23af66f618 Merge branch 'louislam:master' into ntfy-bearer-authorization 2023-04-03 21:17:53 +02:00
Louis Lam
03aa685d3f Update to 1.21.2 2023-04-04 01:58:56 +08:00
Louis Lam
84c1baf706 Merge pull request #3015 from UptimeKumaBot/weblate-uptime-kuma-uptime-kuma
Translations Update from Weblate
2023-04-04 00:09:45 +08:00
Weblate
23808efe2a Merge remote-tracking branch 'origin/master' 2023-04-03 16:07:13 +00:00
Marco
1db25a329f Translated using Weblate (German)
Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Marco <marco@nanoweb.ch>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:58 +00:00
Ömer Faruk Genç
e314d517ad Translated using Weblate (Turkish)
Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Ömer Faruk Genç <omer@farukgenc.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:58 +00:00
__filename
84d1cb73b6 Translated using Weblate (Korean)
Currently translated at 99.7% (717 of 719 strings)

Co-authored-by: __filename <filename@inft.kr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ko/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:58 +00:00
Buchtič
ddd3d3bc92 Translated using Weblate (Czech)
Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Buchtič <martin.buchta@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:58 +00:00
Alex Javadi
190e85d2c8 Translated using Weblate (Persian)
Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Alex Javadi <15309978+aljvdi@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fa/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:58 +00:00
MaxGremory
d8511fa201 Translated using Weblate (Spanish)
Currently translated at 99.0% (712 of 719 strings)

Co-authored-by: MaxGremory <holgaloper@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:58 +00:00
rubesaca
e76d29dee5 Translated using Weblate (Spanish)
Currently translated at 99.0% (712 of 719 strings)

Co-authored-by: rubesaca <rubesaca@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:58 +00:00
MaxGremory
fb38048159 Translated using Weblate (Spanish)
Currently translated at 99.0% (712 of 719 strings)

Co-authored-by: MaxGremory <holgaloper@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:57 +00:00
Louis Lam
80f1959871 Translated using Weblate (Chinese (Traditional, Hong Kong))
Currently translated at 96.1% (691 of 719 strings)

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant_HK/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:57 +00:00
Ademaro
4ddc3b5f5e Translated using Weblate (Russian)
Currently translated at 100.0% (719 of 719 strings)

Co-authored-by: Ademaro <ademaro@ya.ru>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:57 +00:00
Cyril59310
d173a3c663 Translated using Weblate (French)
Currently translated at 100.0% (719 of 719 strings)

Translated using Weblate (French)

Currently translated at 99.8% (718 of 719 strings)

Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma
2023-04-03 16:06:57 +00:00
Louis Lam
45ef7b2f69 Fix Effective Date Range cannot be removed 2023-04-03 21:01:58 +08:00
Josua Frank
6b078b83bd Merge branch 'master' into ntfy-bearer-authorization 2023-04-03 08:33:05 +02:00
Louis Lam
22f730499f Improve the database connection string input 2023-04-03 02:57:14 +08:00
Louis Lam
1be74e2720 Merge pull request #2870 from chakflying/feat/auto-theme-status-page
Feat: Support auto theme in status pages
2023-04-03 02:38:30 +08:00
Louis Lam
32f84b5e4e Merge pull request #2491 from RubenNL/fix-metrics-push
Fixed the metrics for the push type.
2023-04-02 02:05:03 +08:00
Nelson Chan
97c7ad9cc7 Fix: Remove invalid gRPC url regex 2023-04-02 00:07:07 +08:00
Nelson Chan
b975c24531 UI: Improve design on mobile 2023-03-31 21:54:35 +08:00
Josua Frank
ba52e1c885 Merge branch 'louislam:master' into ntfy-bearer-authorization 2023-03-31 11:31:13 +02:00
Josua Frank
fc4312ca1a Merge branch 'master' into ntfy-bearer-authorization 2023-03-26 19:09:48 +02:00
Nelson Chan
ca52047bf5 Feat: Flush WAL on shutdown 2023-03-22 14:46:58 +08:00
Josua Frank
df47609671 Added default dropdown value 2023-03-21 13:55:51 +01:00
Josua Frank
e63f7562f8 linter fixes 2023-03-21 13:48:00 +01:00
Josua Frank
8921ed0cff fix indentation of language json files 2023-03-21 13:44:06 +01:00
Josua Frank
35a56dd9e0 Added dropdown for authentication methods 2023-03-21 13:40:24 +01:00
Josua Frank
442f54de84 Merge branch 'louislam:master' into ntfy-bearer-authorization 2023-03-21 13:01:49 +01:00
Lukas Bableck
cf59832d51 Set viewport-fit=cover in meta viewport tag 2023-03-10 17:42:30 +01:00
Lukas Bableck
8f259e1756 Add padding to bottom-nav 2023-03-10 17:42:06 +01:00
Yoswaris Lawpaiboon
29b2809279 Change Icon, Add missing var 2023-03-10 22:04:47 +07:00
Yoswaris Lawpaiboon
16f2701f61 idk how to fix camelcase lint 😢 2023-03-10 20:24:10 +07:00
Yoswaris Lawpaiboon
3bbf269da0 generator modal 2023-03-10 19:25:04 +07:00
Yoswaris Lawpaiboon
56d716cee4 Create badge-list.md 2023-03-10 17:46:45 +07:00
Josua Frank
e8814e8479 added option for ntfy access tokens 2023-03-08 13:28:02 +00:00
Arniwatt Chonkiattipoom
bb7de6aa88 chore: notification toggle missing when import from backup 2023-03-02 16:23:27 +07:00
Nelson Chan
150607cc93 Feat: Support auto theme in status pages 2023-03-02 07:26:26 +08:00
Michael Telgkamp
cbbd3e20ad Codestyle: Add trailing comma 2023-03-01 23:05:23 +01:00
Nelson Chan
beb22f743d Chore: Update chart.js & improve perf. 2023-03-02 04:47:51 +08:00
Godwin Gabriel Ndlovu
6fc34e44d9 Resolved issue with using IP Address as GRPC URL
I've been having an issue with trying to use an IPAddress Host:Port combination in monitoring my GRPC instances. 
This was because the input type was set to url instead of text.
Even if the pattern passes the match test, the url would block as it requires a fully qualified domain name with HTTP and this would fail to submit
2023-03-01 19:45:31 +02:00
Michael Telgkamp
7b4f90ce92 Improve ntfy notifications
- use tags `red_circle` for down and `green_circle` for up
- increase priority for down alert by 1 if not already max
- add monitor name and status to title
- use heartbeat msg as Message
- add monitor url as action
2023-03-01 08:37:06 +01:00
titanventura
db6b863445 show tags in monitor select list under status page : change select UI from normal select to vue-multiselect 2023-02-26 16:26:09 +05:30
Michael Telgkamp
186ca30508 Improve mattermost notifications 2023-02-23 17:40:39 +01:00
Ruben van Dijk
896e33815d Merge branch 'louislam:master' into fix-metrics-push 2023-02-23 14:11:39 +01:00
Peace
0be8b111e2 chore: better up message
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-02-20 13:48:16 +01:00
Peace
cef0a0faf4 Merge branch 'master' into group-monitors 2023-02-16 21:38:53 +01:00
Dave Baker
dfb95dfdcb Hostnames may be specified with a trailing dot to prevent DNS search lookups 2023-02-16 08:55:19 +00:00
Peace
e10ba9ed7e Merge branch 'master' into group-monitors 2023-02-02 17:59:48 +01:00
Peace
9446c2d102 fix: use active instead of isActive in uploadBackup 2023-02-01 23:39:42 +01:00
Peace
2c581ade90 Merge branch 'louislam:master' into group-monitors 2023-02-01 20:44:09 +01:00
Peace
f286386f59 fix: add message for empty group pending state 2023-02-01 20:19:47 +01:00
Peace
9286dcb6ce fix: add serverside check against endless loops 2023-02-01 20:16:56 +01:00
Sebastian Kaempfe
a6894d36f2 [#2501] Dashboard: Details Page
- enable clickable URL on Dashboard Details if monitor is of type `mp-health`
2023-01-30 15:55:12 +01:00
Peace
66573934f6 refactor: remove old code 2023-01-28 15:21:17 +01:00
Peace
c444d78706 style: fix linting errors 2023-01-28 15:20:40 +01:00
Peace
661fa87134 feat: make parent link clickable 2023-01-28 14:53:40 +01:00
Peace
d48eb24046 docs: more comments 2023-01-28 14:28:53 +01:00
Peace
aee4c22dee perf: only do one filter instead of 3 in editMonitor 2023-01-28 14:28:34 +01:00
Peace
9a46b50989 docs: add comments 2023-01-28 14:22:15 +01:00
Peace
faf3488b1e fix: unfold tree if monitor is accessed directly 2023-01-28 14:15:25 +01:00
Peace
f3ac351d75 feat: set childs under maintenance if parent is too 2023-01-28 14:02:10 +01:00
Peace
aba515e172 feat: disable childs if parent is disabled 2023-01-28 13:39:17 +01:00
Peace
97bd306a09 Merge branch 'louislam:master' into group-monitors 2023-01-28 03:07:42 +01:00
Peace
645fd94bba feat: add ability to group monitors in dashboard 2023-01-28 02:58:03 +01:00
Ruben
71f00b3690 Parse push ping parameter with parseInt. 2023-01-12 18:33:39 +01:00
Sebastian Kaempfe
a21a47de93 [#2593] renamed the method sendCertNotification to better represent what id does. Evaluate certificate expiry from all certs in chain. Send a separate notification for every cert in chain, including cert type and CN. 2023-01-12 11:39:36 +01:00
Sebastian Kaempfe
f6d0f28b3a [#2593] during certificate evaluation also set the cert type for improved notifications 2023-01-12 11:34:37 +01:00
Sjouke de Vries
6de0c6a90c chore: remove yarn lockfile and add it to gitignore 2023-01-11 14:25:54 +01:00
Sjouke de Vries
94b69935fe chore(server): remove comments from status-page router 2023-01-09 11:16:47 +01:00
Sjouke de Vries
3f30feaefb feat(server): add badge for overall status of status-page 2023-01-09 11:04:32 +01:00
Ruben
9404efd86d Fixed the metrics for the push type. 2022-12-28 10:37:25 +01:00
141 changed files with 8900 additions and 5816 deletions

28
.devcontainer/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Codespaces
You can modifiy Uptime Kuma in your browser without setting up a local development.
![image](https://github.com/louislam/uptime-kuma/assets/1336778/31d9f06d-dd0b-4405-8e0d-a96586ee4595)
1. Click `Code` -> `Create codespace on master`
2. Wait a few minutes until you see there are two exposed ports
3. Go to the `3000` url, see if it is working
![image](https://github.com/louislam/uptime-kuma/assets/1336778/909b2eb4-4c5e-44e4-ac26-6d20ed856e7f)
## Frontend
Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded.
You don't need to restart the frontend, unless you try to add a new frontend dependency.
## Backend
The backend does not automatically hot-reload.
You will need to restart the backend after changing something using these steps:
1. Click `Terminal`
2. Click `Codespaces: server-dev` in the right panel
3. Press `Ctrl + C` to stop the server
4. Press `Up` to run `npm run start-server-dev`
![image](https://github.com/louislam/uptime-kuma/assets/1336778/e0c0a350-fe46-4588-9f37-e053c85834d1)

View File

@@ -0,0 +1,22 @@
{
"image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"updateContentCommand": "npm ci",
"postCreateCommand": "",
"postAttachCommand": {
"frontend-dev": "npm run start-frontend-devcontainer",
"server-dev": "npm run start-server-dev",
"open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME"
},
"customizations": {
"vscode": {
"extensions": [
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint"
]
}
},
"forwardPorts": [3000, 3001]
}

View File

@@ -44,7 +44,7 @@ body:
id: operating-system
attributes:
label: "💻 Operating System and Arch"
description: "Which OS is your server/device running on?"
description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
placeholder: "Ex. Ubuntu 20.04 x86"
validations:
required: true
@@ -52,7 +52,7 @@ body:
id: browser-vendor
attributes:
label: "🌐 Browser"
description: "Which browser are you running on?"
description: "Which browser are you running on? (For Replit, please do not report this bug)"
placeholder: "Ex. Google Chrome 95.0.4638.69"
validations:
required: true

View File

@@ -61,8 +61,8 @@ body:
id: operating-system
attributes:
label: "💻 Operating System and Arch"
description: "Which OS is your server/device running on?"
placeholder: "Ex. Ubuntu 20.04 x86"
description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
placeholder: "Ex. Ubuntu 20.04 x64 "
validations:
required: true
- type: input

View File

@@ -1,4 +1,4 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Auto Test
@@ -21,8 +21,8 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
node: [ 14, 16, 18, 19 ]
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 14, 18 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@@ -33,7 +33,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm install npm@latest -g
- run: npm install
- run: npm run build
- run: npm test
@@ -41,6 +41,29 @@ jobs:
HEADLESS_TEST: 1
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
armv7-simple-test:
needs: [ check-linters ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
matrix:
os: [ ARMv7 ]
node: [ 14.21.3, 18.16.1 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- run: npm install npm@latest -g
- run: npm ci --production
check-linters:
runs-on: ubuntu-latest
@@ -52,7 +75,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 14
cache: 'npm'
- run: npm install
- run: npm run lint
@@ -67,7 +89,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 14
cache: 'npm'
- run: npm install
- run: npm run build
- run: npm run cy:test
@@ -83,7 +104,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 14
cache: 'npm'
- run: npm install
- run: npm run build
- run: npm run cy:run:unit

3
.gitignore vendored
View File

@@ -23,3 +23,6 @@ cypress/screenshots
extra/exe-builder/bin
extra/exe-builder/obj
.vs
.vscode

View File

@@ -47,17 +47,17 @@ Here are some references:
❌ Won't Merge
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet)
- Do not pass auto test
- Do not pass the auto test
- Any breaking changes
- Duplicated pull request
- Duplicated pull requests
- Buggy
- 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)
- Modifications or deletions of existing logic without a valid reason.
- Adding functions that is completely out of scope
- Converting existing code into other programming languages
- Unnecessarily large code changes that are hard to review and cause conflicts with other PRs.
The above cases cannot cover all situations.
The above cases may not cover all possible 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.

View File

@@ -23,7 +23,7 @@ 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
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / 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
@@ -49,10 +49,14 @@ Uptime Kuma is now running on http://localhost:3001
### 💪🏻 Non-Docker
Required Tools:
- [Node.js](https://nodejs.org/en/download/) >= 14
Requirements:
- Platform
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
- ❌ Replit / Heroku
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 / 20.4
- [npm](https://docs.npmjs.com/cli/) >= 7
- [Git](https://git-scm.com/downloads)
- [Git](https://git-scm.com/downloads)
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
```bash
@@ -67,7 +71,7 @@ npm run setup
node server/server.js
# (Recommended) Option 2. Run in background using PM2
# Install PM2 if you don't have it:
# Install PM2 if you don't have it:
npm install pm2 -g && pm2 install pm2-logrotate
# Start Server
@@ -87,6 +91,10 @@ pm2 monit
pm2 save && pm2 startup
```
### Windows Portable (x64)
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
### Advanced Installation
If you need more options or need to browse via a reverse proxy, please read:
@@ -144,17 +152,18 @@ Telegram Notification Sample:
If you love this project, please consider giving me a ⭐.
## 🗣️ Discussion
## 🗣️ Discussion / Ask for Help
### Issues Page
⚠️ For any general or technical questions, please don't send me an email, as I am unable to provide support in that manner. I will not response if you asked such questions.
You can discuss or ask for help in [issues](https://github.com/louislam/uptime-kuma/issues).
I recommend using Google, GitHub Issues, or Uptime Kuma's Subreddit for finding answers to your question. If you cannot find the information you need, feel free to ask:
### Subreddit
- [GitHub Issues](https://github.com/louislam/uptime-kuma/issues)
- [Subreddit r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
You can mention me if you ask a question on Reddit.
[r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
## Contribute

View File

@@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
import commonjs from "vite-plugin-commonjs";
const postCssScss = require("postcss-scss");
const postcssRTLCSS = require("postcss-rtlcss");
@@ -16,8 +17,12 @@ export default defineConfig({
},
define: {
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
},
plugins: [
commonjs(),
vue(),
legacy({
targets: [ "since 2015" ],
@@ -42,6 +47,9 @@ export default defineConfig({
}
},
build: {
commonjsOptions: {
include: [ /.js$/ ],
},
rollupOptions: {
output: {
manualChunks(id, { getModuleInfo, getModuleIds }) {

View File

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

View File

@@ -0,0 +1,6 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE;
COMMIT

View File

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

View File

@@ -0,0 +1,22 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD kafka_producer_topic VARCHAR(255);
ALTER TABLE monitor
ADD kafka_producer_brokers TEXT;
ALTER TABLE monitor
ADD kafka_producer_ssl INTEGER;
ALTER TABLE monitor
ADD kafka_producer_allow_auto_topic_creation VARCHAR(255);
ALTER TABLE monitor
ADD kafka_producer_sasl_options TEXT;
ALTER TABLE monitor
ADD kafka_producer_message TEXT;
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 git && \
pip3 --no-cache-dir install apprise==1.3.0 && \
pip3 --no-cache-dir install apprise==1.4.0 && \
rm -rf /root/.cache

View File

@@ -8,21 +8,21 @@ WORKDIR /app
# Install Curl
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
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 git && \
pip3 --no-cache-dir install apprise==1.3.0 && \
RUN apt-get update && \
apt-get --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 git curl ca-certificates && \
pip3 --no-cache-dir install apprise==1.4.0 && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
# Install cloudflared
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
COPY extra/download-cloudflared.js ./extra/download-cloudflared.js
RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
dpkg --add-architecture arm && \
apt update && \
apt --yes --no-install-recommends install ./cloudflared.deb && \
RUN set -eux && \
mkdir -p --mode=0755 /usr/share/keyrings && \
curl --fail --show-error --silent --location --insecure https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared buster main' | tee /etc/apt/sources.list.d/cloudflared.list && \
apt-get update && \
apt-get install --yes --no-install-recommends cloudflared && \
cloudflared version && \
rm -rf /var/lib/apt/lists/* && \
rm -f cloudflared.deb && \
apt --yes autoremove

View File

@@ -26,6 +26,8 @@ RUN chmod +x /app/extra/entrypoint.sh
FROM louislam/uptime-kuma:base-debian AS release
WORKDIR /app
ENV UPTIME_KUMA_IS_CONTAINER=1
# Copy app files from build layer
COPY --from=build /app /app
@@ -70,7 +72,6 @@ RUN git clone https://github.com/louislam/uptime-kuma.git .
RUN npm ci
EXPOSE 3000 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
CMD ["npm", "run", "start-pr-test"]

View File

@@ -1,48 +0,0 @@
//
const http = require("https"); // or 'https' for https:// URLs
const fs = require("fs");
const platform = process.argv[2];
if (!platform) {
console.error("No platform??");
process.exit(1);
}
let arch = null;
if (platform === "linux/amd64") {
arch = "amd64";
} else if (platform === "linux/arm64") {
arch = "arm64";
} else if (platform === "linux/arm/v7") {
arch = "arm";
} else {
console.error("Invalid platform?? " + platform);
}
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) {
console.log("Redirect to " + res.headers.location);
get(res.headers.location);
} else if (res.statusCode >= 200 && res.statusCode < 300) {
res.pipe(file);
res.on("end", function () {
console.log("Downloaded");
});
} else {
console.error(res.statusCode);
process.exit(1);
}
});
}

View File

@@ -1,3 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura />
<Costura DisableCompression='true' IncludeDebugSymbols='false' />
</Weavers>

View File

@@ -6,9 +6,9 @@ using System.Runtime.InteropServices;
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Uptime Kuma")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyCompany("Uptime Kuma")]
[assembly: AssemblyProduct("Uptime Kuma")]
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
[assembly: AssemblyTrademark("")]
@@ -20,7 +20,7 @@ using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("2DB53988-1D93-4AC0-90C4-96ADEAAC5C04")]
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
// Version information for an assembly consists of the following four values:
//
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyVersion("1.0.1.0")]
[assembly: AssemblyFileVersion("1.0.1.0")]

9
extra/test-docker.js Normal file
View File

@@ -0,0 +1,9 @@
// Check if docker is running
const { exec } = require("child_process");
exec("docker ps", (err, stdout, stderr) => {
if (err) {
console.error("Docker is not running. Please start docker and try again.");
process.exit(1);
}
});

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="manifest" href="/manifest.json" />

8197
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "uptime-kuma",
"version": "1.21.2-beta.0",
"version": "1.22.1",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/louislam/uptime-kuma.git"
},
"engines": {
"node": "14.* || >=16.*"
"node": "14 || 16 || 18 || >= 20.4.0"
},
"scripts": {
"install-legacy": "npm install",
@@ -19,6 +19,7 @@
"lint": "npm run lint:js && npm run lint:style",
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
"start": "npm run start-server",
"start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
@@ -34,12 +35,12 @@
"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",
"build-docker-nightly": "node ./extra/test-docker.js && 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",
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
"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.21.1 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.22.1 && 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",
@@ -54,8 +55,8 @@
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
"ncu-patch": "npm-check-updates -u -t patch",
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
"release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
"git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev",
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
@@ -64,19 +65,18 @@
"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",
"depoly-demo-server": "node extra/deploy-demo-server.js",
"deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js"
},
"dependencies": {
"@grpc/grpc-js": "~1.7.3",
"@louislam/ping": "~0.4.4-mod.0",
"@louislam/ping": "~0.4.4-mod.1",
"@louislam/sqlite3": "15.1.6",
"args-parser": "~1.3.0",
"axios": "~0.27.0",
"axios-ntlm": "1.3.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
"bree": "~7.1.5",
"cacheable-lookup": "~6.0.4",
"chardet": "~1.4.0",
"check-password-strength": "^2.0.5",
@@ -85,27 +85,30 @@
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"compression": "~1.7.4",
"croner": "^6.0.3",
"croner": "~6.0.5",
"dayjs": "~1.11.5",
"dotenv": "~16.0.3",
"express": "~4.17.3",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7",
"form-data": "~4.0.0",
"gamedig": "^4.0.5",
"gamedig": "~4.0.5",
"http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
"iconv-lite": "~0.6.3",
"jsesc": "~3.0.2",
"jsonata": "^2.0.3",
"jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2",
"kafkajs": "^2.2.4",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"mongodb": "~4.14.0",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mysql2": "~2.3.3",
"nanoid": "^3.3.4",
"nanoid": "~3.3.4",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
@@ -113,14 +116,17 @@
"password-hash": "~1.2.2",
"pg": "~8.8.0",
"pg-connection-string": "~2.5.0",
"playwright-core": "~1.35.1",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"protobufjs": "~7.1.1",
"protobufjs": "~7.2.4",
"qs": "~6.10.4",
"redbean-node": "~0.2.0",
"queue": "~7.0.0",
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"socket.io": "~4.5.3",
"socket.io-client": "~4.5.3",
"semver": "~7.5.4",
"socket.io": "~4.6.1",
"socket.io-client": "~4.6.1",
"socks-proxy-agent": "6.1.1",
"tar": "~6.1.11",
"tcp-ping": "~0.1.1",
@@ -128,7 +134,7 @@
},
"devDependencies": {
"@actions/github": "~5.0.1",
"@babel/eslint-parser": "~7.17.0",
"@babel/eslint-parser": "^7.22.7",
"@babel/preset-env": "^7.15.8",
"@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4",
@@ -136,29 +142,29 @@
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@popperjs/core": "~2.10.2",
"@types/bootstrap": "~5.1.9",
"@vitejs/plugin-legacy": "~2.1.0",
"@vitejs/plugin-vue": "~3.1.0",
"@vue/compiler-sfc": "~3.2.36",
"@vitejs/plugin-legacy": "~4.1.0",
"@vitejs/plugin-vue": "~4.2.3",
"@vue/compiler-sfc": "~3.3.4",
"@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",
"chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4",
"concurrently": "^7.1.0",
"core-js": "~3.26.1",
"cronstrue": "~2.24.0",
"cross-env": "~7.0.3",
"cypress": "^10.1.0",
"cypress": "^12.17.0",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"dompurify": "~2.4.3",
"eslint": "~8.14.0",
"eslint-plugin-vue": "~8.7.1",
"favico.js": "~0.3.10",
"jest": "~27.2.5",
"jest": "~29.6.1",
"marked": "~4.2.5",
"node-ssh": "~13.0.1",
"node-ssh": "~13.1.0",
"postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2",
"postcss-scss": "~4.0.4",
@@ -166,16 +172,17 @@
"qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1",
"stylelint": "~14.7.1",
"stylelint": "^15.10.1",
"stylelint-config-standard": "~25.0.0",
"terser": "~5.15.0",
"timezones-list": "~3.0.1",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~3.1.0",
"vite": "~4.4.1",
"vite-plugin-commonjs": "^0.8.0",
"vite-plugin-compression": "^0.5.1",
"vue": "~3.2.47",
"vue-chart-3": "3.0.9",
"vue": "~3.3.4",
"vue-chartjs": "~5.2.0",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
"vue-i18n": "~9.2.2",
@@ -186,6 +193,7 @@
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0",
"wait-on": "^6.0.1"
"wait-on": "^6.0.1",
"whatwg-url": "~12.0.1"
}
}

View File

@@ -2,6 +2,7 @@ const basicAuth = require("express-basic-auth");
const passwordHash = require("./password-hash");
const { R } = require("redbean-node");
const { setting } = require("./util-server");
const { log } = require("../src/util");
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
@@ -81,12 +82,16 @@ function apiAuthorizer(username, password, callback) {
apiRateLimiter.pass(null, 0).then((pass) => {
if (pass) {
verifyAPIKey(password).then((valid) => {
if (!valid) {
log.warn("api-auth", "Failed API auth attempt: invalid API Key");
}
callback(null, valid);
// Only allow a set number of api requests per minute
// (currently set to 60)
apiRateLimiter.removeTokens(1);
});
} else {
log.warn("api-auth", "Failed API auth attempt: rate limit exceeded");
callback(null, false);
}
});
@@ -106,10 +111,12 @@ function userAuthorizer(username, password, callback) {
callback(null, user != null);
if (user == null) {
log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
loginRateLimiter.removeTokens(1);
}
});
} else {
log.warn("basic-auth", "Failed basic auth attempt: rate limit exceeded");
callback(null, false);
}
});

View File

@@ -1,27 +1,33 @@
const { setSetting, setting } = require("./util-server");
const axios = require("axios");
const compareVersions = require("compare-versions");
const { log } = require("../src/util");
exports.version = require("../package.json").version;
exports.latestVersion = null;
// How much time in ms to wait between update checks
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
const UPDATE_CHECKER_LATEST_VERSION_URL = "https://uptime.kuma.pet/version";
let interval;
/** Start 48 hour check interval */
exports.startInterval = () => {
let check = async () => {
if (await setting("checkUpdate") === false) {
return;
}
log.debug("update-checker", "Retrieving latest versions");
try {
const res = await axios.get("https://uptime.kuma.pet/version");
const res = await axios.get(UPDATE_CHECKER_LATEST_VERSION_URL);
// For debug
if (process.env.TEST_CHECK_VERSION === "1") {
res.data.slow = "1000.0.0";
}
if (await setting("checkUpdate") === false) {
return;
}
let checkBeta = await setting("checkBeta");
if (checkBeta && res.data.beta) {
@@ -35,12 +41,14 @@ exports.startInterval = () => {
exports.latestVersion = res.data.slow;
}
} catch (_) { }
} catch (_) {
log.info("update-checker", "Failed to check for new versions");
}
};
check();
interval = setInterval(check, 3600 * 1000 * 48);
interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
};
/**

View File

@@ -141,12 +141,21 @@ async function sendAPIKeyList(socket) {
/**
* Emits the version information to the client.
* @param {Socket} socket Socket.io socket instance
* @param {boolean} hideVersion
* @returns {Promise<void>}
*/
async function sendInfo(socket) {
async function sendInfo(socket, hideVersion = false) {
let version;
let latestVersion;
if (!hideVersion) {
version = checkVersion.version;
latestVersion = checkVersion.latestVersion;
}
socket.emit("info", {
version: checkVersion.version,
latestVersion: checkVersion.latestVersion,
version,
latestVersion,
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),

View File

@@ -1,4 +1,5 @@
const args = require("args-parser")(process.argv);
// Interop with browser
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
const demoMode = args["demo"] || false;
const badgeConstants = {

View File

@@ -2,9 +2,7 @@ const fs = require("fs");
const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util");
const dayjs = require("dayjs");
const knex = require("knex");
const { PluginsManager } = require("./plugins-manager");
/**
* Database & App Data Folder
@@ -23,6 +21,8 @@ class Database {
*/
static uploadDir;
static screenshotDir;
static path;
/**
@@ -70,6 +70,10 @@ class Database {
"patch-api-key-table.sql": true,
"patch-monitor-tls.sql": true,
"patch-maintenance-cron.sql": true,
"patch-add-parent-monitor.sql": true,
"patch-add-invert-keyword.sql": true,
"patch-added-json-query.sql": true,
"patch-added-kafka-producer.sql": true,
};
/**
@@ -88,12 +92,6 @@ class Database {
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
// Plugin feature is working only if the dataDir = "./data";
if (Database.dataDir !== "./data/") {
log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/");
PluginsManager.disable = true;
}
Database.path = Database.dataDir + "kuma.db";
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });
@@ -105,6 +103,12 @@ class Database {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
// Create screenshot dir
Database.screenshotDir = Database.dataDir + "screenshots/";
if (! fs.existsSync(Database.screenshotDir)) {
fs.mkdirSync(Database.screenshotDir, { recursive: true });
}
log.info("db", `Data Dir: ${Database.dataDir}`);
}
@@ -161,12 +165,12 @@ class Database {
await R.exec("PRAGMA journal_mode = WAL");
}
await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = FULL");
await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
// 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
await R.exec("PRAGMA synchronous = FULL");
await R.exec("PRAGMA synchronous = NORMAL");
if (!noLog) {
log.info("db", "SQLite config:");
@@ -417,6 +421,9 @@ class Database {
log.info("db", "Closing the database");
// Flush WAL to main database
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
while (true) {
Database.noReject = true;
await R.close();

View File

@@ -1,24 +0,0 @@
const childProcess = require("child_process");
class Git {
static clone(repoURL, cwd, targetDir = ".") {
let result = childProcess.spawnSync("git", [
"clone",
repoURL,
targetDir,
], {
cwd: cwd,
});
if (result.status !== 0) {
throw new Error(result.stderr.toString("utf-8"));
} else {
return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
}
}
}
module.exports = {
Git,
};

View File

@@ -1,41 +1,51 @@
const path = require("path");
const Bree = require("bree");
const { SHARE_ENV } = require("worker_threads");
const { log } = require("../src/util");
let bree;
const { UptimeKumaServer } = require("./uptime-kuma-server");
const { clearOldData } = require("./jobs/clear-old-data");
const { incrementalVacuum } = require("./jobs/incremental-vacuum");
const Cron = require("croner");
const jobs = [
{
name: "clear-old-data",
interval: "at 03:14",
interval: "14 03 * * *",
jobFunc: clearOldData,
croner: null,
},
{
name: "incremental-vacuum",
interval: "*/5 * * * *",
jobFunc: incrementalVacuum,
croner: null,
}
];
/**
* Initialize background jobs
* @param {Object} args Arguments to pass to workers
* @returns {Bree}
* @returns {Promise<void>}
*/
const initBackgroundJobs = function (args) {
bree = new Bree({
root: path.resolve("server", "jobs"),
jobs,
worker: {
env: SHARE_ENV,
workerData: args,
},
workerMessageHandler: (message) => {
log.info("jobs", message);
}
});
const initBackgroundJobs = async function () {
const timezone = await UptimeKumaServer.getInstance().getTimezone();
for (const job of jobs) {
const cornerJob = new Cron(
job.interval,
{
name: job.name,
timezone,
},
job.jobFunc,
);
job.croner = cornerJob;
}
bree.start();
return bree;
};
/** Stop all background jobs if running */
const stopBackgroundJobs = function () {
if (bree) {
bree.stop();
for (const job of jobs) {
if (job.croner) {
job.croner.stop();
job.croner = null;
}
}
};

View File

@@ -1,12 +1,15 @@
const { log, exit, connectDb } = require("./util-worker");
const { R } = require("redbean-node");
const { log } = require("../../src/util");
const { setSetting, setting } = require("../util-server");
const DEFAULT_KEEP_PERIOD = 180;
(async () => {
await connectDb();
/**
* Clears old data from the heartbeat table of the database.
* @return {Promise<void>} A promise that resolves when the data has been cleared.
*/
const clearOldData = async () => {
let period = await setting("keepDataPeriodDays");
// Set Default Period
@@ -20,26 +23,30 @@ const DEFAULT_KEEP_PERIOD = 180;
try {
parsedPeriod = parseInt(period);
} catch (_) {
log("Failed to parse setting, resetting to default..");
log.warn("clearOldData", "Failed to parse setting, resetting to default..");
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
parsedPeriod = DEFAULT_KEEP_PERIOD;
}
if (parsedPeriod < 1) {
log(`Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
log.info("clearOldData", `Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
} else {
log(`Clearing Data older than ${parsedPeriod} days...`);
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
try {
await R.exec(
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
[ parsedPeriod ]
);
await R.exec("PRAGMA optimize;");
} catch (e) {
log(`Failed to clear old data: ${e.message}`);
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
}
}
};
exit();
})();
module.exports = {
clearOldData,
};

View File

@@ -0,0 +1,21 @@
const { R } = require("redbean-node");
const { log } = require("../../src/util");
/**
* Run incremental_vacuum and checkpoint the WAL.
* @return {Promise<void>} A promise that resolves when the process is finished.
*/
const incrementalVacuum = async () => {
try {
log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)...");
await R.exec("PRAGMA incremental_vacuum(200)");
await R.exec("PRAGMA wal_checkpoint(PASSIVE)");
} catch (e) {
log.error("incrementalVacuum", `Failed: ${e.message}`);
}
};
module.exports = {
incrementalVacuum,
};

View File

@@ -1,50 +0,0 @@
const { parentPort, workerData } = require("worker_threads");
const Database = require("../database");
const path = require("path");
/**
* Send message to parent process for logging
* since worker_thread does not have access to stdout, this is used
* instead of console.log()
* @param {any} any The message to log
*/
const log = function (any) {
if (parentPort) {
parentPort.postMessage(any);
}
};
/**
* Exit the worker process
* @param {number} error The status code to exit
*/
const exit = function (error) {
if (error && error !== 0) {
process.exit(error);
} else {
if (parentPort) {
parentPort.postMessage("done");
} else {
process.exit(0);
}
}
};
/** Connects to the database */
const connectDb = async function () {
const dbPath = path.join(
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
);
Database.init({
"data-dir": dbPath,
});
await Database.connect();
};
module.exports = {
log,
exit,
connectDb,
};

View File

@@ -18,9 +18,12 @@ class Maintenance extends BeanModel {
let dateRange = [];
if (this.start_date) {
dateRange.push(this.start_date);
if (this.end_date) {
dateRange.push(this.end_date);
}
} else {
dateRange.push(null);
}
if (this.end_date) {
dateRange.push(this.end_date);
}
let timeRange = [];
@@ -44,7 +47,8 @@ class Maintenance extends BeanModel {
cron: this.cron,
duration: this.duration,
durationMinutes: parseInt(this.duration / 60),
timezone: await this.getTimezone(),
timezone: await this.getTimezone(), // Only valid timezone
timezoneOption: this.timezone, // Mainly for dropdown menu, because there is a option "SAME_AS_SERVER"
timezoneOffset: await this.getTimezoneOffset(),
status: await this.getStatus(),
};
@@ -150,15 +154,19 @@ class Maintenance extends BeanModel {
bean.description = obj.description;
bean.strategy = obj.strategy;
bean.interval_day = obj.intervalDay;
bean.timezone = obj.timezone;
bean.timezone = obj.timezoneOption;
bean.active = obj.active;
if (obj.dateRange[0]) {
bean.start_date = obj.dateRange[0];
} else {
bean.start_date = null;
}
if (obj.dateRange[1]) {
bean.end_date = obj.dateRange[1];
}
if (obj.dateRange[1]) {
bean.end_date = obj.dateRange[1];
} else {
bean.end_date = null;
}
if (bean.strategy === "cron") {
@@ -283,7 +291,7 @@ class Maintenance extends BeanModel {
}
getRunningTimeslot() {
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").format("YYYY-MM-DD HH:mm:ss")));
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate()));
let end = start.add(this.duration, "second");
let current = dayjs();
@@ -309,7 +317,7 @@ class Maintenance extends BeanModel {
}
async getTimezone() {
if (!this.timezone) {
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
return await UptimeKumaServer.getInstance().getTimezone();
}
return this.timezone;

View File

@@ -2,9 +2,11 @@ const https = require("https");
const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing,
redisPingAsync, mongodbPing, kafkaProducerAsync
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -18,6 +20,8 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker");
const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken");
/**
* status:
@@ -68,22 +72,33 @@ class Monitor extends BeanModel {
const tags = await this.getTags();
let screenshot = null;
if (this.type === "real-browser") {
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
}
let data = {
id: this.id,
name: this.name,
description: this.description,
pathName: await this.getPathName(),
parent: this.parent,
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
url: this.url,
method: this.method,
hostname: this.hostname,
port: this.port,
maxretries: this.maxretries,
weight: this.weight,
active: this.active,
active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id),
type: this.type,
interval: this.interval,
retryInterval: this.retryInterval,
resendInterval: this.resendInterval,
keyword: this.keyword,
invertKeyword: this.isInvertKeyword(),
expiryNotification: this.isEnabledExpiryNotification(),
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
@@ -111,7 +126,15 @@ class Monitor extends BeanModel {
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
game: this.game,
httpBodyEncoding: this.httpBodyEncoding
httpBodyEncoding: this.httpBodyEncoding,
jsonPath: this.jsonPath,
expectedValue: this.expectedValue,
kafkaProducerTopic: this.kafkaProducerTopic,
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
};
if (includeSensitiveData) {
@@ -135,6 +158,7 @@ class Monitor extends BeanModel {
tlsCa: this.tlsCa,
tlsCert: this.tlsCert,
tlsKey: this.tlsKey,
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
};
}
@@ -142,6 +166,16 @@ class Monitor extends BeanModel {
return data;
}
/**
* Checks if the monitor is active based on itself and its parents
* @returns {Promise<Boolean>}
*/
async isActive() {
const parentActive = await Monitor.isParentActive(this.id);
return (this.active === 1) && parentActive;
}
/**
* Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>}
@@ -183,6 +217,14 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown);
}
/**
* Parse to boolean
* @returns {boolean}
*/
isInvertKeyword() {
return Boolean(this.invertKeyword);
}
/**
* Parse to boolean
* @returns {boolean}
@@ -257,7 +299,37 @@ class Monitor extends BeanModel {
if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance";
bean.status = MAINTENANCE;
} else if (this.type === "http" || this.type === "keyword") {
} else if (this.type === "group") {
const children = await Monitor.getChildren(this.id);
if (children.length > 0) {
bean.status = UP;
bean.msg = "All children up and running";
for (const child of children) {
if (!child.active) {
// Ignore inactive childs
continue;
}
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
// Only change state if the monitor is in worse conditions then the ones before
if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
bean.status = lastBeat.status;
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
bean.status = lastBeat.status;
}
}
if (bean.status !== UP) {
bean.msg = "Child inaccessible";
}
} else {
// Set status pending if group is empty
bean.status = PENDING;
bean.msg = "Group empty";
}
} else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();
@@ -363,8 +435,8 @@ class Monitor extends BeanModel {
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
log.debug("monitor", `[${this.name}] call sendCertNotification`);
await this.sendCertNotification(tlsInfoObject);
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
await this.checkCertExpiryNotifications(tlsInfoObject);
}
} catch (e) {
@@ -385,7 +457,7 @@ class Monitor extends BeanModel {
if (this.type === "http") {
bean.status = UP;
} else {
} else if (this.type === "keyword") {
let data = res.data;
@@ -394,17 +466,37 @@ class Monitor extends BeanModel {
data = JSON.stringify(data);
}
if (data.includes(this.keyword)) {
bean.msg += ", keyword is found";
let keywordFound = data.includes(this.keyword);
if (keywordFound === !this.isInvertKeyword()) {
bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
bean.status = UP;
} else {
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
if (data.length > 50) {
data = data.substring(0, 47) + "...";
}
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
throw new Error(bean.msg + ", but keyword is " +
(keywordFound ? "present" : "not") + " in [" + data + "]");
}
} else if (this.type === "json-query") {
let data = res.data;
// convert data to object
if (typeof data === "string") {
data = JSON.parse(data);
}
let expression = jsonata(this.jsonPath);
let result = await expression.evaluate(data);
if (result.toString() === this.expectedValue) {
bean.msg += ", expected value is found";
bean.status = UP;
} else {
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
}
}
} else if (this.type === "port") {
@@ -479,7 +571,7 @@ class Monitor extends BeanModel {
// No need to insert successful heartbeat for push type, so end here
retries = 0;
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
this.heartbeatInterval = setTimeout(beat, timeout);
this.heartbeatInterval = setTimeout(safeBeat, timeout);
return;
}
} else {
@@ -572,9 +664,15 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Axios Request`);
let res = await axios.request(options);
if (res.data.State.Running) {
bean.status = UP;
bean.msg = res.data.State.Status;
if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
bean.status = PENDING;
bean.msg = res.data.State.Health.Status;
} else {
bean.status = UP;
bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status;
}
} else {
throw Error("Container State is " + res.data.State.Status);
}
@@ -603,7 +701,6 @@ class Monitor extends BeanModel {
grpcEnableTls: this.grpcEnableTls,
grpcMethod: this.grpcMethod,
grpcBody: this.grpcBody,
keyword: this.keyword
};
const response = await grpcQuery(options);
bean.ping = dayjs().valueOf() - startTime;
@@ -616,13 +713,14 @@ class Monitor extends BeanModel {
bean.status = DOWN;
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
} else {
if (response.data.toString().includes(this.keyword)) {
let keywordFound = response.data.toString().includes(this.keyword);
if (keywordFound === !this.isInvertKeyword()) {
bean.status = UP;
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
} else {
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`);
bean.status = DOWN;
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`;
}
}
} else if (this.type === "postgres") {
@@ -669,7 +767,8 @@ class Monitor extends BeanModel {
this.radiusCalledStationId,
this.radiusCallingStationId,
this.radiusSecret,
port
port,
this.interval * 1000 * 0.8,
);
if (resp.code) {
bean.msg = resp.code;
@@ -694,11 +793,29 @@ class Monitor extends BeanModel {
} else if (this.type in UptimeKumaServer.monitorTypeList) {
let startTime = dayjs().valueOf();
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
await monitorType.check(this, bean);
await monitorType.check(this, bean, UptimeKumaServer.getInstance());
if (!bean.ping) {
bean.ping = dayjs().valueOf() - startTime;
}
} else if (this.type === "kafka-producer") {
let startTime = dayjs().valueOf();
bean.msg = await kafkaProducerAsync(
JSON.parse(this.kafkaProducerBrokers),
this.kafkaProducerTopic,
this.kafkaProducerMessage,
{
allowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation,
ssl: this.kafkaProducerSsl,
clientId: `Uptime-Kuma/${version}`,
interval: this.interval,
},
JSON.parse(this.kafkaProducerSaslOptions),
);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else {
throw new Error("Unknown Monitor Type");
}
@@ -1176,12 +1293,18 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
const heartbeatJSON = bean.toJSON();
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
if (!heartbeatJSON["msg"]) {
heartbeatJSON["msg"] = "N/A";
}
// Also provide the time in server timezone
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
@@ -1204,13 +1327,19 @@ class Monitor extends BeanModel {
}
/**
* Send notification about a certificate
* checks certificate chain for expiring certificates
* @param {Object} tlsInfoObject Information about certificate
*/
async sendCertNotification(tlsInfoObject) {
async checkCertExpiryNotifications(tlsInfoObject) {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
const notificationList = await Monitor.getNotificationList(this);
if (! notificationList.length > 0) {
// fail fast. If no notification is set, all the following checks can be skipped.
log.debug("monitor", "No notification, no need to send cert notification");
return;
}
let notifyDays = await setting("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
@@ -1218,10 +1347,19 @@ class Monitor extends BeanModel {
notifyDays = [ 7, 14, 21 ];
}
if (notifyDays != null && Array.isArray(notifyDays)) {
for (const day of notifyDays) {
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
if (Array.isArray(notifyDays)) {
for (const targetDays of notifyDays) {
let certInfo = tlsInfoObject.certInfo;
while (certInfo) {
let subjectCN = certInfo.subject["CN"];
if (certInfo.daysRemaining > targetDays) {
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
} else {
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
await this.sendCertNotificationByTargetDays(subjectCN, certInfo.certType, certInfo.daysRemaining, targetDays, notificationList);
}
certInfo = certInfo.issuerCertificate;
}
}
}
}
@@ -1230,55 +1368,47 @@ class Monitor extends BeanModel {
/**
* Send a certificate notification when certificate expires in less
* than target days
* @param {number} daysRemaining Number of days remaining on certifcate
* @param {string} certCN Common Name attribute from the certificate subject
* @param {string} certType certificate type
* @param {number} daysRemaining Number of days remaining on certificate
* @param {number} targetDays Number of days to alert after
* @param {LooseObject<any>[]} notificationList List of notification providers
* @returns {Promise<void>}
*/
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
async sendCertNotificationByTargetDays(certCN, certType, daysRemaining, targetDays, notificationList) {
if (daysRemaining > targetDays) {
log.debug("monitor", `No need to send cert notification. ${daysRemaining} > ${targetDays}`);
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
"certificate",
this.id,
targetDays,
]);
// Sent already, no need to send again
if (row) {
log.debug("monitor", "Sent already, no need to send again");
return;
}
if (notificationList.length > 0) {
let sent = false;
log.debug("monitor", "Send certificate notification");
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
for (let notification of notificationList) {
try {
log.debug("monitor", "Sending to " + notification.name);
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will be expired in ${daysRemaining} days`);
sent = true;
} catch (e) {
log.error("monitor", "Cannot send cert notification to " + notification.name);
log.error("monitor", e);
}
}
if (sent) {
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
"certificate",
this.id,
targetDays,
]);
// Sent already, no need to send again
if (row) {
log.debug("monitor", "Sent already, no need to send again");
return;
}
let sent = false;
log.debug("monitor", "Send certificate notification");
for (let notification of notificationList) {
try {
log.debug("monitor", "Sending to " + notification.name);
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will expire in ${daysRemaining} days`);
sent = true;
} catch (e) {
log.error("monitor", "Cannot send cert notification to " + notification.name);
log.error("monitor", e);
}
}
if (sent) {
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
"certificate",
this.id,
targetDays,
]);
}
} else {
log.debug("monitor", "No notification, no need to send cert notification");
}
}
@@ -1314,6 +1444,11 @@ class Monitor extends BeanModel {
}
}
const parent = await Monitor.getParent(monitorID);
if (parent != null) {
return await Monitor.isUnderMaintenance(parent.id);
}
return false;
}
@@ -1326,6 +1461,105 @@ class Monitor extends BeanModel {
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
}
}
/**
* Gets Parent of the monitor
* @param {number} monitorID ID of monitor to get
* @returns {Promise<LooseObject<any>>}
*/
static async getParent(monitorID) {
return await R.getRow(`
SELECT parent.* FROM monitor parent
LEFT JOIN monitor child
ON child.parent = parent.id
WHERE child.id = ?
`, [
monitorID,
]);
}
/**
* Gets all Children of the monitor
* @param {number} monitorID ID of monitor to get
* @returns {Promise<LooseObject<any>>}
*/
static async getChildren(monitorID) {
return await R.getAll(`
SELECT * FROM monitor
WHERE parent = ?
`, [
monitorID,
]);
}
/**
* Gets Full Path-Name (Groups and Name)
* @returns {Promise<String>}
*/
async getPathName() {
let path = this.name;
if (this.parent === null) {
return path;
}
let parent = await Monitor.getParent(this.id);
while (parent !== null) {
path = `${parent.name} / ${path}`;
parent = await Monitor.getParent(parent.id);
}
return path;
}
/**
* Gets recursive all child ids
* @param {number} monitorID ID of the monitor to get
* @returns {Promise<Array>}
*/
static async getAllChildrenIDs(monitorID) {
const childs = await Monitor.getChildren(monitorID);
if (childs === null) {
return [];
}
let childrenIDs = [];
for (const child of childs) {
childrenIDs.push(child.id);
childrenIDs = childrenIDs.concat(await Monitor.getAllChildrenIDs(child.id));
}
return childrenIDs;
}
/**
* Unlinks all children of the the group monitor
* @param {number} groupID ID of group to remove children of
* @returns {Promise<void>}
*/
static async unlinkAllChildren(groupID) {
return await R.exec("UPDATE `monitor` SET parent = ? WHERE parent = ? ", [
null, groupID
]);
}
/**
* Checks recursive if parent (ancestors) are active
* @param {number} monitorID ID of the monitor to get
* @returns {Promise<Boolean>}
*/
static async isParentActive(monitorID) {
const parent = await Monitor.getParent(monitorID);
if (parent === null) {
return true;
}
const parentActive = await Monitor.isParentActive(parent.id);
return parent.active && parentActive;
}
}
module.exports = Monitor;

View File

@@ -6,9 +6,10 @@ class MonitorType {
*
* @param {Monitor} monitor
* @param {Heartbeat} heartbeat
* @param {UptimeKumaServer} server
* @returns {Promise<void>}
*/
async check(monitor, heartbeat) {
async check(monitor, heartbeat, server) {
throw new Error("You need to override check()");
}

View File

@@ -0,0 +1,212 @@
const { MonitorType } = require("./monitor-type");
const { chromium } = require("playwright-core");
const { UP, log } = require("../../src/util");
const { Settings } = require("../settings");
const commandExistsSync = require("command-exists").sync;
const childProcess = require("child_process");
const path = require("path");
const Database = require("../database");
const jwt = require("jsonwebtoken");
const config = require("../config");
let browser = null;
let allowedList = [];
let lastAutoDetectChromeExecutable = null;
if (process.platform === "win32") {
allowedList.push(process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe");
allowedList.push(process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe");
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Google\\Chrome\\Application\\chrome.exe");
// Allow Chromium too
allowedList.push(process.env.LOCALAPPDATA + "\\Chromium\\Application\\chrome.exe");
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
// For Loop A to Z
for (let i = 65; i <= 90; i++) {
let drive = String.fromCharCode(i);
allowedList.push(drive + ":\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
allowedList.push(drive + ":\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
}
} else if (process.platform === "linux") {
allowedList = [
"chromium",
"chromium-browser",
"google-chrome",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/google-chrome",
];
} else if (process.platform === "darwin") {
// TODO: Generated by GitHub Copilot, but not sure if it's correct
allowedList = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
];
}
log.debug("chrome", allowedList);
async function isAllowedChromeExecutable(executablePath) {
console.log(config.args);
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
return true;
}
// Check if the executablePath is in the list of allowed executables
return allowedList.includes(executablePath);
}
async function getBrowser() {
if (!browser) {
let executablePath = await Settings.get("chromeExecutable");
executablePath = await prepareChromeExecutable(executablePath);
browser = await chromium.launch({
//headless: false,
executablePath,
});
}
return browser;
}
async function prepareChromeExecutable(executablePath) {
// Special code for using the playwright_chromium
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
// Set to undefined = use playwright_chromium
executablePath = undefined;
} else if (!executablePath) {
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
executablePath = "/usr/bin/chromium";
// Install chromium in container via apt install
if ( !commandExistsSync(executablePath)) {
await new Promise((resolve, reject) => {
log.info("Chromium", "Installing Chromium...");
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
// On exit
child.on("exit", (code) => {
log.info("Chromium", "apt install chromium exited with code " + code);
if (code === 0) {
log.info("Chromium", "Installed Chromium");
let version = childProcess.execSync(executablePath + " --version").toString("utf8");
log.info("Chromium", "Chromium version: " + version);
resolve();
} else if (code === 100) {
reject(new Error("Installing Chromium, please wait..."));
} else {
reject(new Error("apt install chromium failed with code " + code));
}
});
});
}
} else {
executablePath = findChrome(allowedList);
}
} else {
// User specified a path
// Check if the executablePath is in the list of allowed
if (!await isAllowedChromeExecutable(executablePath)) {
throw new Error("This Chromium executable path is not allowed by default. If you are sure this is safe, please add an environment variable UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC=1 to allow it.");
}
}
return executablePath;
}
function findChrome(executables) {
// Use the last working executable, so we don't have to search for it again
if (lastAutoDetectChromeExecutable) {
if (commandExistsSync(lastAutoDetectChromeExecutable)) {
return lastAutoDetectChromeExecutable;
}
}
for (let executable of executables) {
if (commandExistsSync(executable)) {
lastAutoDetectChromeExecutable = executable;
return executable;
}
}
throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
}
async function resetChrome() {
if (browser) {
await browser.close();
browser = null;
}
}
/**
* Test if the chrome executable is valid and return the version
* @param executablePath
* @returns {Promise<string>}
*/
async function testChrome(executablePath) {
try {
executablePath = await prepareChromeExecutable(executablePath);
log.info("Chromium", "Testing Chromium executable: " + executablePath);
const browser = await chromium.launch({
executablePath,
});
const version = browser.version();
await browser.close();
return version;
} catch (e) {
throw new Error(e.message);
}
}
/**
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
*
*/
class RealBrowserMonitorType extends MonitorType {
name = "real-browser";
async check(monitor, heartbeat, server) {
const browser = await getBrowser();
const context = await browser.newContext();
const page = await context.newPage();
const res = await page.goto(monitor.url, {
waitUntil: "networkidle",
timeout: monitor.interval * 1000 * 0.8,
});
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
await page.screenshot({
path: path.join(Database.screenshotDir, filename),
});
await context.close();
if (res.status() >= 200 && res.status() < 400) {
heartbeat.status = UP;
heartbeat.msg = res.status();
const timing = res.request().timing();
heartbeat.ping = timing.responseEnd;
} else {
throw new Error(res.status() + "");
}
}
}
module.exports = {
RealBrowserMonitorType,
testChrome,
resetChrome,
};

View File

@@ -15,7 +15,7 @@ class DingDing extends NotificationProvider {
msgtype: "markdown",
markdown: {
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
}
};
if (this.sendToDingDing(notification, params)) {

View File

@@ -59,8 +59,8 @@ class Discord extends NotificationProvider {
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
},
{
name: "Error",
@@ -94,8 +94,8 @@ class Discord extends NotificationProvider {
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
},
{
name: "Time (UTC)",
value: heartbeatJSON["time"],
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
},
{
name: "Ping",

View File

@@ -35,8 +35,7 @@ class Feishu extends NotificationProvider {
text:
"[Down] " +
heartbeatJSON["msg"] +
"\nTime (UTC): " +
heartbeatJSON["time"],
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
},
],
],
@@ -62,8 +61,7 @@ class Feishu extends NotificationProvider {
text:
"[Up] " +
heartbeatJSON["msg"] +
"\nTime (UTC): " +
heartbeatJSON["time"],
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
},
],
],

View File

@@ -11,7 +11,7 @@ class HomeAssistant extends NotificationProvider {
try {
await axios.post(
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
{
title: "Uptime Kuma",
message,

View File

@@ -33,7 +33,10 @@ class Line extends NotificationProvider {
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
"text": "UptimeKuma Alert: [🔴 Down]\n" +
"Name: " + monitorJSON["name"] + " \n" +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
}
]
};
@@ -44,7 +47,10 @@ class Line extends NotificationProvider {
"messages": [
{
"type": "text",
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
"text": "UptimeKuma Alert: [✅ Up]\n" +
"Name: " + monitorJSON["name"] + " \n" +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
}
]
};

View File

@@ -24,12 +24,18 @@ class LineNotify extends NotificationProvider {
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
} else if (heartbeatJSON["status"] === DOWN) {
let downMessage = {
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
"message": "\n[🔴 Down]\n" +
"Name: " + monitorJSON["name"] + " \n" +
heartbeatJSON["msg"] + "\n" +
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
} else if (heartbeatJSON["status"] === UP) {
let upMessage = {
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
"message": "\n[✅ Up]\n" +
"Name: " + monitorJSON["name"] + " \n" +
heartbeatJSON["msg"] + "\n" +
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
}

View File

@@ -28,7 +28,9 @@ class LunaSea extends NotificationProvider {
if (heartbeatJSON["status"] === DOWN) {
let downdata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
"body": "[🔴 Down] " +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(lunaseaurl, downdata);
return okMsg;
@@ -37,7 +39,9 @@ class LunaSea extends NotificationProvider {
if (heartbeatJSON["status"] === UP) {
let updata = {
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
"body": "[✅ Up] " +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(lunaseaurl, updata);
return okMsg;

View File

@@ -10,7 +10,7 @@ class Mattermost extends NotificationProvider {
let okMsg = "Sent Successfully.";
try {
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
// If heartbeatJSON is null, assume we're testing.
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
if (heartbeatJSON == null) {
let mattermostTestData = {
username: mattermostUserName,
@@ -27,97 +27,79 @@ class Mattermost extends NotificationProvider {
}
const mattermostIconEmoji = notification.mattermosticonemo;
const mattermostIconUrl = notification.mattermosticonurl;
let mattermostIconEmojiOnline = "";
let mattermostIconEmojiOffline = "";
if (heartbeatJSON["status"] === DOWN) {
let mattermostdowndata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went down.",
color: "#FF0000",
title:
"❌ " +
monitorJSON["name"] +
" service went down. ❌",
title_link: monitorJSON["url"],
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
short: false,
title: "Error",
value: heartbeatJSON["msg"],
},
],
},
],
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostdowndata
);
return okMsg;
} else if (heartbeatJSON["status"] === UP) {
let mattermostupdata = {
username: mattermostUserName,
text: "Uptime Kuma Alert",
channel: mattermostChannel,
icon_emoji: mattermostIconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON["name"] +
" service went up!",
color: "#32CD32",
title:
"✅ " +
monitorJSON["name"] +
" service went up! ✅",
title_link: monitorJSON["url"],
fields: [
{
short: true,
title: "Service Name",
value: monitorJSON["name"],
},
{
short: true,
title: "Time (UTC)",
value: heartbeatJSON["time"],
},
{
short: false,
title: "Ping",
value: heartbeatJSON["ping"] + "ms",
},
],
},
],
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostupdata
);
return okMsg;
if (mattermostIconEmoji && typeof mattermostIconEmoji === "string") {
const emojiArray = mattermostIconEmoji.split(" ");
if (emojiArray.length >= 2) {
mattermostIconEmojiOnline = emojiArray[0];
mattermostIconEmojiOffline = emojiArray[1];
}
}
const mattermostIconUrl = notification.mattermosticonurl;
let iconEmoji = mattermostIconEmoji;
let statusField = {
short: false,
title: "Error",
value: heartbeatJSON.msg,
};
let statusText = "unknown";
let color = "#000000";
if (heartbeatJSON.status === DOWN) {
iconEmoji = mattermostIconEmojiOffline || mattermostIconEmoji;
statusField = {
short: false,
title: "Error",
value: heartbeatJSON.msg,
};
statusText = "down.";
color = "#FF0000";
} else if (heartbeatJSON.status === UP) {
iconEmoji = mattermostIconEmojiOnline || mattermostIconEmoji;
statusField = {
short: false,
title: "Ping",
value: heartbeatJSON.ping + "ms",
};
statusText = "up!";
color = "#32CD32";
}
let mattermostdata = {
username: monitorJSON.name + " " + mattermostUserName,
channel: mattermostChannel,
icon_emoji: iconEmoji,
icon_url: mattermostIconUrl,
attachments: [
{
fallback:
"Your " +
monitorJSON.name +
" service went " +
statusText,
color: color,
title:
monitorJSON.name +
" service went " +
statusText,
title_link: monitorJSON.url,
fields: [
statusField,
{
short: true,
title: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON.localDateTime,
},
],
},
],
};
await axios.post(
notification.mattermostWebhookUrl,
mattermostdata
);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}

View File

@@ -1,5 +1,6 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Ntfy extends NotificationProvider {
@@ -9,16 +10,54 @@ class Ntfy extends NotificationProvider {
let okMsg = "Sent Successfully.";
try {
let headers = {};
if (notification.ntfyusername) {
if (notification.ntfyAuthenticationMethod === "usernamePassword") {
headers = {
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
};
} else if (notification.ntfyAuthenticationMethod === "accessToken") {
headers = {
"Authorization": "Bearer " + notification.ntfyaccesstoken,
};
}
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
if (heartbeatJSON == null) {
let ntfyTestData = {
"topic": notification.ntfytopic,
"title": (monitorJSON?.name || notification.ntfytopic) + " [Uptime-Kuma]",
"message": msg,
"priority": notification.ntfyPriority,
"tags": [ "test_tube" ],
};
await axios.post(`${notification.ntfyserverurl}`, ntfyTestData, { headers: headers });
return okMsg;
}
let tags = [];
let status = "unknown";
let priority = notification.ntfyPriority || 4;
if ("status" in heartbeatJSON) {
if (heartbeatJSON.status === DOWN) {
tags = [ "red_circle" ];
status = "Down";
// if priority is not 5, increase priority for down alerts
priority = priority === 5 ? priority : priority + 1;
} else if (heartbeatJSON["status"] === UP) {
tags = [ "green_circle" ];
status = "Up";
}
}
let data = {
"topic": notification.ntfytopic,
"message": msg,
"priority": notification.ntfyPriority || 4,
"title": "Uptime-Kuma",
"message": heartbeatJSON.msg,
"priority": priority,
"title": monitorJSON.name + " " + status + " [Uptime-Kuma]",
"tags": tags,
"actions": [
{
"action": "view",
"label": "Open " + monitorJSON.name,
"url": monitorJSON.url,
}
]
};
if (notification.ntfyIcon) {

View File

@@ -15,7 +15,7 @@ class Opsgenie extends NotificationProvider {
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let opsgenieAlertsUrl;
let priority = (notification.opsgeniePriority == "") ? 3 : notification.opsgeniePriority;
let priority = (!notification.opsgeniePriority) ? 3 : notification.opsgeniePriority;
const textMsg = "Uptime Kuma Alert";
try {

View File

@@ -29,14 +29,18 @@ class Pushbullet extends NotificationProvider {
let downData = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
"body": "[🔴 Down] " +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
};
await axios.post(pushbulletUrl, downData, config);
} else if (heartbeatJSON["status"] === UP) {
let upData = {
"type": "note",
"title": "UptimeKuma Alert: " + monitorJSON["name"],
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
"body": "[✅ Up] " +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
};
await axios.post(pushbulletUrl, upData, config);
}

View File

@@ -24,13 +24,16 @@ class Pushover extends NotificationProvider {
if (notification.pushoverdevice) {
data.device = notification.pushoverdevice;
}
if (notification.pushoverttl) {
data.ttl = notification.pushoverttl;
}
try {
if (heartbeatJSON == null) {
await axios.post(pushoverlink, data);
return okMsg;
} else {
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>:${heartbeatJSON["localDateTime"]}`;
await axios.post(pushoverlink, data);
return okMsg;
}

View File

@@ -22,8 +22,6 @@ class RocketChat extends NotificationProvider {
return okMsg;
}
const time = heartbeatJSON["time"];
let data = {
"text": "Uptime Kuma Alert",
"channel": notification.rocketchannel,
@@ -31,7 +29,7 @@ class RocketChat extends NotificationProvider {
"icon_emoji": notification.rocketiconemo,
"attachments": [
{
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
"title": `Uptime Kuma Alert *Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
"text": "*Message*\n" + msg,
}
]

View File

@@ -27,6 +27,11 @@ class Slack extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
if (notification.slackchannelnotify) {
msg += " <!channel>";
}
try {
if (heartbeatJSON == null) {
let data = {
@@ -39,7 +44,6 @@ class Slack extends NotificationProvider {
return okMsg;
}
const time = heartbeatJSON["time"];
const textMsg = "Uptime Kuma Alert";
let data = {
"text": `${textMsg}\n${msg}`,
@@ -54,7 +58,7 @@ class Slack extends NotificationProvider {
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
"text": textMsg,
},
},
{
@@ -65,7 +69,7 @@ class Slack extends NotificationProvider {
},
{
"type": "mrkdwn",
"text": "*Time (UTC)*\n" + time,
"text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
}],
}
],

View File

@@ -0,0 +1,42 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SMSC extends NotificationProvider {
name = "smsc";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
"Accept": "text/json",
}
};
let getArray = [
"fmt=3",
"translit=" + notification.smscTranslit,
"login=" + notification.smscLogin,
"psw=" + notification.smscPassword,
"phones=" + notification.smscToNumber,
"mes=" + encodeURIComponent(msg.replace(/[^\x00-\x7F]/g, "")),
];
if (notification.smscSenderName !== "") {
getArray.push("sender=" + notification.smscSenderName);
}
let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config);
if (resp.data.id === undefined) {
let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`;
this.throwGeneralAxiosError(error);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SMSC;

View File

@@ -67,7 +67,7 @@ class SMTP extends NotificationProvider {
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
@@ -91,7 +91,7 @@ class SMTP extends NotificationProvider {
let bodyTextContent = msg;
if (heartbeatJSON) {
bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`;
bodyTextContent = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
// send mail with defined transport object

View File

@@ -25,8 +25,11 @@ class Telegram extends NotificationProvider {
return okMsg;
} catch (error) {
let msg = (error.response.data.description) ? error.response.data.description : "Error without description";
throw new Error(msg);
if (error.response && error.response.data && error.response.data.description) {
throw new Error(error.response.data.description);
} else {
throw new Error(error.message);
}
}
}
}

View File

@@ -10,6 +10,7 @@ class Twilio extends NotificationProvider {
let okMsg = "Sent Successfully.";
let accountSID = notification.twilioAccountSID;
let apiKey = notification.twilioApiKey ? notification.twilioApiKey : accountSID;
let authToken = notification.twilioAuthToken;
try {
@@ -17,7 +18,7 @@ class Twilio extends NotificationProvider {
let config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"),
"Authorization": "Basic " + Buffer.from(apiKey + ":" + authToken).toString("base64"),
}
};

View File

@@ -1,6 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const FormData = require("form-data");
const { Liquid } = require("liquidjs");
class Webhook extends NotificationProvider {
@@ -15,17 +16,27 @@ class Webhook extends NotificationProvider {
monitor: monitorJSON,
msg,
};
let finalData;
let config = {
headers: {}
};
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append("data", JSON.stringify(data));
config.headers = finalData.getHeaders();
} else {
finalData = data;
const formData = new FormData();
formData.append("data", JSON.stringify(data));
config.headers = formData.getHeaders();
data = formData;
} else if (notification.webhookContentType === "custom") {
// Initialize LiquidJS and parse the custom Body Template
const engine = new Liquid();
const tpl = engine.parse(notification.webhookCustomBody);
// Insert templated values into Body
data = await engine.render(tpl,
{
msg,
heartbeatJSON,
monitorJSON
});
}
if (notification.webhookAdditionalHeaders) {
@@ -39,7 +50,7 @@ class Webhook extends NotificationProvider {
}
}
await axios.post(notification.webhookURL, finalData, config);
await axios.post(notification.webhookURL, data, config);
return okMsg;
} catch (error) {

View File

@@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise");
const Bark = require("./notification-providers/bark");
const ClickSendSMS = require("./notification-providers/clicksendsms");
const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Feishu = require("./notification-providers/feishu");
@@ -68,6 +69,7 @@ class Notification {
new Apprise(),
new Bark(),
new ClickSendSMS(),
new SMSC(),
new DingDing(),
new Discord(),
new Feishu(),

View File

@@ -1,13 +0,0 @@
class Plugin {
async load() {
}
async unload() {
}
}
module.exports = {
Plugin,
};

View File

@@ -1,256 +0,0 @@
const fs = require("fs");
const { log } = require("../src/util");
const path = require("path");
const axios = require("axios");
const { Git } = require("./git");
const childProcess = require("child_process");
class PluginsManager {
static disable = false;
/**
* Plugin List
* @type {PluginWrapper[]}
*/
pluginList = [];
/**
* Plugins Dir
*/
pluginsDir;
server;
/**
*
* @param {UptimeKumaServer} server
*/
constructor(server) {
this.server = server;
if (!PluginsManager.disable) {
this.pluginsDir = "./data/plugins/";
if (! fs.existsSync(this.pluginsDir)) {
fs.mkdirSync(this.pluginsDir, { recursive: true });
}
log.debug("plugin", "Scanning plugin directory");
let list = fs.readdirSync(this.pluginsDir);
this.pluginList = [];
for (let item of list) {
this.loadPlugin(item);
}
} else {
log.warn("PLUGIN", "Skip scanning plugin directory");
}
}
/**
* Install a Plugin
*/
async loadPlugin(name) {
log.info("plugin", "Load " + name);
let plugin = new PluginWrapper(this.server, this.pluginsDir + name);
try {
await plugin.load();
this.pluginList.push(plugin);
} catch (e) {
log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name);
log.error("plugin", "Reason: " + e.message);
}
}
/**
* Download a Plugin
* @param {string} repoURL Git repo url
* @param {string} name Directory name, also known as plugin unique name
*/
downloadPlugin(repoURL, name) {
if (fs.existsSync(this.pluginsDir + name)) {
log.info("plugin", "Plugin folder already exists? Removing...");
fs.rmSync(this.pluginsDir + name, {
recursive: true
});
}
log.info("plugin", "Installing plugin: " + name + " " + repoURL);
let result = Git.clone(repoURL, this.pluginsDir, name);
log.info("plugin", "Install result: " + result);
}
/**
* Remove a plugin
* @param {string} name
*/
async removePlugin(name) {
log.info("plugin", "Removing plugin: " + name);
for (let plugin of this.pluginList) {
if (plugin.info.name === name) {
await plugin.unload();
// Delete the plugin directory
fs.rmSync(this.pluginsDir + name, {
recursive: true
});
this.pluginList.splice(this.pluginList.indexOf(plugin), 1);
return;
}
}
log.warn("plugin", "Plugin not found: " + name);
throw new Error("Plugin not found: " + name);
}
/**
* TODO: Update a plugin
* Only available for plugins which were downloaded from the official list
* @param pluginID
*/
updatePlugin(pluginID) {
}
/**
* Get the plugin list from server + local installed plugin list
* Item will be merged if the `name` is the same.
* @returns {Promise<[]>}
*/
async fetchPluginList() {
let remotePluginList;
try {
const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
remotePluginList = res.data.pluginList;
} catch (e) {
log.error("plugin", "Failed to fetch plugin list: " + e.message);
remotePluginList = [];
}
for (let plugin of this.pluginList) {
let find = false;
// Try to merge
for (let remotePlugin of remotePluginList) {
if (remotePlugin.name === plugin.info.name) {
find = true;
remotePlugin.installed = true;
remotePlugin.name = plugin.info.name;
remotePlugin.fullName = plugin.info.fullName;
remotePlugin.description = plugin.info.description;
remotePlugin.version = plugin.info.version;
break;
}
}
// Local plugin
if (!find) {
plugin.info.local = true;
remotePluginList.push(plugin.info);
}
}
// Sort Installed first, then sort by name
return remotePluginList.sort((a, b) => {
if (a.installed === b.installed) {
if (a.fullName < b.fullName) {
return -1;
}
if (a.fullName > b.fullName) {
return 1;
}
return 0;
} else if (a.installed) {
return -1;
} else {
return 1;
}
});
}
}
class PluginWrapper {
server = undefined;
pluginDir = undefined;
/**
* Must be an `new-able` class.
* @type {function}
*/
pluginClass = undefined;
/**
*
* @type {Plugin}
*/
object = undefined;
info = {};
/**
*
* @param {UptimeKumaServer} server
* @param {string} pluginDir
*/
constructor(server, pluginDir) {
this.server = server;
this.pluginDir = pluginDir;
}
async load() {
let indexFile = this.pluginDir + "/index.js";
let packageJSON = this.pluginDir + "/package.json";
log.info("plugin", "Installing dependencies");
if (fs.existsSync(indexFile)) {
// Install dependencies
let result = childProcess.spawnSync("npm", [ "install" ], {
cwd: this.pluginDir,
env: {
...process.env,
PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor
}
});
if (result.stdout) {
log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8"));
} else {
log.warn("plugin", "Install dependencies result: no output");
}
this.pluginClass = require(path.join(process.cwd(), indexFile));
let pluginClassType = typeof this.pluginClass;
if (pluginClassType === "function") {
this.object = new this.pluginClass(this.server);
await this.object.load();
} else {
throw new Error("Invalid plugin, it does not export a class");
}
if (fs.existsSync(packageJSON)) {
this.info = require(path.join(process.cwd(), packageJSON));
} else {
this.info.fullName = this.pluginDir;
this.info.name = "[unknown]";
this.info.version = "[unknown-version]";
}
this.info.installed = true;
log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
}
}
async unload() {
await this.object.unload();
}
}
module.exports = {
PluginsManager,
PluginWrapper
};

View File

@@ -28,7 +28,7 @@ const monitorResponseTime = new PrometheusClient.Gauge({
const monitorStatus = new PrometheusClient.Gauge({
name: "monitor_status",
help: "Monitor Status (1 = UP, 0= DOWN)",
help: "Monitor Status (1 = UP, 0= DOWN, 2= PENDING, 3= MAINTENANCE)",
labelNames: commonLabels
});

View File

@@ -10,6 +10,7 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
const { UptimeCacheList } = require("../uptime-cache-list");
const { makeBadge } = require("badge-maker");
const { badgeConstants } = require("../config");
const { Prometheus } = require("../prometheus");
let router = express.Router();
@@ -37,7 +38,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK";
let ping = request.query.ping || null;
let ping = parseInt(request.query.ping) || null;
let statusString = request.query.status || "up";
let status = (statusString === "up") ? UP : DOWN;
@@ -89,6 +90,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
UptimeCacheList.clearCache(monitor.id);
Monitor.sendStats(io, monitor.id, monitor.user_id);
new Prometheus(monitor).update(bean, undefined);
response.json({
ok: true,
@@ -440,7 +442,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon
if (!tlsInfo.valid) {
// return a "Bad Cert" badge in naColor (grey), when cert is not valid
badgeValues.message = "Bad Cert";
badgeValues.color = badgeConstants.downColor;
badgeValues.color = downColor;
} else {
const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);

View File

@@ -5,6 +5,8 @@ const StatusPage = require("../model/status_page");
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
const { R } = require("redbean-node");
const Monitor = require("../model/monitor");
const { badgeConstants } = require("../config");
const { makeBadge } = require("badge-maker");
let router = express.Router();
@@ -139,4 +141,100 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
}
});
// overall status-page status badge
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
const slug = request.params.slug;
const statusPageID = await StatusPage.slugToID(slug);
const {
label,
upColor = badgeConstants.defaultUpColor,
downColor = badgeConstants.defaultDownColor,
partialColor = "#F6BE00",
maintenanceColor = "#808080",
style = badgeConstants.defaultStyle
} = request.query;
try {
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
let hasUp = false;
let hasDown = false;
let hasMaintenance = false;
for (let monitorID of monitorIDList) {
// retrieve the latest heartbeat
let beat = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 1
`, [
monitorID,
]);
// to be sure, when corresponding monitor not found
if (beat.length === 0) {
continue;
}
// handle status of beat
if (beat[0].status === 3) {
hasMaintenance = true;
} else if (beat[0].status === 2) {
// ignored
} else if (beat[0].status === 1) {
hasUp = true;
} else {
hasDown = true;
}
}
const badgeValues = { style };
if (!hasUp && !hasDown && !hasMaintenance) {
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
badgeValues.message = "N/A";
badgeValues.color = badgeConstants.naColor;
} else {
if (hasMaintenance) {
badgeValues.label = label ? label : "";
badgeValues.color = maintenanceColor;
badgeValues.message = "Maintenance";
} else if (hasUp && !hasDown) {
badgeValues.label = label ? label : "";
badgeValues.color = upColor;
badgeValues.message = "Up";
} else if (hasUp && hasDown) {
badgeValues.label = label ? label : "";
badgeValues.color = partialColor;
badgeValues.message = "Degraded";
} else {
badgeValues.label = label ? label : "";
badgeValues.color = downColor;
badgeValues.message = "Down";
}
}
// build the svg based on given values
const svg = makeBadge(badgeValues);
response.type("image/svg+xml");
response.send(svg);
} catch (error) {
sendHttpError(response, error.message);
}
});
module.exports = router;

View File

@@ -15,15 +15,27 @@ dayjs.extend(require("dayjs/plugin/customParseFormat"));
require("dotenv").config();
// Check Node.js Version
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
const requiredVersion = 14;
const nodeVersion = process.versions.node;
// Get the required Node.js version from package.json
const requiredNodeVersions = require("../package.json").engines.node;
const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
console.log(`Your Node.js version: ${nodeVersion}`);
if (nodeVersion < requiredVersion) {
console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`);
const semver = require("semver");
const requiredNodeVersionsComma = requiredNodeVersions.split("||").map((version) => version.trim()).join(", ");
// Exit Uptime Kuma immediately if the Node.js version is banned
if (semver.satisfies(nodeVersion, bannedNodeVersions)) {
console.error("\x1b[31m%s\x1b[0m", `Error: Your Node.js version: ${nodeVersion} is not supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
process.exit(-1);
}
// Warning if the Node.js version is not in the support list, but it maybe still works
if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
console.warn("\x1b[31m%s\x1b[0m", `Warning: Your Node.js version: ${nodeVersion} is not officially supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
}
const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config");
@@ -142,8 +154,8 @@ const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handle
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
const { Settings } = require("./settings");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
const apicache = require("./modules/apicache");
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
app.use(express.json());
@@ -156,12 +168,6 @@ app.use(function (req, res, next) {
next();
});
/**
* Use for decode the auth object
* @type {null}
*/
let jwtSecret = null;
/**
* Show Setup Page
* @type {boolean}
@@ -172,7 +178,6 @@ let needSetup = false;
Database.init(args);
await initDatabase(testMode);
await server.initAfterDatabaseReady();
server.loadPlugins();
server.entryPage = await Settings.get("entryPage");
await StatusPage.loadDomainMappingList();
@@ -210,6 +215,7 @@ let needSetup = false;
});
if (isDev) {
app.use(express.urlencoded({ extended: true }));
app.post("/test-webhook", async (request, response) => {
log.debug("test", request.headers);
log.debug("test", request.body);
@@ -264,7 +270,7 @@ let needSetup = false;
log.info("server", "Adding socket handler");
io.on("connection", async (socket) => {
sendInfo(socket);
sendInfo(socket, true);
if (needSetup) {
log.info("server", "Redirect to setup page");
@@ -281,7 +287,7 @@ let needSetup = false;
log.info("auth", `Login by token. IP=${clientIP}`);
try {
let decoded = jwt.verify(token, jwtSecret);
let decoded = jwt.verify(token, server.jwtSecret);
log.info("auth", "Username from JWT: " + decoded.username);
@@ -352,7 +358,7 @@ let needSetup = false;
ok: true,
token: jwt.sign({
username: data.username,
}, jwtSecret),
}, server.jwtSecret),
});
}
@@ -382,7 +388,7 @@ let needSetup = false;
ok: true,
token: jwt.sign({
username: data.username,
}, jwtSecret),
}, server.jwtSecret),
});
} else {
@@ -637,6 +643,9 @@ let needSetup = false;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
bean.import(monitor);
bean.user_id = socket.userID;
@@ -671,6 +680,7 @@ let needSetup = false;
// Edit a monitor
socket.on("editMonitor", async (monitor, callback) => {
try {
let removeGroupChildren = false;
checkLogin(socket);
let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]);
@@ -679,8 +689,22 @@ let needSetup = false;
throw new Error("Permission denied.");
}
// Check if Parent is Descendant (would cause endless loop)
if (monitor.parent !== null) {
const childIDs = await Monitor.getAllChildrenIDs(monitor.id);
if (childIDs.includes(monitor.parent)) {
throw new Error("Invalid Monitor Group");
}
}
// Remove children if monitor type has changed (from group to non-group)
if (bean.type === "group" && monitor.type !== bean.type) {
removeGroupChildren = true;
}
bean.name = monitor.name;
bean.description = monitor.description;
bean.parent = monitor.parent;
bean.type = monitor.type;
bean.url = monitor.url;
bean.method = monitor.method;
@@ -699,6 +723,7 @@ let needSetup = false;
bean.maxretries = monitor.maxretries;
bean.port = parseInt(monitor.port);
bean.keyword = monitor.keyword;
bean.invertKeyword = monitor.invertKeyword;
bean.ignoreTls = monitor.ignoreTls;
bean.expiryNotification = monitor.expiryNotification;
bean.upsideDown = monitor.upsideDown;
@@ -733,14 +758,25 @@ let needSetup = false;
bean.radiusCallingStationId = monitor.radiusCallingStationId;
bean.radiusSecret = monitor.radiusSecret;
bean.httpBodyEncoding = monitor.httpBodyEncoding;
bean.expectedValue = monitor.expectedValue;
bean.jsonPath = monitor.jsonPath;
bean.kafkaProducerTopic = monitor.kafkaProducerTopic;
bean.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
bean.validate();
await R.store(bean);
if (removeGroupChildren) {
await Monitor.unlinkAllChildren(monitor.id);
}
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (bean.active) {
if (bean.isActive()) {
await restartMonitor(socket.userID, bean.id);
}
@@ -883,6 +919,8 @@ let needSetup = false;
delete server.monitorList[monitorID];
}
const startTime = Date.now();
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
monitorID,
socket.userID,
@@ -891,6 +929,10 @@ let needSetup = false;
// Fix #2880
apicache.clear();
const endTime = Date.now();
log.info("DB", `Delete Monitor completed in : ${endTime - startTime} ms`);
callback({
ok: true,
msg: "Deleted Successfully.",
@@ -1134,6 +1176,8 @@ let needSetup = false;
await doubleCheckPassword(socket, currentPassword);
}
const previousChromeExecutable = await Settings.get("chromeExecutable");
await setSettings("general", data);
server.entryPage = data.entryPage;
@@ -1144,6 +1188,12 @@ let needSetup = false;
await server.setTimezone(data.serverTimezone);
}
// If Chrome Executable is changed, need to reset the browser
if (previousChromeExecutable !== data.chromeExecutable) {
log.info("settings", "Chrome executable is changed. Resetting Chrome...");
await resetChrome();
}
callback({
ok: true,
msg: "Saved"
@@ -1345,13 +1395,14 @@ let needSetup = false;
maxretries: monitorListData[i].maxretries,
port: monitorListData[i].port,
keyword: monitorListData[i].keyword,
invertKeyword: monitorListData[i].invertKeyword,
ignoreTls: monitorListData[i].ignoreTls,
upsideDown: monitorListData[i].upsideDown,
maxredirects: monitorListData[i].maxredirects,
accepted_statuscodes: monitorListData[i].accepted_statuscodes,
dns_resolve_type: monitorListData[i].dns_resolve_type,
dns_resolve_server: monitorListData[i].dns_resolve_server,
notificationIDList: {},
notificationIDList: monitorListData[i].notificationIDList,
proxy_id: monitorListData[i].proxy_id || null,
};
@@ -1513,7 +1564,6 @@ let needSetup = false;
maintenanceSocketHandler(socket);
apiKeySocketHandler(socket);
generalSocketHandler(socket, server);
pluginsHandler(socket, server);
log.debug("server", "added all socket handlers");
@@ -1557,7 +1607,7 @@ let needSetup = false;
}
});
initBackgroundJobs(args);
await initBackgroundJobs();
// Start cloudflared at the end if configured
await cloudflaredAutoStart(cloudflaredToken);
@@ -1616,6 +1666,7 @@ async function afterLogin(socket, user) {
socket.join(user.id);
let monitorList = await server.sendMonitorList(socket);
sendInfo(socket);
server.sendMaintenanceList(socket);
sendNotificationList(socket);
sendProxyList(socket);
@@ -1683,7 +1734,7 @@ async function initDatabase(testMode = false) {
needSetup = true;
}
jwtSecret = jwtSecretBean.value;
server.jwtSecret = jwtSecretBean.value;
}
/**

View File

@@ -3,6 +3,7 @@ const { Settings } = require("../settings");
const { sendInfo } = require("../client");
const { checkLogin } = require("../util-server");
const GameResolver = require("gamedig/lib/GameResolver");
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
let gameResolver = new GameResolver();
let gameList = null;
@@ -47,4 +48,18 @@ module.exports.generalSocketHandler = (socket, server) => {
});
});
socket.on("testChrome", (executable, callback) => {
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
testChrome(executable).then((version) => {
callback({
ok: true,
msg: "Found Chromium/Chrome. Version: " + version,
});
}).catch((e) => {
callback({
ok: false,
msg: e.message,
});
});
});
};

View File

@@ -186,7 +186,7 @@ module.exports.maintenanceSocketHandler = (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 = ? ", [
let monitors = await R.getAll("SELECT monitor.id FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
maintenanceID,
]);

View File

@@ -1,69 +0,0 @@
const { checkLogin } = require("../util-server");
const { PluginsManager } = require("../plugins-manager");
const { log } = require("../../src/util.js");
/**
* Handlers for plugins
* @param {Socket} socket Socket.io instance
* @param {UptimeKumaServer} server
*/
module.exports.pluginsHandler = (socket, server) => {
const pluginManager = server.getPluginManager();
// Get Plugin List
socket.on("getPluginList", async (callback) => {
try {
checkLogin(socket);
log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable);
if (PluginsManager.disable) {
throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
}
let pluginList = await pluginManager.fetchPluginList();
callback({
ok: true,
pluginList,
});
} catch (error) {
log.warn("plugin", "Error: " + error.message);
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("installPlugin", async (repoURL, name, callback) => {
try {
checkLogin(socket);
pluginManager.downloadPlugin(repoURL, name);
await pluginManager.loadPlugin(name);
callback({
ok: true,
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("uninstallPlugin", async (name, callback) => {
try {
checkLogin(socket);
await pluginManager.removePlugin(name);
callback({
ok: true,
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
};

View File

@@ -276,7 +276,7 @@ module.exports.statusPageSocketHandler = (socket) => {
let statusPage = R.dispense("status_page");
statusPage.slug = slug;
statusPage.title = title;
statusPage.theme = "light";
statusPage.theme = "auto";
statusPage.icon = "";
await R.store(statusPage);

View File

@@ -10,8 +10,7 @@ const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
const { PluginsManager } = require("./plugins-manager");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@@ -47,12 +46,6 @@ class UptimeKumaServer {
*/
indexHTML = "";
/**
* Plugins Manager
* @type {PluginsManager}
*/
pluginsManager = null;
/**
*
* @type {{}}
@@ -61,6 +54,12 @@ class UptimeKumaServer {
};
/**
* Use for decode the auth object
* @type {null}
*/
jwtSecret = null;
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
@@ -98,11 +97,17 @@ class UptimeKumaServer {
}
}
// Set Monitor Types
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
this.io = new Server(this.httpServer);
}
/** Initialise app after the database has been set up */
async initAfterDatabaseReady() {
// Static
this.app.use("/screenshots", express.static(Database.screenshotDir));
await CacheableDnsHttpAgent.update();
process.env.TZ = await this.getTimezone();
@@ -244,9 +249,9 @@ class UptimeKumaServer {
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"]
|| clientIP.replace(/^.*:/, "");
|| clientIP.replace(/^::ffff:/, "");
} else {
return clientIP.replace(/^.*:/, "");
return clientIP.replace(/^::ffff:/, "");
}
}
@@ -257,13 +262,43 @@ class UptimeKumaServer {
* @returns {Promise<string>}
*/
async getTimezone() {
// From process.env.TZ
try {
if (process.env.TZ) {
this.checkTimezone(process.env.TZ);
return process.env.TZ;
}
} catch (e) {
log.warn("timezone", e.message + " in process.env.TZ");
}
let timezone = await Settings.get("serverTimezone");
if (timezone) {
return timezone;
} else if (process.env.TZ) {
return process.env.TZ;
} else {
return dayjs.tz.guess();
// From Settings
try {
log.debug("timezone", "Using timezone from settings: " + timezone);
if (timezone) {
this.checkTimezone(timezone);
return timezone;
}
} catch (e) {
log.warn("timezone", e.message + " in settings");
}
// Guess
try {
let guess = dayjs.tz.guess();
log.debug("timezone", "Guessing timezone: " + guess);
if (guess) {
this.checkTimezone(guess);
return guess;
} else {
return "UTC";
}
} catch (e) {
// Guess failed, fall back to UTC
log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback");
return "UTC";
}
}
@@ -275,11 +310,24 @@ class UptimeKumaServer {
return dayjs().format("Z");
}
/**
* Throw an error if the timezone is invalid
* @param timezone
*/
checkTimezone(timezone) {
try {
dayjs.utc("2013-11-18 11:55").tz(timezone).format();
} catch (e) {
throw new Error("Invalid timezone:" + timezone);
}
}
/**
* Set the current server timezone and environment variables
* @param {string} timezone
*/
async setTimezone(timezone) {
this.checkTimezone(timezone);
await Settings.set("serverTimezone", timezone, "general");
process.env.TZ = timezone;
dayjs.tz.setDefault(timezone);
@@ -289,51 +337,11 @@ class UptimeKumaServer {
async stop() {
}
loadPlugins() {
this.pluginsManager = new PluginsManager(this);
}
/**
*
* @returns {PluginsManager}
*/
getPluginManager() {
return this.pluginsManager;
}
/**
*
* @param {MonitorType} monitorType
*/
addMonitorType(monitorType) {
if (monitorType instanceof MonitorType && monitorType.name) {
if (monitorType.name in UptimeKumaServer.monitorTypeList) {
log.error("", "Conflict Monitor Type name");
}
UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType;
} else {
log.error("", "Invalid Monitor Type: " + monitorType.name);
}
}
/**
*
* @param {MonitorType} monitorType
*/
removeMonitorType(monitorType) {
if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) {
delete UptimeKumaServer.monitorTypeList[monitorType.name];
} else {
log.error("", "Remove MonitorType failed: " + monitorType.name);
}
}
}
module.exports = {
UptimeKumaServer
};
// Must be at the end
const { MonitorType } = require("./monitor-types/monitor-type");
// Must be at the end to avoid circular dependencies
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");

View File

@@ -28,8 +28,11 @@ const {
} = require("node-radius-utils");
const dayjs = require("dayjs");
const isWindows = process.platform === /^win/.test(process.platform);
// SASLOptions used in JSDoc
// eslint-disable-next-line no-unused-vars
const { Kafka, SASLOptions } = require("kafkajs");
const isWindows = process.platform === /^win/.test(process.platform);
/**
* Init or reset JWT secret
* @returns {Promise<Bean>}
@@ -196,6 +199,94 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
});
};
/**
* Monitor Kafka using Producer
* @param {string} topic Topic name to produce into
* @param {string} message Message to produce
* @param {Object} [options={interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma"}]
* Kafka client options. Contains ssl, clientId, allowAutoTopicCreation and
* interval (interval defaults to 20, allowAutoTopicCreation defaults to false, clientId defaults to "Uptime-Kuma"
* and ssl defaults to false)
* @param {string[]} brokers List of kafka brokers to connect, host and port joined by ':'
* @param {SASLOptions} [saslOptions={}] Options for kafka client Authentication (SASL) (defaults to
* {})
* @returns {Promise<string>}
*/
exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, saslOptions = {}) {
return new Promise((resolve, reject) => {
const { interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma" } = options;
let connectedToKafka = false;
const timeoutID = setTimeout(() => {
log.debug("kafkaProducer", "KafkaProducer timeout triggered");
connectedToKafka = true;
reject(new Error("Timeout"));
}, interval * 1000 * 0.8);
if (saslOptions.mechanism === "None") {
saslOptions = undefined;
}
let client = new Kafka({
brokers: brokers,
clientId: clientId,
sasl: saslOptions,
retry: {
retries: 0,
},
ssl: ssl,
});
let producer = client.producer({
allowAutoTopicCreation: allowAutoTopicCreation,
retry: {
retries: 0,
}
});
producer.connect().then(
() => {
try {
producer.send({
topic: topic,
messages: [{
value: message,
}],
});
connectedToKafka = true;
clearTimeout(timeoutID);
resolve("Message sent successfully");
} catch (e) {
connectedToKafka = true;
producer.disconnect();
clearTimeout(timeoutID);
reject(new Error("Error sending message: " + e.message));
}
}
).catch(
(e) => {
connectedToKafka = true;
producer.disconnect();
clearTimeout(timeoutID);
reject(new Error("Error in producer connection: " + e.message));
}
);
producer.on("producer.network.request_timeout", (_) => {
clearTimeout(timeoutID);
reject(new Error("producer.network.request_timeout"));
});
producer.on("producer.disconnect", (_) => {
if (!connectedToKafka) {
clearTimeout(timeoutID);
reject(new Error("producer.disconnect"));
}
});
});
};
/**
* Use NTLM Auth for a http request.
* @param {Object} options The http request options
@@ -342,7 +433,12 @@ exports.mysqlQuery = function (connectionString, query) {
resolve("No Error, but the result is not an array. Type: " + typeof res);
}
}
connection.destroy();
try {
connection.end();
} catch (_) {
connection.destroy();
}
});
});
};
@@ -373,6 +469,7 @@ exports.mongodbPing = async function (connectionString) {
* @param {string} callingStationId ID of calling station
* @param {string} secret Secret to use
* @param {number} [port=1812] Port to contact radius server on
* @param {number} [timeout=2500] Timeout for connection to use
* @returns {Promise<any>}
*/
exports.radius = function (
@@ -383,10 +480,12 @@ exports.radius = function (
callingStationId,
secret,
port = 1812,
timeout = 2500,
) {
const client = new radiusClient({
host: hostname,
hostPort: port,
timeout: timeout,
dictionaries: [ file ],
});
@@ -408,12 +507,18 @@ exports.radius = function (
exports.redisPingAsync = function (dsn) {
return new Promise((resolve, reject) => {
const client = redis.createClient({
url: dsn,
url: dsn
});
client.on("error", (err) => {
if (client.isOpen) {
client.disconnect();
}
reject(err);
});
client.connect().then(() => {
if (!client.isOpen) {
client.emit("error", new Error("connection isn't open"));
}
client.ping().then((res, err) => {
if (client.isOpen) {
client.disconnect();
@@ -423,7 +528,7 @@ exports.redisPingAsync = function (dsn) {
} else {
resolve(res);
}
});
}).catch(error => reject(error));
});
});
};
@@ -519,12 +624,16 @@ const parseCertificateInfo = function (info) {
// Move up the chain until loop is encountered
if (link.issuerCertificate == null) {
link.certType = (i === 0) ? "self-signed" : "root CA";
break;
} else if (link.issuerCertificate.fingerprint in existingList) {
// a root CA certificate is typically "signed by itself" (=> "self signed certificate") and thus the "issuerCertificate" is a reference to itself.
log.debug("cert", `[Last] ${link.issuerCertificate.fingerprint}`);
link.certType = (i === 0) ? "self-signed" : "root CA";
link.issuerCertificate = null;
break;
} else {
link.certType = (i === 0) ? "server" : "intermediate CA";
link = link.issuerCertificate;
}

View File

@@ -266,6 +266,11 @@ optgroup {
background-color: $dark-bg2;
}
.form-select:disabled {
color: rgba($dark-font-color, 0.7);
background-color: $dark-bg;
}
.form-control, .form-select {
border-color: $dark-border-color;
}
@@ -431,12 +436,12 @@ optgroup {
.monitor-list {
&.scrollbar {
overflow-y: auto;
height: calc(100% - 65px);
height: calc(100% - 107px);
}
@media (max-width: 770px) {
&.scrollbar {
height: calc(100% - 40px);
height: calc(100% - 97px);
}
}

View File

@@ -1,6 +1,12 @@
@import "vars.scss";
@import "node_modules/vue-multiselect/dist/vue-multiselect";
.multiselect {
.dark & {
color: $dark-font-color;
}
}
.multiselect__tags {
border-radius: 1.5rem;
border: 1px solid #ced4da;
@@ -14,10 +20,12 @@
.multiselect__option--highlight {
background: $primary !important;
color: $dark-font-color2 !important;
}
.multiselect__option--highlight::after {
background: $primary !important;
color: $dark-font-color2 !important;
}
.multiselect__tag {
@@ -61,6 +69,7 @@
.multiselect__content-wrapper {
background-color: $dark-bg2;
border-color: $dark-border-color;
z-index: 150;
}
.multiselect--above .multiselect__content-wrapper {

View File

@@ -48,15 +48,14 @@
</div>
</div>
</div>
<div class="modal-footer">
<button
id="monitor-submit-btn" class="btn btn-primary" type="submit"
:disabled="processing"
>
{{ $t("Generate") }}
</button>
</div>
</div>
<div class="modal-footer">
<button
id="monitor-submit-btn" class="btn btn-primary" type="submit"
:disabled="processing"
>
{{ $t("Generate") }}
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,305 @@
<template>
<div ref="BadgeGeneratorModal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("Badge Generator", [monitor.name]) }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="mb-3">
<label for="type" class="form-label">{{ $t("Badge Type") }}</label>
<select id="type" v-model="badge.type" class="form-select">
<option value="status">status</option>
<option value="uptime">uptime</option>
<option value="ping">ping</option>
<option value="avg-response">avg-response</option>
<option value="cert-exp">cert-exp</option>
<option value="response">response</option>
</select>
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3">
<label for="duration" class="form-label">{{ $t("Badge Duration (in hours)") }}</label>
<input id="duration" v-model="badge.duration" type="number" min="0" placeholder="24" class="form-control">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('label') " class="mb-3">
<label for="label" class="form-label">{{ $t("Badge Label") }}</label>
<input id="label" v-model="badge.label" type="text" class="form-control">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('prefix') " class="mb-3">
<label for="prefix" class="form-label">{{ $t("Badge Prefix") }}</label>
<input id="prefix" v-model="badge.prefix" type="text" class="form-control">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('suffix') " class="mb-3">
<label for="suffix" class="form-label">{{ $t("Badge Suffix") }}</label>
<input id="suffix" v-model="badge.suffix" type="text" placeholder="%" class="form-control">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelColor') " class="mb-3">
<label for="labelColor" class="form-label">{{ $t("Badge Label Color") }}</label>
<input id="labelColor" v-model="badge.labelColor" type="text" placeholder="#555" class="form-control">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('color') " class="mb-3">
<label for="color" class="form-label">{{ $t("Badge Color") }}</label>
<input id="color" v-model="badge.color" type="text" :placeholder="badgeConstants.defaultUpColor" class="form-control">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelPrefix') " class="mb-3">
<label for="labelPrefix" class="form-label">{{ $t("Badge Label Prefix") }}</label>
<input id="labelPrefix" v-model="badge.labelPrefix" type="text" class="form-control">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelSuffix') " class="mb-3">
<label for="labelSuffix" class="form-label">{{ $t("Badge Label Suffix") }}</label>
<input id="labelSuffix" v-model="badge.labelSuffix" type="text" placeholder="h" class="form-control">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('upColor') " class="mb-3">
<label for="upColor" class="form-label">{{ $t("Badge Up Color") }}</label>
<input id="upColor" v-model="badge.upColor" type="text" class="form-control" :placeholder="badgeConstants.defaultUpColor">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downColor') " class="mb-3">
<label for="downColor" class="form-label">{{ $t("Badge Down Color") }}</label>
<input id="downColor" v-model="badge.downColor" type="text" class="form-control" :placeholder="badgeConstants.defaultDownColor">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('pendingColor') " class="mb-3">
<label for="pendingColor" class="form-label">{{ $t("Badge Pending Color") }}</label>
<input id="pendingColor" v-model="badge.pendingColor" type="text" class="form-control" :placeholder="badgeConstants.defaultPendingColor">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('maintenanceColor') " class="mb-3">
<label for="maintenanceColor" class="form-label">{{ $t("Badge Maintenance Color") }}</label>
<input id="maintenanceColor" v-model="badge.maintenanceColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnColor') " class="mb-3">
<label for="warnColor" class="form-label">{{ $t("Badge Warn Color") }}</label>
<input id="warnColor" v-model="badge.warnColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnDays') " class="mb-3">
<label for="warnDays" class="form-label">{{ $t("Badge Warn Days") }}</label>
<input id="warnDays" v-model="badge.warnDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireWarnDays">
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downDays') " class="mb-3">
<label for="downDays" class="form-label">{{ $t("Badge Down Days") }}</label>
<input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireDownDays">
</div>
<div class="mb-3">
<label for="style" class="form-label">{{ $t("Badge Style") }}</label>
<select id="style" v-model="badge.style" class="form-select">
<option value="plastic">plastic</option>
<option value="flat">flat</option>
<option value="flat-square">flat-square</option>
<option value="for-the-badge">for-the-badge</option>
<option value="social">social</option>
</select>
</div>
<div class="mb-3">
<label for="value" class="form-label">{{ $t("Badge value (For Testing only.)") }}</label>
<input id="value" v-model="badge.value" type="text" class="form-control">
</div>
<div class="mb-3 pt-3 d-flex justify-content-center">
<img :src="badgeURL" :alt="$t('Badge Preview')">
</div>
<div class="my-3">
<label for="badge-url" class="form-label">{{ $t("Badge URL") }}</label>
<CopyableInput id="badge-url" v-model="badgeURL" type="url" disabled="disabled" />
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">
{{ $t("Close") }}
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Modal } from "bootstrap";
import CopyableInput from "./CopyableInput.vue";
import { default as serverConfig } from "../../server/config.js";
export default {
components: {
CopyableInput
},
props: {},
emits: [],
data() {
return {
model: null,
processing: false,
monitor: {
id: null,
name: null,
},
badge: {
type: "status",
duration: null,
label: null,
prefix: null,
suffix: null,
labelColor: null,
color: null,
labelPrefix: null,
labelSuffix: null,
upColor: null,
downColor: null,
pendingColor: null,
maintenanceColor: null,
warnColor: null,
warnDays: null,
downDays: null,
style: "flat",
value: null,
},
parameters: {
status: [
"upLabel",
"downLabel",
"pendingLabel",
"maintenanceLabel",
"upColor",
"downColor",
"pendingColor",
"maintenanceColor",
],
uptime: [
"duration",
"labelPrefix",
"labelSuffix",
"prefix",
"suffix",
"color",
"labelColor",
],
ping: [
"duration",
"labelPrefix",
"labelSuffix",
"prefix",
"suffix",
"color",
"labelColor",
],
"avg-response": [
"duration",
"labelPrefix",
"labelSuffix",
"prefix",
"suffix",
"color",
"labelColor",
],
"cert-exp": [
"labelPrefix",
"labelSuffix",
"prefix",
"suffix",
"upColor",
"warnColor",
"downColor",
"warnDays",
"downDays",
"labelColor",
],
response: [
"labelPrefix",
"labelSuffix",
"prefix",
"suffix",
"color",
"labelColor",
],
},
badgeConstants: serverConfig.badgeConstants,
};
},
computed: {
badgeURL() {
if (!this.monitor.id || !this.badge.type) {
return;
}
let badgeURL = this.$root.baseURL + "/api/badge/" + this.monitor.id + "/" + this.badge.type;
let parameterList = {};
for (let parameter of this.parameters[this.badge.type] || []) {
if (parameter === "duration" && this.badge.duration) {
badgeURL += "/" + this.badge.duration;
continue;
}
if (this.badge[parameter]) {
parameterList[parameter] = this.badge[parameter];
}
}
for (let parameter of [ "label", "style", "value" ]) {
if (parameter === "style" && this.badge.style === "flat") {
continue;
}
if (this.badge[parameter]) {
parameterList[parameter] = this.badge[parameter];
}
}
if (Object.keys(parameterList).length > 0) {
return badgeURL + "?" + new URLSearchParams(parameterList);
}
return badgeURL;
},
},
mounted() {
this.BadgeGeneratorModal = new Modal(this.$refs.BadgeGeneratorModal);
},
methods: {
/**
* Setting monitor
* @param {number} monitorId ID of monitor
* @param {string} monitorName Name of monitor
*/
show(monitorId, monitorName) {
this.monitor = {
id: monitorId,
name: monitorName,
};
this.BadgeGeneratorModal.show();
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dark {
.modal-dialog .form-text, .modal-dialog p {
color: $dark-font-color;
}
}
</style>

View File

@@ -1,17 +1,25 @@
<template>
<div class="shadow-box mb-3" :style="boxStyle">
<div class="list-header">
<div class="placeholder"></div>
<div class="search-wrapper">
<a v-if="searchText == ''" class="search-icon">
<font-awesome-icon icon="search" />
</a>
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
<font-awesome-icon icon="times" />
</a>
<form>
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" />
</form>
<div class="header-top">
<div class="placeholder"></div>
<div class="search-wrapper">
<a v-if="searchText == ''" class="search-icon">
<font-awesome-icon icon="search" />
</a>
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
<font-awesome-icon icon="times" />
</a>
<form>
<input
v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')"
autocomplete="off"
/>
</form>
</div>
</div>
<div class="header-filter">
<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
</div>
</div>
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
@@ -19,43 +27,23 @@
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div>
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" :title="item.description">
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info">
<Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }}
</div>
<div class="tags">
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
</router-link>
<MonitorListItem
v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item"
:isSearch="searchText !== ''"
/>
</div>
</div>
</template>
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Tag from "../components/Tag.vue";
import Uptime from "../components/Uptime.vue";
import MonitorListItem from "../components/MonitorListItem.vue";
import MonitorListFilter from "./MonitorListFilter.vue";
import { getMonitorRelativeURL } from "../util.ts";
export default {
components: {
Uptime,
HeartbeatBar,
Tag,
MonitorListItem,
MonitorListFilter,
},
props: {
/** Should the scrollbar be shown */
@@ -67,6 +55,11 @@ export default {
return {
searchText: "",
windowTop: 0,
filterState: {
status: null,
active: null,
tags: null,
}
};
},
computed: {
@@ -91,6 +84,20 @@ export default {
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText));
});
} else {
result = result.filter(monitor => monitor.parent === null);
}
// Filter result by active state, weight and alphabetical
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
@@ -116,14 +123,24 @@ export default {
return m1.name.localeCompare(m2.name);
});
// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
if (this.filterState.status != null && this.filterState.status.length > 0) {
result.map(monitor => {
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
}
});
result = result.filter(monitor => this.filterState.status.includes(monitor.status));
}
if (this.filterState.active != null && this.filterState.active.length > 0) {
result = result.filter(monitor => this.filterState.active.includes(monitor.active));
}
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText));
return monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
.length > 0;
});
}
@@ -156,7 +173,14 @@ export default {
/** Clear the search bar */
clearSearchText() {
this.searchText = "";
}
},
/**
* Update the MonitorList Filter
* @param {object} newFilter Object with new filter
*/
updateFilter(newFilter) {
this.filterState = newFilter;
},
},
};
</script>
@@ -181,8 +205,6 @@ export default {
margin: -10px;
margin-bottom: 10px;
padding: 10px;
display: flex;
justify-content: space-between;
.dark & {
background-color: $dark-header-bg;
@@ -190,6 +212,17 @@ export default {
}
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-filter {
display: flex;
align-items: center;
}
@media (max-width: 770px) {
.list-header {
margin: -20px;
@@ -238,5 +271,4 @@ export default {
padding-left: 67px;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,284 @@
<template>
<div class="px-2 pt-2 d-flex">
<button
type="button"
:title="$t('Clear current filters')"
class="clear-filters-btn btn"
:class="{ 'active': numFiltersActive > 0}"
tabindex="0"
:disabled="numFiltersActive === 0"
@click="clearFilters"
>
<font-awesome-icon icon="stream" />
<span v-if="numFiltersActive > 0" class="px-1 fw-bold">{{ numFiltersActive }}</span>
<font-awesome-icon v-if="numFiltersActive > 0" icon="times" />
</button>
<MonitorListFilterDropdown
:filterActive="filterState.status?.length > 0"
>
<template #status>
<Status v-if="filterState.status?.length === 1" :status="filterState.status[0]" />
<span v-else>
{{ $t('Status') }}
</span>
</template>
<template #dropdown>
<li>
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(1)">
<div class="d-flex align-items-center justify-content-between">
<Status :status="1" />
<span class="ps-3">
{{ $root.stats.up }}
<span v-if="filterState.status?.includes(1)" class="px-1 filter-active">
<font-awesome-icon icon="check" />
</span>
</span>
</div>
</div>
</li>
<li>
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(0)">
<div class="d-flex align-items-center justify-content-between">
<Status :status="0" />
<span class="ps-3">
{{ $root.stats.down }}
<span v-if="filterState.status?.includes(0)" class="px-1 filter-active">
<font-awesome-icon icon="check" />
</span>
</span>
</div>
</div>
</li>
<li>
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(2)">
<div class="d-flex align-items-center justify-content-between">
<Status :status="2" />
<span class="ps-3">
{{ $root.stats.pending }}
<span v-if="filterState.status?.includes(2)" class="px-1 filter-active">
<font-awesome-icon icon="check" />
</span>
</span>
</div>
</div>
</li>
<li>
<div class="dropdown-item" tabindex="0" @click.stop="toggleStatusFilter(3)">
<div class="d-flex align-items-center justify-content-between">
<Status :status="3" />
<span class="ps-3">
{{ $root.stats.maintenance }}
<span v-if="filterState.status?.includes(3)" class="px-1 filter-active">
<font-awesome-icon icon="check" />
</span>
</span>
</div>
</div>
</li>
</template>
</MonitorListFilterDropdown>
<MonitorListFilterDropdown :filterActive="filterState.active?.length > 0">
<template #status>
<span v-if="filterState.active?.length === 1">
<span v-if="filterState.active[0]">{{ $t("Running") }}</span>
<span v-else>{{ $t("filterActivePaused") }}</span>
</span>
<span v-else>
{{ $t("filterActive") }}
</span>
</template>
<template #dropdown>
<li>
<div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(true)">
<div class="d-flex align-items-center justify-content-between">
<span>{{ $t("Running") }}</span>
<span class="ps-3">
{{ $root.stats.active }}
<span v-if="filterState.active?.includes(true)" class="px-1 filter-active">
<font-awesome-icon icon="check" />
</span>
</span>
</div>
</div>
</li>
<li>
<div class="dropdown-item" tabindex="0" @click.stop="toggleActiveFilter(false)">
<div class="d-flex align-items-center justify-content-between">
<span>{{ $t("filterActivePaused") }}</span>
<span class="ps-3">
{{ $root.stats.pause }}
<span v-if="filterState.active?.includes(false)" class="px-1 filter-active">
<font-awesome-icon icon="check" />
</span>
</span>
</div>
</div>
</li>
</template>
</MonitorListFilterDropdown>
<MonitorListFilterDropdown :filterActive="filterState.tags?.length > 0">
<template #status>
<Tag
v-if="filterState.tags?.length === 1"
:item="tagsList.find(tag => tag.id === filterState.tags[0])"
:size="'sm'"
/>
<span v-else>
{{ $t('Tags') }}
</span>
</template>
<template #dropdown>
<li v-for="tag in tagsList" :key="tag.id">
<div class="dropdown-item" tabindex="0" @click.stop="toggleTagFilter(tag)">
<div class="d-flex align-items-center justify-content-between">
<span><Tag :item="tag" :size="'sm'" /></span>
<span class="ps-3">
{{ getTaggedMonitorCount(tag) }}
<span v-if="filterState.tags?.includes(tag.id)" class="px-1 filter-active">
<font-awesome-icon icon="check" />
</span>
</span>
</div>
</div>
</li>
</template>
</MonitorListFilterDropdown>
</div>
</template>
<script>
import MonitorListFilterDropdown from "./MonitorListFilterDropdown.vue";
import Status from "./Status.vue";
import Tag from "./Tag.vue";
export default {
components: {
MonitorListFilterDropdown,
Status,
Tag,
},
props: {
filterState: {
type: Object,
required: true,
}
},
emits: [ "updateFilter" ],
data() {
return {
tagsList: [],
};
},
computed: {
numFiltersActive() {
let num = 0;
Object.values(this.filterState).forEach(item => {
if (item != null && item.length > 0) {
num += 1;
}
});
return num;
}
},
mounted() {
this.getExistingTags();
},
methods: {
toggleStatusFilter(status) {
let newFilter = {
...this.filterState
};
if (newFilter.status == null) {
newFilter.status = [ status ];
} else {
if (newFilter.status.includes(status)) {
newFilter.status = newFilter.status.filter(item => item !== status);
} else {
newFilter.status.push(status);
}
}
this.$emit("updateFilter", newFilter);
},
toggleActiveFilter(active) {
let newFilter = {
...this.filterState
};
if (newFilter.active == null) {
newFilter.active = [ active ];
} else {
if (newFilter.active.includes(active)) {
newFilter.active = newFilter.active.filter(item => item !== active);
} else {
newFilter.active.push(active);
}
}
this.$emit("updateFilter", newFilter);
},
toggleTagFilter(tag) {
let newFilter = {
...this.filterState
};
if (newFilter.tags == null) {
newFilter.tags = [ tag.id ];
} else {
if (newFilter.tags.includes(tag.id)) {
newFilter.tags = newFilter.tags.filter(item => item !== tag.id);
} else {
newFilter.tags.push(tag.id);
}
}
this.$emit("updateFilter", newFilter);
},
clearFilters() {
this.$emit("updateFilter", {
status: null,
});
},
getExistingTags() {
this.$root.getSocket().emit("getTags", (res) => {
if (res.ok) {
this.tagsList = res.tags;
}
});
},
getTaggedMonitorCount(tag) {
return Object.values(this.$root.monitorList).filter(monitor => {
return monitor.tags.find(monitorTag => monitorTag.tag_id === tag.id);
}).length;
}
}
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.clear-filters-btn {
font-size: 0.8em;
margin-right: 5px;
display: flex;
align-items: center;
padding: 2px 10px;
border-radius: 16px;
background-color: transparent;
.dark & {
color: $dark-font-color;
border: 1px solid $dark-font-color2;
}
&.active {
border: 1px solid $highlight;
background-color: $highlight-white;
.dark & {
background-color: $dark-font-color2;
}
}
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<div class="dropdown" @focusin="open = true" @focusout="handleFocusOut">
<button type="button" class="filter-dropdown-status" :class="{ 'active': filterActive }" tabindex="0">
<div class="px-1 d-flex align-items-center">
<slot name="status"></slot>
</div>
<span class="px-1">
<font-awesome-icon icon="angle-down" />
</span>
</button>
<ul class="filter-dropdown-menu" :class="{ 'open': open }">
<slot name="dropdown"></slot>
</ul>
</div>
</template>
<script>
export default {
components: {
},
props: {
filterActive: {
type: Boolean,
required: true,
}
},
data() {
return {
open: false
};
},
methods: {
handleFocusOut(e) {
if (e.relatedTarget != null && this.$el.contains(e.relatedTarget)) {
return;
}
this.open = false;
}
}
};
</script>
<style lang="scss">
@import "../assets/vars.scss";
.filter-dropdown-menu {
z-index: 100;
transition: all 0.2s;
padding: 5px 0 !important;
border-radius: 16px;
overflow: hidden;
position: absolute;
inset: 0 auto auto 0;
margin: 0;
transform: translate(0, 36px);
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
visibility: hidden;
list-style: none;
height: 0;
opacity: 0;
background: white;
&.open {
height: unset;
visibility: inherit;
opacity: 1;
}
.dropdown-item {
padding: 5px 15px;
}
.dropdown-item:focus {
background: $highlight-white;
.dark & {
background: $dark-bg2;
}
}
.dark & {
background-color: $dark-bg;
color: $dark-font-color;
border-color: $dark-border-color;
.dropdown-item {
color: $dark-font-color;
&.active {
color: $dark-font-color2;
background-color: $highlight !important;
}
&:hover {
background-color: $dark-bg2;
}
}
}
}
.filter-dropdown-status {
display: flex;
align-items: center;
padding: 4px 10px;
margin-left: 5px;
border: 1px solid #ced4da;
border-radius: 25px;
background-color: transparent;
.dark & {
color: $dark-font-color;
border: 1px solid $dark-font-color2;
}
&.active {
border: 1px solid $highlight;
background-color: $highlight-white;
.dark & {
background-color: $dark-font-color2;
}
}
}
.filter-active {
color: $highlight;
}
</style>

View File

@@ -0,0 +1,204 @@
<template>
<div>
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info" :style="depthMargin">
<Uptime :monitor="monitor" type="24" :pill="true" />
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
</span>
{{ monitorName }}
</div>
<div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="monitor.id" />
</div>
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar size="small" :monitor-id="monitor.id" />
</div>
</div>
</router-link>
<transition name="slide-fade-up">
<div v-if="!isCollapsed" class="childs">
<MonitorListItem v-for="(item, index) in sortedChildMonitorList" :key="index" :monitor="item" :isSearch="isSearch" :depth="depth + 1" />
</div>
</transition>
</div>
</template>
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Tag from "../components/Tag.vue";
import Uptime from "../components/Uptime.vue";
import { getMonitorRelativeURL } from "../util.ts";
export default {
name: "MonitorListItem",
components: {
Uptime,
HeartbeatBar,
Tag,
},
props: {
/** Monitor this represents */
monitor: {
type: Object,
default: null,
},
/** If the user is currently searching */
isSearch: {
type: Boolean,
default: false,
},
/** How many ancestors are above this monitor */
depth: {
type: Number,
default: 0,
},
},
data() {
return {
isCollapsed: true,
};
},
computed: {
sortedChildMonitorList() {
let result = Object.values(this.$root.monitorList);
result = result.filter(childMonitor => childMonitor.parent === this.monitor.id);
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.name.localeCompare(m2.name);
});
return result;
},
hasChildren() {
return this.sortedChildMonitorList.length > 0;
},
depthMargin() {
return {
marginLeft: `${31 * this.depth}px`,
};
},
monitorName() {
if (this.isSearch) {
return this.monitor.pathName;
} else {
return this.monitor.name;
}
}
},
beforeMount() {
// Always unfold if monitor is accessed directly
if (this.monitor.childrenIDs.includes(parseInt(this.$route.params.id))) {
this.isCollapsed = false;
return;
}
// Set collapsed value based on local storage
let storage = window.localStorage.getItem("monitorCollapsed");
if (storage === null) {
return;
}
let storageObject = JSON.parse(storage);
if (storageObject[`monitor_${this.monitor.id}`] == null) {
return;
}
this.isCollapsed = storageObject[`monitor_${this.monitor.id}`];
},
methods: {
/**
* Changes the collapsed value of the current monitor and saves it to local storage
*/
changeCollapsed() {
this.isCollapsed = !this.isCollapsed;
// Save collapsed value into local storage
let storage = window.localStorage.getItem("monitorCollapsed");
let storageObject = {};
if (storage !== null) {
storageObject = JSON.parse(storage);
}
storageObject[`monitor_${this.monitor.id}`] = this.isCollapsed;
window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject));
},
/**
* Get URL of monitor
* @param {number} id ID of monitor
* @returns {string} Relative URL of monitor
*/
monitorURL(id) {
return getMonitorRelativeURL(id);
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.small-padding {
padding-left: 5px !important;
padding-right: 5px !important;
}
.collapse-padding {
padding-left: 8px !important;
padding-right: 2px !important;
}
// .monitor-item {
// width: 100%;
// }
.tags {
margin-top: 4px;
padding-left: 67px;
display: flex;
flex-wrap: wrap;
gap: 0;
}
.collapsed {
transform: rotate(-90deg);
}
.animated {
transition: all 0.2s $easing-in;
}
</style>

View File

@@ -0,0 +1,123 @@
<template>
<div ref="MonitorSettingDialog" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("Monitor Setting", [monitor.name]) }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="my-3 form-check">
<input id="show-clickable-link" v-model="monitor.isClickAble" class="form-check-input" type="checkbox" @click="toggleLink(monitor.group_index, monitor.monitor_index)" />
<label class="form-check-label" for="show-clickable-link">
{{ $t("Show Clickable Link") }}
</label>
<div class="form-text">
{{ $t("Show Clickable Link Description") }}
</div>
</div>
<button
class="btn btn-primary btn-add-group me-2"
@click="$refs.badgeGeneratorDialog.show(monitor.id, monitor.name)"
>
<font-awesome-icon icon="certificate" />
{{ $t("Open Badge Generator") }}
</button>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">
{{ $t("Close") }}
</button>
</div>
</div>
</div>
</div>
<BadgeGeneratorDialog ref="badgeGeneratorDialog" />
</template>
<script lang="ts">
import { Modal } from "bootstrap";
import BadgeGeneratorDialog from "./BadgeGeneratorDialog.vue";
export default {
components: {
BadgeGeneratorDialog
},
props: {},
emits: [],
data() {
return {
monitor: {
id: null,
name: null,
},
};
},
computed: {},
mounted() {
this.MonitorSettingDialog = new Modal(this.$refs.MonitorSettingDialog);
},
methods: {
/**
* Setting monitor
* @param {Object} group Data of monitor
* @param {Object} monitor Data of monitor
*/
show(group, monitor) {
this.monitor = {
id: monitor.element.id,
name: monitor.element.name,
monitor_index: monitor.index,
group_index: group.index,
isClickAble: this.showLink(monitor),
};
this.MonitorSettingDialog.show();
},
/**
* Toggle the value of sendUrl
* @param {number} groupIndex Index of group monitor is member of
* @param {number} index Index of monitor within group
*/
toggleLink(groupIndex, index) {
this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl = !this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl;
},
/**
* Should a link to the monitor be shown?
* Attempts to guess if a link should be shown based upon if
* sendUrl is set and if the URL is default or not.
* @param {Object} monitor Monitor to check
* @param {boolean} [ignoreSendUrl=false] Should the presence of the sendUrl
* property be ignored. This will only work in edit mode.
* @returns {boolean}
*/
showLink(monitor, ignoreSendUrl = false) {
// We must check if there are any elements in monitorList to
// prevent undefined errors if it hasn't been loaded yet
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
}
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dark {
.modal-dialog .form-text, .modal-dialog p {
color: $dark-font-color;
}
}
</style>

View File

@@ -131,6 +131,7 @@ export default {
"OneBot": "OneBot",
"Opsgenie": "Opsgenie",
"PagerDuty": "PagerDuty",
"PagerTree": "PagerTree",
"pushbullet": "Pushbullet",
"PushByTechulus": "Push by Techulus",
"pushover": "Pushover",
@@ -163,6 +164,7 @@ export default {
"SMSManager": "SmsManager (smsmanager.cz)",
"WeCom": "WeCom (企业微信群机器人)",
"ServerChan": "ServerChan (Server酱)",
"smsc": "SMSC",
};
// Sort by notification name

View File

@@ -11,16 +11,16 @@
</ul>
</div>
<div class="chart-wrapper" :class="{ loading : loading}">
<LineChart :chart-data="chartData" :options="chartOptions" />
<Line :data="chartData" :options="chartOptions" />
</div>
</div>
</template>
<script lang="js">
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import "chartjs-adapter-dayjs";
import "chartjs-adapter-dayjs-4";
import dayjs from "dayjs";
import { LineChart } from "vue-chart-3";
import { Line } from "vue-chartjs";
import { useToast } from "vue-toastification";
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
@@ -29,7 +29,7 @@ const toast = useToast();
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
export default {
components: { LineChart },
components: { Line },
props: {
/** ID of monitor */
monitorId: {
@@ -104,8 +104,10 @@ export default {
}
},
ticks: {
sampleSize: 3,
maxRotation: 0,
autoSkipPadding: 30,
padding: 3,
},
grid: {
color: this.$root.theme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)",
@@ -197,6 +199,7 @@ export default {
borderColor: "#5CDD8B",
backgroundColor: "#5CDD8B38",
yAxisID: "y",
label: "ping",
},
{
// Bar Chart
@@ -208,6 +211,8 @@ export default {
barThickness: "flex",
barPercentage: 1,
categoryPercentage: 1,
inflateAmount: 0.05,
label: "status",
},
],
};

View File

@@ -1,102 +0,0 @@
<template>
<div v-if="! (!plugin.installed && plugin.local)" class="plugin-item pt-4 pb-2">
<div class="info">
<h5>{{ plugin.fullName }}</h5>
<p class="description">
{{ plugin.description }}
</p>
<span class="version">{{ $t("Version") }}: {{ plugin.version }} <a v-if="plugin.repo" :href="plugin.repo" target="_blank">Repo</a></span>
</div>
<div class="buttons">
<button v-if="status === 'installing'" class="btn btn-primary" disabled>{{ $t("installing") }}</button>
<button v-else-if="status === 'uninstalling'" class="btn btn-danger" disabled>{{ $t("uninstalling") }}</button>
<button v-else-if="plugin.installed || status === 'installed'" class="btn btn-danger" @click="deleteConfirm">{{ $t("uninstall") }}</button>
<button v-else class="btn btn-primary" @click="install">{{ $t("install") }}</button>
</div>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="uninstall">
{{ $t("confirmUninstallPlugin") }}
</Confirm>
</div>
</template>
<script>
import Confirm from "./Confirm.vue";
export default {
components: {
Confirm,
},
props: {
plugin: {
type: Object,
required: true,
},
},
data() {
return {
status: "",
};
},
methods: {
/**
* Show confirmation for deleting a tag
*/
deleteConfirm() {
this.$refs.confirmDelete.show();
},
install() {
this.status = "installing";
this.$root.getSocket().emit("installPlugin", this.plugin.repo, this.plugin.name, (res) => {
if (res.ok) {
this.status = "";
// eslint-disable-next-line vue/no-mutating-props
this.plugin.installed = true;
} else {
this.$root.toastRes(res);
}
});
},
uninstall() {
this.status = "uninstalling";
this.$root.getSocket().emit("uninstallPlugin", this.plugin.name, (res) => {
if (res.ok) {
this.status = "";
// eslint-disable-next-line vue/no-mutating-props
this.plugin.installed = false;
} else {
this.$root.toastRes(res);
}
});
}
}
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.plugin-item {
display: flex;
justify-content: space-between;
align-content: center;
align-items: center;
.info {
margin-right: 10px;
}
.description {
font-size: 13px;
margin-bottom: 0;
}
.version {
font-size: 13px;
}
}
</style>

View File

@@ -49,16 +49,15 @@
{{ monitor.element.name }}
</a>
<p v-else class="item-name"> {{ monitor.element.name }} </p>
<span
v-if="showLink(monitor, true)"
title="Toggle Clickable Link"
title="Setting"
>
<font-awesome-icon
v-if="editMode"
:class="{'link-active': monitor.element.sendUrl, 'btn-link': true}"
icon="link" class="action me-3"
@click="toggleLink(group.index, monitor.index)"
:class="{'link-active': true, 'btn-link': true}"
icon="cog" class="action me-3"
@click="$refs.monitorSettingDialog.show(group, monitor)"
/>
</span>
</div>
@@ -77,9 +76,11 @@
</div>
</template>
</Draggable>
<MonitorSettingDialog ref="monitorSettingDialog" />
</template>
<script>
import MonitorSettingDialog from "./MonitorSettingDialog.vue";
import Draggable from "vuedraggable";
import HeartbeatBar from "./HeartbeatBar.vue";
import Uptime from "./Uptime.vue";
@@ -87,6 +88,7 @@ import Tag from "./Tag.vue";
export default {
components: {
MonitorSettingDialog,
Draggable,
HeartbeatBar,
Uptime,
@@ -135,15 +137,6 @@ export default {
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
},
/**
* Toggle the value of sendUrl
* @param {number} groupIndex Index of group monitor is member of
* @param {number} index Index of monitor within group
*/
toggleLink(groupIndex, index) {
this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl = !this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl;
},
/**
* Should a link to the monitor be shown?
* Attempts to guess if a link should be shown based upon if
@@ -157,7 +150,7 @@ export default {
// We must check if there are any elements in monitorList to
// prevent undefined errors if it hasn't been loaded yet
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword";
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
}
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
},

View File

@@ -6,7 +6,7 @@
'm-2': size == 'normal',
'px-2': size == 'sm',
'py-0': size == 'sm',
'm-1': size == 'sm',
'mx-1': size == 'sm',
}"
:style="{ backgroundColor: item.color, fontSize: size == 'sm' ? '0.7em' : '1em' }"
>

View File

@@ -76,17 +76,30 @@
</button>
</router-link>
</div>
<div v-if="allMonitorList.length > 0" class="pt-3 px-3">
<div v-if="allMonitorList.length > 0" class="pt-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>
<VueMultiselect
v-model="selectedAddMonitor"
:options="allMonitorList"
:multiple="false"
:searchable="true"
:placeholder="$t('Add a monitor')"
label="name"
trackBy="name"
class="mt-1"
>
<template #option="{ option }">
<div class="d-inline-flex">
<span>{{ option.name }} <Tag v-for="monitorTag in option.tags" :key="monitorTag" :item="monitorTag" :size="'sm'" /></span>
</div>
</template>
</VueMultiselect>
</div>
</div>
</div>
<div class="modal-footer">
<button v-if="tag" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
<button v-if="tag && tag.id !== null" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
{{ $t("Delete") }}
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
@@ -107,6 +120,7 @@
<script>
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
import Tag from "./Tag.vue";
import VueMultiselect from "vue-multiselect";
import { colorOptions } from "../util-frontend";
import { useToast } from "vue-toastification";
@@ -117,6 +131,7 @@ export default {
components: {
VueMultiselect,
Confirm,
Tag,
},
props: {
updated: {

View File

@@ -16,17 +16,29 @@
<input id="ntfy-priority" v-model="$parent.notification.ntfyPriority" type="number" class="form-control" required min="1" max="5" step="1">
</div>
<div class="mb-3">
<label for="ntfy-username" class="form-label">{{ $t("Username") }} ({{ $t("Optional") }})</label>
<label for="authentication-method" class="form-label">{{ $t("ntfyAuthenticationMethod") }}</label>
<select id="authentication-method" v-model="$parent.notification.ntfyAuthenticationMethod" class="form-select">
<option v-for="(name, type) in authenticationMethods" :key="type" :value="type">{{ name }}</option>
</select>
</div>
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
<label for="ntfy-username" class="form-label">{{ $t("Username") }}</label>
<div class="input-group mb-3">
<input id="ntfy-username" v-model="$parent.notification.ntfyusername" type="text" class="form-control">
</div>
</div>
<div class="mb-3">
<label for="ntfy-password" class="form-label">{{ $t("Password") }} ({{ $t("Optional") }})</label>
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'usernamePassword'" class="mb-3">
<label for="ntfy-password" class="form-label">{{ $t("Password") }}</label>
<div class="input-group mb-3">
<HiddenInput id="ntfy-password" v-model="$parent.notification.ntfypassword" autocomplete="new-password"></HiddenInput>
</div>
</div>
<div v-if="$parent.notification.ntfyAuthenticationMethod === 'accessToken'" class="mb-3">
<label for="ntfy-access-token" class="form-label">{{ $t("Access Token") }}</label>
<div class="input-group mb-3">
<HiddenInput id="ntfy-access-token" v-model="$parent.notification.ntfyaccesstoken"></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">
@@ -40,11 +52,29 @@ export default {
components: {
HiddenInput,
},
computed: {
authenticationMethods() {
return {
none: this.$t("None"),
usernamePassword: this.$t("ntfyUsernameAndPassword"),
accessToken: this.$t("Access Token")
};
}
},
mounted() {
if (typeof this.$parent.notification.ntfyPriority === "undefined") {
this.$parent.notification.ntfyserverurl = "https://ntfy.sh";
this.$parent.notification.ntfyPriority = 5;
}
// Handling notifications that added before 1.22.0
if (typeof this.$parent.notification.ntfyAuthenticationMethod === "undefined") {
if (!this.$parent.notification.ntfyusername) {
this.$parent.notification.ntfyAuthenticationMethod = "none";
} else {
this.$parent.notification.ntfyAuthenticationMethod = "usernamePassword";
}
}
},
};
</script>

View File

@@ -42,6 +42,8 @@
<option value="vibrate">{{ $t("pushoversounds vibrate") }}</option>
<option value="none">{{ $t("pushoversounds none") }}</option>
</select>
<label for="pushover-ttl" class="form-label">{{ $t("pushoverMessageTtl") }}</label>
<input id="pushover-ttl" v-model="$parent.notification.pushoverttl" type="number" min="0" step="1" class="form-control">
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">

View File

@@ -0,0 +1,43 @@
<template>
<div class="mb-3">
<label for="smsc-login" class="form-label">{{ $t("API Username") }}</label>
<i18n-t tag="div" class="form-text" keypath="wayToGetClickSendSMSToken">
<a href="https://smsc.kz/" target="_blank">{{ $t("here") }}</a>
</i18n-t>
<input id="smsc-login" v-model="$parent.notification.smscLogin" type="text" class="form-control" required>
<label for="smsc-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="smsc-key" v-model="$parent.notification.smscPassword" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<div class="form-text">
{{ $t("checkPrice", ['СМСЦ']) }}
<a href="https://smsc.kz/tariffs/" target="_blank">https://smsc.kz/tariffs/</a>
</div>
</div>
<div class="mb-3">
<label for="smsc-to-number" class="form-label">{{ $t("Recipient Number") }}</label>
<input id="smsc-to-number" v-model="$parent.notification.smscToNumber" type="text" minlength="11" class="form-control" required>
</div>
<div class="mb-3">
<label for="smsc-sender-name" class="form-label">{{ $t("From Name/Number") }}</label>
<input id="smsc-sender-name" v-model="$parent.notification.smscSenderName" type="text" minlength="1" maxlength="15" class="form-control">
<div class="form-text">{{ $t("Leave blank to use a shared sender number.") }}</div>
</div>
<div class="mb-3">
<label for="smsc-platform" class="form-label">{{ $t("smscTranslit") }}</label><span style="color: red;"><sup>*</sup></span>
<select id="smsc-platform" v-model="$parent.notification.smscTranslit" class="form-select">
<option value="0">{{ $t("Default") }}</option>
<option value="1">Translit</option>
<option value="2">MpaHc/Ium</option>
</select>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View File

@@ -24,5 +24,13 @@
<a href="https://www.webfx.com/tools/emoji-cheat-sheet/" target="_blank">https://www.webfx.com/tools/emoji-cheat-sheet/</a>
</i18n-t>
</div>
<div class="form-check form-switch">
<input id="slack-channel-notify" v-model="$parent.notification.slackchannelnotify" type="checkbox" class="form-check-input">
<label for="slack-channel-notify" class="form-label">{{ $t("Notify Channel") }}</label>
</div>
<div class="form-text">
{{ $t("aboutNotifyChannel") }}
</div>
</div>
</template>

View File

@@ -5,7 +5,18 @@
</div>
<div class="mb-3">
<label for="twilio-auth-token" class="form-label">{{ $t("Auth Token") }}</label>
<label for="twilio-apikey-token" class="form-label">{{ $t("Api Key (optional)") }}</label>
<input id="twilio-apikey-token" v-model="$parent.notification.twilioApiKey" type="text" class="form-control">
<div class="form-text">
<p>
The API key is optional but recommended. You can provide either Account SID and AuthToken
from the may TwilioConsole page or Account SID and the pair of Api Key and Api Key secret
</p>
</div>
</div>
<div class="mb-3">
<label for="twilio-auth-token" class="form-label">{{ $t("Auth Token / Api Key Secret") }}</label>
<input id="twilio-auth-token" v-model="$parent.notification.twilioAuthToken" type="text" class="form-control" required>
</div>

View File

@@ -12,61 +12,97 @@
</div>
<div class="mb-3">
<label for="webhook-content-type" class="form-label">{{
$t("Content Type")
<label for="webhook-request-body" class="form-label">{{
$t("Request Body")
}}</label>
<select
id="webhook-content-type"
id="webhook-request-body"
v-model="$parent.notification.webhookContentType"
class="form-select"
required
>
<option value="json">application/json</option>
<option value="form-data">multipart/form-data</option>
<option value="json">{{ $t("webhookBodyPresetOption", ["application/json"]) }}</option>
<option value="form-data">{{ $t("webhookBodyPresetOption", ["multipart/form-data"]) }}</option>
<option value="custom">{{ $t("webhookBodyCustomOption") }}</option>
</select>
<div class="form-text">
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
<i18n-t tag="p" keypath="webhookFormDataDesc">
<template #multipart>"multipart/form-data"</template>
<template #decodeFunction>
<strong>json_decode($_POST['data'])</strong>
</template>
</i18n-t>
<div v-if="$parent.notification.webhookContentType == 'json'">
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
</div>
<div v-if="$parent.notification.webhookContentType == 'form-data'">
<i18n-t tag="p" keypath="webhookFormDataDesc">
<template #multipart>multipart/form-data"</template>
<template #decodeFunction>
<strong>json_decode($_POST['data'])</strong>
</template>
</i18n-t>
</div>
<div v-if="$parent.notification.webhookContentType == 'custom'">
<i18n-t tag="p" keypath="webhookCustomBodyDesc">
<template #msg>
<code>msg</code>
</template>
<template #heartbeat>
<code>heartbeatJSON</code>
</template>
<template #monitor>
<code>monitorJSON</code>
</template>
</i18n-t>
</div>
</div>
<textarea
v-if="$parent.notification.webhookContentType == 'custom'"
id="customBody"
v-model="$parent.notification.webhookCustomBody"
class="form-control"
:placeholder="customBodyPlaceholder"
></textarea>
</div>
<div class="mb-3">
<i18n-t
tag="label"
class="form-label"
for="additionalHeaders"
keypath="webhookAdditionalHeadersTitle"
>
</i18n-t>
<div class="form-check form-switch">
<input v-model="showAdditionalHeadersField" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("webhookAdditionalHeadersTitle") }}</label>
</div>
<div class="form-text">
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
</div>
<textarea
v-if="showAdditionalHeadersField"
id="additionalHeaders"
v-model="$parent.notification.webhookAdditionalHeaders"
class="form-control"
:placeholder="headersPlaceholder"
></textarea>
<div class="form-text">
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showAdditionalHeadersField: this.$parent.notification.webhookAdditionalHeaders != null,
};
},
computed: {
headersPlaceholder() {
return this.$t("Example:", [
`
{
"HeaderName": "HeaderValue"
"Authorization": "Authorization Token"
}`,
]);
},
customBodyPlaceholder() {
return `Example:
{
"Title": "Uptime Kuma Alert - {{ monitorJSON['name'] }}",
"Body": "{{ msg }}"
}`;
}
},
};
</script>

View File

@@ -4,6 +4,7 @@ import AliyunSMS from "./AliyunSms.vue";
import Apprise from "./Apprise.vue";
import Bark from "./Bark.vue";
import ClickSendSMS from "./ClickSendSMS.vue";
import SMSC from "./SMSC.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
import Feishu from "./Feishu.vue";
@@ -61,6 +62,7 @@ const NotificationFormList = {
"apprise": Apprise,
"Bark": Bark,
"clicksendsms": ClickSendSMS,
"smsc": SMSC,
"DingDing": DingDing,
"discord": Discord,
"Feishu": Feishu,

View File

@@ -190,6 +190,30 @@
</div>
</div>
<!-- Chrome Executable -->
<div class="mb-4">
<label class="form-label" for="primaryBaseURL">
{{ $t("chromeExecutable") }}
</label>
<div class="input-group mb-3">
<input
id="primaryBaseURL"
v-model="settings.chromeExecutable"
class="form-control"
name="primaryBaseURL"
:placeholder="$t('chromeExecutableAutoDetect')"
/>
<button class="btn btn-outline-primary" type="button" @click="testChrome">
{{ $t("Test") }}
</button>
</div>
<div class="form-text">
{{ $t("chromeExecutableDescription") }}
</div>
</div>
<!-- Save Button -->
<div>
<button class="btn btn-primary" type="submit">
@@ -241,6 +265,12 @@ export default {
autoGetPrimaryBaseURL() {
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
},
testChrome() {
this.$root.getSocket().emit("testChrome", this.settings.chromeExecutable, (res) => {
this.$root.toastRes(res);
});
},
},
};
</script>

View File

@@ -1,57 +0,0 @@
<template>
<div>
<div class="mt-3">{{ remotePluginListMsg }}</div>
<PluginItem v-for="plugin in remotePluginList" :key="plugin.id" :plugin="plugin" />
</div>
</template>
<script>
import PluginItem from "../PluginItem.vue";
export default {
components: {
PluginItem
},
data() {
return {
remotePluginList: [],
remotePluginListMsg: "",
};
},
computed: {
pluginList() {
return this.$parent.$parent.$parent.pluginList;
},
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
},
async mounted() {
this.loadList();
},
methods: {
loadList() {
this.remotePluginListMsg = this.$t("Loading") + "...";
this.$root.getSocket().emit("getPluginList", (res) => {
if (res.ok) {
this.remotePluginList = res.pluginList;
this.remotePluginListMsg = "";
} else {
this.remotePluginListMsg = this.$t("loadingError") + " " + res.msg;
}
});
}
},
};
</script>

View File

@@ -1,21 +1,18 @@
<template>
<div class="my-4">
<div class="mx-4 pt-1 my-3">
<div class="mx-0 mx-lg-4 pt-1 mb-4">
<button class="btn btn-primary" @click.stop="addTag"><font-awesome-icon icon="plus" /> {{ $t("Add New Tag") }}</button>
</div>
<div class="tags-list my-3">
<div v-for="(tag, index) in tagsList" :key="tag.id" class="d-flex align-items-center mx-4 py-1 tags-list-row" :disabled="processing" @click="editTag(index)">
<div class="col-5 ps-1">
<div v-for="(tag, index) in tagsList" :key="tag.id" class="d-flex align-items-center mx-0 mx-lg-4 py-1 tags-list-row" :disabled="processing" @click="editTag(index)">
<div class="col-10 col-sm-5">
<Tag :item="tag" />
</div>
<div class="col-5 px-1">
<div class="col-5 px-1 d-none d-sm-block">
<div>{{ monitorsByTag(tag.id).length }} {{ $tc("Monitor", monitorsByTag(tag.id).length) }}</div>
</div>
<div class="col-2 pe-3 d-flex justify-content-end">
<button type="button" class="btn ms-2 py-1">
<font-awesome-icon class="" icon="edit" />
</button>
<div class="col-2 pe-2 pe-lg-3 d-flex justify-content-end">
<button type="button" class="btn-rm-tag btn btn-outline-danger ms-2 py-1" :disabled="processing" @click.stop="deleteConfirm(index)">
<font-awesome-icon class="" icon="trash" />
</button>
@@ -156,8 +153,8 @@ export default {
@import "../../assets/vars.scss";
.btn-rm-tag {
padding-left: 11px;
padding-right: 11px;
padding-left: 9px;
padding-right: 9px;
}
.tags-list .tags-list-row {

View File

@@ -49,6 +49,7 @@ import {
faFilter,
faInfoCircle,
faClone,
faCertificate,
} from "@fortawesome/free-solid-svg-icons";
library.add(
@@ -95,6 +96,7 @@ library.add(
faFilter,
faInfoCircle,
faClone,
faCertificate,
);
export { FontAwesomeIcon };

View File

@@ -683,6 +683,6 @@
"backupDescription2": "ملحوظة",
"languageName": "العربية",
"Game": "الألعاب",
"List": "قائمة",
"List": "القائمة",
"statusMaintenance": "الصيانة"
}

View File

@@ -178,7 +178,7 @@
"Degraded Service": "Всички услуги са недостъпни",
"Add Group": "Добави група",
"Add a monitor": "Добави монитор",
"Edit Status Page": "Редактиране Статус страница",
"Edit Status Page": "Редактиране на статус страницата",
"Go to Dashboard": "Към Таблото",
"telegram": "Telegram",
"webhook": "Уеб кука",
@@ -200,7 +200,7 @@
"mattermost": "Mattermost",
"Status Page": "Статус страница",
"Status Pages": "Статус страници",
"Primary Base URL": "Основен базов URL адрес",
"Primary Base URL": "Базов URL адрес",
"Push URL": "Генериран Push URL адрес",
"needPushEvery": "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди.",
"pushOptionalParams": "Допълнителни, но не задължителни параметри: {0}",
@@ -591,7 +591,7 @@
"All Status Pages": "Всички статус страници",
"Select status pages...": "Изберете статус страници…",
"recurringIntervalMessage": "Изпълнявай ежедневно | Изпълнявай всеки {0} дни",
"affectedMonitorsDescription": "Изберете монитори, засегнати от текущата поддръжка",
"affectedMonitorsDescription": "Изберете монитори, попадащи в обсега на текущата поддръжка",
"affectedStatusPages": "Покажи това съобщение за поддръжка на избрани статус страници",
"atLeastOneMonitor": "Изберете поне един засегнат монитор",
"deleteMaintenanceMsg": "Сигурни ли сте, че желаете да изтриете тази поддръжка?",
@@ -652,7 +652,7 @@
"dnsCacheDescription": "Възможно е да не работи в IPv6 среда - деактивирайте, ако срещнете проблеми.",
"Single Maintenance Window": "Единичен времеви интервал за поддръжка",
"Maintenance Time Window of a Day": "Времеви интервал от деня за поддръжка",
"Effective Date Range": "Интервал от дни на влизане в сила",
"Effective Date Range": "Ефективен интервал от дни (по желание)",
"Schedule Maintenance": "Планирай поддръжка",
"Date and Time": "Дата и час",
"DateTime Range": "Изтрий времеви интервал",
@@ -707,7 +707,7 @@
"telegramSendSilently": "Изпрати тихо",
"Clone Monitor": "Клониране на монитор",
"Clone": "Клонирай",
"cloneOf": "Клонинг на {0}",
"cloneOf": "Клониран {0}",
"Expiry": "Валиден до",
"Expiry date": "Дата на изтичане",
"Add Another": "Добави друг",
@@ -738,5 +738,51 @@
"Add New Tag": "Добави нов етикет",
"lunaseaTarget": "Цел",
"lunaseaDeviceID": "ID на устройството",
"lunaseaUserID": "ID на потребител"
"lunaseaUserID": "ID на потребител",
"twilioAccountSID": "Профил SID",
"twilioAuthToken": "Удостоверяващ токен",
"twilioFromNumber": "От номер",
"twilioToNumber": "Към номер",
"sameAsServerTimezone": "Kато часовата зона на сървъра",
"startDateTime": "Старт Дата/Час",
"endDateTime": "Край Дата/Час",
"cronSchedule": "График: ",
"invalidCronExpression": "Невалиден \"Cron\" израз: {0}",
"cronExpression": "Израз тип \"Cron\"",
"statusPageRefreshIn": "Обновяване след: {0}",
"ntfyUsernameAndPassword": "Потребителско име и парола",
"ntfyAuthenticationMethod": "Метод за удостоверяване",
"pushoverMessageTtl": "TTL на съобщението (секунди)",
"Open Badge Generator": "Отвори генератора на баджове",
"Badge Generator": "Генератор на баджове на {0}",
"Badge Type": "Тип бадж",
"Badge Duration": "Продължителност на баджа",
"Badge Prefix": "Префикс на баджа",
"Badge Label Color": "Цвят на етикета на баджа",
"Badge Color": "Цвят на баджа",
"Badge Label Suffix": "Суфикс на етикета на значката",
"Badge Up Color": "Цвят на баджа за достъпен",
"Badge Down Color": "Цвят на баджа за недостъпен",
"Badge Maintenance Color": "Цвят на баджа за поддръжка",
"Badge Warn Color": "Цвят на баджа за предупреждение",
"Badge Warn Days": "Дни за показване на баджа",
"Badge Style": "Стил на баджа",
"Badge value (For Testing only.)": "Стойност на баджа (само за тест.)",
"Badge URL": "URL адрес на баджа",
"Monitor Setting": "Настройка на монитор {0}",
"Show Clickable Link": "Покажи връзка, която може да се кликне",
"Show Clickable Link Description": "Ако е отбелязано, всеки който има достъп до тази статус страница, ще може да достъпва URL адреса на монитора.",
"Badge Label": "Етикет на баджа",
"Badge Suffix": "Суфикс на баджа",
"Badge Label Prefix": "Префикс на етикета на значката",
"Badge Pending Color": "Цвят на баджа за изчакващ",
"Badge Down Days": "Колко дни баджът да не се показва",
"Group": "Група",
"Monitor Group": "Монитор група",
"Cannot connect to the socket server": "Не може да се свърже със сокет сървъра",
"Reconnecting...": "Повторно свързване...",
"Edit Maintenance": "Редактиране на поддръжка",
"Home": "Главна страница",
"noGroupMonitorMsg": "Не е налично. Първо създайте групов монитор.",
"Close": "Затвори"
}

28
src/lang/ca.json Normal file
View File

@@ -0,0 +1,28 @@
{
"Settings": "Paràmetres",
"Dashboard": "Tauler",
"Help": "Ajuda",
"New Update": "Nova actualització",
"Language": "Idioma",
"Appearance": "Aparença",
"Theme": "Tema",
"General": "General",
"Game": "Joc",
"Version": "Versió",
"Check Update On GitHub": "Comprovar actualitzacions a GitHub",
"List": "Llista",
"Home": "Inici",
"Add": "Afegir",
"Add New Monitor": "Afegir nou monitor",
"Quick Stats": "Estadístiques ràpides",
"Up": "Funcional",
"Down": "Caigut",
"Pending": "Pendent",
"Maintenance": "Manteniment",
"Unknown": "Desconegut",
"Cannot connect to the socket server": "No es pot connectar al servidor socket",
"Reconnecting...": "S'està tornant a connectar...",
"languageName": "Català",
"Primary Base URL": "URL Base Primària",
"statusMaintenance": "Manteniment"
}

46
src/lang/ckb.json Normal file
View File

@@ -0,0 +1,46 @@
{
"languageName": "کوردی",
"Settings": "ڕێکخستنەکان",
"Help": "یارمەتی",
"New Update": "وەشانی نوێ",
"Language": "زمان",
"Appearance": "ڕووکار",
"Theme": "شێوەی ڕووکار",
"General": "گشتی",
"Game": "یاری",
"Version": "وەشان",
"Check Update On GitHub": "سەیری وەشانی نوێ بکە لە Github",
"List": "لیست",
"Add": "زیادکردن",
"Quick Stats": "ئاماری خێرا",
"Up": "سەروو",
"Down": "خواروو",
"Pending": "هەڵپەسێردراو",
"statusMaintenance": "چاکردنەوە",
"Maintenance": "چاکردنەوە",
"Unknown": "نەزانراو",
"Passive Monitor Type": "جۆری مۆنیتەری پاسیڤ",
"Specific Monitor Type": "جۆری مۆنیتەری تایبەت",
"markdownSupported": "ڕستەسازی مارکداون پشتگیری دەکرێت",
"pauseDashboardHome": "وچان",
"Pause": "وچان",
"Name": "ناو",
"Status": "دۆخ",
"Message": "پەیام",
"No important events": "هیچ ڕووداوێکی گرنگ نییە",
"Resume": "‬دەستپێکردنەوە",
"Edit": "بژارکردن",
"Delete": "سڕینەوە",
"Uptime": "کاتی کارکردن",
"Cert Exp.": "بەسەرچوونی بڕوانامەی SSL.",
"day": "ڕۆژ | ڕۆژەکان",
"-day": "-ڕۆژ",
"hour": "کاتژمێر",
"Dashboard": "داشبۆرد",
"Primary Base URL": "بەستەری بنچینەیی سەرەکی",
"Add New Monitor": "مۆنیتەرێکی نوێ زیاد بکە",
"General Monitor Type": "جۆری مۆنیتەری گشتی",
"DateTime": "رێکەوت",
"Current": "هەنووکە",
"Monitor": "مۆنیتەر | مۆنیتەرەکان"
}

View File

@@ -1,5 +1,5 @@
{
"languageName": "Czech",
"languageName": "Čeština",
"checkEverySecond": "Kontrolovat každých {0} sekund",
"retryCheckEverySecond": "Opakovat každých {0} sekund",
"resendEveryXTimes": "Znovu zaslat {0}krát",
@@ -134,7 +134,7 @@
"Remember me": "Zapamatovat si mě",
"Login": "Přihlášení",
"No Monitors, please": "Žádné dohledy, prosím",
"add one": "přidat jeden",
"add one": "začněte přidáním nového",
"Notification Type": "Typ oznámení",
"Email": "E-mail",
"Test": "Test",
@@ -518,7 +518,7 @@
"PushDeer Key": "PushDeer klíč",
"Footer Text": "Text v patičce",
"Show Powered By": "Zobrazit \"Poskytuje\"",
"Domain Names": "Názvy domén",
"Domain Names": "Doménová jména",
"signedInDisp": "Přihlášen jako {0}",
"signedInDispDisabled": "Ověření je vypnuté.",
"RadiusSecret": "Tajemství Radius",
@@ -542,11 +542,11 @@
"promosmsPassword": "API Password",
"pushoversounds pushover": "Pushover (výchozí)",
"pushoversounds bike": "Kolo",
"pushoversounds bugle": "Bugle",
"pushoversounds bugle": "Trumpeta",
"pushoversounds cashregister": "Pokladna",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Kosmický",
"pushoversounds falling": "Falling",
"pushoversounds falling": "Padající",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Příchozí",
"pushoversounds intermission": "Přestávka",
@@ -554,9 +554,9 @@
"pushoversounds mechanical": "Mechanika",
"pushoversounds pianobar": "Barové piano",
"pushoversounds siren": "Siréna",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (dlouhý)",
"pushoversounds spacealarm": "Vesmírný alarm",
"pushoversounds tugboat": "Remorkér",
"pushoversounds alien": "Mimozemský poplach (dlouhý)",
"pushoversounds climb": "Climb (dlouhý)",
"pushoversounds persistent": "Persistent (dlouhý)",
"pushoversounds echo": "Pushover Echo (dlouhý)",
@@ -661,7 +661,7 @@
"dnsCacheDescription": "V některých IPv6 prostředích nemusí fungovat. Pokud narazíte na nějaké problémy, vypněte jej.",
"Single Maintenance Window": "Konkrétní časové okno pro údržbu",
"Maintenance Time Window of a Day": "Časové okno pro údržbu v daný den",
"Effective Date Range": "Časové období",
"Effective Date Range": "Časové období (volitelné)",
"Schedule Maintenance": "Naplánovat údržbu",
"Date and Time": "Datum a čas",
"DateTime Range": "Rozsah data a času",
@@ -669,7 +669,7 @@
"Free Mobile User Identifier": "Identifikátor uživatele Free Mobile",
"Free Mobile API Key": "API klíč Free Mobile",
"Enable TLS": "Povolit TLS",
"Proto Service Name": "Proto Service Name",
"Proto Service Name": "Jméno Proto Service",
"Proto Method": "Proto metoda",
"Proto Content": "Proto obsah",
"Economy": "Úsporná",
@@ -705,9 +705,9 @@
"telegramProtectContent": "Ochrana přeposílání/ukládání",
"telegramSendSilently": "Odeslat potichu",
"telegramSendSilentlyDescription": "Zprávu odešle tiše. Uživatelé obdrží oznámení bez zvuku.",
"Clone": "Klonovat",
"cloneOf": "Klonovat {0}",
"Clone Monitor": "Klonovat dohled",
"Clone": "Duplikovat",
"cloneOf": "Kopie {0}",
"Clone Monitor": "Duplikovat dohled",
"API Keys": "API klíče",
"Expiry": "Platnost",
"Don't expire": "Nevyprší",
@@ -739,5 +739,50 @@
"lunaseaTarget": "Cíl",
"lunaseaDeviceID": "ID zařízení",
"lunaseaUserID": "ID uživatele",
"statusPageRefreshIn": "Obnovení za: {0}"
"statusPageRefreshIn": "Obnovení za: {0}",
"twilioAccountSID": "SID účtu",
"twilioFromNumber": "Číslo odesílatele",
"twilioToNumber": "Číslo příjemce",
"twilioAuthToken": "Autorizační token",
"sameAsServerTimezone": "Stejné jako časové pásmo serveru",
"cronExpression": "Cron výraz",
"cronSchedule": "Plán: ",
"invalidCronExpression": "Neplatný cron výraz: {0}",
"startDateTime": "Počáteční datum/čas",
"endDateTime": "Datum/čas konce",
"ntfyAuthenticationMethod": "Způsob ověření",
"ntfyUsernameAndPassword": "Uživatelské jméno a heslo",
"pushoverMessageTtl": "Zpráva TTL (Sekund)",
"Show Clickable Link": "Zobrazit klikatelný odkaz",
"Show Clickable Link Description": "Pokud je zaškrtnuto, všichni, kdo mají přístup k této stavové stránce, mají přístup k adrese URL monitoru.",
"Open Badge Generator": "Otevřít generátor odznaků",
"Badge Type": "Typ odznaku",
"Badge Duration": "Platnost odznaku",
"Badge Label": "Štítek odznaku",
"Badge Prefix": "Prefix odznaku",
"Monitor Setting": "{0}'s Nastavení dohledu",
"Badge Generator": "Generátor odznaků pro {0}",
"Badge Label Color": "Barva štítku odznaku",
"Badge Color": "Barva odznaku",
"Badge Style": "Styl odznaku",
"Badge Label Suffix": "Přípona štítku odznaku",
"Badge URL": "URL odznaku",
"Badge Suffix": "Přípona odznaku",
"Badge Label Prefix": "Prefix štítku odznaku",
"Badge Up Color": "Barva odznaku při Běží",
"Badge Down Color": "Barva odznaku při Nedostupné",
"Badge Pending Color": "Barva odznaku při Pauze",
"Badge Maintenance Color": "Barva odznaku při Údržbě",
"Badge Warn Color": "Barva odznaku při Upozornění",
"Reconnecting...": "Obnovení spojení...",
"Cannot connect to the socket server": "Nelze se připojit k soketovému serveru",
"Edit Maintenance": "Upravit Údržbu",
"Home": "Hlavní stránka",
"Badge Down Days": "Odznak nedostupných dní",
"Group": "Skupina",
"Monitor Group": "Sledovaná skupina",
"noGroupMonitorMsg": "Není k dispozici. Nejprve vytvořte skupin dohledů.",
"Close": "Zavřít",
"Badge value (For Testing only.)": "Hodnota odznaku (pouze pro testování)",
"Badge Warn Days": "Odznak dní s upozorněním"
}

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