Compare commits

...

285 Commits

Author SHA1 Message Date
Louis Lam
621419e434 Update to 1.23.9 2023-12-10 20:43:29 +08:00
Louis Lam
482049c72b Merge pull request from GHSA-88j4-pcx8-q4q3
* WIP, still need to handle npm run reset-password

* Implement it for "npm run reset-password"

Bug fixes and change along with this commit
- Move `ssl`, `hostname`, `port` to ./server/config.js, so `reset-password` is able to read it
- Fix: FBSD is missing, no idea who dropped it.
- Fix: Frontend code should not require any backend code (./server/config.js), moved "badgeConstants" to the common util (./src/util.ts) and drop vite-common.js

* Minor
2023-12-10 20:40:40 +08:00
Louis Lam
2815cc73cf Merge pull request from GHSA-mj22-23ff-2hrr
* WIP

* WIP

* Handle parsing error

* Fix matching origin issue
2023-12-10 20:39:43 +08:00
Louis Lam
e1147c06aa Update denpendecies 2023-12-10 02:45:42 +08:00
Ritik Singh
abc8f2b131 Fix: Correct Maintenance Start/End Time Input to Use Explicitly Specified Timezone (#4186) 2023-12-09 18:27:07 +08:00
Frank Elsinga
777ef6bc7b chore: added a helptext for ntfy's priority field (#4175)
* added a helptext for `ntfy`'s `priority` field

* linting fixes

* removed an unnecessary `Math.max` call
2023-12-09 17:37:33 +08:00
Louis Lam
b244e8fcbb Re-export the icon on vectr.com, so it can be editable again. The current icon.svg was reduced size by a contributor previously, but the border is detached after that, which cannot edit by any svg editor anymore. 2023-12-09 04:24:19 +08:00
Frank Elsinga
031947319a Add an aria-label to the monitor search box (#4163)
* added the `Search monitored sites` label

* rebase
2023-12-05 01:15:28 +08:00
Adam Stachowicz
74a908a069 Max ESLint warnings 0 (#4158)
* Fix ESLint warnings. Update workflows. 0 ESLint warnings for auto-test

* json-yaml-validate: Fix `unable to find version `v2``
2023-12-04 18:19:18 +08:00
Frank Elsinga
9c56c9b346 Fixed the buttons of ActionsSelect and ActionsInput having a default type="submit" (#4162)
* fixed the buttons having a default type="submit"

* fixed linting issue
2023-12-04 01:15:40 +08:00
Louis Lam
37666bf35f Update to 1.23.8 2023-12-02 17:51:06 +08:00
Louis Lam
90badfabee Update dependencies 2023-12-02 15:41:59 +08:00
Frank Elsinga
e3396251a8 accessible domain selector (#4133)
* made the status domain selector more accessible

* linting fix

* implemented the suggested changes

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-12-01 15:05:01 +08:00
Frank Elsinga
9c9a086788 accessible ActionSelect/ ActionInput (#4132)
* made sure that the ActionSelect'or has correct accessibiltiy tags

* fixed linting error

* improved the ActionInputs accessibility
2023-12-01 14:34:37 +08:00
Louis Lam
9fb95fe95e Add support for /snap/bin/chromium (#4141) 2023-12-01 14:25:41 +08:00
Louis Lam
1e75d81bcf Update apprise from 1.4.5 to 1.6.0 (#4140) 2023-12-01 14:16:35 +08:00
Louis Lam
cb3a104dc0 Default Retries from 1 to 0 (#4139)
* Default "Retries" from 1 to 0
2023-12-01 14:09:13 +08:00
Louis Lam
57a18958d6 Update gamedig from ~4.1.0 to ^4.2.0 (#4136) 2023-12-01 11:42:31 +08:00
Louis Lam
1708b67949 Change execSync/spawnSync to async (#4123)
* WIP

* Add missing await

* Update package-lock.json
2023-11-30 16:12:04 +08:00
Louis Lam
73239d441d Update to 1.23.7 2023-11-24 18:49:27 +08:00
Louis Lam
4ceeb304f1 Add a script to prepare a changelog 2023-11-24 18:44:54 +08:00
Louis Lam
711380bbbe Merge pull request #4095 from louislam/update-3
Rewrite Tailscale ping using spawnSync
2023-11-24 17:44:24 +08:00
Louis Lam
9536c6aa6a Minor 2023-11-24 17:33:13 +08:00
Louis Lam
4255496b11 Rewrite Tailscale ping using spawnSync 2023-11-24 17:29:42 +08:00
Louis Lam
f28dccf4e1 Merge pull request from GHSA-v4v2-8h88-65qj 2023-11-24 17:18:01 +08:00
Louis Lam
b689733d59 Fix getGameList, testChrome without checkLogin 2023-11-24 16:37:52 +08:00
Louis Lam
afaa7bb2f0 Do not process debug log for production 2023-11-24 16:03:35 +08:00
Louis Lam
121d1a11af Revert "Restart running monitors if no heartbeat (#3952)" (#4088)
This reverts commit c43223a16d.
2023-11-24 02:23:38 +08:00
Louis Lam
8e61158758 Close the client postgresql connection after rejection. (#4084)
Co-authored-by: Manuel Vázquez Acosta <manuel@merchise.org>
2023-11-22 19:50:03 +08:00
Louis Lam
bf58838b89 +10 seconds for Abort signal (#4053)
* Debug only

* Remove debug
2023-11-22 16:03:03 +08:00
Nelson Chan
33ce0ef02c Fix: Improve error message on timeout (#4054)
* Fix: Improve error message on timeout

* Chore: Format
2023-11-21 23:56:17 +08:00
Louis Lam
c1aaad0d85 Update to 1.23.6 2023-11-18 11:37:14 +08:00
Louis Lam
954e05b72f Fix #4051 2023-11-18 11:33:34 +08:00
Louis Lam
6d4a45f18c Update to 1.23.5 2023-11-18 01:23:53 +08:00
Louis Lam
f0975cd929 Should be a final ulitmate fix for request timeout issue (#4045)
* Try to fix timeout again

* Ops
2023-11-18 01:17:54 +08:00
Louis Lam
40d6a21453 Fix kafka migration script again (#4043) 2023-11-17 15:21:29 +08:00
Louis Lam
b383392e8f Remains Node.js 16' SSL behavior for 1.23.X (#4044) 2023-11-17 15:21:08 +08:00
Nelson Chan
9964b6c4d8 Fix: Update monitor object on pause (#4032) 2023-11-16 20:41:35 +08:00
Louis Lam
d56bf08cd7 Update to 1.23.4 2023-11-13 15:23:32 +08:00
Louis Lam
291d5d7c55 Update dependencies 2023-11-13 15:22:51 +08:00
Louis Lam
8e3ff25f7b Followup #3864, rebase for 1.23.x (#4016)
* Fix: Use ActionSelect Docker Host & validate input

* Fix: Handle docker host deleted while editing

* UI: Use add for ActionSelect & prevent delete instead

---------

Co-authored-by: Nelson Chan <chakflying@hotmail.com>
2023-11-12 20:32:40 +08:00
Louis Lam
6e80c850f4 Should be an ulitmate fix for request timeout issue (#4011) 2023-11-12 13:50:51 +08:00
Muhammed Hussein karimi
0608881954 🐛 fix: kafka producer booleans migration null values (#3984)
Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
2023-11-10 00:32:54 +08:00
Nelson Chan
38efd97b28 Fix: Support float ping in push route (#3987) 2023-11-09 23:39:44 +08:00
Louis Lam
ce0ba6c0ca Fix/axios abort signal for 1.23.X (#3971)
* Fix: Add axios abort signal

* Chore: Fix comment

---------

Co-authored-by: Nelson Chan <chakflying@hotmail.com>
2023-11-01 10:10:48 +08:00
Louis Lam
c43223a16d Restart running monitors if no heartbeat (#3952) 2023-11-01 09:36:12 +08:00
Muhammed Hussein karimi
9f170a68d7 🐛 fix: boolean fields in kafka producer monitor (#3949)
* 🐛 fix: boolean fields in kafka producer monitor

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

* 🐛 fix: boolean fields db patch table modify

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

* ✏️  typo: remove `_old` COLUMNs in patch-fix-kafka-producer-booleans

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

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
2023-10-28 14:42:55 +08:00
Louis Lam
1a862e47ab Check if the password changed when user is not null 2023-10-23 06:21:39 +08:00
Nelson Chan
e64bf0e3fe Fix: Stop notification check on root certs (#3874)
* Fix: Stop notification check on root certs

* Chore: Use Set for optimization

* Fix: Manually calculate SHA256 to support node v14
2023-10-16 02:20:38 +08:00
Louis Lam
523d137e2b Lint 2023-10-16 00:43:07 +08:00
Louis Lam
18169c59a1 [MySQL monitor] Split password into a standalone field (#3899) 2023-10-16 00:38:56 +08:00
Louis Lam
4ccf263481 Update docker image base from Node.js 16 to Node.js 18 for Uptime Kuma v1 (#3901) 2023-10-16 00:27:47 +08:00
Louis Lam
1c13a75970 Fix #3868 postgres monitor could possibly crash Uptime Kuma (#3880)
* Bump pg

* Handle uncaughtException

* Fix parsing issue of postgres connection and fix the query example
2023-10-13 02:50:10 +08:00
Louis Lam
c3e3f27457 Update to 1.23.3 2023-10-09 20:26:18 +08:00
Louis Lam
794f1810bf Minor 2023-10-09 20:09:29 +08:00
Louis Lam
168357d93c Update dependencies 2023-10-09 20:05:50 +08:00
Louis Lam
476deb9fec Pin npm@9 2023-10-09 07:33:52 +08:00
Louis Lam
a36f2a75ca Enable auto-test for 1.23.X branch 2023-10-09 07:23:30 +08:00
Louis Lam
88afab6571 Merge pull request from GHSA-g9v2-wqcj-j99g
* Fix attempt

* Update message
2023-10-09 07:01:54 +08:00
Nelson Chan
bd9c44cccf Fix: Disable status page saving before getData 2023-10-09 06:41:07 +08:00
前端小武
1b148786a5 Fix: Update x-forwarded-host field when using reverse proxy (#3726) 2023-10-09 06:31:52 +08:00
Louis Lam
66a10b8993 Pump gamedig from 4.0.X to 4.1.X and update dependencies 2023-10-03 04:51:02 +08:00
Muhammed Hussein karimi
2ab21ccf8a 🐛 fix: kafka producer bugs (#3771)
* 🐛 fix: missing Kafka Producer SSL option in frontend object

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

* ♻️  refactor: better error handling of kafka producer

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

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
2023-09-24 03:30:15 +08:00
Marvin A. Ruder
90d0e8ccde Enable status page certificate expiry badge for all HTTP(s) monitors (#3649) 2023-09-24 03:18:18 +08:00
Louis Lam
16a396debb Similar to #3763, but for 1.23.3 2023-09-21 20:38:54 +08:00
Louis Lam
6b3d69e1d3 Ignore /extra/healthcheck-armv7 2023-09-18 03:59:56 +08:00
Louis Lam
b3b8e9f3a0 Update to 1.23.2 2023-09-18 03:04:29 +08:00
Louis Lam
e5345848a2 Update dependencies 2023-09-18 03:01:58 +08:00
FC (Fay) Stegerman
d8a8f6c08b EditMonitor: add missing config UI for json-query (#3751) 2023-09-17 20:39:07 +08:00
Louis Lam
f98a1ce077 Ignore some build files 2023-09-17 20:32:49 +08:00
Louis Lam
86fa57449e Fix #3596 2023-09-17 02:56:19 +08:00
Louis Lam
ff51704cdf Fix #3712 2023-09-17 02:40:08 +08:00
Henrik Gerdes
33804d8823 fix: respect the user defined oauth2 auth method (#3727) 2023-09-16 05:13:20 +08:00
Nelson Chan
1e12ca4786 Fix: Disable save button on submit (#3729) 2023-09-16 05:11:18 +08:00
Nelson Chan
0af4ee6c34 Fix: Missing await for isActive (#3717) 2023-09-10 01:54:03 +08:00
ZaneL1u
1f29fabe64 fix: fix the judgment condition of Tailscale (#3701) 2023-09-07 16:15:21 +08:00
Louis Lam
c4e222d1e6 Update README.md 2023-08-29 20:49:03 +08:00
Louis Lam
f2a1c26ef8 Update to 1.23.1 2023-08-29 03:52:53 +08:00
Louis Lam
8772baad9a Update dependencies 2023-08-29 03:39:47 +08:00
Louis Lam
215c89e8d3 Merge pull request #3612 from UptimeKumaBot/weblate-uptime-kuma-uptime-kuma
Translations Update from Weblate
2023-08-29 03:09:48 +08:00
Unai Tolosa Pontesta
e6a055af19 Translated using Weblate (Basque)
Currently translated at 68.9% (557 of 808 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-08-28 08:15:57 +00:00
Yoswaris Lawpaiboon
88d71d2c7a Translated using Weblate (Thai)
Currently translated at 79.9% (646 of 808 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-08-28 08:15:57 +00:00
Mirinek
cd2d5325df Translated using Weblate (Slovak)
Currently translated at 26.7% (216 of 808 strings)

Co-authored-by: Mirinek <mirek.nozicka77@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/sk/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
AlwaleedAlwabel
75a1245b70 Translated using Weblate (Arabic)
Currently translated at 83.9% (678 of 808 strings)

Co-authored-by: AlwaleedAlwabel <xomsd1@hotmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ar/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
DevMirza
f666eb6d83 Translated using Weblate (Urdu)
Currently translated at 59.1% (478 of 808 strings)

Co-authored-by: DevMirza <pzhafeez@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ur/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
Haim Cohen
cb10643f57 Translated using Weblate (Hebrew (Israel))
Currently translated at 87.6% (708 of 808 strings)

Co-authored-by: Haim Cohen <haim1979@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/he_IL/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
Wishw
c9ba4e7e8b Translated using Weblate (Telugu)
Currently translated at 23.3% (189 of 808 strings)

Added translation using Weblate (Telugu)

Co-authored-by: Wishw <62600445+Wisw@users.noreply.github.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/te/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
stanol
94187bca5d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (808 of 808 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-08-28 08:15:57 +00:00
Matteo Mazzoni
39b4aa5966 Translated using Weblate (Italian)
Currently translated at 73.0% (590 of 808 strings)

Co-authored-by: Matteo Mazzoni <matteo@matteomazzoni.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/it/
Translation: Uptime Kuma/Uptime Kuma
2023-08-28 08:15:57 +00:00
Louis Lam
cd79df07e1 Add an ability to enable/disable nscd (#3652) 2023-08-28 16:15:48 +08:00
Louis Lam
0c40f02584 Put Monitor Group under Advanced section 2023-08-28 04:59:47 +08:00
Nelson Chan
db42c13e05 Fix: Remove legacy unused tags cleanup (#3651) 2023-08-27 18:56:50 +08:00
Nelson Chan
5f85d8f749 Fix: Focus & styling on safari (#3650) 2023-08-27 18:55:36 +08:00
Lior Slakman
c0e273df5b Show hostname:port for gamedig monitor on Discord notification (#3643) 2023-08-27 02:24:33 +08:00
Nelson Chan
4da1341aa5 Fix: Improve dnsMessage handling (#3614) 2023-08-26 21:27:32 +08:00
James Osborn
e765e6a1b8 Spelling & Grammar updates (#3638)
* Spelling & Grammar updates to SECURITY.md

No policy changes were made in this change, only improving readability.

* Spelling & Grammar updates to README.md

No policy changes were made in this change, only improving readability.

* Spelling & Grammar updates to CONTRIBUTING.md

No policy changes were made in this change, only improving readability.
2023-08-25 00:01:47 +08:00
bt90
eee9a1f004 Passwordmanager friendly inputs (#3593)
* Mark username/password inputs as required

* Mark 2FA input as required

* Add autocomplete attributes

* Add autocomplete suggestion to password change

* Add autocomplete to 2FA dialog

* Mark 2fa input as required
2023-08-21 02:25:35 +08:00
Frank Elsinga
4d07b65bdd fixed local docker not working anymore (#3606) 2023-08-20 04:45:58 +08:00
Frank Elsinga
1772158d62 fixed opsgenieRegion not being the same enum between the frontend and backend (#3616) 2023-08-20 04:41:42 +08:00
Frank Elsinga
7bfdb82f5d bug: fixed the curser being a pointer if hovering above the dropdown-item's (#3607)
* fixed the curser being a pointer if hovering above the `dropdown-item`'s

* formatting fix
2023-08-20 04:40:12 +08:00
Frank Elsinga
8945316ce6 added helptexs to the installation script (#3603) 2023-08-20 03:01:37 +08:00
Louis Lam
9564550d5f Update to 1.23.0 2023-08-17 22:30:28 +08:00
Louis Lam
a78e7a423e Merge pull request #3569 from UptimeKumaBot/weblate-uptime-kuma-uptime-kuma
Translations Update from Weblate
2023-08-17 22:02:41 +08:00
Alex Javadi
9dddd0b657 Translated using Weblate (Persian)
Currently translated at 100.0% (808 of 808 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-08-16 11:16:35 +00:00
astroking362
d04d86d74e Translated using Weblate (Portuguese)
Currently translated at 4.5% (37 of 808 strings)

Co-authored-by: astroking362 <astroking362@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt/
Translation: Uptime Kuma/Uptime Kuma
2023-08-16 11:16:35 +00:00
ITQ
eb11c18203 Translated using Weblate (Russian)
Currently translated at 100.0% (808 of 808 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-08-16 11:16:35 +00:00
Harry Suryapambagya
3da2d78ad9 Translated using Weblate (Indonesian)
Currently translated at 93.9% (759 of 808 strings)

Co-authored-by: Harry Suryapambagya <harsxv@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/
Translation: Uptime Kuma/Uptime Kuma
2023-08-16 11:16:35 +00:00
Buchtič
2b4ec765ff Translated using Weblate (Czech)
Currently translated at 100.0% (808 of 808 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-08-16 11:16:35 +00:00
losing
72dcefff76 Translated using Weblate (Slovak)
Currently translated at 26.6% (215 of 808 strings)

Co-authored-by: losing <me@losing.top>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/sk/
Translation: Uptime Kuma/Uptime Kuma
2023-08-16 11:16:35 +00:00
Nelson Chan
3a894958eb Translated using Weblate (Chinese (Traditional, Hong Kong))
Currently translated at 86.7% (701 of 808 strings)

Co-authored-by: Nelson Chan <chakflying@hotmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant_HK/
Translation: Uptime Kuma/Uptime Kuma
2023-08-16 11:16:35 +00:00
Nelson Chan
fe431d6385 Chore: Run test on PR to v2 branch (#3582) 2023-08-16 16:05:52 +08:00
Louis Lam
ce0289855d Remove hard-coded node versions as upstream fixed (#3576) 2023-08-15 17:13:35 +08:00
zhenqiang
c0174dc1c4 fix(notification-aliyun-sms): throw error when sending SMS failed (#3573) 2023-08-15 04:14:28 +08:00
M1CK431
e745bd69da [webapp] Setup: translate fields placeholders (#3563) 2023-08-15 02:00:42 +08:00
UptimeKumaBot
72741ebb10 Translations Update from Weblate (#3560)
* Translated using Weblate (Portuguese)

Currently translated at 4.1% (33 of 801 strings)

Added translation using Weblate (Portuguese)

Co-authored-by: Miguel Castilho Dias <miguelcastilhodias@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Czech)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (French)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (French)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (German)

Currently translated at 98.3% (788 of 801 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 98.3% (788 of 801 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

* Translated using Weblate (Spanish)

Currently translated at 91.2% (733 of 803 strings)

Co-authored-by: Jorge Sanz Sanfructuoso <sanchi2@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Spanish)

Currently translated at 91.2% (733 of 803 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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (Czech)

Currently translated at 99.7% (806 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (French)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Polish)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pl/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/

* Translated using Weblate (German)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/

* Translated using Weblate (Russian)

Currently translated at 98.2% (794 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Italian)

Currently translated at 72.8% (589 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/it/

* Translated using Weblate (Russian)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/

---------

Co-authored-by: Miguel Castilho Dias <miguelcastilhodias@gmail.com>
Co-authored-by: MrEddX <mreddx@chatrix.one>
Co-authored-by: Michal <black23@gmail.com>
Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Co-authored-by: Ömer Faruk Genç <omer@farukgenc.com>
Co-authored-by: stanol <stanol777@gmail.com>
Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Co-authored-by: Marco <marco@nanoweb.ch>
Co-authored-by: Jorge Sanz Sanfructuoso <sanchi2@gmail.com>
Co-authored-by: rubesaca <rubesaca@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
Co-authored-by: sovushik <evgeniy@grachev.biz>
Co-authored-by: Lance <2124757129@qq.com>
Co-authored-by: diego <deiana.diego@icloud.com>
Co-authored-by: ITQ <itq.dev@ya.ru>
2023-08-12 22:37:55 +08:00
Frank Elsinga
ff88018b0c rephrased the dependency documentation (#3565) 2023-08-12 22:28:12 +08:00
Matthew Nickson
921c8f8100 Add noscript tag as stand in for #3553 (#3555)
Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-08-10 19:35:13 +08:00
Louis Lam
de83863627 Update to 1.23.0-beta.1 2023-08-08 18:44:11 +08:00
Louis Lam
d7e0ff4b8c Update dependencies 2023-08-08 18:43:45 +08:00
UptimeKumaBot
112cc3d7b8 Translations Update from Weblate (#3536)
* Translated using Weblate (Portuguese)

Currently translated at 4.1% (33 of 801 strings)

Added translation using Weblate (Portuguese)

Co-authored-by: Miguel Castilho Dias <miguelcastilhodias@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Czech)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (French)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (French)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (German)

Currently translated at 98.3% (788 of 801 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 98.3% (788 of 801 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

* Translated using Weblate (Spanish)

Currently translated at 91.2% (733 of 803 strings)

Co-authored-by: Jorge Sanz Sanfructuoso <sanchi2@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Spanish)

Currently translated at 91.2% (733 of 803 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

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (Czech)

Currently translated at 99.7% (806 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (French)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Polish)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pl/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (808 of 808 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

---------

Co-authored-by: Miguel Castilho Dias <miguelcastilhodias@gmail.com>
Co-authored-by: MrEddX <mreddx@chatrix.one>
Co-authored-by: Michal <black23@gmail.com>
Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Co-authored-by: Ömer Faruk Genç <omer@farukgenc.com>
Co-authored-by: stanol <stanol777@gmail.com>
Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Co-authored-by: Marco <marco@nanoweb.ch>
Co-authored-by: Jorge Sanz Sanfructuoso <sanchi2@gmail.com>
Co-authored-by: rubesaca <rubesaca@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2023-08-08 18:32:26 +08:00
Louis Lam
b44f6e4af2 Fix #3539 2023-08-08 18:29:48 +08:00
Louis Lam
18a2a8eb1e Update to 1.23.0-beta.0 2023-08-08 03:57:59 +08:00
Louis Lam
e9585ccbf4 Add .devcontainer to .dockerignore 2023-08-08 03:54:21 +08:00
UptimeKumaBot
5e39b0daf6 Translations Update from Weblate (#3512)
* Translated using Weblate (Portuguese)

Currently translated at 4.1% (33 of 801 strings)

Added translation using Weblate (Portuguese)

Co-authored-by: Miguel Castilho Dias <miguelcastilhodias@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Czech)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (French)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (French)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (801 of 801 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

* Translated using Weblate (German)

Currently translated at 98.3% (788 of 801 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 98.3% (788 of 801 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

* Translated using Weblate (Spanish)

Currently translated at 91.2% (733 of 803 strings)

Co-authored-by: Jorge Sanz Sanfructuoso <sanchi2@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/es/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Spanish)

Currently translated at 91.2% (733 of 803 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

---------

Co-authored-by: Miguel Castilho Dias <miguelcastilhodias@gmail.com>
Co-authored-by: MrEddX <mreddx@chatrix.one>
Co-authored-by: Michal <black23@gmail.com>
Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Co-authored-by: Ömer Faruk Genç <omer@farukgenc.com>
Co-authored-by: stanol <stanol777@gmail.com>
Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Co-authored-by: Marco <marco@nanoweb.ch>
Co-authored-by: Jorge Sanz Sanfructuoso <sanchi2@gmail.com>
Co-authored-by: rubesaca <rubesaca@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
2023-08-08 03:26:54 +08:00
Matthew Nickson
71fca3f0c3 Fixed #3520 Validate accepted status codes type (#3530)
* Fixed #3520 Validate accepted status codes type

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

* Avoid crash on invalid status code

Instead of failing on an invalid status code, log the error and skip to
next.

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

---------

Signed-off-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-08-08 03:22:32 +08:00
Nelson Chan
2921f33c24 Feat: add givenPortOnly as gamedig option (#3532) 2023-08-08 03:14:21 +08:00
Louis Lam
4e0bb394db Minor 2023-08-08 03:10:21 +08:00
Nelson Chan
ced576feba Feat: Show elapsed time in HeartbeatBar (#3219)
* Feat: Show elapsed time in HeartbeatBar

* Chore: Fix lint

* Feat: Fix calculation & improve efficiency

* Fix: Fix getting tolerance in statusPage

* Chore: Improve comments & apply suggestions

* Optional elapsed time

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-08-08 03:00:40 +08:00
Anders Kvist
ceb5708bfd Fix case for setting httpBodyEncoding to null - does work for http, b… (#3498)
* Fix case for setting httpBodyEncoding to null - does work for http, but not for keyword if anything else but json.

* Updating case.

* Update src/pages/EditMonitor.vue

Added suggestion from @CommanderStorm

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

* Update src/pages/EditMonitor.vue

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

---------

Co-authored-by: Anders Kvist <ak@cego.dk>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-08-07 15:13:51 +08:00
Nelson Chan
23fdd32de0 Fix: filtersActive placed incorrectly (#3531) 2023-08-07 15:10:03 +08:00
Nelson Chan
36777c5eff Fix: Filtering works with group monitor (#3469)
* Fix: Filtering works with group monitor

* Chore: Update comment

* Apply suggestions from code review

Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
2023-08-07 00:35:50 +08:00
Jinhyeok Lee
439b6517d1 Feat: Add http/http keyword timeout option (#2142)
* feat: Add timeoutMs field

* chore: Update Languages (incl. ko-KR)

* Revert "chore: Update Languages (incl. ko-KR)"

This reverts commit 349331a00b.

* chore: Update ko-KR selectively

* chore: Update en selectively

* Merge manually

* Reorder and show only if http related monitors

* fix: Update Korean translation

* fix: Rename timeoutMs to timeout, rename label, make DOUBLE

* fix: Change minimum step to 0.1, matching DOUBLE type

* Put the sql patch at the end

* Update EditMonitor.vue

* Colocate timeout with retry, fix clampTimeout logic, show default on 0

* Update src/pages/EditMonitor.vue to remove a comment

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

* Fix merge issue

* Update the timeout value while finished editing the interval value

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-08-07 00:14:56 +08:00
Louis Lam
c6e68fac97 Follow up #2852 (#3527) 2023-08-04 23:08:44 +08:00
Marcell Fülöp
bce4835362 FEAT: Allow client side TLS for Docker hosts (#2852)
* FEAT: Allow client side TLS for Docker hosts

Inlcude TLS certificate in HTTPS requests when certificate
files are locally available to Kuma for a host.

* fix: refactor to satisfy linter requirements

* fix: linter
2023-08-04 22:49:33 +08:00
Nelson Chan
a032e11a2e Feat: Create Group in EditMonitor page (#3379)
* Feat: Create Group in EditMonitor page

* Fix: Start group mon. after child is added

* Chore: Swap confirm & cancel for ergonomics

* Fix rarely issue that group monitor can throw an error if lastBeat is null

* Resume the group monitor in the callback

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-08-04 14:48:21 +08:00
Louis Lam
d231a05526 Remove babel-plugin-rewire (#3522) 2023-08-04 01:10:15 +08:00
GitStart
f4967615c0 UPTM-14 - Dashboard Display on vertical 4K screen (#3451)
* Update src/pages/DashboardHome.vue

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

* resolve comment

Co-authored-by: AqidaHaidari <aghida55@gmail.com>
Co-authored-by: Pascal Ojinnaka <56399504+passy4ucj@users.noreply.github.com>
Co-authored-by: Aqida <42426077+AqidaHaidari@users.noreply.github.com>

---------

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: AqidaHaidari <aghida55@gmail.com>
Co-authored-by: gitstart_bot <gitstart_bot@users.noreply.github.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
Co-authored-by: Pascal Ojinnaka <56399504+passy4ucj@users.noreply.github.com>
Co-authored-by: Aqida <42426077+AqidaHaidari@users.noreply.github.com>
2023-08-03 21:07:01 +08:00
Louis Lam
67b1974718 Allow specifying DATA_DIR without a tailing slash 2023-08-03 20:54:11 +08:00
M1CK431
062e9db2a7 fix: typo in contributing guide (#3517) 2023-08-03 14:09:45 +08:00
DevMirza
90e1b4cf56 Update dependencies 2023-08-03 14:06:39 +08:00
Frank Elsinga
43698b23c6 Chore: Updated translation contributing guide (#3511)
* made the translation part of the contribution guide/Readme more clear

* made some formatting adjustments and added a few links

* fixed a typo

* Apply suggestions from code review

* Added DBeaver
2023-08-02 17:30:54 +08:00
DevMirza
f19b27c1c7 fix: readme (#3502) 2023-08-02 16:11:18 +08:00
Henrik Gerdes
42b5d30a33 feat: Implement oauth2 monitors (#3119)
* [empty commit] pull request for implement oauth2 monitor

* feat: implement oauth2 client credentials flow

* fix: auth methods clarification & error handling

* docs: fix JSdocs types and clarifications
2023-08-02 15:40:19 +08:00
Nelson Chan
cda77c1a32 Fix: Prevent race condition in toast (#3443) 2023-08-02 15:09:13 +08:00
UptimeKumaBot
157cf47d38 Translations Update from Weblate (#3318)
* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (762 of 762 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (754 of 754 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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 78.1% (589 of 754 strings)

Co-authored-by: Bruno Guerreiro <brunogcarvalho@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (762 of 762 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (754 of 754 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

* Translated using Weblate (Czech)

Currently translated at 99.8% (761 of 762 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

* Translated using Weblate (French)

Currently translated at 99.3% (757 of 762 strings)

Co-authored-by: Dim <DimitriDR@users.noreply.weblate.kuma.pet>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (757 of 757 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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (757 of 757 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

* Translated using Weblate (German)

Currently translated at 100.0% (757 of 757 strings)

Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (757 of 757 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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 77.9% (590 of 757 strings)

Co-authored-by: Paulo <git@pauloo27.dev>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_BR/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Russian)

Currently translated at 100.0% (757 of 757 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (755 of 757 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

* Translated using Weblate (Thai)

Currently translated at 85.3% (646 of 757 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

* Translated using Weblate (Polish)

Currently translated at 99.4% (753 of 757 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

* Translated using Weblate (Vietnamese)

Currently translated at 61.6% (467 of 757 strings)

Co-authored-by: Bond <xuantan97@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/vi/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Urdu)

Currently translated at 60.7% (460 of 757 strings)

Co-authored-by: DevMirza <pzhafeez@gmail.com>
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ur/
Translation: Uptime Kuma/Uptime Kuma

* Translated using Weblate (Czech)

Currently translated at 100.0% (757 of 757 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 95.3% (722 of 757 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 95.3% (722 of 757 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant/

* Translated using Weblate (Vietnamese)

Currently translated at 62.7% (475 of 757 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/vi/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (759 of 759 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (French)

Currently translated at 100.0% (759 of 759 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (759 of 759 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (German)

Currently translated at 100.0% (759 of 759 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (759 of 759 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (759 of 759 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (French)

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (762 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Bulgarian)

Currently translated at 99.8% (762 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (Czech)

Currently translated at 99.6% (760 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (Indonesian)

Currently translated at 81.3% (621 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/

* Translated using Weblate (Russian)

Currently translated at 99.2% (757 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Czech)

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (Danish)

Currently translated at 73.6% (562 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/da/

* Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/

* Translated using Weblate (German)

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/

* Translated using Weblate (Croatian)

Currently translated at 71.2% (544 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/hr/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/

* Translated using Weblate (Portuguese (Portugal))

Currently translated at 57.0% (435 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/pt_PT/

* Translated using Weblate (Thai)

Currently translated at 84.5% (645 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/th/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (763 of 763 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/id/

* Translated using Weblate (French)

Currently translated at 100.0% (767 of 767 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (767 of 767 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (767 of 767 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (767 of 767 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (Czech)

Currently translated at 99.6% (764 of 767 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (Czech)

Currently translated at 99.8% (766 of 767 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (768 of 768 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (French)

Currently translated at 100.0% (768 of 768 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (768 of 768 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (768 of 768 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (770 of 770 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (French)

Currently translated at 100.0% (770 of 770 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (770 of 770 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (Czech)

Currently translated at 99.8% (769 of 770 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (Czech)

Currently translated at 99.8% (769 of 770 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.7% (768 of 770 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.7% (768 of 770 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (770 of 770 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.7% (768 of 770 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (French)

Currently translated at 100.0% (772 of 772 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (772 of 772 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (772 of 772 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (Czech)

Currently translated at 99.8% (771 of 772 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (French)

Currently translated at 100.0% (772 of 772 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (772 of 772 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/nl/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (772 of 772 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.7% (770 of 772 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Czech)

Currently translated at 98.9% (778 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (786 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/

* Translated using Weblate (German)

Currently translated at 100.0% (786 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/

* Translated using Weblate (French)

Currently translated at 100.0% (786 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.7% (784 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Chinese (Traditional, Hong Kong))

Currently translated at 89.3% (702 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hant_HK/

* Added translation using Weblate (Burmese)

* Translated using Weblate (Burmese)

Currently translated at 4.5% (36 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/my/

* Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (786 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/

* Translated using Weblate (German)

Currently translated at 100.0% (786 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/

* Translated using Weblate (Russian)

Currently translated at 100.0% (786 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (786 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (786 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (Burmese)

Currently translated at 11.4% (90 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/my/

* Translated using Weblate (French)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/

* Translated using Weblate (German)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/

* Translated using Weblate (Russian)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ru/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.7% (785 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Added translation using Weblate (Bengali)

* Translated using Weblate (English)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/en/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/

* Translated using Weblate (German)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/

* Translated using Weblate (German)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/

* Translated using Weblate (French)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.7% (785 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Japanese)

Currently translated at 69.5% (547 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/

* Translated using Weblate (Japanese)

Currently translated at 69.5% (547 of 786 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/bg/

* Translated using Weblate (Czech)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (German (Switzerland))

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de_CH/

* Translated using Weblate (German)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/de/

* Translated using Weblate (French)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Japanese)

Currently translated at 69.7% (549 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/ja/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/tr/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/uk/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (787 of 787 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Czech)

Currently translated at 100.0% (790 of 790 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (French)

Currently translated at 100.0% (790 of 790 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (789 of 790 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (796 of 797 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/zh_Hans/

* Translated using Weblate (Czech)

Currently translated at 99.7% (797 of 799 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/cs/

* Translated using Weblate (French)

Currently translated at 100.0% (799 of 799 strings)

Translation: Uptime Kuma/Uptime Kuma
Translate-URL: https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/fr/

---------

Co-authored-by: zappityzap <128872140+zappityzap@users.noreply.github.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: MrEddX <mreddx@chatrix.one>
Co-authored-by: Bruno Guerreiro <brunogcarvalho@gmail.com>
Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Co-authored-by: Michal <black23@gmail.com>
Co-authored-by: Dim <DimitriDR@users.noreply.weblate.kuma.pet>
Co-authored-by: stanol <stanol777@gmail.com>
Co-authored-by: Ömer Faruk Genç <omer@farukgenc.com>
Co-authored-by: Marco <marco@nanoweb.ch>
Co-authored-by: Paulo <git@pauloo27.dev>
Co-authored-by: ITQ <itq.dev@ya.ru>
Co-authored-by: Yoswaris Lawpaiboon <konglha19@outlook.co.th>
Co-authored-by: Tomasz Ad <djtms84@gmail.com>
Co-authored-by: Bond <xuantan97@gmail.com>
Co-authored-by: DevMirza <pzhafeez@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Tomy Hsieh <git@tomy.me>
Co-authored-by: melt-tscci <beta_hz@163.com>
Co-authored-by: AyanoKiru <tn0562260@gmail.com>
Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Co-authored-by: jonas <jonasriedel@pm.me>
Co-authored-by: Dikky Hardian <dikkyhardian@gmail.com>
Co-authored-by: Buchtič <martin.buchta@gmail.com>
Co-authored-by: deluxghost <deluxghost@gmail.com>
Co-authored-by: Justin Kruit <justinkruitnl@gmail.com>
Co-authored-by: Marco Li <macaty@gmail.com>
Co-authored-by: naing-htoo <nainghtoo@tnitech.net>
Co-authored-by: drshounak <contact@drshounak.com>
Co-authored-by: Simon Thiel <simon.thiel2+github@gmail.com>
Co-authored-by: Donker_Jumala <weareh0711@gmail.com>
Co-authored-by: mottcha <yuki627f@gmail.com>
2023-08-01 15:41:50 +08:00
Nelson Chan
09c6798a30 Fix: Add debounce to favicon update (#3442) 2023-08-01 15:12:17 +08:00
Damon
587d9e4781 Add Flashduty notification (#3475)
* feat: add FlashDuty notification channel

* refactor: #3475 nofify with Up or Down;
refactor code;
add en zh-hk zh-tw lang

* refactor: default select Info

* refactor: add space in word

* refactor the flashduty notification code

* refactor:compatible when Test flashduty nofication

* refactor: add function param description

* refactor: revert zh-hk zh-tw changes of flashduty
2023-08-01 02:13:04 +08:00
Frank Elsinga
ae2867e305 removed input-groups without any visual impact (#3485) 2023-07-31 23:59:12 +08:00
Louis Lam
65ffd77d35 Update dependencies 2023-07-31 18:04:20 +08:00
Louis Lam
9dd652733e Merge pull request #3467 from ngc7331/pushdeer
[reopened] feat: allow the user to configure PushDeer Server URL
2023-07-31 17:57:21 +08:00
Louis Lam
0a59fef7d8 Merge pull request #3490 from chakflying/fix/radius-retry
Fix: Incorrect radius error & retry handling
2023-07-31 17:47:26 +08:00
Louis Lam
27ce47277b Merge pull request #3357 from tarun7singh/status-page-expiry
Status page certificate expiry
2023-07-31 17:46:13 +08:00
Louis Lam
7f68e4a987 Merge remote-tracking branch 'origin/master' into status-page-expiry
# Conflicts:
#	src/lang/en.json
2023-07-31 17:30:49 +08:00
Frank Elsinga
149d08ecbb Chore: make sql monitors eazier to use (#3474)
* Fixed the sql monitor not having the query required

* Apply suggestions from code review

* added `redis` and  `mongodb` to the connectionString as requested by @chakflying
2023-07-31 17:28:39 +08:00
zappityzap
eb6167aaf1 Nostr dm notifications (#3473)
* 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

* better websocket polyfill, update deprecated function

* add conditional polyfills for node versions

* lint

* use correct npm for package-lock

---------

Co-authored-by: Frank Elsinga <frank@elsinga.de>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-07-31 17:24:45 +08:00
Shaun
db66195f7d Added ability to bulk select, pause & resume (#1886)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-07-31 16:24:00 +08:00
Louis Lam
59245e624d Update workflow's Node.js version to 20 (#3503) 2023-07-30 19:02:36 +08:00
Nelson Chan
c0eb0cb42c Chore: Add test radius dockerfile 2023-07-27 17:43:33 +08:00
Nelson Chan
573f158f7f Fix: incorrect radius error handling 2023-07-27 17:42:22 +08:00
Frank Elsinga
6413d4cbdf added a heltext for the nfty url helptext (#3484) 2023-07-27 00:11:18 +08:00
Adam Stachowicz
cf5a04bc5e Do not translate Android. Addendum to #2812 (#3480) 2023-07-26 19:36:47 +08:00
Louis Lam
a0203372ce Built-in nscd into the docker image (a better dns caching service) (#3472) 2023-07-24 17:04:50 +08:00
Raymond Hackley
5ccf2d23fc Fix: SMTP notification "Ignore TLS Error" option (#2999) (#3465) 2023-07-23 14:45:05 +08:00
小造xu_zh
bf68e0a7bc fix: define pushdeerlink 2023-07-21 14:11:13 +00:00
小造xu_zh
6984596568 Apply suggestions from code review
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-07-21 22:01:20 +08:00
小造xu_zh
f3562864ee Apply suggestions from code review
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-07-21 13:45:50 +00:00
Louis Lam
9058a829a1 Update install.sh (#3464) 2023-07-21 16:09:35 +08:00
小造xu_zh
0684313ec9 feat: remove blank characters and ending '/' first 2023-07-20 09:33:27 +00:00
小造xu_zh
d7e12dc92d feat: allow the user to configure PushDeer Server URL 2023-07-20 09:24:16 +00:00
SGprooo
1d9a28e9ab feat: Tailscale ping monitor (#3178)
* Add boilerplate for tailscale ping

* tailscale initial commit draft

* Refactor TailscalePing & better error handling

Split check function into two methods and added async/await syntax for readability/modularity
Switched to promise-based error handling (takes into account different types of error such as "Execution error", "Error in output", "no matching peer", and "is local Tailscale IP") and throws them as JavaScript errors.

* Minor update

* minor update (again)

* Update server/monitor-types/tailscale-ping.js

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

* Update server/monitor-types/tailscale-ping.js

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

* Update server/monitor-types/tailscale-ping.js

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

* Update server/monitor-types/tailscale-ping.js

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

* timeout revision

* JSDoc

* Removed long explainers

* eslint tailscale-ping.js --fix

* reran eslint

* Fix: Use hostname rather than url

* Fixed NaN on monitor interval

now interval value is correctly passed to runTailscalePing

* Add warning message

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-07-19 20:58:21 +08:00
Thomas McWork
ddbf367011 Mattermost: update dead docs link (#3450) 2023-07-19 18:25:58 +08:00
Tarun Singh
50d4091ded Merge branch 'master' of https://github.com/louislam/uptime-kuma into status-page-expiry 2023-07-18 22:57:48 -04: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
Tarun Singh
f6c1b92fc6 Added http type check and tags based design 2023-07-17 02:01:10 -04: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
Tarun Singh
2e75142fe5 Bug fix and unknown days added 2023-07-13 20:50:17 -04:00
Tarun Singh
ad3ffacf45 PR comment fix 2023-07-13 20:37:21 -04:00
Tarun Singh
6f4af30701 Merge branch 'master' of https://github.com/louislam/uptime-kuma into status-page-expiry 2023-07-13 20:02:50 -04:00
Tarun Singh
b1f266ceb1 Added expiry changes to status page and fixes 2023-07-13 20:00:23 -04: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
Tarun Singh
4ea5771f97 Status page certificate expiry 2023-07-04 19:37:45 -04: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
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
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
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
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
Lasse Bang Mikkelsen
a00561ff09 Strip trailing slashes to avoid 404 2023-06-18 18:28:30 +02: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
kefoster951
d37c33ad42 Update server/util-server.js
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-06-12 11:06:20 -04: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
Kenneth Foster
9d71e34a83 [empty commit] pull request for fixing redis auth 2023-06-08 17:48:18 -04: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
duane
376d84c742 Merge branch 'master' into feature/add-channel-notification-for-slack 2023-05-31 10:31:33 -05: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
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
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
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
Arniwatt Chonkiattipoom
bb7de6aa88 chore: notification toggle missing when import from backup 2023-03-02 16:23:27 +07: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
184 changed files with 10340 additions and 4746 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

@@ -18,6 +18,7 @@ README.md
.vscode
.eslint*
.stylelint*
/.devcontainer
/.github
yarn.lock
app.json
@@ -33,7 +34,13 @@ tsconfig.json
/ecosystem.config.js
/extra/healthcheck.exe
/extra/healthcheck
extra/exe-builder
/extra/exe-builder
/extra/push-examples
/extra/uptime-kuma-push
# Comment the following line if you want to rebuild the healthcheck binary
/extra/healthcheck-armv7
### .gitignore content (commented rules are duplicated)

View File

@@ -78,7 +78,7 @@ module.exports = {
"checkLoops": false,
}],
"space-before-blocks": "warn",
//'no-console': 'warn',
//"no-console": "warn",
"no-extra-boolean-cast": "off",
"no-multiple-empty-lines": [ "warn", {
"max": 1,
@@ -90,7 +90,8 @@ module.exports = {
"no-unneeded-ternary": "error",
"array-bracket-newline": [ "error", "consistent" ],
"eol-last": [ "error", "always" ],
//'prefer-template': 'error',
//"prefer-template": "error",
"template-curly-spacing": [ "warn", "never" ],
"comma-dangle": [ "warn", "only-multiline" ],
"no-empty": [ "error", {
"allowEmptyCatch": true

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

@@ -1,15 +1,15 @@
# 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
on:
push:
branches: [ master ]
branches: [ master, 1.23.X ]
paths-ignore:
- '*.md'
pull_request:
branches: [ master ]
branches: [ master, 1.23.X ]
paths-ignore:
- '*.md'
@@ -21,19 +21,19 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
node: [ 14, 16, 18, 20 ]
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 14, 20 ]
# 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
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm install npm@9 -g
- run: npm install
- run: npm run build
- run: npm test
@@ -41,33 +41,54 @@ 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, 20 ]
# 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@v4
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm install npm@9 -g
- run: npm ci --production
check-linters:
runs-on: ubuntu-latest
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 14
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 14
cache: 'npm'
- run: npm install
- run: npm run lint
- run: npm run lint:prod
e2e-tests:
needs: [ check-linters ]
runs-on: ubuntu-latest
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 14
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 14
cache: 'npm'
- run: npm install
- run: npm run build
- run: npm run cy:test
@@ -77,13 +98,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js 14
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 14
cache: 'npm'
- run: npm install
- run: npm run build
- run: npm run cy:run:unit

View File

@@ -14,10 +14,10 @@ jobs:
node-version: [16]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

View File

@@ -1,4 +1,4 @@
name: json-yaml-validate
name: json-yaml-validate
on:
push:
branches:
@@ -6,6 +6,7 @@ on:
pull_request:
branches:
- master
- 1.23.X
workflow_dispatch:
permissions:
@@ -16,11 +17,11 @@ jobs:
json-yaml-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: json-yaml-validate
id: json-yaml-validate
uses: GrantBirki/json-yaml-validate@v1.3.0
uses: GrantBirki/json-yaml-validate@v2.4.0
with:
comment: "true" # enable comment mode
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions

View File

@@ -9,7 +9,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v7
- uses: actions/stale@v8
with:
stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 3 months with no activity. Remove stale label or comment or this will be closed in 2 days.'
close-issue-message: 'This issue was closed because it has been stalled for 2 days with no activity.'

View File

@@ -10,6 +10,7 @@
"color-function-notation": "legacy",
"shorthand-property-no-redundant-values": null,
"color-hex-length": null,
"declaration-block-no-redundant-longhand-properties": null
"declaration-block-no-redundant-longhand-properties": null,
"at-rule-no-unknown": null
}
}

View File

@@ -2,13 +2,13 @@
First of all, I want to thank everyone who made pull requests for Uptime Kuma. I never thought the GitHub Community would be so nice! Because of this, I also never thought that other people would actually read and edit my code. It is not very well structured or commented, sorry about that.
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for server part. Both frontend and backend share the same package.json.
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for the server part. Both frontend and backend share the same package.json.
The frontend code build into "dist" directory. The server (express.js) exposes the "dist" directory as root of the endpoint. This is how production is working.
The frontend code builds into "dist" directory. The server (express.js) exposes the "dist" directory as the root of the endpoint. This is how production is working.
## Key Technical Skills
- Node.js (You should know what are promise, async/await and arrow function etc.)
- Node.js (You should know about promise, async/await and arrow function etc.)
- Socket.io
- SCSS
- Vue.js
@@ -30,24 +30,24 @@ The frontend code build into "dist" directory. The server (express.js) exposes t
## Can I create a pull request for Uptime Kuma?
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**. Especially for a large pull request or you don't know it will be merged or not.
Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**. Especially for a large pull request or you don't know if it will be merged or not.
Here are some references:
✅ Usually Accept:
### ✅ Usually accepted:
- Bug fix
- Security fix
- Adding notification providers
- Adding new language files (You should go to https://weblate.kuma.pet for existing languages)
- Adding new language files (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
- Adding new language keys: `$t("...")`
⚠️ Discussion First
### ⚠️ Discussion required:
- Large pull requests
- New features
❌ Won't Merge
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet)
- Do not pass the auto test
### ❌ Won't be merged:
- A dedicated PR for translating existing languages (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
- Do not pass the auto-test
- Any breaking changes
- Duplicated pull requests
- Buggy
@@ -61,9 +61,9 @@ 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.
I will mark your pull request in the [milestones](https://github.com/louislam/uptime-kuma/milestones), if I am plan to review and merge it.
I will assign your pull request to a [milestone](https://github.com/louislam/uptime-kuma/milestones), if I plan to review and merge it.
Also, please don't rush or ask for ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
Also, please don't rush or ask for an ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests.
### Recommended Pull Request Guideline
@@ -83,11 +83,11 @@ Before deep into coding, discussion first is preferred. Creating an empty pull r
## Project Styles
I personally do not like something that requires so many configurations before you can finally start the app. I hope Uptime Kuma installation could be as easy as like installing a mobile app.
I personally do not like something that requires so many configurations before you can finally start the app. I hope Uptime Kuma installation will be as easy as like installing a mobile app.
- Easy to install for non-Docker users, no native build dependency is needed (for x86_64/armv7/arm64), no extra config, no extra effort required to get it running
- Easy to install for non-Docker users, no native build dependency is needed (for x86_64/armv7/arm64), no extra config, and no extra effort required to get it running
- Single container for Docker users, no very complex docker-compose file. Just map the volume and expose the port, then good to go
- Settings should be configurable in the frontend. Environment variable is not encouraged, unless it is related to startup such as `DATA_DIR`
- Settings should be configurable in the frontend. Environment variables are discouraged, unless it is related to startup such as `DATA_DIR`
- Easy to use
- The web UI styling should be consistent and nice
@@ -106,11 +106,11 @@ I personally do not like something that requires so many configurations before y
## Tools
- Node.js >= 14
- NPM >= 8.5
- Git
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
- A SQLite GUI tool (SQLite Expert Personal is suggested)
- [`Node.js`](https://nodejs.org/) >= 14
- [`npm`](https://www.npmjs.com/) >= 8.5
- [`git`](https://git-scm.com/)
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
## Install Dependencies for Development
@@ -130,7 +130,7 @@ Port `3000` and port `3001` will be used.
npm run dev
```
But sometimes, you would like to keep restart the server, but not the frontend, you can run these command in two terminals:
But sometimes, you would like to restart the server, but not the frontend, you can run these commands in two terminals:
```
npm run start-frontend-dev
npm run start-server-dev
@@ -146,13 +146,13 @@ It is mainly a socket.io app + express.js.
express.js is used for:
- entry point such as redirecting to a status page or the dashboard
- serving the frontend built files (index.html, .js and .css etc.)
- serving internal APIs of status page
- serving internal APIs of the status page
### Structure in /server/
- jobs/ (Jobs that are running in another process)
- model/ (Object model, auto mapping to the database table name)
- model/ (Object model, auto-mapping to the database table name)
- modules/ (Modified 3rd-party modules)
- monitor_types (Monitor Types)
- notification-providers/ (individual notification logic)
@@ -163,7 +163,7 @@ express.js is used for:
## Frontend Dev Server
It binds to `0.0.0.0:3000` by default. Frontend dev server is used for development only.
It binds to `0.0.0.0:3000` by default. The frontend dev server is used for development only.
For production, it is not used. It will be compiled to `dist` directory instead.
@@ -181,7 +181,7 @@ Uptime Kuma Frontend is a single page application (SPA). Most paths are handled
The router is in `src/router.js`
As you can see, most data in frontend is stored in root level, even though you changed the current router to any other pages.
As you can see, most data in the frontend is stored at the root level, even though you changed the current router to any other pages.
The data and socket logic are in `src/mixins/socket.js`.
@@ -210,15 +210,25 @@ Both frontend and backend share the same package.json. However, the frontend dep
### Update Dependencies
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update patch release version only.
Since previously updating Vite 2.5.10 to 2.6.0 broke the application completely, from now on, it should update the patch release version only.
Patch release = the third digit ([Semantic Versioning](https://semver.org/))
If for maybe security reasons, a library must be updated. Then you must need to check if there are any breaking changes.
If for security / bug / other reasons, a library must be updated, breaking changes need to be checked by the person proposing the change.
## Translations
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are omitted, they can not be translated).
**Don't include any other languages in your initial Pull-Request** (even if this is your mother tongue), to avoid merge-conflicts between weblate and `master`.
The translations can then (after merging a PR into `master`) be translated by awesome people donating their language skills.
If you want to help by translating Uptime Kuma into your language, please visit the [instructions on how to translate using weblate](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
## Spelling & Grammar
Feel free to correct the grammar in the documentation or code.
My mother language is not English and my grammar is not that great.
## Wiki

View File

@@ -1,16 +1,16 @@
<div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" />
</div>
# Uptime Kuma
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
[![GitHub Sponsors](https://img.shields.io/github/sponsors/louislam?label=GitHub%20Sponsors)](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
</a>
<div align="center" width="100%">
<img src="./public/icon.svg" width="128" alt="" />
</div>
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
## 🥔 Live Demo
@@ -23,10 +23,10 @@ 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
* 20-second intervals
* [Multi Languages](https://github.com/louislam/uptime-kuma/tree/master/src/lang)
* Multiple status pages
* Map status pages to specific domains
@@ -49,14 +49,14 @@ Uptime Kuma is now running on http://localhost:3001
### 💪🏻 Non-Docker
Requirements:
Requirements:
- Platform
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
- ✅ 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 is not supported)
- [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
@@ -70,8 +70,8 @@ npm run setup
# Option 1. Try it
node server/server.js
# (Recommended) Option 2. Run in background using PM2
# Install PM2 if you don't have it:
# (Recommended) Option 2. Run in the background using PM2
# Install PM2 if you don't have it:
npm install pm2 -g && pm2 install pm2-logrotate
# Start Server
@@ -93,7 +93,7 @@ pm2 save && pm2 startup
### Windows Portable (x64)
https://github.com/louislam/uptime-kuma/releases/download/1.21.0/uptime-kuma-win64-portable-1.0.0.zip
https://github.com/louislam/uptime-kuma/releases/download/1.23.1/uptime-kuma-windows-x64-portable-1.23.1.zip
### Advanced Installation
@@ -109,7 +109,7 @@ https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
## 🆕 What's Next?
I will mark requests/issues to the next milestone.
I will assign requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones
@@ -184,7 +184,10 @@ If you want to report a bug or request a new feature, feel free to open a [new i
### Translations
If you want to translate Uptime Kuma into your language, please visit [Weblate Readme](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
## Spelling & Grammar
Feel free to correct the grammar in the documentation or code.
My mother language is not english and my grammar is not that great.
### Create Pull Requests
If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md

View File

@@ -3,19 +3,19 @@
## Reporting a Vulnerability
1. Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
1. Please also create a empty security issues for alerting me, as GitHub Advisory do not send a notification, I probably will miss without this. https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md
1. Please also create an empty security issue to alert me, as GitHub Advisories do not send a notification, I probably will miss it without this. https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md
Do not use the public issue tracker or discuss it in the public as it will cause more damage.
Do not use the public issue tracker or discuss it in public as it will cause more damage.
## Do you accept other 3rd-party bug bounty platforms?
At this moment, I DO NOT accept other bug bounty platforms, because I am not familiar with these platforms and someone have tried to send a phishing link to me by this already. To minimize my own risk, please report through GitHub Advisories only. I will ignore all 3rd-party bug bounty platforms emails.
At this moment, I DO NOT accept other bug bounty platforms, because I am not familiar with these platforms and someone has tried to send a phishing link to me by doing this already. To minimize my own risk, please report through GitHub Advisories only. I will ignore all 3rd-party bug bounty platforms emails.
## Supported Versions
### Uptime Kuma Versions
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the lastest version.
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` versions are upgradable to the latest version.
### Upgradable Docker Tags

View File

@@ -4,8 +4,4 @@ if (process.env.TEST_FRONTEND) {
config.presets = [ "@babel/preset-env" ];
}
if (process.env.TEST_BACKEND) {
config.plugins = [ "babel-plugin-rewire" ];
}
module.exports = config;

View File

@@ -16,6 +16,9 @@ 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: [
vue(),
@@ -42,6 +45,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 status_page
ADD show_certificate_expiry BOOLEAN default 0 NOT NULL;
COMMIT;

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 gamedig_given_port_only BOOLEAN default 1 not null;
COMMIT;

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 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD timeout DOUBLE default 0 not null;
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

@@ -0,0 +1,34 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
-- Rename COLUMNs to another one (suffixed by `_old`)
ALTER TABLE monitor
RENAME COLUMN kafka_producer_ssl TO kafka_producer_ssl_old;
ALTER TABLE monitor
RENAME COLUMN kafka_producer_allow_auto_topic_creation TO kafka_producer_allow_auto_topic_creation_old;
-- Add correct COLUMNs
ALTER TABLE monitor
ADD COLUMN kafka_producer_ssl BOOLEAN default 0 NOT NULL;
ALTER TABLE monitor
ADD COLUMN kafka_producer_allow_auto_topic_creation BOOLEAN default 0 NOT NULL;
-- These SQL is still not fully safe. See https://github.com/louislam/uptime-kuma/issues/4039.
-- Set bring old values from `_old` COLUMNs to correct ones
-- UPDATE monitor SET kafka_producer_allow_auto_topic_creation = monitor.kafka_producer_allow_auto_topic_creation_old
-- WHERE monitor.kafka_producer_allow_auto_topic_creation_old IS NOT NULL;
-- UPDATE monitor SET kafka_producer_ssl = monitor.kafka_producer_ssl_old
-- WHERE monitor.kafka_producer_ssl_old IS NOT NULL;
-- Remove old COLUMNs
ALTER TABLE monitor
DROP COLUMN kafka_producer_allow_auto_topic_creation_old;
ALTER TABLE monitor
DROP COLUMN kafka_producer_ssl_old;
COMMIT;

View File

@@ -0,0 +1,19 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD oauth_client_id TEXT default null;
ALTER TABLE monitor
ADD oauth_client_secret TEXT default null;
ALTER TABLE monitor
ADD oauth_token_url TEXT default null;
ALTER TABLE monitor
ADD oauth_scopes TEXT default null;
ALTER TABLE monitor
ADD oauth_auth_method TEXT default null;
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;
-- SQLite: Change the data type of the column "config" from VARCHAR to TEXT
ALTER TABLE notification RENAME COLUMN config TO config_old;
ALTER TABLE notification ADD COLUMN config TEXT;
UPDATE notification SET config = config_old;
ALTER TABLE notification DROP COLUMN config_old;
COMMIT;

7
db/patch-timeout.sql Normal file
View File

@@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
UPDATE monitor SET timeout = (interval * 0.8)
WHERE timeout IS NULL OR timeout <= 0;
COMMIT;

View File

@@ -1,17 +1,33 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too
FROM node:16-buster-slim
# DON'T UPDATE TO bullseye-slim, see #372.
# There is no 20-buster-slim for armv7 unfortunately, 18-buster-slim is the last one for Uptime Kuma v1.
FROM node:18-buster-slim
ARG TARGETPLATFORM
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!
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
# python3* = apprise's dependencies
# sqlite3 = for debugging
# iputils-ping = for ping
# util-linux = for setpriv (Should be dropped in 2.0.0?)
# dumb-init = avoid zombie processes (#480)
# curl = for debugging
# ca-certificates = keep the cert up-to-date
# sudo = for start service nscd with non-root user
# nscd = for better DNS caching
# (pip) apprise = for notifications
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 && \
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 \
curl \
ca-certificates \
sudo \
nscd && \
pip3 --no-cache-dir install apprise==1.6.0 && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
@@ -26,3 +42,7 @@ RUN set -eux && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
# For nscd
COPY ./docker/etc/nscd.conf /etc/nscd.conf
COPY ./docker/etc/sudoers /etc/sudoers

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

90
docker/etc/nscd.conf Normal file
View File

@@ -0,0 +1,90 @@
#
# /etc/nscd.conf
#
# An example Name Service Cache config file. This file is needed by nscd.
#
# Legal entries are:
#
# logfile <file>
# debug-level <level>
# threads <initial #threads to use>
# max-threads <maximum #threads to use>
# server-user <user to run server as instead of root>
# server-user is ignored if nscd is started with -S parameters
# stat-user <user who is allowed to request statistics>
# reload-count unlimited|<number>
# paranoia <yes|no>
# restart-interval <time in seconds>
#
# enable-cache <service> <yes|no>
# positive-time-to-live <service> <time in seconds>
# negative-time-to-live <service> <time in seconds>
# suggested-size <service> <prime number>
# check-files <service> <yes|no>
# persistent <service> <yes|no>
# shared <service> <yes|no>
# max-db-size <service> <number bytes>
# auto-propagate <service> <yes|no>
#
# Currently supported cache names (services): passwd, group, hosts, services
#
# logfile /var/log/nscd.log
# threads 4
# max-threads 32
# server-user node
# stat-user somebody
debug-level 0
# reload-count 5
paranoia no
# restart-interval 3600
enable-cache passwd no
positive-time-to-live passwd 600
negative-time-to-live passwd 20
suggested-size passwd 211
check-files passwd yes
persistent passwd yes
shared passwd yes
max-db-size passwd 33554432
auto-propagate passwd yes
enable-cache group no
positive-time-to-live group 3600
negative-time-to-live group 60
suggested-size group 211
check-files group yes
persistent group yes
shared group yes
max-db-size group 33554432
auto-propagate group yes
enable-cache hosts yes
positive-time-to-live hosts 3600
negative-time-to-live hosts 20
suggested-size hosts 211
check-files hosts yes
persistent hosts yes
# Set shared to "no" to display stats in `nscd -g`
# Read more: https://stackoverflow.com/questions/40429245/nscdcentos7curl-0-dns-cache-hit-rate
shared hosts no
max-db-size hosts 33554432
enable-cache services no
positive-time-to-live services 28800
negative-time-to-live services 20
suggested-size services 211
check-files services yes
persistent services yes
shared services yes
max-db-size services 33554432
enable-cache netgroup no
positive-time-to-live netgroup 28800
negative-time-to-live netgroup 20
suggested-size netgroup 211
check-files netgroup yes
persistent netgroup yes
shared netgroup yes
max-db-size netgroup 33554432

31
docker/etc/sudoers Normal file
View File

@@ -0,0 +1,31 @@
#
# This file MUST be edited with the 'visudo' command as root.
#
# Please consider adding local content in /etc/sudoers.d/ instead of
# directly modifying this file.
#
# See the man page for details on how to write a sudoers file.
#
Defaults env_reset
Defaults mail_badpass
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Host alias specification
# User alias specification
# Cmnd alias specification
# User privilege specification
root ALL=(ALL:ALL) ALL
# Allow members of group sudo to execute any command
%sudo ALL=(ALL:ALL) ALL
# See sudoers(5) for more information on "#include" directives:
#includedir /etc/sudoers.d
# Allow `node` to control service (mainly for nscd)
node ALL=(root) NOPASSWD: /usr/sbin/nscdservice
node ALL=(root) NOPASSWD: /usr/sbin/service

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

View File

@@ -6,7 +6,7 @@
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
* This script should be run after a period of time (180s), because the server may need some time to prepare.
*/
const { FBSD } = require("../server/util-server");
const FBSD = /^freebsd/.test(process.platform);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

View File

@@ -5,15 +5,15 @@
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
println("=====================");
println("Uptime Kuma Installer");
println("Uptime Kuma Install Script");
println("=====================");
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
println("Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8");
println("---------------------------------------");
println("This script is designed for Linux and basic usage.");
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
println("---------------------------------------");
println("");
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
println("Local - Install Uptime Kuma on your current machine with git, Node.js and pm2");
println("Docker - Install Uptime Kuma Docker container");
println("");
@@ -29,14 +29,10 @@ function checkNode() {
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
println("Node Version: " ++ nodeVersion);
if (nodeVersion < "12") {
if (nodeVersion <= "12") {
println("Error: Required Node.js 14");
call("exit", "1");
}
if (nodeVersion == "12") {
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
}
}
function deb() {
@@ -60,8 +56,8 @@ function deb() {
bash("apt --yes install curl");
}
println("Installing Node.js 14");
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
println("Installing Node.js 16");
bash("curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt");
bash("apt --yes install nodejs");
bash("node -v");
@@ -91,6 +87,10 @@ if (type == "local") {
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
if (os == "Ubuntu") {
distribution = "ubuntu";
// Get ubuntu version
bash(". /etc/lsb-release");
version = DISTRIB_RELEASE;
}
if (os == "Debian") {
distribution = "debian";
@@ -101,6 +101,7 @@ if (type == "local") {
println("Your OS: " ++ os);
println("Distribution: " ++ distribution);
println("Version: " ++ version);
println("Arch: " ++ arch);
if ("$3" != "") {
@@ -131,15 +132,32 @@ if (type == "local") {
checkNode();
} else {
bash("curlCheck=$(curl --version)");
if (curlCheck == "") {
println("Installing Curl");
bash("yum -y -q install curl");
bash("dnfCheck=$(dnf --version)");
// Use yum
if (dnfCheck == "") {
bash("curlCheck=$(curl --version)");
if (curlCheck == "") {
println("Installing Curl");
bash("yum -y -q install curl");
}
println("Installing Node.js 16");
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
bash("yum install -y -q nodejs");
} else {
bash("curlCheck=$(curl --version)");
if (curlCheck == "") {
println("Installing Curl");
bash("dnf -y install curl");
}
println("Installing Node.js 16");
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
bash("dnf install -y nodejs");
}
println("Installing Node.js 14");
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
bash("yum install -y -q nodejs");
bash("node -v");
bash("nodeCheckAgain=$(node -v)");
@@ -171,13 +189,15 @@ if (type == "local") {
bash("check=$(git --version)");
if (check == "") {
error = 1;
println("Error: git is missing");
println("Error: git is not found!");
println("help: an installation guide is available at https://git-scm.com/book/en/v2/Getting-Started-Installing-Git");
}
bash("check=$(node -v)");
if (check == "") {
error = 1;
println("Error: node is missing");
println("Error: node is not found");
println("help: an installation guide is available at https://nodejs.org/en/download");
}
if (error > 0) {
@@ -193,6 +213,15 @@ if (type == "local") {
bash("pm2 startup");
}
// Check again
bash("check=$(pm2 --version)");
if (check == "") {
println("Error: pm2 is not found!");
println("help: an installation guide is available at https://pm2.keymetrics.io/docs/usage/quick-start/");
bash("exit 1");
}
bash("mkdir -p $installPath");
bash("cd $installPath");
bash("git clone https://github.com/louislam/uptime-kuma.git .");
@@ -206,6 +235,7 @@ if (type == "local") {
bash("check=$(docker -v)");
if (check == "") {
println("Error: docker is not found!");
println("help: an installation guide is available at https://docs.docker.com/desktop/");
bash("exit 1");
}
@@ -213,6 +243,7 @@ if (type == "local") {
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then
\"echo\" \"Error: docker is not running\"
\"echo\" \"help: a troubleshooting guide is available at https://docs.docker.com/config/daemon/troubleshoot/\"
\"exit\" \"1\"
fi");

View File

@@ -0,0 +1,44 @@
// Generate on GitHub
const input = `
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
`;
const template = `
### 🆕 New Features
### 💇‍♀️ Improvements
### 🐞 Bug Fixes
### ⬆️ Security Fixes
### 🦎 Translation Contributions
### Others
- Other small changes, code refactoring and comment/doc updates in this repo:
`;
const lines = input.split("\n").filter((line) => line.trim() !== "");
for (const line of lines) {
// Split the last " by "
const usernamePullRequesURL = line.split(" by ").pop();
if (!usernamePullRequesURL) {
console.log("Unable to parse", line);
continue;
}
const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
const pullRequestID = "#" + pullRequestURL.split("/").pop();
let message = line.split(" by ").shift();
if (!message) {
console.log("Unable to parse", line);
continue;
}
message = message.split("* ").pop();
console.log("-", pullRequestID, message, `(Thanks ${username})`);
}
console.log(template);

View File

@@ -5,6 +5,8 @@ const { R } = require("redbean-node");
const readline = require("readline");
const { initJWTSecret } = require("../server/util-server");
const User = require("../server/model/user");
const { io } = require("socket.io-client");
const { localWebSocketURL } = require("../server/config");
const args = require("args-parser")(process.argv);
const rl = readline.createInterface({
input: process.stdin,
@@ -36,12 +38,16 @@ const main = async () => {
// Reset all sessions by reset jwt secret
await initJWTSecret();
// Disconnect all other socket clients of the user
await disconnectAllSocketClients(user.username, password);
break;
} else {
console.log("Passwords do not match, please try again.");
}
}
console.log("Password reset successfully.");
}
} catch (e) {
console.error("Error: " + e.message);
@@ -66,6 +72,45 @@ function question(question) {
});
}
function disconnectAllSocketClients(username, password) {
return new Promise((resolve) => {
console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients");
// Disconnect all socket connections
const socket = io(localWebSocketURL, {
transports: [ "websocket" ],
reconnection: false,
timeout: 5000,
});
socket.on("connect", () => {
socket.emit("login", {
username,
password,
}, (res) => {
if (res.ok) {
console.log("Logged in.");
socket.emit("disconnectOtherSocketClients");
} else {
console.warn("Login failed.");
console.warn("Please restart the server to disconnect all sessions.");
}
socket.close();
});
});
socket.on("connect_error", function () {
// The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
// Ask the user to restart the server manually
console.warn("Failed to connect to " + localWebSocketURL);
console.warn("Please restart the server to disconnect all sessions manually.");
resolve();
});
socket.on("disconnect", () => {
resolve();
});
});
}
if (!process.env.TEST_BACKEND) {
main();
}

View File

@@ -9,8 +9,24 @@
<meta name="theme-color" id="theme-color" content="" />
<meta name="description" content="Uptime Kuma monitoring tool" />
<title>Uptime Kuma</title>
<style>
.noscript-message {
font-size: 20px;
text-align: center;
padding: 10px;
max-width: 500px;
margin: 0 auto;
}
</style>
</head>
<body>
<noscript>
<div class="noscript-message">
Sorry, you don't seem to have JavaScript enabled or your browser
doesn't support it.<br />This website requires JavaScript to function.
Please enable JavaScript in your browser settings to continue.
</div>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

View File

@@ -3,15 +3,15 @@
# The command is working on Windows PowerShell and Docker for Windows only.
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
"echo" "-e" "====================="
"echo" "-e" "Uptime Kuma Installer"
"echo" "-e" "Uptime Kuma Install Script"
"echo" "-e" "====================="
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
"echo" "-e" "Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8"
"echo" "-e" "---------------------------------------"
"echo" "-e" "This script is designed for Linux and basic usage."
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
"echo" "-e" "---------------------------------------"
"echo" "-e" ""
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
"echo" "-e" "Local - Install Uptime Kuma on your current machine with git, Node.js and pm2"
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
"echo" "-e" ""
if [ "$1" != "" ]; then
@@ -25,12 +25,9 @@ function checkNode {
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
"echo" "-e" "Node Version: ""$nodeVersion"
_0="12"
if [ $(($nodeVersion < $_0)) == 1 ]; then
if [ $(($nodeVersion <= $_0)) == 1 ]; then
"echo" "-e" "Error: Required Node.js 14"
"exit" "1"
fi
if [ "$nodeVersion" == "12" ]; then
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
fi
}
function deb {
@@ -50,8 +47,8 @@ fi
"echo" "-e" "Installing Curl"
apt --yes install curl
fi
"echo" "-e" "Installing Node.js 14"
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
"echo" "-e" "Installing Node.js 16"
curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt
apt --yes install nodejs
node -v
nodeCheckAgain=$(node -v)
@@ -75,7 +72,10 @@ if [ "$type" == "local" ]; then
if [ -e "/etc/issue" ]; then
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
if [ "$os" == "Ubuntu" ]; then
distribution="ubuntu"
distribution="ubuntu"
# Get ubuntu version
. /etc/lsb-release
version="$DISTRIB_RELEASE"
fi
if [ "$os" == "Debian" ]; then
distribution="debian"
@@ -85,6 +85,7 @@ fi
arch=$(uname -i)
"echo" "-e" "Your OS: ""$os"
"echo" "-e" "Distribution: ""$distribution"
"echo" "-e" "Version: ""$version"
"echo" "-e" "Arch: ""$arch"
if [ "$3" != "" ]; then
port="$3"
@@ -108,14 +109,27 @@ fi
if [ "$nodeCheck" != "" ]; then
"checkNode"
else
curlCheck=$(curl --version)
if [ "$curlCheck" == "" ]; then
"echo" "-e" "Installing Curl"
yum -y -q install curl
dnfCheck=$(dnf --version)
# Use yum
if [ "$dnfCheck" == "" ]; then
curlCheck=$(curl --version)
if [ "$curlCheck" == "" ]; then
"echo" "-e" "Installing Curl"
yum -y -q install curl
fi
"echo" "-e" "Installing Node.js 14"
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
yum install -y -q nodejs
"echo" "-e" "Installing Node.js 16"
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
yum install -y -q nodejs
else
curlCheck=$(curl --version)
if [ "$curlCheck" == "" ]; then
"echo" "-e" "Installing Curl"
dnf -y install curl
fi
"echo" "-e" "Installing Node.js 16"
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
dnf install -y nodejs
fi
node -v
nodeCheckAgain=$(node -v)
if [ "$nodeCheckAgain" == "" ]; then
@@ -142,12 +156,14 @@ fi
check=$(git --version)
if [ "$check" == "" ]; then
error=$((1))
"echo" "-e" "Error: git is missing"
"echo" "-e" "Error: git is not found!"
"echo" "-e" "help: an installation guide is available at https://git-scm.com/book/en/v2/Getting-Started-Installing-Git"
fi
check=$(node -v)
if [ "$check" == "" ]; then
error=$((1))
"echo" "-e" "Error: node is missing"
"echo" "-e" "Error: node is not found"
"echo" "-e" "help: an installation guide is available at https://nodejs.org/en/download"
fi
if [ $(($error > 0)) == 1 ]; then
"echo" "-e" "Please install above missing software"
@@ -161,6 +177,13 @@ fi
"echo" "-e" "Installing PM2"
npm install pm2 -g && pm2 install pm2-logrotate
pm2 startup
fi
# Check again
check=$(pm2 --version)
if [ "$check" == "" ]; then
"echo" "-e" "Error: pm2 is not found!"
"echo" "-e" "help: an installation guide is available at https://pm2.keymetrics.io/docs/usage/quick-start/"
exit 1
fi
mkdir -p $installPath
cd $installPath
@@ -172,11 +195,13 @@ else
check=$(docker -v)
if [ "$check" == "" ]; then
"echo" "-e" "Error: docker is not found!"
"echo" "-e" "help: an installation guide is available at https://docs.docker.com/desktop/"
exit 1
fi
check=$(docker info)
if [[ "$check" == *"Is the docker daemon running"* ]]; then
"echo" "Error: docker is not running"
"echo" "help: a troubleshooting guide is available at https://docs.docker.com/config/daemon/troubleshoot/"
"exit" "1"
fi
if [ "$3" != "" ]; then

7503
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,27 @@
{
"name": "uptime-kuma",
"version": "1.22.1",
"version": "1.23.9",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/louislam/uptime-kuma.git"
},
"engines": {
"node": "14.* || 16.* || 18.*"
"node": "14 || 16 || 18 || >= 20.4.0"
},
"scripts": {
"install-legacy": "npm install",
"update-legacy": "npm update",
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint:js-prod": "npm run lint:js -- --max-warnings 0",
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style",
"lint:prod": "npm run lint:js-prod && 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",
@@ -39,19 +42,25 @@
"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.22.1 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.23.9 && 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",
"remove-2fa": "node extra/remove-2fa.js",
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
"test-install-script-rockylinux": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/rockylinux.dockerfile .",
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
"test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile .",
"test-install-script-debian-buster": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian-buster.dockerfile .",
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
"test-install-script-ubuntu1804": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1804.dockerfile .",
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
"simple-dns-server": "node extra/simple-dns-server.js",
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
"simple-mongo": "docker run --rm -p 27017:27017 mongo",
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
"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/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",
@@ -65,11 +74,12 @@
"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",
"deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js"
"sort-contributors": "node extra/sort-contributors.js",
"start-server-node14-win": "private\\node14\\node.exe server/server.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",
@@ -91,43 +101,54 @@
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7",
"form-data": "~4.0.0",
"gamedig": "~4.0.5",
"gamedig": "^4.2.0",
"html-escaper": "^3.0.3",
"http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
"iconv-lite": "~0.6.3",
"isomorphic-ws": "^5.0.0",
"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",
"mongodb": "~4.14.0",
"liquidjs": "^10.7.0",
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mysql2": "~2.3.3",
"mysql2": "~3.6.2",
"nanoid": "~3.3.4",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
"nostr-tools": "^1.13.1",
"notp": "~2.0.3",
"openid-client": "^5.4.2",
"password-hash": "~1.2.2",
"pg": "~8.8.0",
"pg-connection-string": "~2.5.0",
"pg": "~8.11.3",
"pg-connection-string": "~2.6.2",
"playwright-core": "~1.35.1",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1",
"protobufjs": "~7.1.1",
"promisify-child-process": "~4.1.2",
"protobufjs": "~7.2.4",
"qs": "~6.10.4",
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"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",
"thirty-two": "~1.0.2"
"thirty-two": "~1.0.2",
"ws": "^8.13.0"
},
"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",
@@ -135,12 +156,11 @@
"@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": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4",
@@ -148,16 +168,16 @@
"core-js": "~3.26.1",
"cronstrue": "~2.24.0",
"cross-env": "~7.0.3",
"cypress": "^10.1.0",
"cypress": "^13.2.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",
@@ -165,15 +185,15 @@
"qrcode": "~1.5.0",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "~1.42.1",
"stylelint": "~15.9.0",
"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.2.7",
"vite": "~4.4.1",
"vite-plugin-compression": "^0.5.1",
"vue": "~3.2.47",
"vue": "~3.3.4",
"vue-chartjs": "~5.2.0",
"vue-confirm-dialog": "~1.0.2",
"vue-contenteditable": "~3.0.4",
@@ -185,7 +205,7 @@
"vue-router": "~4.0.14",
"vue-toastification": "~2.0.0-rc.5",
"vuedraggable": "~4.1.0",
"wait-on": "^6.0.1",
"wait-on": "^7.2.0",
"whatwg-url": "~12.0.1"
}
}

View File

@@ -1,10 +1,9 @@
<svg width="640" height="640" viewBox="0 0 640 640" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" fill="url(#paint0_linear_381_799)"/>
<path d="M490.4 235.64C544.09 358.38 544.09 435.34 490.4 466.5C409.85 513.24 199.96 527.49 139.54 455.64C99.2601 407.74 99.2601 334.4 139.54 235.64C180.5 168.18 238.71 134.45 314.17 134.45C389.64 134.45 448.38 168.18 490.4 235.64Z" stroke="#F2F2F2" stroke-opacity="0.51" stroke-width="200"/>
<defs>
<linearGradient id="paint0_linear_381_799" x1="259.78" y1="261.15" x2="463.85" y2="456.49" gradientUnits="userSpaceOnUse">
<svg width="640" height="640" viewBox="0 0 640 640" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 320 320)">
<linearGradient id="S3" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1 0 0 1 -319.99875 -320.0001577393)" x1="259.78" y1="261.15" x2="463.85" y2="456.49">
<stop stop-color="#5CDD8B"/>
<stop offset="1" stop-color="#86E6A9"/>
</linearGradient>
</defs>
<path style="stroke: rgb(242,242,242); stroke-opacity: 0.51; stroke-width: 200; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#S3); fill-rule: nonzero; opacity: 1;" transform=" translate(0, 0)" d="M 170.40125 -84.36016 C 224.09125 38.37984 224.09125 115.33984 170.40125 146.49984 C 89.85125000000001 193.23984000000002 -120.03875 207.48984000000002 -180.45875 135.63984 C -220.73875 87.73983999999999 -220.73875 14.399839999999998 -180.45875 -84.36016000000001 C -139.49875 -151.82016 -81.28875000000001 -185.55016 -5.828750000000014 -185.55016 C 69.64124999999999 -185.55016 128.38125 -151.82016000000002 170.40124999999998 -84.36016000000001 z" stroke-linecap="round" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 893 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -141,12 +141,24 @@ 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;
let isContainer;
if (!hideVersion) {
version = checkVersion.version;
latestVersion = checkVersion.latestVersion;
isContainer = (process.env.UPTIME_KUMA_IS_CONTAINER === "1");
}
socket.emit("info", {
version: checkVersion.version,
latestVersion: checkVersion.latestVersion,
version,
latestVersion,
isContainer,
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),

View File

@@ -1,28 +1,42 @@
const args = require("args-parser")(process.argv);
const demoMode = args["demo"] || false;
const isFreeBSD = /^freebsd/.test(process.platform);
const badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue", // as defined by badge-maker / shields.io
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
defaultCertExpValueSuffix: " days",
defaultCertExpLabelSuffix: "h",
// Values Come From Default Notification Times
defaultCertExpireWarnDays: "14",
defaultCertExpireDownDays: "7"
};
// Interop with browser
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
let hostEnv = isFreeBSD ? null : process.env.HOST;
const hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
.map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue));
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
const isSSL = sslKey && sslCert;
function getLocalWebSocketURL() {
const protocol = isSSL ? "wss" : "ws";
const host = hostname || "localhost";
return `${protocol}://${host}:${port}`;
}
const localWebSocketURL = getLocalWebSocketURL();
const demoMode = args["demo"] || false;
module.exports = {
args,
hostname,
port,
sslKey,
sslCert,
sslKeyPassphrase,
isSSL,
localWebSocketURL,
demoMode,
badgeConstants,
};

View File

@@ -3,7 +3,7 @@ const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util");
const knex = require("knex");
const path = require("path");
/**
* Database & App Data Folder
@@ -22,8 +22,12 @@ class Database {
*/
static uploadDir;
static screenshotDir;
static path;
static dockerTLSDir;
/**
* @type {boolean}
*/
@@ -70,6 +74,16 @@ class Database {
"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,
"patch-add-certificate-expiry-status-page.sql": true,
"patch-monitor-oauth-cc.sql": true,
"patch-add-timeout-monitor.sql": true,
"patch-add-gamedig-given-port.sql": true,
"patch-notification-config.sql": true,
"patch-fix-kafka-producer-booleans.sql": true,
"patch-timeout.sql": true,
};
/**
@@ -88,17 +102,28 @@ class Database {
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
Database.path = Database.dataDir + "kuma.db";
Database.path = path.join(Database.dataDir, "kuma.db");
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });
}
Database.uploadDir = Database.dataDir + "upload/";
Database.uploadDir = path.join(Database.dataDir, "upload/");
if (! fs.existsSync(Database.uploadDir)) {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
// Create screenshot dir
Database.screenshotDir = path.join(Database.dataDir, "screenshots/");
if (! fs.existsSync(Database.screenshotDir)) {
fs.mkdirSync(Database.screenshotDir, { recursive: true });
}
Database.dockerTLSDir = path.join(Database.dataDir, "docker-tls/");
if (! fs.existsSync(Database.dockerTLSDir)) {
fs.mkdirSync(Database.dockerTLSDir, { recursive: true });
}
log.info("db", `Data Dir: ${Database.dataDir}`);
}
@@ -155,12 +180,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:");

View File

@@ -2,8 +2,16 @@ const axios = require("axios");
const { R } = require("redbean-node");
const version = require("../package.json").version;
const https = require("https");
const fs = require("fs");
const path = require("path");
const Database = require("./database");
class DockerHost {
static CertificateFileNameCA = "ca.pem";
static CertificateFileNameCert = "cert.pem";
static CertificateFileNameKey = "key.pem";
/**
* Save a docker host
* @param {Object} dockerHost Docker host to save
@@ -66,16 +74,13 @@ class DockerHost {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: false,
}),
};
if (dockerHost.dockerType === "socket") {
options.socketPath = dockerHost.dockerDaemon;
} else if (dockerHost.dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
}
let res = await axios.request(options);
@@ -111,6 +116,53 @@ class DockerHost {
}
return url;
}
/**
* Returns HTTPS agent options with client side TLS parameters if certificate files
* for the given host are available under a predefined directory path.
*
* The base path where certificates are looked for can be set with the
* 'DOCKER_TLS_DIR_PATH' environmental variable or defaults to 'data/docker-tls/'.
*
* If a directory in this path exists with a name matching the FQDN of the docker host
* (e.g. the FQDN of 'https://example.com:2376' is 'example.com' so the directory
* 'data/docker-tls/example.com/' would be searched for certificate files),
* then 'ca.pem', 'key.pem' and 'cert.pem' files are included in the agent options.
* File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
*
* @param {String} dockerType i.e. "tcp" or "socket"
* @param {String} url The docker host URL rewritten to https://
* @return {Object}
* */
static getHttpsAgentOptions(dockerType, url) {
let baseOptions = {
maxCachedSessions: 0,
rejectUnauthorized: true
};
let certOptions = {};
let dirName = (new URL(url)).hostname;
let caPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCA);
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
let ca = fs.readFileSync(caPath);
let key = fs.readFileSync(keyPath);
let cert = fs.readFileSync(certPath);
certOptions = {
ca,
key,
cert
};
}
return {
...baseOptions,
...certOptions
};
}
}
module.exports = {

View File

@@ -1,4 +1,5 @@
const jsesc = require("jsesc");
const { escape } = require("html-escaper");
/**
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts
@@ -7,15 +8,18 @@ const jsesc = require("jsesc");
* @returns {string}
*/
function getGoogleAnalyticsScript(tagId) {
let escapedTagId = jsesc(tagId, { isScriptContext: true });
let escapedTagIdJS = jsesc(tagId, { isScriptContext: true });
if (escapedTagId) {
escapedTagId = escapedTagId.trim();
if (escapedTagIdJS) {
escapedTagIdJS = escapedTagIdJS.trim();
}
// Escape the tag ID for use in an HTML attribute.
let escapedTagIdHTMLAttribute = escape(tagId);
return `
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagId}"></script>
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagId}'); </script>
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagIdHTMLAttribute}"></script>
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagIdJS}'); </script>
`;
}

View File

@@ -1,5 +1,6 @@
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 = [
@@ -9,6 +10,12 @@ const jobs = [
jobFunc: clearOldData,
croner: null,
},
{
name: "incremental-vacuum",
interval: "*/5 * * * *",
jobFunc: incrementalVacuum,
croner: null,
}
];
/**

View File

@@ -39,6 +39,8 @@ const clearOldData = async () => {
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
[ parsedPeriod ]
);
await R.exec("PRAGMA optimize;");
} catch (e) {
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
}

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

@@ -9,12 +9,12 @@ class Group extends BeanModel {
* @param {boolean} [showTags=false] Should the JSON include monitor tags
* @returns {Object}
*/
async toPublicJSON(showTags = false) {
async toPublicJSON(showTags = false, certExpiry = false) {
let monitorBeanList = await this.getMonitorList();
let monitorList = [];
for (let bean of monitorBeanList) {
monitorList.push(await bean.toPublicJSON(showTags));
monitorList.push(await bean.toPublicJSON(showTags, certExpiry));
}
return {

View File

@@ -6,7 +6,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVA
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, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -20,6 +20,11 @@ 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");
const crypto = require("crypto");
const rootCertificates = rootCertificatesFingerprints();
/**
* status:
@@ -35,11 +40,12 @@ class Monitor extends BeanModel {
* Only show necessary data to public
* @returns {Object}
*/
async toPublicJSON(showTags = false) {
async toPublicJSON(showTags = false, certExpiry = false) {
let obj = {
id: this.id,
name: this.name,
sendUrl: this.sendUrl,
type: this.type,
};
if (this.sendUrl) {
@@ -49,6 +55,13 @@ class Monitor extends BeanModel {
if (showTags) {
obj.tags = await this.getTags();
}
if (certExpiry && (this.type === "http" || this.type === "keyword" || this.type === "json-query") && this.getURLProtocol() === "https:") {
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
obj.validCert = validCert;
}
return obj;
}
@@ -70,6 +83,12 @@ 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,
@@ -86,10 +105,12 @@ class Monitor extends BeanModel {
active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id),
type: this.type,
timeout: this.timeout,
interval: this.interval,
retryInterval: this.retryInterval,
resendInterval: this.resendInterval,
keyword: this.keyword,
invertKeyword: this.isInvertKeyword(),
expiryNotification: this.isEnabledExpiryNotification(),
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
@@ -117,7 +138,16 @@ class Monitor extends BeanModel {
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
game: this.game,
httpBodyEncoding: this.httpBodyEncoding
gamedigGivenPortOnly: this.getGameDigGivenPortOnly(),
httpBodyEncoding: this.httpBodyEncoding,
jsonPath: this.jsonPath,
expectedValue: this.expectedValue,
kafkaProducerTopic: this.kafkaProducerTopic,
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
kafkaProducerSsl: this.getKafkaProducerSsl(),
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
};
if (includeSensitiveData) {
@@ -129,6 +159,11 @@ class Monitor extends BeanModel {
grpcMetadata: this.grpcMetadata,
basic_auth_user: this.basic_auth_user,
basic_auth_pass: this.basic_auth_pass,
oauth_client_id: this.oauth_client_id,
oauth_client_secret: this.oauth_client_secret,
oauth_token_url: this.oauth_token_url,
oauth_scopes: this.oauth_scopes,
oauth_auth_method: this.oauth_auth_method,
pushToken: this.pushToken,
databaseConnectionString: this.databaseConnectionString,
radiusUsername: this.radiusUsername,
@@ -141,6 +176,7 @@ class Monitor extends BeanModel {
tlsCa: this.tlsCa,
tlsCert: this.tlsCert,
tlsKey: this.tlsKey,
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
};
}
@@ -155,7 +191,7 @@ class Monitor extends BeanModel {
async isActive() {
const parentActive = await Monitor.isParentActive(this.id);
return this.active && parentActive;
return (this.active === 1) && parentActive;
}
/**
@@ -166,6 +202,31 @@ class Monitor extends BeanModel {
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]);
}
/**
* Gets certificate expiry for this monitor
* @param {number} monitorID ID of monitor to send
* @returns {Promise<LooseObject<any>>}
*/
async getCertExpiry(monitorID) {
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
monitorID,
]);
let tlsInfo;
if (tlsInfoBean) {
tlsInfo = JSON.parse(tlsInfoBean?.info_json);
if (tlsInfo?.valid && tlsInfo?.certInfo?.daysRemaining) {
return {
certExpiryDaysRemaining: tlsInfo.certInfo.daysRemaining,
validCert: true
};
}
}
return {
certExpiryDaysRemaining: "",
validCert: false
};
}
/**
* Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617
@@ -199,6 +260,14 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown);
}
/**
* Parse to boolean
* @returns {boolean}
*/
isInvertKeyword() {
return Boolean(this.invertKeyword);
}
/**
* Parse to boolean
* @returns {boolean}
@@ -215,6 +284,26 @@ class Monitor extends BeanModel {
return JSON.parse(this.accepted_statuscodes_json);
}
getGameDigGivenPortOnly() {
return Boolean(this.gamedigGivenPortOnly);
}
/**
* Parse to boolean
* @returns {boolean} Kafka Producer Ssl enabled?
*/
getKafkaProducerSsl() {
return Boolean(this.kafkaProducerSsl);
}
/**
* Parse to boolean
* @returns {boolean} Kafka Producer Allow Auto Topic Creation Enabled?
*/
getKafkaProducerAllowAutoTopicCreation() {
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
}
/**
* Start monitor
* @param {Server} io Socket server instance
@@ -269,6 +358,12 @@ class Monitor extends BeanModel {
bean.duration = 0;
}
// Runtime patch timeout if it is 0
// See https://github.com/louislam/uptime-kuma/pull/3961#issuecomment-1804149144
if (!this.timeout || this.timeout <= 0) {
this.timeout = this.interval * 1000 * 0.8;
}
try {
if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance";
@@ -287,7 +382,10 @@ class Monitor extends BeanModel {
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)) {
// lastBeat.status could be null
if (!lastBeat) {
bean.status = PENDING;
} else 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;
@@ -303,7 +401,7 @@ class Monitor extends BeanModel {
bean.msg = "Group empty";
}
} else if (this.type === "http" || this.type === "keyword") {
} 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();
@@ -315,9 +413,28 @@ class Monitor extends BeanModel {
};
}
// OIDC: Basic client credential flow.
// Additional grants might be implemented in the future
let oauth2AuthHeader = {};
if (this.auth_method === "oauth2-cc") {
try {
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new one`);
this.oauthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken.expires_at * 1000)}`);
}
oauth2AuthHeader = {
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
};
} catch (e) {
throw new Error("The oauth config is invalid. " + e.message);
}
}
const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
};
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
@@ -343,18 +460,20 @@ class Monitor extends BeanModel {
const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
timeout: this.interval * 1000 * 0.8,
timeout: this.timeout * 1000,
headers: {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"User-Agent": "Uptime-Kuma/" + version,
...(contentType ? { "Content-Type": contentType } : {}),
...(basicAuthHeader),
...(oauth2AuthHeader),
...(this.headers ? JSON.parse(this.headers) : {})
},
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
signal: axiosAbortSignal((this.timeout + 10) * 1000),
};
if (bodyValue) {
@@ -431,7 +550,7 @@ class Monitor extends BeanModel {
if (this.type === "http") {
bean.status = UP;
} else {
} else if (this.type === "keyword") {
let data = res.data;
@@ -440,17 +559,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, " ").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") {
@@ -469,13 +608,13 @@ class Monitor extends BeanModel {
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type);
bean.ping = dayjs().valueOf() - startTime;
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT") {
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT" || this.dns_resolve_type === "PTR") {
dnsMessage += "Records: ";
dnsMessage += dnsRes.join(" | ");
} else if (this.dns_resolve_type === "CNAME" || this.dns_resolve_type === "PTR") {
dnsMessage = dnsRes[0];
} else if (this.dns_resolve_type === "CNAME") {
dnsMessage += dnsRes[0];
} else if (this.dns_resolve_type === "CAA") {
dnsMessage = dnsRes[0].issue;
dnsMessage += dnsRes[0].issue;
} else if (this.dns_resolve_type === "MX") {
dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
@@ -493,7 +632,7 @@ class Monitor extends BeanModel {
dnsMessage = dnsMessage.slice(0, -2);
}
if (this.dnsLastResult !== dnsMessage) {
if (this.dnsLastResult !== dnsMessage && dnsMessage !== undefined) {
R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [
dnsMessage,
this.id
@@ -525,7 +664,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 {
@@ -542,7 +681,7 @@ class Monitor extends BeanModel {
}
let res = await axios.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8,
timeout: this.timeout * 1000,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
@@ -550,6 +689,7 @@ class Monitor extends BeanModel {
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
@@ -580,7 +720,7 @@ class Monitor extends BeanModel {
type: this.game,
host: this.hostname,
port: this.port,
givenPortOnly: true,
givenPortOnly: this.getGameDigGivenPortOnly(),
});
bean.msg = state.name;
@@ -592,8 +732,6 @@ class Monitor extends BeanModel {
} else if (this.type === "docker") {
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
const dockerHost = await R.load("docker_host", this.docker_host);
const options = {
url: `/containers/${this.docker_container}/json`,
timeout: this.interval * 1000 * 0.8,
@@ -604,23 +742,39 @@ class Monitor extends BeanModel {
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}),
};
const dockerHost = await R.load("docker_host", this.docker_host);
if (!dockerHost) {
throw new Error("Failed to load docker host config");
}
if (dockerHost._dockerType === "socket") {
options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent(
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
);
}
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);
}
@@ -635,7 +789,7 @@ class Monitor extends BeanModel {
} else if (this.type === "sqlserver") {
let startTime = dayjs().valueOf();
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
@@ -649,7 +803,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;
@@ -662,19 +815,20 @@ 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") {
let startTime = dayjs().valueOf();
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
@@ -682,7 +836,11 @@ class Monitor extends BeanModel {
} else if (this.type === "mysql") {
let startTime = dayjs().valueOf();
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
// Use `radius_password` as `password` field, since there are too many unnecessary fields
// TODO: rename `radius_password` to `password` later for general use
let mysqlPassword = this.radiusPassword;
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mongodb") {
@@ -707,28 +865,19 @@ class Monitor extends BeanModel {
port = this.port;
}
try {
const resp = await radius(
this.hostname,
this.radiusUsername,
this.radiusPassword,
this.radiusCalledStationId,
this.radiusCallingStationId,
this.radiusSecret,
port
);
if (resp.code) {
bean.msg = resp.code;
}
bean.status = UP;
} catch (error) {
bean.status = DOWN;
if (error.response?.code) {
bean.msg = error.response.code;
} else {
bean.msg = error.message;
}
}
const resp = await radius(
this.hostname,
this.radiusUsername,
this.radiusPassword,
this.radiusCalledStationId,
this.radiusCallingStationId,
this.radiusSecret,
port,
this.interval * 1000 * 0.4,
);
bean.msg = resp.code;
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "redis") {
let startTime = dayjs().valueOf();
@@ -740,11 +889,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");
}
@@ -761,7 +928,11 @@ class Monitor extends BeanModel {
} catch (error) {
bean.msg = error.message;
if (error?.name === "CanceledError") {
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
} else {
bean.msg = error.message;
}
// If UP come in here, it must be upside down mode
// Just reset the retries
@@ -945,6 +1116,19 @@ class Monitor extends BeanModel {
}
}
/**
* Example: http: or https:
* @returns {(null|string)}
*/
getURLProtocol() {
const url = this.getUrl();
if (url) {
return this.getUrl().protocol;
} else {
return null;
}
}
/**
* Store TLS info to database
* @param checkCertificateResult
@@ -1281,7 +1465,10 @@ class Monitor extends BeanModel {
let certInfo = tlsInfoObject.certInfo;
while (certInfo) {
let subjectCN = certInfo.subject["CN"];
if (certInfo.daysRemaining > targetDays) {
if (rootCertificates.has(certInfo.fingerprint256)) {
log.debug("monitor", `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
break;
} else 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}.`);

View File

@@ -90,6 +90,8 @@ class StatusPage extends BeanModel {
* @param {StatusPage} statusPage
*/
static async getStatusPageData(statusPage) {
const config = await statusPage.toPublicJSON();
// Incident
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
statusPage.id,
@@ -110,13 +112,13 @@ class StatusPage extends BeanModel {
]);
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags);
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
publicGroupList.push(monitorGroup);
}
// Response
return {
config: await statusPage.toPublicJSON(),
config,
incident,
publicGroupList,
maintenanceList,
@@ -234,6 +236,7 @@ class StatusPage extends BeanModel {
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
};
}
@@ -255,6 +258,7 @@ class StatusPage extends BeanModel {
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
};
}

View File

@@ -1,6 +1,8 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const passwordHash = require("../password-hash");
const { R } = require("redbean-node");
const jwt = require("jsonwebtoken");
const { shake256, SHAKE256_LENGTH } = require("../util-server");
class User extends BeanModel {
/**
@@ -27,6 +29,19 @@ class User extends BeanModel {
this.password = newPassword;
}
/**
* Create a new JWT for a user
* @param {User} user
* @param {string} jwtSecret
* @return {string}
*/
static createJWT(user, jwtSecret) {
return jwt.sign({
username: user.username,
h: shake256(user.password, SHAKE256_LENGTH),
}, jwtSecret);
}
}
module.exports = User;

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,213 @@
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",
"/snap/bin/chromium", // Ubuntu
];
} 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

@@ -0,0 +1,87 @@
const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const childProcessAsync = require("promisify-child-process");
/**
* A TailscalePing class extends the MonitorType.
* It runs Tailscale ping to monitor the status of a specific node.
*/
class TailscalePing extends MonitorType {
name = "tailscale-ping";
/**
* Checks the ping status of the URL associated with the monitor.
* It then parses the Tailscale ping command output to update the heatrbeat.
*
* @param {Object} monitor - The monitor object associated with the check.
* @param {Object} heartbeat - The heartbeat object to update.
* @throws Will throw an error if checking Tailscale ping encounters any error
*/
async check(monitor, heartbeat) {
try {
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
} catch (err) {
// trigger log function somewhere to display a notification or alert to the user (but how?)
throw new Error(`Error checking Tailscale ping: ${err}`);
}
}
/**
* Runs the Tailscale ping command to the given URL.
*
* @param {string} hostname - The hostname to ping.
* @param {number} interval
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
* @throws Will throw an error if the command execution encounters any error.
*/
async runTailscalePing(hostname, interval) {
let timeout = interval * 1000 * 0.8;
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
timeout: timeout
});
if (res.stderr && res.stderr.toString()) {
throw new Error(`Error in output: ${res.stderr.toString()}`);
}
if (res.stdout && res.stdout.toString()) {
return res.stdout.toString();
} else {
throw new Error("No output from Tailscale ping");
}
}
/**
* Parses the output of the Tailscale ping command to update the heartbeat.
*
* @param {string} tailscaleOutput - The output of the Tailscale ping command.
* @param {Object} heartbeat - The heartbeat object to update.
* @throws Will throw an eror if the output contains any unexpected string.
*/
parseTailscaleOutput(tailscaleOutput, heartbeat) {
let lines = tailscaleOutput.split("\n");
for (let line of lines) {
if (line.includes("pong from")) {
heartbeat.status = UP;
let time = line.split(" in ")[1].split(" ")[0];
heartbeat.ping = parseInt(time);
heartbeat.msg = "OK";
break;
} else if (line.includes("timed out")) {
throw new Error(`Ping timed out: "${line}"`);
// Immediately throws upon "timed out" message, the server is expected to re-call the check function
} else if (line.includes("no matching peer")) {
throw new Error(`Nonexistant or inaccessible due to ACLs: "${line}"`);
} else if (line.includes("is local Tailscale IP")) {
throw new Error(`Tailscale only works if used on other machines: "${line}"`);
} else if (line !== "") {
throw new Error(`Unexpected output: "${line}"`);
}
}
}
}
module.exports = {
TailscalePing,
};

View File

@@ -18,7 +18,7 @@ class AliyunSMS extends NotificationProvider {
status: this.statusToString(heartbeatJSON["status"]),
msg: heartbeatJSON["msg"],
});
if (this.sendSms(notification, msgBody)) {
if (await this.sendSms(notification, msgBody)) {
return okMsg;
}
} else {
@@ -28,7 +28,7 @@ class AliyunSMS extends NotificationProvider {
status: "",
msg: msg,
});
if (this.sendSms(notification, msgBody)) {
if (await this.sendSms(notification, msgBody)) {
return okMsg;
}
}
@@ -73,7 +73,8 @@ class AliyunSMS extends NotificationProvider {
if (result.data.Message === "OK") {
return true;
}
return false;
throw new Error(result.data.Message);
}
/**

View File

@@ -1,5 +1,5 @@
const NotificationProvider = require("./notification-provider");
const childProcess = require("child_process");
const childProcessAsync = require("promisify-child-process");
class Apprise extends NotificationProvider {
@@ -11,7 +11,7 @@ class Apprise extends NotificationProvider {
args.push("-t");
args.push(notification.title);
}
const s = childProcess.spawnSync("apprise", args);
const s = await childProcessAsync.spawn("apprise", args);
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";

View File

@@ -30,6 +30,7 @@ class Discord extends NotificationProvider {
break;
case "port":
case "dns":
case "gamedig":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {

View File

@@ -0,0 +1,98 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const successMessage = "Sent Successfully.";
class FlashDuty extends NotificationProvider {
name = "FlashDuty";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
try {
if (heartbeatJSON == null) {
const title = "Uptime Kuma Alert";
const monitor = {
type: "ping",
url: msg,
name: "https://flashcat.cloud"
};
return this.postNotification(notification, title, msg, monitor);
}
if (heartbeatJSON.status === UP) {
const title = "Uptime Kuma Monitor ✅ Up";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "Ok");
}
if (heartbeatJSON.status === DOWN) {
const title = "Uptime Kuma Monitor 🔴 Down";
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, notification.flashdutySeverity);
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Generate a monitor url from the monitors infomation
* @param {Object} monitorInfo Monitor details
* @returns {string|undefined}
*/
genMonitorUrl(monitorInfo) {
if (monitorInfo.type === "port" && monitorInfo.port) {
return monitorInfo.hostname + ":" + monitorInfo.port;
}
if (monitorInfo.hostname != null) {
return monitorInfo.hostname;
}
return monitorInfo.url;
}
/**
* Send the message
* @param {BeanModel} notification Message title
* @param {string} title Message
* @param {string} body Message
* @param {Object} monitorInfo Monitor details
* @param {string} eventStatus Monitor status (Info, Warning, Critical, Ok)
* @returns {string}
*/
async postNotification(notification, title, body, monitorInfo, eventStatus) {
const options = {
method: "POST",
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
headers: { "Content-Type": "application/json" },
data: {
description: `[${title}] [${monitorInfo.name}] ${body}`,
title,
event_status: eventStatus || "Info",
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
labels: monitorInfo?.tags?.reduce((acc, item) => ({ ...acc,
[item.name]: item.value
}), { resource: this.genMonitorUrl(monitorInfo) }),
}
};
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
}
let result = await axios.request(options);
if (result.status == null) {
throw new Error("FlashDuty notification failed with invalid response!");
}
if (result.status < 200 || result.status >= 300) {
throw new Error("FlashDuty notification failed with status code " + result.status);
}
if (result.statusText != null) {
return "FlashDuty notification succeed: " + result.statusText;
}
return successMessage;
}
}
module.exports = FlashDuty;

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

@@ -0,0 +1,119 @@
const { log } = require("../../src/util");
const NotificationProvider = require("./notification-provider");
const {
relayInit,
getPublicKey,
getEventHash,
getSignature,
nip04,
nip19
} = require("nostr-tools");
// polyfills for node versions
const semver = require("semver");
const nodeVersion = process.version;
if (semver.lt(nodeVersion, "16.0.0")) {
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
} else if (semver.lt(nodeVersion, "18.0.0")) {
// polyfills for node 16
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
crypto.subtle = crypto.webcrypto.subtle;
}
} else if (semver.lt(nodeVersion, "20.0.0")) {
// polyfills for node 18
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");
} else {
// polyfills for node 20
global.WebSocket = require("isomorphic-ws");
}
class Nostr extends NotificationProvider {
name = "nostr";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
// All DMs should have same timestamp
const createdAt = Math.floor(Date.now() / 1000);
const senderPrivateKey = await this.getPrivateKey(notification.sender);
const senderPublicKey = getPublicKey(senderPrivateKey);
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
// Create NIP-04 encrypted direct message event for each recipient
const events = [];
for (const recipientPublicKey of recipientsPublicKeys) {
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
let event = {
kind: 4,
pubkey: senderPublicKey,
created_at: createdAt,
tags: [[ "p", recipientPublicKey ]],
content: ciphertext,
};
event.id = getEventHash(event);
event.sig = getSignature(event, senderPrivateKey);
events.push(event);
}
// Publish events to each relay
const relays = notification.relays.split("\n");
let successfulRelays = 0;
// Connect to each relay
for (const relayUrl of relays) {
const relay = relayInit(relayUrl);
try {
await relay.connect();
successfulRelays++;
// Publish events
for (const event of events) {
relay.publish(event);
}
} catch (error) {
continue;
} finally {
relay.close();
}
}
// Report success or failure
if (successfulRelays === 0) {
throw Error("Failed to connect to any relays.");
}
return `${successfulRelays}/${relays.length} relays connected.`;
}
async getPrivateKey(sender) {
try {
const senderDecodeResult = await nip19.decode(sender);
const { data } = senderDecodeResult;
return data;
} catch (error) {
throw new Error(`Failed to get private key: ${error.message}`);
}
}
async getPublicKeys(recipients) {
const recipientsList = recipients.split("\n");
const publicKeys = [];
for (const recipient of recipientsList) {
try {
const recipientDecodeResult = await nip19.decode(recipient);
const { type, data } = recipientDecodeResult;
if (type === "npub") {
publicKeys.push(data);
} else {
throw new Error("not an npub");
}
} catch (error) {
throw new Error(`Error decoding recipient: ${error}`);
}
}
return publicKeys;
}
}
module.exports = Nostr;

View File

@@ -20,10 +20,10 @@ class Opsgenie extends NotificationProvider {
try {
switch (notification.opsgenieRegion) {
case "US":
case "us":
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
break;
case "EU":
case "eu":
opsgenieAlertsUrl = opsgenieAlertsUrlEU;
break;
default:

View File

@@ -8,7 +8,9 @@ class PushDeer extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let pushdeerlink = "https://api2.pushdeer.com/message/push";
let endpoint = "/message/push";
let serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
let pushdeerlink = `${serverUrl.trim().replace(/\/*$/, "")}${endpoint}`;
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;

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 = {
@@ -53,7 +58,7 @@ class Slack extends NotificationProvider {
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
"text": textMsg,
},
},
{

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

@@ -13,7 +13,7 @@ class SMTP extends NotificationProvider {
port: notification.smtpPort,
secure: notification.smtpSecure,
tls: {
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
}
};
@@ -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"];

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");
@@ -20,11 +21,13 @@ const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea");
const Matrix = require("./notification-providers/matrix");
const Mattermost = require("./notification-providers/mattermost");
const Nostr = require("./notification-providers/nostr");
const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush");
const OneBot = require("./notification-providers/onebot");
const Opsgenie = require("./notification-providers/opsgenie");
const PagerDuty = require("./notification-providers/pagerduty");
const FlashDuty = require("./notification-providers/flashduty");
const PagerTree = require("./notification-providers/pagertree");
const PromoSMS = require("./notification-providers/promosms");
const Pushbullet = require("./notification-providers/pushbullet");
@@ -68,6 +71,7 @@ class Notification {
new Apprise(),
new Bark(),
new ClickSendSMS(),
new SMSC(),
new DingDing(),
new Discord(),
new Feishu(),
@@ -82,11 +86,13 @@ class Notification {
new LunaSea(),
new Matrix(),
new Mattermost(),
new Nostr(),
new Ntfy(),
new Octopush(),
new OneBot(),
new Opsgenie(),
new PagerDuty(),
new FlashDuty(),
new PagerTree(),
new PromoSMS(),
new Pushbullet(),
@@ -113,7 +119,6 @@ class Notification {
new GoAlert(),
new ZohoCliq()
];
for (let item of list) {
if (! item.name) {
throw new Error("Notification provider without name");

View File

@@ -1,15 +1,21 @@
let express = require("express");
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, sendHttpError } = require("../util-server");
const {
setting,
allowDevAllOrigin,
allowAllOrigin,
percentageToColor,
filterAndJoin,
sendHttpError,
} = require("../util-server");
const { R } = require("redbean-node");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util");
const StatusPage = require("../model/status_page");
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();
@@ -22,10 +28,14 @@ router.get("/api/entry-page", async (request, response) => {
allowDevAllOrigin(response);
let result = { };
let hostname = request.hostname;
if ((await setting("trustProxy")) && request.headers["x-forwarded-host"]) {
hostname = request.headers["x-forwarded-host"];
}
if (request.hostname in StatusPage.domainMappingList) {
if (hostname in StatusPage.domainMappingList) {
result.type = "statusPageMatchedDomain";
result.statusPageSlug = StatusPage.domainMappingList[request.hostname];
result.statusPageSlug = StatusPage.domainMappingList[hostname];
} else {
result.type = "entryPage";
result.entryPage = server.entryPage;
@@ -38,7 +48,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK";
let ping = parseInt(request.query.ping) || null;
let ping = parseFloat(request.query.ping) || null;
let statusString = request.query.status || "up";
let status = (statusString === "up") ? UP : DOWN;
@@ -442,7 +452,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("../../src/util");
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,18 +15,25 @@ 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}`);
// See more: https://github.com/louislam/uptime-kuma/issues/3138
if (nodeVersion >= 20) {
console.warn("\x1b[31m%s\x1b[0m", "Warning: Uptime Kuma is currently not stable on Node.js >= 20, please use Node.js 18.");
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);
}
if (nodeVersion < requiredVersion) {
console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`);
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);
@@ -41,7 +48,13 @@ if (! process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
}
if (!process.env.UPTIME_KUMA_WS_ORIGIN_CHECK) {
process.env.UPTIME_KUMA_WS_ORIGIN_CHECK = "cors-like";
}
log.info("server", "Node Env: " + process.env.NODE_ENV);
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
log.info("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK);
log.info("server", "Importing Node libraries");
const fs = require("fs");
@@ -68,15 +81,18 @@ const notp = require("notp");
const base32 = require("thirty-two");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const server = UptimeKumaServer.getInstance(args);
const server = UptimeKumaServer.getInstance();
const io = module.exports.io = server.io;
const app = server.app;
log.info("server", "Importing this project modules");
log.debug("server", "Importing Monitor");
const Monitor = require("./model/monitor");
const User = require("./model/user");
log.debug("server", "Importing Settings");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
} = require("./util-server");
log.debug("server", "Importing Notification");
const { Notification } = require("./notification");
@@ -99,19 +115,13 @@ const passwordHash = require("./password-hash");
const checkVersion = require("./check-version");
log.info("server", "Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
let hostEnv = FBSD ? null : process.env.HOST;
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
const hostname = config.hostname;
if (hostname) {
log.info("server", "Custom hostname: " + hostname);
}
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
.map(portValue => parseInt(portValue))
.find(portValue => !isNaN(portValue));
const port = config.port;
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
@@ -148,6 +158,7 @@ const { generalSocketHandler } = require("./socket-handlers/general-socket-handl
const { Settings } = require("./settings");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const apicache = require("./modules/apicache");
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
app.use(express.json());
@@ -160,12 +171,6 @@ app.use(function (req, res, next) {
next();
});
/**
* Use for decode the auth object
* @type {null}
*/
let jwtSecret = null;
/**
* Show Setup Page
* @type {boolean}
@@ -213,6 +218,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);
@@ -267,7 +273,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");
@@ -284,7 +290,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);
@@ -293,6 +299,11 @@ let needSetup = false;
]);
if (user) {
// Check if the password changed
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
throw new Error("The token is invalid due to password change or old token");
}
log.debug("auth", "afterLogin");
afterLogin(socket, user);
log.debug("auth", "afterLogin ok");
@@ -312,9 +323,10 @@ let needSetup = false;
});
}
} catch (error) {
log.error("auth", `Invalid token. IP=${clientIP}`);
if (error.message) {
log.error("auth", error.message, `IP=${clientIP}`);
}
callback({
ok: false,
msg: "Invalid token.",
@@ -353,9 +365,7 @@ let needSetup = false;
callback({
ok: true,
token: jwt.sign({
username: data.username,
}, jwtSecret),
token: User.createJWT(user, server.jwtSecret),
});
}
@@ -383,9 +393,7 @@ let needSetup = false;
callback({
ok: true,
token: jwt.sign({
username: data.username,
}, jwtSecret),
token: User.createJWT(user, server.jwtSecret),
});
} else {
@@ -637,9 +645,16 @@ let needSetup = false;
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
// Ensure status code ranges are strings
if (!monitor.accepted_statuscodes.every((code) => typeof code === "string")) {
throw new Error("Accepted status codes are not all strings");
}
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;
@@ -650,7 +665,10 @@ let needSetup = false;
await updateMonitorNotification(bean.id, notificationIDList);
await server.sendMonitorList(socket);
await startMonitor(socket.userID, bean.id);
if (monitor.active !== false) {
await startMonitor(socket.userID, bean.id);
}
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
@@ -696,6 +714,11 @@ let needSetup = false;
removeGroupChildren = true;
}
// Ensure status code ranges are strings
if (!monitor.accepted_statuscodes.every((code) => typeof code === "string")) {
throw new Error("Accepted status codes are not all strings");
}
bean.name = monitor.name;
bean.description = monitor.description;
bean.parent = monitor.parent;
@@ -706,6 +729,12 @@ let needSetup = false;
bean.headers = monitor.headers;
bean.basic_auth_user = monitor.basic_auth_user;
bean.basic_auth_pass = monitor.basic_auth_pass;
bean.timeout = monitor.timeout;
bean.oauth_client_id = monitor.oauth_client_id;
bean.oauth_client_secret = monitor.oauth_client_secret;
bean.oauth_auth_method = monitor.oauth_auth_method;
bean.oauth_token_url = monitor.oauth_token_url;
bean.oauth_scopes = monitor.oauth_scopes;
bean.tlsCa = monitor.tlsCa;
bean.tlsCert = monitor.tlsCert;
bean.tlsKey = monitor.tlsKey;
@@ -717,6 +746,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;
@@ -751,6 +781,17 @@ 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.kafkaProducerSsl = monitor.kafkaProducerSsl;
bean.kafkaProducerAllowAutoTopicCreation =
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.validate();
@@ -762,7 +803,7 @@ let needSetup = false;
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (bean.isActive()) {
if (await bean.isActive()) {
await restartMonitor(socket.userID, bean.id);
}
@@ -905,6 +946,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,
@@ -913,6 +956,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.",
@@ -1076,9 +1123,6 @@ let needSetup = false;
value,
]);
// Cleanup unused Tags
await R.exec("delete from tag where ( select count(*) from monitor_tag mt where tag.id = mt.tag_id ) = 0");
callback({
ok: true,
msg: "Deleted Successfully.",
@@ -1107,6 +1151,8 @@ let needSetup = false;
let user = await doubleCheckPassword(socket, password.currentPassword);
await user.resetPassword(password.newPassword);
server.disconnectAllSocketClient(user.id, socket.id);
callback({
ok: true,
msg: "Password has been updated successfully.",
@@ -1156,6 +1202,9 @@ let needSetup = false;
await doubleCheckPassword(socket, currentPassword);
}
const previousChromeExecutable = await Settings.get("chromeExecutable");
const previousNSCDStatus = await Settings.get("nscd");
await setSettings("general", data);
server.entryPage = data.entryPage;
@@ -1166,6 +1215,21 @@ 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();
}
// Update nscd status
if (previousNSCDStatus !== data.nscd) {
if (data.nscd) {
await server.startNSCDServices();
} else {
await server.stopNSCDServices();
}
}
callback({
ok: true,
msg: "Saved"
@@ -1335,6 +1399,7 @@ let needSetup = false;
// Define default values
let retryInterval = 0;
let timeout = monitorListData[i].timeout || (monitorListData[i].interval * 0.8); // fallback to old value
/*
Only replace the default value with the backup file data for the specific version, where it appears the first time
@@ -1360,6 +1425,7 @@ let needSetup = false;
basic_auth_pass: monitorListData[i].basic_auth_pass,
authWorkstation: monitorListData[i].authWorkstation,
authDomain: monitorListData[i].authDomain,
timeout,
interval: monitorListData[i].interval,
retryInterval: retryInterval,
resendInterval: monitorListData[i].resendInterval || 0,
@@ -1367,13 +1433,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,
};
@@ -1560,6 +1627,8 @@ let needSetup = false;
await shutdownFunction();
});
server.start();
server.httpServer.listen(port, hostname, () => {
if (hostname) {
log.info("server", `Listening on ${hostname}:${port}`);
@@ -1637,6 +1706,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);
@@ -1704,7 +1774,7 @@ async function initDatabase(testMode = false) {
needSetup = true;
}
jwtSecret = jwtSecretBean.value;
server.jwtSecret = jwtSecretBean.value;
}
/**
@@ -1763,6 +1833,7 @@ async function pauseMonitor(userID, monitorID) {
if (monitorID in server.monitorList) {
server.monitorList[monitorID].stop();
server.monitorList[monitorID].active = 0;
}
}
@@ -1821,8 +1892,10 @@ gracefulShutdown(server.httpServer, {
});
// Catch unexpected errors here
process.addListener("unhandledRejection", (error, promise) => {
let unexpectedErrorHandler = (error, promise) => {
console.trace(error);
UptimeKumaServer.errorLog(error, false);
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
});
};
process.addListener("unhandledRejection", unexpectedErrorHandler);
process.addListener("uncaughtException", unexpectedErrorHandler);

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;
@@ -41,10 +42,50 @@ module.exports.generalSocketHandler = (socket, server) => {
});
socket.on("getGameList", async (callback) => {
callback({
ok: true,
gameList: getGameList(),
});
try {
checkLogin(socket);
callback({
ok: true,
gameList: getGameList(),
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("testChrome", (executable, callback) => {
try {
checkLogin(socket);
// 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,
});
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
// Disconnect all other socket clients of the user
socket.on("disconnectOtherSocketClients", async () => {
try {
checkLogin(socket);
server.disconnectAllSocketClients(socket.userID, socket.id);
} catch (e) {
log.warn("disconnectAllSocketClients", e.message);
}
});
};

View File

@@ -162,6 +162,7 @@ module.exports.statusPageSocketHandler = (socket) => {
statusPage.footer_text = config.footerText;
statusPage.custom_css = config.customCSS;
statusPage.show_powered_by = config.showPoweredBy;
statusPage.show_certificate_expiry = config.showCertificateExpiry;
statusPage.modified_date = R.isoDateTime();
statusPage.google_analytics_tag_id = config.googleAnalyticsId;

View File

@@ -4,13 +4,16 @@ const fs = require("fs");
const http = require("http");
const { Server } = require("socket.io");
const { R } = require("redbean-node");
const { log } = require("../src/util");
const { log, isDev } = require("../src/util");
const Database = require("./database");
const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
const childProcessAsync = require("promisify-child-process");
const path = require("path");
const { isSSL, sslKey, sslCert, sslKeyPassphrase } = require("./config");
// 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.
@@ -54,22 +57,23 @@ class UptimeKumaServer {
};
static getInstance(args) {
/**
* Use for decode the auth object
* @type {null}
*/
jwtSecret = null;
static getInstance() {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
UptimeKumaServer.instance = new UptimeKumaServer();
}
return UptimeKumaServer.instance;
}
constructor(args) {
// SSL
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
constructor() {
log.info("server", "Creating express and socket.io instance");
this.app = express();
if (sslKey && sslCert) {
if (isSSL) {
log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({
key: fs.readFileSync(sslKey),
@@ -91,11 +95,52 @@ class UptimeKumaServer {
}
}
this.io = new Server(this.httpServer);
// Set Monitor Types
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
this.io = new Server(this.httpServer, {
allowRequest: (req, callback) => {
let isOriginValid = true;
const bypass = isDev || process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass";
if (!bypass) {
let host = req.headers.host;
// If this is set, it means the request is from the browser
let origin = req.headers.origin;
// If this is from the browser, check if the origin is allowed
if (origin) {
try {
let originURL = new URL(origin);
if (host !== originURL.host) {
isOriginValid = false;
log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${req.socket.remoteAddress}`);
}
} catch (e) {
// Invalid origin url, probably not from browser
isOriginValid = false;
log.error("auth", `Invalid origin url (${origin}), IP: ${req.socket.remoteAddress}`);
}
} else {
log.info("auth", `Origin is not set, IP: ${req.socket.remoteAddress}`);
}
} else {
log.debug("auth", "Origin check is bypassed");
}
callback(null, isOriginValid);
}
});
}
/** 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();
@@ -200,7 +245,7 @@ class UptimeKumaServer {
* @param {boolean} outputToConsole Should the error also be output to console?
*/
static errorLog(error, outputToConsole = true) {
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
const errorLogStream = fs.createWriteStream(path.join(Database.dataDir, "/error.log"), {
flags: "a"
});
@@ -237,9 +282,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:/, "");
}
}
@@ -250,13 +295,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";
}
}
@@ -268,19 +343,99 @@ 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);
}
/** Stop the server */
async stop() {
/**
* TODO: Listen logic should be moved to here
* @returns {Promise<void>}
*/
async start() {
let enable = await Settings.get("nscd");
if (enable || enable === null) {
await this.startNSCDServices();
}
}
/**
* Stop the server
* @returns {Promise<void>}
*/
async stop() {
let enable = await Settings.get("nscd");
if (enable || enable === null) {
await this.stopNSCDServices();
}
}
/**
* Start all system services (e.g. nscd)
* For now, only used in Docker
*/
async startNSCDServices() {
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
try {
log.info("services", "Starting nscd");
await childProcessAsync.exec("sudo service nscd start");
} catch (e) {
log.info("services", "Failed to start nscd");
}
}
}
/**
* Stop all system services
*/
async stopNSCDServices() {
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
try {
log.info("services", "Stopping nscd");
await childProcessAsync.exec("sudo service nscd stop");
} catch (e) {
log.info("services", "Failed to stop nscd");
}
}
}
/**
* Force connected sockets of a user to refresh and disconnect.
* Used for resetting password.
* @param {string} userID
* @param {string?} currentSocketID
*/
disconnectAllSocketClients(userID, currentSocketID = undefined) {
for (const socket of this.io.sockets.sockets.values()) {
if (socket.userID === userID && socket.id !== currentSocketID) {
try {
socket.emit("refresh");
socket.disconnect();
} catch (e) {
}
}
}
}
}
@@ -288,5 +443,6 @@ 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");
const { TailscalePing } = require("./monitor-types/tailscale-ping");

View File

@@ -1,7 +1,7 @@
const tcpp = require("tcp-ping");
const ping = require("@louislam/ping");
const { R } = require("redbean-node");
const { log, genSecret } = require("../src/util");
const { log, genSecret, badgeConstants } = require("../src/util");
const passwordHash = require("./password-hash");
const { Resolver } = require("dns");
const childProcess = require("child_process");
@@ -9,7 +9,6 @@ const iconv = require("iconv-lite");
const chardet = require("chardet");
const mqtt = require("mqtt");
const chroma = require("chroma-js");
const { badgeConstants } = require("./config");
const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
@@ -21,6 +20,9 @@ const grpc = require("@grpc/grpc-js");
const protojs = require("protobufjs");
const radiusClient = require("node-radius-client");
const redis = require("redis");
const oidc = require("openid-client");
const tls = require("tls");
const {
dictionaries: {
rfc2865: { file, attributes },
@@ -28,8 +30,12 @@ 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 crypto = require("crypto");
const isWindows = process.platform === /^win/.test(process.platform);
/**
* Init or reset JWT secret
* @returns {Promise<Bean>}
@@ -49,6 +55,43 @@ exports.initJWTSecret = async () => {
return jwtSecretBean;
};
/**
* Decodes a jwt and returns the payload portion without verifying the jqt.
* @param {string} jwt The input jwt as a string
* @returns {Object} Decoded jwt payload object
*/
exports.decodeJwt = (jwt) => {
return JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString());
};
/**
* Gets a Access Token form a oidc/oauth2 provider
* @param {string} tokenEndpoint The token URI form the auth service provider
* @param {string} clientId The oidc/oauth application client id
* @param {string} clientSecret The oidc/oauth application client secret
* @param {string} scope The scope the for which the token should be issued for
* @param {string} authMethod The method on how to sent the credentials. Default client_secret_basic
* @returns {Promise<oidc.TokenSet>} TokenSet promise if the token request was successful
*/
exports.getOidcTokenClientCredentials = async (tokenEndpoint, clientId, clientSecret, scope, authMethod = "client_secret_basic") => {
const oauthProvider = new oidc.Issuer({ token_endpoint: tokenEndpoint });
let client = new oauthProvider.Client({
client_id: clientId,
client_secret: clientSecret,
token_endpoint_auth_method: authMethod
});
// Increase default timeout and clock tolerance
client[oidc.custom.http_options] = () => ({ timeout: 10000 });
client[oidc.custom.clock_tolerance] = 5;
let grantParams = { grant_type: "client_credentials" };
if (scope) {
grantParams.scope = scope;
}
return await client.grant(grantParams);
};
/**
* Send TCP request to specified hostname and port
* @param {string} hostname Hostname / address of machine
@@ -196,6 +239,96 @@ 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(
() => {
producer.send({
topic: topic,
messages: [{
value: message,
}],
}).then((_) => {
resolve("Message sent successfully");
}).catch((e) => {
connectedToKafka = true;
producer.disconnect();
clearTimeout(timeoutID);
reject(new Error("Error sending message: " + e.message));
}).finally(() => {
connectedToKafka = true;
clearTimeout(timeoutID);
});
}
).catch(
(e) => {
connectedToKafka = true;
producer.disconnect();
clearTimeout(timeoutID);
reject(new Error("Error in producer connection: " + e.message));
}
);
producer.on("producer.network.request_timeout", (_) => {
if (!connectedToKafka) {
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
@@ -262,6 +395,9 @@ exports.mssqlQuery = async function (connectionString, query) {
try {
pool = new mssql.ConnectionPool(connectionString);
await pool.connect();
if (!query) {
query = "SELECT 1";
}
await pool.request().query(query);
pool.close();
} catch (e) {
@@ -282,12 +418,22 @@ exports.postgresQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const config = postgresConParse(connectionString);
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
return reject(new Error("Password is undefined."));
// Fix #3868, which true/false is not parsed to boolean
if (typeof config.ssl === "string") {
config.ssl = config.ssl === "true";
}
const client = new Client({ connectionString });
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
reject(new Error("Password is undefined."));
return;
}
const client = new Client(config);
client.on("error", (error) => {
log.debug("postgres", "Error caught in the error event handler.");
reject(error);
});
client.connect((err) => {
if (err) {
@@ -311,6 +457,7 @@ exports.postgresQuery = function (connectionString, query) {
});
} catch (e) {
reject(e);
client.end();
}
}
});
@@ -322,11 +469,15 @@ exports.postgresQuery = function (connectionString, query) {
* Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @param {?string} password The password to use
* @returns {Promise<(string)>}
*/
exports.mysqlQuery = function (connectionString, query) {
exports.mysqlQuery = function (connectionString, query, password = undefined) {
return new Promise((resolve, reject) => {
const connection = mysql.createConnection(connectionString);
const connection = mysql.createConnection({
uri: connectionString,
password
});
connection.on("error", (err) => {
reject(err);
@@ -378,6 +529,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 (
@@ -388,10 +540,13 @@ exports.radius = function (
callingStationId,
secret,
port = 1812,
timeout = 2500,
) {
const client = new radiusClient({
host: hostname,
hostPort: port,
timeout: timeout,
retries: 1,
dictionaries: [ file ],
});
@@ -403,6 +558,12 @@ exports.radius = function (
[ attributes.CALLING_STATION_ID, callingStationId ],
[ attributes.CALLED_STATION_ID, calledStationId ],
],
}).catch((error) => {
if (error.response?.code) {
throw Error(error.response.code);
} else {
throw Error(error.message);
}
});
};
@@ -413,12 +574,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();
@@ -428,7 +595,7 @@ exports.redisPingAsync = function (dsn) {
} else {
resolve(res);
}
});
}).catch(error => reject(error));
});
});
};
@@ -574,7 +741,6 @@ exports.checkCertificate = function (res) {
* @param {number} status The status code to check
* @param {string[]} acceptedCodes An array of accepted status codes
* @returns {boolean} True if status code within range, false otherwise
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
*/
exports.checkStatusCode = function (status, acceptedCodes) {
if (acceptedCodes == null || acceptedCodes.length === 0) {
@@ -582,6 +748,11 @@ exports.checkStatusCode = function (status, acceptedCodes) {
}
for (const codeRange of acceptedCodes) {
if (typeof codeRange !== "string") {
log.error("monitor", `Accepted status code not a string. ${codeRange} is of type ${typeof codeRange}`);
continue;
}
const codeRangeSplit = codeRange.split("-").map(string => parseInt(string));
if (codeRangeSplit.length === 1) {
if (status === codeRangeSplit[0]) {
@@ -592,7 +763,8 @@ exports.checkStatusCode = function (status, acceptedCodes) {
return true;
}
} else {
throw new Error("Invalid status code range");
log.error("monitor", `${codeRange} is not a valid status code range`);
continue;
}
}
@@ -901,3 +1073,80 @@ module.exports.grpcQuery = async (options) => {
});
};
/**
* Returns an array of SHA256 fingerprints for all known root certificates.
* @returns {Set} A set of SHA256 fingerprints.
*/
module.exports.rootCertificatesFingerprints = () => {
let fingerprints = tls.rootCertificates.map(cert => {
let certLines = cert.split("\n");
certLines.shift();
certLines.pop();
let certBody = certLines.join("");
let buf = Buffer.from(certBody, "base64");
const shasum = crypto.createHash("sha256");
shasum.update(buf);
return shasum.digest("hex").toUpperCase().replace(/(.{2})(?!$)/g, "$1:");
});
fingerprints.push("6D:99:FB:26:5E:B1:C5:B3:74:47:65:FC:BC:64:8F:3C:D8:E1:BF:FA:FD:C4:C2:F9:9B:9D:47:CF:7F:F1:C2:4F"); // ISRG X1 cross-signed with DST X3
fingerprints.push("8B:05:B6:8C:C6:59:E5:ED:0F:CB:38:F2:C9:42:FB:FD:20:0E:6F:2F:F9:F8:5D:63:C6:99:4E:F5:E0:B0:27:01"); // ISRG X2 cross-signed with ISRG X1
return new Set(fingerprints);
};
module.exports.SHAKE256_LENGTH = 16;
/**
*
* @param {string} data
* @param {number} len
* @return {string}
*/
module.exports.shake256 = (data, len) => {
if (!data) {
return "";
}
return crypto.createHash("shake256", { outputLength: len })
.update(data)
.digest("hex");
};
// For unit test, export functions
if (process.env.TEST_BACKEND) {
module.exports.__test = {
parseCertificateInfo,
};
module.exports.__getPrivateFunction = (functionName) => {
return module.exports.__test[functionName];
};
}
/**
* Generates an abort signal with the specified timeout.
* @param {number} timeoutMs - The timeout in milliseconds.
* @returns {AbortSignal | null} - The generated abort signal, or null if not supported.
*/
module.exports.axiosAbortSignal = (timeoutMs) => {
try {
// Just in case, as 0 timeout here will cause the request to be aborted immediately
if (!timeoutMs || timeoutMs <= 0) {
timeoutMs = 5000;
}
return AbortSignal.timeout(timeoutMs);
} catch (_) {
// v16-: AbortSignal.timeout is not supported
try {
const abortController = new AbortController();
setTimeout(() => abortController.abort(), timeoutMs);
return abortController.signal;
} catch (_) {
// v15-: AbortController is not supported
return null;
}
}
};

View File

@@ -111,6 +111,10 @@ optgroup {
padding-right: 20px;
}
.btn-sm {
border-radius: 25px;
}
.btn-primary {
color: white;
@@ -158,6 +162,26 @@ optgroup {
background-color: #161B22;
}
.btn-outline-normal {
padding: 4px 10px;
border: 1px solid #ced4da;
border-radius: 25px;
background-color: transparent;
.dark & {
color: $dark-font-color;
border: 1px solid $dark-font-color2;
}
&.active {
background-color: $highlight-white;
.dark & {
background-color: $dark-font-color2;
}
}
}
@media (max-width: 550px) {
.table-shadow-box {
padding: 10px !important;
@@ -436,12 +460,11 @@ optgroup {
.monitor-list {
&.scrollbar {
overflow-y: auto;
height: calc(100% - 65px);
}
@media (max-width: 770px) {
&.scrollbar {
height: calc(100% - 40px);
height: calc(100% - 97px);
}
}

View File

@@ -69,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

@@ -8,9 +8,9 @@
:placeholder="placeholder"
:disabled="!enabled"
>
<a class="btn btn-outline-primary" @click="action()">
<button type="button" class="btn btn-outline-primary" :aria-label="actionAriaLabel" @click="action()">
<font-awesome-icon :icon="icon" />
</a>
</button>
</div>
</template>
@@ -66,6 +66,13 @@ export default {
action: {
type: Function,
default: () => {},
},
/**
* The aria-label of the action button
*/
actionAriaLabel: {
type: String,
required: true,
}
},
emits: [ "update:modelValue" ],

View File

@@ -0,0 +1,100 @@
<template>
<div class="input-group mb-3">
<select :id="id" ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
</select>
<button type="button" class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" :aria-label="actionAriaLabel" @click="action()">
<font-awesome-icon :icon="icon" aria-hidden="true" />
</button>
</div>
</template>
<script>
/**
* Generic select field with a customizable action on the right.
* Action is passed in as a function.
*/
export default {
props: {
options: {
type: Array,
default: () => [],
},
/**
* The id of the form which will be targeted by a <label for=..
*/
id: {
type: String,
required: true,
},
/**
* The value of the select field.
*/
modelValue: {
type: Number,
default: null,
},
/**
* Whether the select field is enabled / disabled.
*/
disabled: {
type: Boolean,
default: false
},
/**
* The icon displayed in the right button of the select field.
* Accepts a Font Awesome icon string identifier.
* @example "plus"
*/
icon: {
type: String,
required: true,
},
/**
* The action to be performed when the button is clicked.
* Action is passed in as a function.
*/
action: {
type: Function,
default: () => {},
},
/**
* The aria-label of the action button
*/
actionAriaLabel: {
type: String,
required: true,
},
/**
* Whether the action button is disabled.
* @example true
*/
actionDisabled: {
type: Boolean,
default: false
},
/**
* Whether the select field is required.
* @example true
*/
required: {
type: Boolean,
default: false,
}
},
emits: [ "update:modelValue" ],
computed: {
/**
* Send value update to parent on change.
*/
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
};
</script>

View File

@@ -22,78 +22,78 @@
</div>
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3">
<label for="duration" class="form-label">{{ $t("Badge Duration") }}</label>
<input id="duration" v-model="badge.duration" type="number" min="0" class="form-control" required>
<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" required>
<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" required>
<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" class="form-control" required>
<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" class="form-control" required>
<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" class="form-control" required>
<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" required>
<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" class="form-control" required>
<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" required>
<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" required>
<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" required>
<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" required>
<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="number" min="0" class="form-control" required>
<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" required>
<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" required>
<input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireDownDays">
</div>
<div class="mb-3">
@@ -109,12 +109,16 @@
<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" required>
<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="push-url" class="form-label">{{ $t("Badge URL") }}</label>
<CopyableInput id="push-url" v-model="badgeURL" type="url" disabled="disabled" />
<label for="badge-url" class="form-label">{{ $t("Badge URL") }}</label>
<CopyableInput id="badge-url" v-model="badgeURL" type="url" disabled="disabled" />
</div>
</div>
@@ -131,6 +135,7 @@
<script lang="ts">
import { Modal } from "bootstrap";
import CopyableInput from "./CopyableInput.vue";
import { badgeConstants } from "../util.ts";
export default {
components: {
@@ -224,7 +229,8 @@ export default {
"color",
"labelColor",
],
}
},
badgeConstants,
};
},

View File

@@ -0,0 +1,56 @@
<template>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("New Group") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<form @submit.prevent="confirm">
<div>
<label for="draftGroupName" class="form-label">{{ $t("Group Name") }}</label>
<input id="draftGroupName" v-model="groupName" type="text" class="form-control">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{{ $t("Cancel") }}
</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" :disabled="groupName == '' || groupName == null" @click="confirm">
{{ $t("Confirm") }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from "bootstrap";
export default {
props: {},
emits: [ "added" ],
data: () => ({
modal: null,
groupName: null,
}),
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
/** Show the confirm dialog */
show() {
this.modal.show();
},
confirm() {
this.$emit("added", this.groupName);
this.modal.hide();
},
},
};
</script>

View File

@@ -70,7 +70,7 @@ export default {
Confirm,
},
props: {},
emits: [ "added" ],
emits: [ "added", "deleted" ],
data() {
return {
modal: null,
@@ -167,6 +167,7 @@ export default {
this.processing = false;
if (res.ok) {
this.$emit("deleted", this.id);
this.modal.hide();
}
});

View File

@@ -5,15 +5,24 @@
v-for="(beat, index) in shortBeatList"
:key="index"
class="beat"
:class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0), 'pending' : (beat.status === 2), 'maintenance' : (beat.status === 3) }"
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
:style="beatStyle"
:title="getBeatTitle(beat)"
/>
</div>
<div
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
>
<div>{{ timeSinceFirstBeat }} ago</div>
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
<div>{{ timeSinceLastBeat }}</div>
</div>
</div>
</template>
<script>
import dayjs from "dayjs";
export default {
props: {
@@ -56,8 +65,30 @@ export default {
}
},
/**
* Calculates the amount of beats of padding needed to fill the length of shortBeatList.
*
* @return {number} The amount of beats of padding needed to fill the length of shortBeatList.
*/
numPadding() {
if (!this.beatList) {
return 0;
}
let num = this.beatList.length - this.maxBeat;
if (this.move) {
num = num - 1;
}
if (num > 0) {
return 0;
}
return -1 * num;
},
shortBeatList() {
if (! this.beatList) {
if (!this.beatList) {
return [];
}
@@ -115,6 +146,53 @@ export default {
};
},
/**
* Returns the style object for positioning the time element.
* @return {Object} The style object containing the CSS properties for positioning the time element.
*/
timeStyle() {
return {
"margin-left": this.numPadding * (this.beatWidth + this.beatMargin * 2) + "px",
};
},
/**
* Calculates the time elapsed since the first valid beat.
*
* @return {string} The time elapsed in minutes or hours.
*/
timeSinceFirstBeat() {
const firstValidBeat = this.shortBeatList.at(this.numPadding);
const minutes = dayjs().diff(dayjs.utc(firstValidBeat?.time), "minutes");
if (minutes > 60) {
return (minutes / 60).toFixed(0) + "h";
} else {
return minutes + "m";
}
},
/**
* Calculates the elapsed time since the last valid beat was registered.
*
* @return {string} The elapsed time in a minutes, hours or "now".
*/
timeSinceLastBeat() {
const lastValidBeat = this.shortBeatList.at(-1);
const seconds = dayjs().diff(dayjs.utc(lastValidBeat?.time), "seconds");
let tolerance = 60 * 2; // default for when monitorList not available
if (this.$root.monitorList[this.monitorId] != null) {
tolerance = this.$root.monitorList[this.monitorId].interval * 2;
}
if (seconds < tolerance) {
return "now";
} else if (seconds < 60 * 60) {
return (seconds / 60).toFixed(0) + "m ago";
} else {
return (seconds / 60 / 60).toFixed(0) + "h ago";
}
}
},
watch: {
beatList: {
@@ -133,14 +211,14 @@ export default {
},
beforeMount() {
if (this.heartbeatList === null) {
if (! (this.monitorId in this.$root.heartbeatList)) {
if (!(this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
}
},
mounted() {
if (this.size === "small") {
if (this.size !== "big") {
this.beatWidth = 5;
this.beatHeight = 16;
this.beatMargin = 2;
@@ -151,11 +229,11 @@ export default {
const actualWidth = this.beatWidth * window.devicePixelRatio;
const actualMargin = this.beatMargin * window.devicePixelRatio;
if (! Number.isInteger(actualWidth)) {
if (!Number.isInteger(actualWidth)) {
this.beatWidth = Math.round(actualWidth) / window.devicePixelRatio;
}
if (! Number.isInteger(actualMargin)) {
if (!Number.isInteger(actualMargin)) {
this.beatMargin = Math.round(actualMargin) / window.devicePixelRatio;
}
@@ -229,4 +307,21 @@ export default {
}
}
.word {
color: #aaa;
font-size: 12px;
}
.connecting-line {
flex-grow: 1;
height: 1px;
background-color: #ededed;
margin-left: 10px;
margin-right: 10px;
margin-top: 2px;
.dark & {
background-color: #333;
}
}
</style>

View File

@@ -5,18 +5,18 @@
<h1 class="h3 mb-3 fw-normal" />
<div v-if="!tokenRequired" class="form-floating">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" autocomplete="username" required>
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
<div v-if="!tokenRequired" class="form-floating mt-3">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" autocomplete="current-password" required>
<label for="floatingPassword">{{ $t("Password") }}</label>
</div>
<div v-if="tokenRequired">
<div class="form-floating mt-3">
<input id="otp" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
<input id="otp" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456" autocomplete="one-time-code" required>
<label for="otp">{{ $t("Token") }}</label>
</div>
</div>

View File

@@ -29,10 +29,10 @@ export default {
},
computed: {
startDateTime() {
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
},
endDateTime() {
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone, true).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
}
},
};

View File

@@ -1,36 +1,84 @@
<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">
<button class="btn btn-outline-normal ms-2" :class="{ 'active': selectMode }" type="button" @click="selectMode = !selectMode">
{{ $t("Select") }}
</button>
<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...')"
:aria-label="$t('Search monitored sites')"
autocomplete="off"
/>
</form>
</div>
</div>
<div class="header-filter">
<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
</div>
<!-- Selection Controls -->
<div v-if="selectMode" class="selection-controls px-2 pt-2">
<input
v-model="selectAll"
class="form-check-input select-input"
type="checkbox"
/>
<button class="btn-outline-normal" @click="pauseDialog"><font-awesome-icon icon="pause" size="sm" /> {{ $t("Pause") }}</button>
<button class="btn-outline-normal" @click="resumeSelected"><font-awesome-icon icon="play" size="sm" /> {{ $t("Resume") }}</button>
<span v-if="selectedMonitorCount > 0">
{{ $t("selectedMonitorCount", [ selectedMonitorCount ]) }}
</span>
</div>
</div>
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div>
<MonitorListItem v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" :isSearch="searchText !== ''" />
<MonitorListItem
v-for="(item, index) in sortedMonitorList"
:key="index"
:monitor="item"
:showPathName="filtersActive"
:isSelectMode="selectMode"
:isSelected="isSelected"
:select="select"
:deselect="deselect"
/>
</div>
</div>
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
{{ $t("pauseMonitorMsg") }}
</Confirm>
</template>
<script>
import Confirm from "../components/Confirm.vue";
import MonitorListItem from "../components/MonitorListItem.vue";
import MonitorListFilter from "./MonitorListFilter.vue";
import { getMonitorRelativeURL } from "../util.ts";
export default {
components: {
Confirm,
MonitorListItem,
MonitorListFilter,
},
props: {
/** Should the scrollbar be shown */
@@ -41,7 +89,16 @@ export default {
data() {
return {
searchText: "",
selectMode: false,
selectAll: false,
disableSelectAllWatcher: false,
selectedMonitors: {},
windowTop: 0,
filterState: {
status: null,
active: null,
tags: null,
}
};
},
computed: {
@@ -63,31 +120,68 @@ export default {
},
/**
* Returns a sorted list of monitors based on the applied filters and search text.
*
* @return {Array} The sorted list of monitors.
*/
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);
}
result = result.filter(monitor => {
// filter by search text
// finds monitor name, tag name or tag value
let searchTextMatch = true;
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
searchTextMatch =
monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText));
}
// filter by status
let statusMatch = true;
if (this.filterState.status != null && this.filterState.status.length > 0) {
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
}
statusMatch = this.filterState.status.includes(monitor.status);
}
// filter by active
let activeMatch = true;
if (this.filterState.active != null && this.filterState.active.length > 0) {
activeMatch = this.filterState.active.includes(monitor.active);
}
// filter by tags
let tagsMatch = true;
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
tagsMatch = 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;
}
// Hide children if not filtering
let showChild = true;
if (this.filterState.status == null && this.filterState.active == null && this.filterState.tags == null && this.searchText === "") {
if (monitor.parent !== null) {
showChild = false;
}
}
return searchTextMatch && statusMatch && activeMatch && tagsMatch && showChild;
});
// Filter result by active state, weight and alphabetical
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
if (m1.active === false) {
return 1;
}
if (m2.active === 0) {
if (m2.active === false) {
return -1;
}
}
@@ -107,6 +201,67 @@ export default {
return result;
},
isDarkTheme() {
return document.body.classList.contains("dark");
},
monitorListStyle() {
let listHeaderHeight = 107;
if (this.selectMode) {
listHeaderHeight += 42;
}
return {
"height": `calc(100% - ${listHeaderHeight}px)`
};
},
selectedMonitorCount() {
return Object.keys(this.selectedMonitors).length;
},
/**
* Determines if any filters are active.
*
* @return {boolean} True if any filter is active, false otherwise.
*/
filtersActive() {
return this.filterState.status != null || this.filterState.active != null || this.filterState.tags != null || this.searchText !== "";
}
},
watch: {
searchText() {
for (let monitor of this.sortedMonitorList) {
if (!this.selectedMonitors[monitor.id]) {
if (this.selectAll) {
this.disableSelectAllWatcher = true;
this.selectAll = false;
}
break;
}
}
},
selectAll() {
if (!this.disableSelectAllWatcher) {
this.selectedMonitors = {};
if (this.selectAll) {
this.sortedMonitorList.forEach((item) => {
this.selectedMonitors[item.id] = true;
});
}
} else {
this.disableSelectAllWatcher = false;
}
},
selectMode() {
if (!this.selectMode) {
this.selectAll = false;
this.selectedMonitors = {};
}
},
},
mounted() {
window.addEventListener("scroll", this.onScroll);
@@ -134,7 +289,61 @@ 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;
},
/**
* Deselect a monitor
* @param {number} id ID of monitor
*/
deselect(id) {
delete this.selectedMonitors[id];
},
/**
* Select a monitor
* @param {number} id ID of monitor
*/
select(id) {
this.selectedMonitors[id] = true;
},
/**
* Determine if monitor is selected
* @param {number} id ID of monitor
* @returns {bool}
*/
isSelected(id) {
return id in this.selectedMonitors;
},
/** Disable select mode and reset selection */
cancelSelectMode() {
this.selectMode = false;
this.selectedMonitors = {};
},
/** Show dialog to confirm pause */
pauseDialog() {
this.$refs.confirmPause.show();
},
/** Pause each selected monitor */
pauseSelected() {
Object.keys(this.selectedMonitors)
.filter(id => this.$root.monitorList[id].active)
.forEach(id => this.$root.getSocket().emit("pauseMonitor", id));
this.cancelSelectMode();
},
/** Resume each selected monitor */
resumeSelected() {
Object.keys(this.selectedMonitors)
.filter(id => !this.$root.monitorList[id].active)
.forEach(id => this.$root.getSocket().emit("resumeMonitor", id));
this.cancelSelectMode();
},
},
};
</script>
@@ -159,8 +368,6 @@ export default {
margin: -10px;
margin-bottom: 10px;
padding: 10px;
display: flex;
justify-content: space-between;
.dark & {
background-color: $dark-header-bg;
@@ -168,6 +375,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;
@@ -217,4 +435,11 @@ export default {
margin-top: 5px;
}
.selection-controls {
margin-top: 5px;
display: flex;
align-items: center;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,288 @@
<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";
.dropdown-item {
cursor: pointer;
}
.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,137 @@
<template>
<div tabindex="-1" 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";
@import "../assets/app.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 {
@extend .btn-outline-normal;
display: flex;
align-items: center;
margin-left: 5px;
color: $link-color;
.dark & {
color: $dark-font-color;
}
&:focus {
background-color: $highlight-white;
.dark & {
background-color: $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

@@ -1,34 +1,56 @@
<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 :style="depthMargin">
<!-- Checkbox -->
<div v-if="isSelectMode" class="select-input-wrapper">
<input
class="form-check-input select-input"
type="checkbox"
:aria-label="$t('Check/Uncheck')"
:checked="isSelected(monitor.id)"
@click.stop="toggleSelection"
/>
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar size="small" :monitor-id="monitor.id" />
<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">
<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 v-if="monitor.tags.length > 0" 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 ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
</div>
</div>
</div>
</router-link>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
</div>
</div>
</router-link>
</div>
<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" />
<MonitorListItem
v-for="(item, index) in sortedChildMonitorList"
:key="index" :monitor="item"
:showPathName="showPathName"
:isSelectMode="isSelectMode"
:isSelected="isSelected"
:select="select"
:deselect="deselect"
:depth="depth + 1"
/>
</div>
</transition>
</div>
@@ -53,8 +75,13 @@ export default {
type: Object,
default: null,
},
/** If the user is currently searching */
isSearch: {
/** Should the monitor name show it's parent */
showPathName: {
type: Boolean,
default: false,
},
/** If the user is in select mode */
isSelectMode: {
type: Boolean,
default: false,
},
@@ -63,6 +90,21 @@ export default {
type: Number,
default: 0,
},
/** Callback to determine if monitor is selected */
isSelected: {
type: Function,
default: () => {}
},
/** Callback fired when monitor is selected */
select: {
type: Function,
default: () => {}
},
/** Callback fired when monitor is deselected */
deselect: {
type: Function,
default: () => {}
},
},
data() {
return {
@@ -111,13 +153,19 @@ export default {
};
},
monitorName() {
if (this.isSearch) {
if (this.showPathName) {
return this.monitor.pathName;
} else {
return this.monitor.name;
}
}
},
watch: {
isSelectMode() {
// TODO: Resize the heartbeat bar, but too slow
// this.$refs.heartbeatBar.resize();
}
},
beforeMount() {
// Always unfold if monitor is accessed directly
@@ -164,6 +212,16 @@ export default {
monitorURL(id) {
return getMonitorRelativeURL(id);
},
/**
* Toggle selection of monitor
*/
toggleSelection() {
if (this.isSelected(this.monitor.id)) {
this.deselect(this.monitor.id);
} else {
this.select(this.monitor.id);
}
},
},
};
</script>
@@ -201,4 +259,14 @@ export default {
transition: all 0.2s $easing-in;
}
.select-input-wrapper {
float: left;
margin-top: 15px;
margin-left: 3px;
margin-right: 10px;
padding-left: 4px;
position: relative;
z-index: 15;
}
</style>

View File

@@ -104,7 +104,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

@@ -126,6 +126,7 @@ export default {
"lunasea": "LunaSea",
"matrix": "Matrix",
"mattermost": "Mattermost",
"nostr": "Nostr",
"ntfy": "Ntfy",
"octopush": "Octopush",
"OneBot": "OneBot",
@@ -157,6 +158,7 @@ export default {
"AliyunSMS": "AliyunSMS (阿里云短信服务)",
"DingDing": "DingDing (钉钉自定义机器人)",
"Feishu": "Feishu (飞书)",
"FlashDuty": "FlashDuty (快猫星云)",
"FreeMobile": "FreeMobile (mobile.free.fr)",
"PushDeer": "PushDeer",
"promosms": "PromoSMS",
@@ -164,6 +166,7 @@ export default {
"SMSManager": "SmsManager (smsmanager.cz)",
"WeCom": "WeCom (企业微信群机器人)",
"ServerChan": "ServerChan (Server酱)",
"smsc": "SMSC",
};
// Sort by notification name

View File

@@ -61,12 +61,17 @@
/>
</span>
</div>
<div v-if="showTags" class="tags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
<div class="extra-info">
<div v-if="showCertificateExpiry && monitor.element.certExpiryDaysRemaining">
<Tag :item="{name: $t('Cert Exp.'), value: formattedCertExpiryMessage(monitor), color: certExpiryColor(monitor)}" :size="'sm'" />
</div>
<div v-if="showTags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
</div>
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="monitor.element.id" />
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
</div>
</div>
</div>
@@ -103,6 +108,10 @@ export default {
/** Should tags be shown? */
showTags: {
type: Boolean,
},
/** Should expiry be shown? */
showCertificateExpiry: {
type: Boolean,
}
},
data() {
@@ -150,10 +159,37 @@ 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;
},
/**
* Returns formatted certificate expiry or Bad cert message
* @param {Object} monitor Monitor to show expiry for
* @returns {string}
*/
formattedCertExpiryMessage(monitor) {
if (monitor?.element?.validCert && monitor?.element?.certExpiryDaysRemaining) {
return monitor.element.certExpiryDaysRemaining + " " + this.$tc("day", monitor.element.certExpiryDaysRemaining);
} else if (monitor?.element?.validCert === false) {
return this.$t("noOrBadCertificate");
} else {
return this.$t("Unknown") + " " + this.$tc("day", 2);
}
},
/**
* Returns certificate expiry based on days remaining
* @param {Object} monitor Monitor to show expiry for
* @returns {string}
*/
certExpiryColor(monitor) {
if (monitor?.element?.validCert && monitor.element.certExpiryDaysRemaining > 7) {
return "#059669";
}
return "#DC2626";
},
}
};
</script>
@@ -161,6 +197,15 @@ export default {
<style lang="scss" scoped>
@import "../assets/vars";
.extra-info {
display: flex;
margin-bottom: 0.5rem;
}
.extra-info > div > div:first-child {
margin-left: 0 !important;
}
.no-monitor-msg {
position: absolute;
width: 100%;

View File

@@ -99,7 +99,7 @@
</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">

View File

@@ -28,6 +28,7 @@
v-model="currentPassword"
type="password"
class="form-control"
autocomplete="current-password"
required
/>
</div>
@@ -43,7 +44,7 @@
<div v-if="uri && twoFAStatus == false" class="mt-3">
<label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
<div class="input-group">
<input v-model="token" type="text" maxlength="6" class="form-control">
<input v-model="token" type="text" maxlength="6" class="form-control" autocomplete="one-time-code" required>
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
</div>
<p v-show="tokenValid" class="mt-2" style="color: green;">{{ $t("tokenValidSettingsMsg") }}</p>

View File

@@ -0,0 +1,29 @@
<template>
<div class="mb-3">
<label for="flashduty-integration-url" class="form-label">Integration Key</label>
<HiddenInput id="flashduty-integration-url" v-model="$parent.notification.flashdutyIntegrationKey" autocomplete="false"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetFlashDutyKey" class="form-text">
<a href="https://flashcat.cloud/product/flashduty?from=kuma" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="flashduty-severity" class="form-label">{{ $t("FlashDuty Severity") }}</label>
<select id="flashduty-severity" v-model="$parent.notification.flashdutySeverity" class="form-select" :required="true">
<option value="Info" selected>Info</option>
<option value="Warning" selected>Warning</option>
<option value="Critical">Critical</option>
</select>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
mounted() {
}
};
</script>

View File

@@ -1,9 +1,7 @@
<template>
<div class="mb-3">
<label for="goalert-base-url" class="form-label">{{ $t("Base URL") }}</label>
<div class="input-group mb-3">
<input id="goalert-base-url" v-model="$parent.notification.goAlertBaseURL" type="text" class="form-control" required>
</div>
<input id="goalert-base-url" v-model="$parent.notification.goAlertBaseURL" type="text" class="form-control" required>
<i18n-t tag="div" keypath="goAlertInfo" class="form-text">
<a href="https://goalert.me" target="_blank">https://goalert.me</a>
</i18n-t>

View File

@@ -1,23 +1,19 @@
<template>
<div class="mb-3">
<label for="gorush-device-token" class="form-label">{{ $t("Device Token") }}</label><span style="color: red;"><sup>*</sup></span>
<div class="input-group mb-3">
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
</div>
<input id="gorush-device-token" v-model="$parent.notification.gorushDeviceToken" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="gorush-server-url" class="form-label">{{ $t("Server URL") }}</label><span style="color: red;"><sup>*</sup></span>
<div class="input-group mb-3">
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
</div>
<input id="gorush-server-url" v-model="$parent.notification.gorushServerURL" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="gorush-platform" class="form-label">{{ $t("Platform") }}</label><span style="color: red;"><sup>*</sup></span>
<select id="gorush-platform" v-model="$parent.notification.gorushPlatform" class="form-select">
<option value="ios">iOS</option>
<option value="android">{{ $t("Android") }}</option>
<option value="android">Android</option>
<option value="huawei">{{ $t("Huawei") }}</option>
</select>
</div>

View File

@@ -5,9 +5,7 @@
</div>
<div class="mb-3">
<label for="gotify-server-url" class="form-label">{{ $t("Server URL") }}</label>
<div class="input-group mb-3">
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
</div>
<input id="gotify-server-url" v-model="$parent.notification.gotifyserverurl" type="text" class="form-control" required>
</div>
<div class="mb-3">

View File

@@ -9,10 +9,7 @@
<div class="mb-3">
<label for="kook-guild-id" class="form-label">{{ $t("Guild ID") }}</label>
<div class="input-group mb-3">
<input id="kook-guild-id" v-model="$parent.notification.kookGuildID" type="text" class="form-control" required>
</div>
<input id="kook-guild-id" v-model="$parent.notification.kookGuildID" type="text" class="form-control" required>
<div class="form-text">
<p style="margin-top: 8px;">

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