mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-10 12:56:13 +08:00
Compare commits
426 Commits
uptime-imp
...
1.21.2-bet
Author | SHA1 | Date | |
---|---|---|---|
|
8f449ab738 | ||
|
dbfaddafca | ||
|
511038b45a | ||
|
e8d48561fc | ||
|
aeb2feacd3 | ||
|
c01055efb3 | ||
|
17ae47d091 | ||
|
de0d1edfd4 | ||
|
0f5a243450 | ||
|
524cf7c607 | ||
|
a6acd065bb | ||
|
227cec86a8 | ||
|
02291730fe | ||
|
dcc065c86f | ||
|
501dc29e6d | ||
|
d2b09ef042 | ||
|
f608590526 | ||
|
a7f21bffec | ||
|
ebd42444d1 | ||
|
f0f7645c57 | ||
|
3ada6fa99b | ||
|
4fb10a4e3f | ||
|
1aac15bccc | ||
|
cbcd2a1027 | ||
|
17f038767d | ||
|
edd1c6b662 | ||
|
29be8d3ddb | ||
|
2ac1abd424 | ||
|
738a494dcb | ||
|
e575d41f7d | ||
|
8ee4b844fd | ||
|
4ae437dd61 | ||
|
6cb296b07a | ||
|
644c6a872f | ||
|
8c69c18f6d | ||
|
c1a1160767 | ||
|
a0ebd88849 | ||
|
2e9413cf33 | ||
|
278b52ec34 | ||
|
caa757a27c | ||
|
3ce117a943 | ||
|
cefe484b47 | ||
|
a700892709 | ||
|
13d721ccf8 | ||
|
6c66bff518 | ||
|
bea51d048b | ||
|
2e1a0fe4d5 | ||
|
27b0895722 | ||
|
e687698851 | ||
|
fbdeb30ce7 | ||
|
41bda4e1d7 | ||
|
4869e6531c | ||
|
302b9cf644 | ||
|
3c3a192943 | ||
|
b64c835cee | ||
|
5266e713e6 | ||
|
86579d245f | ||
|
b6169408be | ||
|
4f05912276 | ||
|
bf525371d9 | ||
|
89bfc3bf33 | ||
|
a2014278b8 | ||
|
70572af1af | ||
|
b31c23a43b | ||
|
f4ee5271af | ||
|
7330db3563 | ||
|
097567e5f0 | ||
|
35f300c8eb | ||
|
4c9d7ac8ca | ||
|
d9558833fc | ||
|
776a482a1d | ||
|
d2527d7254 | ||
|
6dfca0c163 | ||
|
72317633d9 | ||
|
1973db28bf | ||
|
972ae60cfc | ||
|
56410afc3b | ||
|
df975c2750 | ||
|
0cfd3fa642 | ||
|
0b91391ced | ||
|
02fde56aec | ||
|
da225a225f | ||
|
a40c03f6f3 | ||
|
c238508060 | ||
|
abcbc3c55e | ||
|
350451edc8 | ||
|
b4b2ae55c0 | ||
|
9ecb890f56 | ||
|
4329fc6751 | ||
|
836e125256 | ||
|
0fe98de256 | ||
|
3825dd9f42 | ||
|
c6cd0d9312 | ||
|
82975f8d7b | ||
|
2ebbcc25a9 | ||
|
f2323b012b | ||
|
e5a6238cde | ||
|
61506b1af2 | ||
|
dbe73bd6ae | ||
|
0778549a6d | ||
|
09fa60de55 | ||
|
1e80365b73 | ||
|
491239415e | ||
|
0efabb4e39 | ||
|
b7aad4677d | ||
|
0a9ebf7fa4 | ||
|
09aa7be4f5 | ||
|
d3a3dcbde2 | ||
|
7447a6570f | ||
|
66e80aa6c3 | ||
|
f1c3604ab0 | ||
|
e8a81379bb | ||
|
1f13db26cc | ||
|
1777270bb7 | ||
|
062191d61f | ||
|
86e9c8ade9 | ||
|
35cf31ced1 | ||
|
4608560b1d | ||
|
6a37a2a05b | ||
|
ba7af9b569 | ||
|
6635412980 | ||
|
6e0aa109bc | ||
|
533bc1505b | ||
|
391692a708 | ||
|
a599f5149b | ||
|
f32fcb204f | ||
|
230de63460 | ||
|
2dedc1cfbd | ||
|
65933de0cd | ||
|
ce8eebc838 | ||
|
ae5a683af8 | ||
|
70bb69fc73 | ||
|
2702335d91 | ||
|
6de57f3283 | ||
|
0dce492226 | ||
|
3e60912992 | ||
|
f17d23f5d8 | ||
|
306cd37ec0 | ||
|
26dfa248c5 | ||
|
993bdb3550 | ||
|
6335b72c2b | ||
|
7ec09d0118 | ||
|
92c9b8bb63 | ||
|
86a8ec27f3 | ||
|
010c7d681f | ||
|
1d5246c34b | ||
|
8d1847c032 | ||
|
13387cdd90 | ||
|
44077b38cb | ||
|
b60c11ff28 | ||
|
36aef2ae0d | ||
|
db5641c4bf | ||
|
901795729b | ||
|
7c1cca38dc | ||
|
ce767d2350 | ||
|
b433a8fe5a | ||
|
9fb190cc3c | ||
|
917f406f30 | ||
|
06ec7f2b61 | ||
|
6af1306b89 | ||
|
7bbfb640ba | ||
|
9758e1b71a | ||
|
eed49fed59 | ||
|
1d7883208a | ||
|
5005c56e51 | ||
|
cd61b28c85 | ||
|
8b1affb9f4 | ||
|
a838432aef | ||
|
1718890be7 | ||
|
efd4dece1b | ||
|
c8a830047b | ||
|
db85f1758a | ||
|
f02e8be3e2 | ||
|
40cd5d41e3 | ||
|
f2dc27c8fa | ||
|
2bb3b634c0 | ||
|
3f3c5dca9f | ||
|
482d2657ca | ||
|
fa2221781e | ||
|
3e97563ee4 | ||
|
68ea65bbd9 | ||
|
6cea6dc001 | ||
|
a990dc89d8 | ||
|
16ce1f9ddf | ||
|
2f5d9e7e46 | ||
|
f75ff2da98 | ||
|
d1808fe9a3 | ||
|
22577b88e9 | ||
|
61699bb238 | ||
|
8d31187a74 | ||
|
9a1301d0a9 | ||
|
5f1b58e836 | ||
|
193a273557 | ||
|
bc87abf5c2 | ||
|
ad26f0e817 | ||
|
94c3861608 | ||
|
71c800b880 | ||
|
0986457017 | ||
|
b21c2adcc2 | ||
|
beafbf27ad | ||
|
958354e4db | ||
|
38ab5e0f3e | ||
|
7e178d93df | ||
|
97e276bdb5 | ||
|
fc8a324f41 | ||
|
bba8c6fe4e | ||
|
fee8fd9320 | ||
|
669f8700b2 | ||
|
11fa690e09 | ||
|
06ee68dc0e | ||
|
42a69c16ca | ||
|
b91c526d2e | ||
|
5b0b743f81 | ||
|
3c5f998191 | ||
|
a80f228136 | ||
|
ea3b3abe36 | ||
|
48c6f0578c | ||
|
7f9332c753 | ||
|
d668812df1 | ||
|
f32d3af62c | ||
|
8a115670cd | ||
|
a7b49fcd98 | ||
|
487eae71c7 | ||
|
4fed0c152e | ||
|
43c797a34e | ||
|
f9a6d7ec44 | ||
|
4a5a424198 | ||
|
f47f2d5c87 | ||
|
54cd7a0402 | ||
|
f0ae67f89a | ||
|
98bb854832 | ||
|
46894793fc | ||
|
ef64077980 | ||
|
e873fea86d | ||
|
c4a9374671 | ||
|
c65a920050 | ||
|
7b8ed01f27 | ||
|
cecb0b6425 | ||
|
8e3dd4202f | ||
|
af82ea742c | ||
|
2fa233ae7f | ||
|
e9475ed3c0 | ||
|
b923ba72ca | ||
|
06278dc51f | ||
|
10228874fa | ||
|
c5034c8f38 | ||
|
7e3734af53 | ||
|
5789112f55 | ||
|
4dfc1a0221 | ||
|
6235ce6b29 | ||
|
81a829bda7 | ||
|
fa7f75a930 | ||
|
7c8cff7708 | ||
|
5e1489a6ed | ||
|
df5da0054e | ||
|
7da48b27a5 | ||
|
de7df46aa8 | ||
|
9ccaa4d120 | ||
|
42033c692f | ||
|
aad1a7e0fa | ||
|
2c62d197a0 | ||
|
dad21065cf | ||
|
02ddb58686 | ||
|
c061455475 | ||
|
0e38391454 | ||
|
22902139d2 | ||
|
4642f9be0a | ||
|
372c6b9078 | ||
|
cf32b8bc64 | ||
|
6a98ffe2f6 | ||
|
72106ba4c4 | ||
|
3ab0faee91 | ||
|
9e3a77f419 | ||
|
d1c43f432a | ||
|
ba3c7210ab | ||
|
3b74f5f359 | ||
|
c9b4a7f53e | ||
|
bd2c1d9c34 | ||
|
4df8db3f54 | ||
|
b8720b46c3 | ||
|
1d4af39820 | ||
|
dd1d71530f | ||
|
01c71a0242 | ||
|
d553c4c4f7 | ||
|
e7feca1cd6 | ||
|
cd796898d0 | ||
|
05443f9bb7 | ||
|
d7f2fa982a | ||
|
ee2eb5109b | ||
|
fdc3b2d57a | ||
|
e04a8203dc | ||
|
2620ec3fae | ||
|
1877b90b3a | ||
|
609a61e600 | ||
|
32ddff4e64 | ||
|
11b45dd274 | ||
|
fb2f7179e9 | ||
|
e3573ced65 | ||
|
8afc55db4e | ||
|
31fa074ffc | ||
|
379d54e520 | ||
|
a518188e6f | ||
|
7d363ea146 | ||
|
d1175ff471 | ||
|
aad087caac | ||
|
0d6a8b2101 | ||
|
cd18b96f69 | ||
|
c19dcdba44 | ||
|
d9316f43ac | ||
|
33bb9f1ade | ||
|
6048bc5dfc | ||
|
5bf00fbe0b | ||
|
76bdb62a5b | ||
|
99eebf18b8 | ||
|
1bcca60574 | ||
|
aeea1ff03f | ||
|
b49e3b65c1 | ||
|
e9876986eb | ||
|
9268ad2f2c | ||
|
f1aa567a50 | ||
|
9d53db1504 | ||
|
a6f68a2e06 | ||
|
2ef98c1b10 | ||
|
fea33a6475 | ||
|
3816c696cd | ||
|
51860261e9 | ||
|
d8a517e843 | ||
|
971977b295 | ||
|
36c32c3636 | ||
|
49396e2ccc | ||
|
90d6fbd20b | ||
|
db918be126 | ||
|
5f71515253 | ||
|
bcac18cc2b | ||
|
06d1309d78 | ||
|
fe66a24f00 | ||
|
cb563950e5 | ||
|
b9dd04ab05 | ||
|
c70ccd1183 | ||
|
25d50e7660 | ||
|
050c388bc3 | ||
|
34b1169ad6 | ||
|
6bda8d0b55 | ||
|
0b9c5c70b2 | ||
|
73f85f4861 | ||
|
48d89d8e61 | ||
|
727de9838b | ||
|
a16ea4c6f3 | ||
|
46413a57e8 | ||
|
e10ea1049d | ||
|
7984a52929 | ||
|
8f78c54ca2 | ||
|
c5ff010669 | ||
|
29976d8a03 | ||
|
f1bac7ce8a | ||
|
51dbb23230 | ||
|
8092640e20 | ||
|
19c8538149 | ||
|
c30e88ece2 | ||
|
cb953361b1 | ||
|
c12b06348b | ||
|
48b637d4c8 | ||
|
3439074835 | ||
|
36d160ad02 | ||
|
3a361d2621 | ||
|
53d9e98e47 | ||
|
8725e5daf9 | ||
|
d45aee450d | ||
|
727acb32bf | ||
|
43941fa2c6 | ||
|
faa78443d6 | ||
|
ab3b2bddba | ||
|
1c0174c319 | ||
|
ef54d9e3b6 | ||
|
39c99b0ec4 | ||
|
c42e550382 | ||
|
4323dee781 | ||
|
1bfb290718 | ||
|
666838f334 | ||
|
603b3a7fb6 | ||
|
b2ddb5c9eb | ||
|
4287f7e885 | ||
|
7dacc6a002 | ||
|
064bc00f46 | ||
|
35bd129d66 | ||
|
2673b509a5 | ||
|
9329ec9234 | ||
|
f155ec9ba8 | ||
|
7866d1ec12 | ||
|
cd017fce98 | ||
|
aef85078eb | ||
|
86ba6f829e | ||
|
cf21aa3737 | ||
|
9890a0754b | ||
|
15c64d458b | ||
|
be850dd596 | ||
|
3adc9e65d6 | ||
|
e427c70fef | ||
|
27e0b1eea1 | ||
|
521356e38a | ||
|
b91fe9d96d | ||
|
66d5408aad | ||
|
fe03170540 | ||
|
239910a27c | ||
|
4d0bdae6bf | ||
|
608e3f5582 | ||
|
da16796ec4 | ||
|
a487347b33 | ||
|
655ba015a0 | ||
|
3eaccb560e | ||
|
e741e7582d | ||
|
4c45654780 | ||
|
da778f05ac | ||
|
4a7e96f9ea | ||
|
f6919aef1d | ||
|
6537f4fe74 | ||
|
5809088f27 | ||
|
0814d643c1 | ||
|
fa1fc0fb05 | ||
|
6ec6410808 | ||
|
31cc328839 | ||
|
0d58526f25 | ||
|
2b9bf095a6 | ||
|
3a18801722 | ||
|
fbf2df9e7a | ||
|
934d633d4d |
@@ -33,7 +33,7 @@ tsconfig.json
|
||||
/ecosystem.config.js
|
||||
/extra/healthcheck.exe
|
||||
/extra/healthcheck
|
||||
|
||||
extra/exe-builder
|
||||
|
||||
### .gitignore content (commented rules are duplicated)
|
||||
|
||||
@@ -48,6 +48,4 @@ dist-ssr
|
||||
#!/data/.gitkeep
|
||||
#.vscode
|
||||
|
||||
|
||||
|
||||
### End of .gitignore content
|
||||
|
6
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
6
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
@@ -26,6 +26,12 @@ body:
|
||||
label: "📝 Describe your problem"
|
||||
description: "Please walk us through it step by step."
|
||||
placeholder: "Describe what are you asking for..."
|
||||
- type: textarea
|
||||
id: error-msg
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "📝 Error Message(s) or Log"
|
||||
- type: input
|
||||
id: uptime-kuma-version
|
||||
attributes:
|
||||
|
19
.github/ISSUE_TEMPLATE/security.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/security.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
|
||||
name: "Security Issue"
|
||||
about: "Just for alerting @louislam, do not provide any details here"
|
||||
title: "Security Issue"
|
||||
ref: "main"
|
||||
labels:
|
||||
|
||||
- security
|
||||
|
||||
---
|
||||
|
||||
DO NOT PROVIDE ANY DETAILS HERE. Please privately report to https://github.com/louislam/uptime-kuma/security/advisories/new.
|
||||
|
||||
|
||||
Why need this issue? It is because GitHub Advisory do not send a notification to @louislam, it is a workaround to do so.
|
||||
|
||||
Your GitHub Advisory URL:
|
||||
|
1
.github/config/exclude.txt
vendored
Normal file
1
.github/config/exclude.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# This is a .gitignore style file for 'GrantBirki/json-yaml-validate' Action workflow
|
26
.github/workflows/json-yaml-validate.yml
vendored
Normal file
26
.github/workflows/json-yaml-validate.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: json-yaml-validate
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write # enable write permissions for pull request comments
|
||||
|
||||
jobs:
|
||||
json-yaml-validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: json-yaml-validate
|
||||
id: json-yaml-validate
|
||||
uses: GrantBirki/json-yaml-validate@v1.3.0
|
||||
with:
|
||||
comment: "true" # enable comment mode
|
||||
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,3 +20,6 @@ cypress/screenshots
|
||||
/extra/healthcheck.exe
|
||||
/extra/healthcheck
|
||||
/extra/healthcheck-armv7
|
||||
|
||||
extra/exe-builder/bin
|
||||
extra/exe-builder/obj
|
||||
|
@@ -10,5 +10,6 @@
|
||||
"color-function-notation": "legacy",
|
||||
"shorthand-property-no-redundant-values": null,
|
||||
"color-hex-length": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null
|
||||
}
|
||||
}
|
||||
|
@@ -235,12 +235,13 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
|
||||
|
||||
1. Draft a release note
|
||||
2. Make sure the repo is cleared
|
||||
3. If the healthcheck is updated, remember to re-compile it: `npm run build-docker-builder-go`
|
||||
3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN`
|
||||
4. Wait until the `Press any key to continue`
|
||||
5. `git push`
|
||||
6. Publish the release note as 1.X.X
|
||||
7. Press any key to continue
|
||||
8. SSH to demo site server and update to 1.X.X
|
||||
8. Deploy to the demo server: `npm run deploy-demo-server`
|
||||
|
||||
Checking:
|
||||
|
||||
|
@@ -51,6 +51,7 @@ Uptime Kuma is now running on http://localhost:3001
|
||||
|
||||
Required Tools:
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 14
|
||||
- [npm](https://docs.npmjs.com/cli/) >= 7
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
||||
|
||||
|
@@ -2,10 +2,15 @@
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
|
||||
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
|
||||
|
||||
Do not use the public issue tracker or discuss it in the 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.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
### Uptime Kuma Versions
|
||||
|
7
db/patch-add-description-monitor.sql
Normal file
7
db/patch-add-description-monitor.sql
Normal 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 description TEXT default null;
|
||||
|
||||
COMMIT;
|
13
db/patch-api-key-table.sql
Normal file
13
db/patch-api-key-table.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE [api_key] (
|
||||
[id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
[key] VARCHAR(255) NOT NULL,
|
||||
[name] VARCHAR(255) NOT NULL,
|
||||
[user_id] INTEGER NOT NULL,
|
||||
[created_date] DATETIME DEFAULT (DATETIME('now')) NOT NULL,
|
||||
[active] BOOLEAN DEFAULT 1 NOT NULL,
|
||||
[expires] DATETIME DEFAULT NULL,
|
||||
CONSTRAINT FK_user FOREIGN KEY ([user_id]) REFERENCES [user]([id]) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
COMMIT;
|
12
db/patch-http-body-encoding.sql
Normal file
12
db/patch-http-body-encoding.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor ADD http_body_encoding VARCHAR(25);
|
||||
|
||||
COMMIT;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
UPDATE monitor SET http_body_encoding = 'json' WHERE (type = 'http' or type = 'keyword') AND http_body_encoding IS NULL;
|
||||
|
||||
COMMIT;
|
11
db/patch-maintenance-cron.sql
Normal file
11
db/patch-maintenance-cron.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DROP TABLE maintenance_timeslot;
|
||||
|
||||
-- 999 characters. https://stackoverflow.com/questions/46134830/maximum-length-for-cron-job
|
||||
ALTER TABLE maintenance ADD cron TEXT;
|
||||
ALTER TABLE maintenance ADD timezone VARCHAR(255);
|
||||
ALTER TABLE maintenance ADD duration INTEGER;
|
||||
|
||||
COMMIT;
|
13
db/patch-monitor-tls.sql
Normal file
13
db/patch-monitor-tls.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD tls_ca TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD tls_cert TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD tls_key TEXT default null;
|
||||
|
||||
COMMIT;
|
@@ -4,5 +4,5 @@ WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib git && \
|
||||
pip3 --no-cache-dir install apprise==1.2.1 && \
|
||||
pip3 --no-cache-dir install apprise==1.3.0 && \
|
||||
rm -rf /root/.cache
|
||||
|
@@ -11,7 +11,7 @@ WORKDIR /app
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux dumb-init git && \
|
||||
pip3 --no-cache-dir install apprise==1.2.1 && \
|
||||
pip3 --no-cache-dir install apprise==1.3.0 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt --yes autoremove
|
||||
|
||||
|
@@ -71,7 +71,7 @@ RUN npm ci
|
||||
|
||||
EXPOSE 3000 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
|
||||
CMD ["npm", "run", "start-pr-test"]
|
||||
|
||||
############################################
|
||||
|
@@ -22,7 +22,8 @@ if (! exists) {
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
// Also update package-lock.json
|
||||
childProcess.spawnSync("npm", [ "install" ]);
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
childProcess.spawnSync(npm, [ "install" ]);
|
||||
|
||||
commit(version);
|
||||
tag(version);
|
||||
|
60
extra/deploy-demo-server.js
Normal file
60
extra/deploy-demo-server.js
Normal file
@@ -0,0 +1,60 @@
|
||||
require("dotenv").config();
|
||||
const { NodeSSH } = require("node-ssh");
|
||||
const readline = require("readline");
|
||||
const rl = readline.createInterface({ input: process.stdin,
|
||||
output: process.stdout });
|
||||
const prompt = (query) => new Promise((resolve) => rl.question(query, resolve));
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log("SSH to demo server");
|
||||
const ssh = new NodeSSH();
|
||||
await ssh.connect({
|
||||
host: process.env.UPTIME_KUMA_DEMO_HOST,
|
||||
port: process.env.UPTIME_KUMA_DEMO_PORT,
|
||||
username: process.env.UPTIME_KUMA_DEMO_USERNAME,
|
||||
privateKeyPath: process.env.UPTIME_KUMA_DEMO_PRIVATE_KEY_PATH
|
||||
});
|
||||
|
||||
let cwd = process.env.UPTIME_KUMA_DEMO_CWD;
|
||||
let result;
|
||||
|
||||
const version = await prompt("Enter Version: ");
|
||||
|
||||
result = await ssh.execCommand("git fetch --all", {
|
||||
cwd,
|
||||
});
|
||||
console.log(result.stdout + result.stderr);
|
||||
|
||||
await prompt("Press any key to continue...");
|
||||
|
||||
result = await ssh.execCommand(`git checkout ${version} --force`, {
|
||||
cwd,
|
||||
});
|
||||
console.log(result.stdout + result.stderr);
|
||||
|
||||
result = await ssh.execCommand("npm run download-dist", {
|
||||
cwd,
|
||||
});
|
||||
console.log(result.stdout + result.stderr);
|
||||
|
||||
result = await ssh.execCommand("npm install --production", {
|
||||
cwd,
|
||||
});
|
||||
console.log(result.stdout + result.stderr);
|
||||
|
||||
/*
|
||||
result = await ssh.execCommand("pm2 restart 1", {
|
||||
cwd,
|
||||
});
|
||||
console.log(result.stdout + result.stderr);*/
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
})();
|
||||
|
||||
// When done reading prompt, exit program
|
||||
rl.on("close", () => process.exit(0));
|
@@ -47,6 +47,7 @@ function download(url) {
|
||||
});
|
||||
}
|
||||
console.log("Done");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
tarStream.on("error", () => {
|
||||
|
1
extra/exe-builder/.gitignore
vendored
Normal file
1
extra/exe-builder/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
packages/
|
35
extra/exe-builder/App.config
Normal file
35
extra/exe-builder/App.config
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||
</startup>
|
||||
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Reflection" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.1" newVersion="4.1.1.1" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.InteropServices" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
84
extra/exe-builder/DownloadForm.Designer.cs
generated
Normal file
84
extra/exe-builder/DownloadForm.Designer.cs
generated
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace UptimeKuma {
|
||||
partial class DownloadForm {
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing) {
|
||||
if (disposing && (components != null)) {
|
||||
components.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent() {
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DownloadForm));
|
||||
this.progressBar = new System.Windows.Forms.ProgressBar();
|
||||
this.label = new System.Windows.Forms.Label();
|
||||
this.labelData = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// progressBar
|
||||
//
|
||||
this.progressBar.Location = new System.Drawing.Point(12, 12);
|
||||
this.progressBar.Name = "progressBar";
|
||||
this.progressBar.Size = new System.Drawing.Size(472, 41);
|
||||
this.progressBar.TabIndex = 0;
|
||||
//
|
||||
// label
|
||||
//
|
||||
this.label.Location = new System.Drawing.Point(12, 59);
|
||||
this.label.Name = "label";
|
||||
this.label.Size = new System.Drawing.Size(472, 23);
|
||||
this.label.TabIndex = 1;
|
||||
this.label.Text = "Preparing...";
|
||||
//
|
||||
// labelData
|
||||
//
|
||||
this.labelData.Location = new System.Drawing.Point(12, 82);
|
||||
this.labelData.Name = "labelData";
|
||||
this.labelData.Size = new System.Drawing.Size(472, 23);
|
||||
this.labelData.TabIndex = 2;
|
||||
//
|
||||
// DownloadForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(496, 117);
|
||||
this.Controls.Add(this.labelData);
|
||||
this.Controls.Add(this.label);
|
||||
this.Controls.Add(this.progressBar);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
|
||||
this.MaximizeBox = false;
|
||||
this.Name = "DownloadForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "Uptime Kuma";
|
||||
this.Load += new System.EventHandler(this.DownloadForm_Load);
|
||||
this.ResumeLayout(false);
|
||||
}
|
||||
|
||||
private System.Windows.Forms.Label labelData;
|
||||
|
||||
private System.Windows.Forms.Label label;
|
||||
|
||||
private System.Windows.Forms.ProgressBar progressBar;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
204
extra/exe-builder/DownloadForm.cs
Normal file
204
extra/exe-builder/DownloadForm.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace UptimeKuma {
|
||||
public partial class DownloadForm : Form {
|
||||
private readonly Queue<DownloadItem> downloadQueue = new();
|
||||
private readonly WebClient webClient = new();
|
||||
private DownloadItem currentDownloadItem;
|
||||
|
||||
public DownloadForm() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void DownloadForm_Load(object sender, EventArgs e) {
|
||||
webClient.DownloadProgressChanged += DownloadProgressChanged;
|
||||
webClient.DownloadFileCompleted += DownloadFileCompleted;
|
||||
|
||||
label.Text = "Reading latest version...";
|
||||
|
||||
// Read json from https://uptime.kuma.pet/version
|
||||
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
|
||||
var versionObj = JsonConvert.DeserializeObject<Version>(versionJson);
|
||||
|
||||
var nodeVersion = versionObj.nodejs;
|
||||
var uptimeKumaVersion = versionObj.latest;
|
||||
var hasUpdateFile = File.Exists("update");
|
||||
|
||||
if (!Directory.Exists("node")) {
|
||||
downloadQueue.Enqueue(new DownloadItem {
|
||||
URL = $"https://nodejs.org/dist/v{nodeVersion}/node-v{nodeVersion}-win-x64.zip",
|
||||
Filename = "node.zip",
|
||||
TargetFolder = "node"
|
||||
});
|
||||
}
|
||||
|
||||
if (!Directory.Exists("core") || hasUpdateFile) {
|
||||
|
||||
// It is update, rename the core folder to core.old
|
||||
if (Directory.Exists("core")) {
|
||||
// Remove the old core.old folder
|
||||
if (Directory.Exists("core.old")) {
|
||||
Directory.Delete("core.old", true);
|
||||
}
|
||||
|
||||
Directory.Move("core", "core.old");
|
||||
}
|
||||
|
||||
downloadQueue.Enqueue(new DownloadItem {
|
||||
URL = $"https://github.com/louislam/uptime-kuma/archive/refs/tags/{uptimeKumaVersion}.zip",
|
||||
Filename = "core.zip",
|
||||
TargetFolder = "core"
|
||||
});
|
||||
|
||||
File.WriteAllText("version.json", versionJson);
|
||||
|
||||
// Delete the update file
|
||||
if (hasUpdateFile) {
|
||||
File.Delete("update");
|
||||
}
|
||||
}
|
||||
|
||||
DownloadNextFile();
|
||||
}
|
||||
|
||||
void DownloadNextFile() {
|
||||
if (downloadQueue.Count > 0) {
|
||||
var item = downloadQueue.Dequeue();
|
||||
|
||||
currentDownloadItem = item;
|
||||
|
||||
// Download if the zip file is not existing
|
||||
if (!File.Exists(item.Filename)) {
|
||||
label.Text = item.URL;
|
||||
webClient.DownloadFileAsync(new Uri(item.URL), item.Filename);
|
||||
} else {
|
||||
progressBar.Value = 100;
|
||||
label.Text = "Use local " + item.Filename;
|
||||
DownloadFileCompleted(null, null);
|
||||
}
|
||||
} else {
|
||||
npmSetup();
|
||||
}
|
||||
}
|
||||
|
||||
void npmSetup() {
|
||||
labelData.Text = "";
|
||||
|
||||
var npm = "..\\node\\npm.cmd";
|
||||
var cmd = $"{npm} ci --production & {npm} run download-dist & exit";
|
||||
|
||||
var startInfo = new ProcessStartInfo {
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/k \"{cmd}\"",
|
||||
RedirectStandardOutput = false,
|
||||
RedirectStandardError = false,
|
||||
RedirectStandardInput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = false,
|
||||
WorkingDirectory = "core"
|
||||
};
|
||||
|
||||
var process = new Process();
|
||||
process.StartInfo = startInfo;
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += (_, e) => {
|
||||
progressBar.Value = 100;
|
||||
|
||||
if (process.ExitCode == 0) {
|
||||
Task.Delay(2000).ContinueWith(_ => {
|
||||
Application.Restart();
|
||||
});
|
||||
label.Text = "Done";
|
||||
} else {
|
||||
label.Text = "Failed, exit code: " + process.ExitCode;
|
||||
}
|
||||
|
||||
};
|
||||
process.Start();
|
||||
label.Text = "Installing dependencies and download dist files";
|
||||
progressBar.Value = 50;
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) {
|
||||
progressBar.Value = e.ProgressPercentage;
|
||||
var total = e.TotalBytesToReceive / 1024;
|
||||
var current = e.BytesReceived / 1024;
|
||||
|
||||
if (total > 0) {
|
||||
labelData.Text = $"{current}KB/{total}KB";
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadFileCompleted(object sender, AsyncCompletedEventArgs e) {
|
||||
Extract(currentDownloadItem);
|
||||
DownloadNextFile();
|
||||
}
|
||||
|
||||
void Extract(DownloadItem item) {
|
||||
if (Directory.Exists(item.TargetFolder)) {
|
||||
var dir = new DirectoryInfo(item.TargetFolder);
|
||||
dir.Delete(true);
|
||||
}
|
||||
|
||||
if (Directory.Exists("temp")) {
|
||||
var dir = new DirectoryInfo("temp");
|
||||
dir.Delete(true);
|
||||
}
|
||||
|
||||
labelData.Text = $"Extracting {item.Filename}...";
|
||||
|
||||
ZipFile.ExtractToDirectory(item.Filename, "temp");
|
||||
|
||||
string[] dirList;
|
||||
|
||||
// Move to the correct level
|
||||
dirList = Directory.GetDirectories("temp");
|
||||
|
||||
|
||||
|
||||
if (dirList.Length > 0) {
|
||||
var dir = dirList[0];
|
||||
|
||||
// As sometime ExtractToDirectory is still locking the directory, loop until ok
|
||||
while (true) {
|
||||
try {
|
||||
Directory.Move(dir, item.TargetFolder);
|
||||
break;
|
||||
} catch (Exception exception) {
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
MessageBox.Show("Unexcepted Error: Cannot move extracted files, folder not found.");
|
||||
}
|
||||
|
||||
labelData.Text = $"Extracted";
|
||||
|
||||
if (Directory.Exists("temp")) {
|
||||
var dir = new DirectoryInfo("temp");
|
||||
dir.Delete(true);
|
||||
}
|
||||
|
||||
File.Delete(item.Filename);
|
||||
}
|
||||
}
|
||||
|
||||
public class DownloadItem {
|
||||
public string URL { get; set; }
|
||||
public string Filename { get; set; }
|
||||
public string TargetFolder { get; set; }
|
||||
}
|
||||
}
|
||||
|
377
extra/exe-builder/DownloadForm.resx
Normal file
377
extra/exe-builder/DownloadForm.resx
Normal file
@@ -0,0 +1,377 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAA
|
||||
AABgAAAAAQAgAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAA////BPT09Bfu7u4e8fHxJPPz8yv19fUy9fX1M/Pz8yvx8fEk9vb2HPPz8xXMzMwFAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//
|
||||
/wHv7+8f7u7uPPPz81Tx8fFs8fHxgPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGB8fHxcfHx8V3x8fFI9PT0MOvr6w0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AADy8vIU8fHxS/Dw8Hbx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fFr9PT0R/Dw8CIAAAABAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAA8vLyFPHx8Vnx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fFs9fX1Mb+/vwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAICAgALy8vI88fHxfvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy8nby8vI8gICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAzMzMBfHx8Vrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyYf///wwAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8vLyYPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8W/z8/MWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+9R8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLw8PB26urqDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLy8ijx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu7w7Ifj79ud2u7PtNLrw83P677dzeu85c3r
|
||||
u+rM67rwzOu68c7rverQ68Dj0uvD3NbuyM3b7c+64u7apujv5ZPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxXgAAAAEAAAAAAAAAAAAAAAAAAAAA4+PjCfDw
|
||||
8Hfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLd7tSmzeu92MbqsvvG6bH/xumy/8fq
|
||||
s//H6rP/yOq0/8jqtf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//Q7MDx1u7Kz9/t
|
||||
163s8OuJ8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu/v7y8AAAAAAAAAAAAA
|
||||
AAAAAAAA7u7uPfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC5PDdl8jqtuTE6a7/xOmv/8Xp
|
||||
sP/G6bH/xumx/8bpsv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zr
|
||||
u//N67v/zey8/87svf/P67742e3Mx+jv5ZLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw
|
||||
8HWAgIACAAAAAAAAAACqqqoD8vLyc/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLf7degxOiu+cPo
|
||||
rf/D6a7/xOmu/8Xpr//F6bD/xumx/8bpsf/G6bL/x+qz/8fqs//I6rT/yOq1/8nqtv/J6rb/yuu3/8rr
|
||||
uP/L67j/y+u5/8zruv/M67v/zeu7/83svP/O7L3/zuy9/87svfzc7tK28fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fEkAAAAAAAAAADz8/Mq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgunv
|
||||
5o3D6a/0wuis/8Lorf/D6K3/xOmu/8Tprv/F6a//xemw/8bpsf/G6bH/xumy/8fqs//H6rP/yOq0/8jq
|
||||
tf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/87svf/O7L3/3e/TtPHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAADy8vJM8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgszqutDB6Kv/weir/8LorP/D6K3/w+it/8Tprv/E6a7/xemv/8XpsP/G6bH/xumx/8bp
|
||||
sv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zru//N67v/zey8/87s
|
||||
vf/O7L3/zuy++u3w6Yzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJ1AAAAAAAAAADx8fFr8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC6O/kjsDoqvzA6Kr/weir/8Loq//C6Kz/w+it/8Porf/E6a7/xOmu/8Xp
|
||||
r//F6bD/xumx/8bpsf/G6bL/x+qz/8fqtP/I6rT/yOq1/8nqtv/J6rb/yuu3/8rruP/L67n/y+u5/8zr
|
||||
uv/M67v/zeu7/83svP/O7L3/zuy9/93u07Xx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC////Bv//
|
||||
/wfx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1ezJsr/nqf/A56n/weiq/8Hoq//C6Kv/wuis/8Po
|
||||
rf/D6K3/xOmu/8Pprv+856T/uOed/7bmmv+05Zf/teWZ/7jnnf+86KP/wOio/8fqs//J6rb/yeq2/8rr
|
||||
t//K67j/y+u5/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/9buyNLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8vLyE/Ly8hPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCy+q6zr/nqP/A56n/wOep/8Ho
|
||||
qv/B6Kv/wuir/8LorP+u5Y//neF2/5bgav+V4Gr/luBr/5fhbP+Y4W7/meFv/5rhcf+b4nL/nOJ0/53i
|
||||
dv+j5H//reaM/7nnnf/E6q//y+y4/8vruf/L67n/zOu6/8zru//N67v/zey8/9Lsxd/x8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC7+/vIPb29hzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/n
|
||||
qP+/56j/wOep/8Dnqf/B6Kr/weir/7nmn/+R32T/kt9l/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nh
|
||||
b/+a4XH/m+Jy/5zidP+d4nX/nuN3/5/jeP+f4nn/weqq/8rruP/L67n/y+u5/8zruv/M67v/zeu7/9Ls
|
||||
w+Lx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwI/Hx8SXx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGCxeix5L/nqP+/56j/v+eo/8Dnqf/A56n/weiq/7Pllv+Q3mP/kd9k/5LfZf+T32f/lOBo/5Xg
|
||||
av+W4Gv/l+Ft/5jhbv+Z4W//muFx/5vicv+c4nT/neJ1/57jd/+f43j/xOmu/8rrt//K67j/y+u5/8vr
|
||||
uf/M67r/zOu7/9Tsxtfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC9PT0GO/v7yDx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGCx+m037/nqP+/56j/v+eo/7/nqP/A56n/wOip/7TmmP+P3mH/kN5j/5Hf
|
||||
ZP+S32b/k99n/5TgaP+V4Gr/luBr/5fhbf+Y4W7/meFw/5rhcf+b4nL/nOJ0/53idf+h5Hz/yuu2/8nq
|
||||
t//K67f/yuu4/8vruf/L67n/zOu6/9ftysrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7e3tDvT0
|
||||
9Bfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCyOq117/nqP+/56j/v+eo/7/nqP+/56j/wOep/7vn
|
||||
of+O3mD/j95h/5DeY/+R32T/kt9m/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nhcP+a4nH/m+Jy/5zi
|
||||
dP+r5Yr/yOq1/8nqtv/J6rf/yuu3/8rruP/L67n/y+u5/9zu1LHx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLz8/OA////A+7u7g/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCz+q+xb/nqP+/56j/v+eo/7/n
|
||||
qP+/56j/v+eo/8Dnqf+S4Gb/jt5g/4/eYf+Q3mP/kd9k/5LfZv+T32f/lOBo/5Xgav+W4Gv/l+Ft/5jh
|
||||
bv+Z4XD/muJx/5vic/+4553/yOq0/8jqtf/J6rb/yeq3/8rrt//K67j/y+u5/+bw4Zfx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fFrAAAAAP///wHz8/N88fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1+zMrr/n
|
||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+f4Xn/jd5f/47eYP+P3mH/kN5j/5HfZP+S32b/k99n/5Tg
|
||||
af+V4Gr/luBr/5fhbf+Y4W7/meFw/5vic//F6rD/x+q0/8jqtP/I6rX/yeq2/8nqt//K67f/zOu88u/x
|
||||
74Px8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLv7+9QAAAAAAAAAADw8PBm8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC5e7gk7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//jN1d/43eX/+O3mD/j95h/5De
|
||||
Y/+R32T/kt9m/5PfZ/+U4Gn/leBq/5bga/+X4W3/mOFu/6rliP/G6rL/x+qz/8fqtP/I6rT/yOq1/8nq
|
||||
tv/J6rf/1OzGy/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YL19fUzAAAAAAAAAADy8vJO8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgsPoru2/56j/v+eo/7/nqP+/56j/v+eo/7/nqP++6Kf/j95i/4zd
|
||||
Xf+N3l//jt5g/4/eYv+Q3mP/kd9k/5LfZv+T32f/lOBp/5Xgav+W4Gz/l+Ft/7voov/G6bL/xuqy/8fq
|
||||
s//H6rT/yOq1/8jqtf/J6rb/4e/Zo/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PARAAAAAAAA
|
||||
AADu7u4u8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgszpvMm/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||
qP+/56j/q+SL/4vdXP+M3V3/jd5f/47eYP+P3mL/kN9j/5HfZP+S32b/k99n/5Tgaf+V4Gr/qOOH/8Xp
|
||||
sP/G6bH/xumy/8bqsv/H6rP/x+q0/8jqtf/K67jy8PHwhPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8WoAAAAAAAAAAAAAAADo6OgL8fHxgfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguDv2J2/56j/v+eo/7/n
|
||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/6Xjgv+L3Vz/jN1d/43eX/+O3mD/j95i/5DfY/+R32T/kt9m/5Pf
|
||||
Z/+k44D/xOmu/8XpsP/F6bD/xumx/8bpsv/G6rL/x+qz/8fqtP/W7cnB8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvPz80AAAAAAAAAAAAAAAAAAAAAA8PDwZ/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLD6K/rv+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//kt5n/4zdXf+N3l//jt5g/4/e
|
||||
Yv+Q32P/luFs/67kj//D6K3/xOmu/8Tpr//F6bD/xemw/8bpsf/G6bL/xuqy/8fqtP7o7+WR8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xYAAAAAAAAAAAAAAAAAAAAA8vLyPPHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLV7ci0v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOio/7Xl
|
||||
mv+u5I7/rOSM/67kj/+35pz/wumr/8Lorf/D6K3/w+it/8Tprv/E6a//xemw/8XpsP/G6bH/xumy/9Ds
|
||||
wNPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyZQAAAAAAAAAAAAAAAAAAAAAAAAAA////DPHx
|
||||
8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||
qP+/56j/v+eo/7/nqP+/56j/wOep/8Doqv/B6Kr/weir/8LorP/C6K3/w+it/8Porv/E6a7/xOmv/8Xp
|
||||
sP/F6bD/yOq18uvw6Yvx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7+/vMQAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAPHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6O/ij8LorPG/56j/v+eo/7/n
|
||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weiq/8Hoq//C6Kz/wuit/8Po
|
||||
rf/D6K7/xOmu/8Tpr//F6bH74u/anvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PB6////BQAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPPz8yrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguHu
|
||||
2pnB56v2v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/wOiq/8Ho
|
||||
q//B6Kv/wuis/8Lorf/D6K3/w+mu/8Tprv3b7dKq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fFJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHy8vJf8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLi7tyXwumt8L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||
qP+/56j/wOep/8Doqv/B6Kv/weir/8LorP/C6K3/xOiv+d7u1aTx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvLy8nb///8KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+8Q8/Pze/Hx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6/Dpiszqu82/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weir/8Hoq//H6bTj5e7elfHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8yoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAA9fX1MvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLe7tShx+mz3r/n
|
||||
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/xumy5drtz6rv8e+D8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyTgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPHx8Unx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgubv45DU68e2y+q6z8XoseTD6a7uweir9MPpru7F6bHly+q50tLsxLrl796U8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJh////AwAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wHx8fFZ8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8Wzf398IAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8D8/PzVfHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwZujo
|
||||
6AsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAA////AfHx8Ujx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fFa////BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/Mp8vLydvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8/PzfPHx8TcAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CvLy8lDz8/N/8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvPz84Hx8fFa8PDwEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AADw8PAR8vLyTvHx8X3x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fF/8/PzVvT09BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wXz8/Mq8/PzU/Hx8XDx8fGB8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLy8vJz8fHxWO/v7y////8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8G7e3tHfLy
|
||||
8ifu7u4u8PDwNPT09C/y8vIo7+/vH+Pj4wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAP///////wAA////////AAD///////8AAP//gAf//wAA//gAAD//AAD/wAAAB/8AAP+A
|
||||
AAAB/wAA/gAAAAB/AAD8AAAAAD8AAPgAAAAAHwAA8AAAAAAPAADwAAAAAAcAAOAAAAAABwAA4AAAAAAD
|
||||
AADAAAAAAAMAAMAAAAAAAwAAwAAAAAABAACAAAAAAAEAAIAAAAAAAQAAgAAAAAABAACAAAAAAAEAAIAA
|
||||
AAAAAQAAgAAAAAABAACAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAAABwAAwAAAAAAH
|
||||
AADgAAAAAAcAAOAAAAAADwAA4AAAAAAPAADwAAAAAB8AAPAAAAAAHwAA+AAAAAA/AAD8AAAAAD8AAPwA
|
||||
AAAAfwAA/gAAAAD/AAD/AAAAAf8AAP+AAAAD/wAA/8AAAAf/AAD/8AAAH/8AAP/8AAA//wAA//8AAf//
|
||||
AAD//+AP//8AAP///////wAA////////AAD///////8AACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAgICAAu/v7xD09PQX7u7uHvDw8CP29vYb8vLyFOrq6gwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICA
|
||||
gALy8vIm7+/vT/Pz82fz8/N98fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw8Hrw8PBm7+/vUPT0
|
||||
9C3o6OgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOPj
|
||||
4wnz8/NC8vLydPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YHy8vJj8/PzKoCAgAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AADx8fEl8vLydfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxcfHx8SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAA9PT0LfHx8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8/PzgPLy8j0AAAABAAAAAAAA
|
||||
AAAAAAAAAAAAAO3t7Rzx8fGA8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLr8OmM5O7emeTv
|
||||
3Z7h79mj5fDem+nv45Tu8u6H8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy
|
||||
8joAAAAAAAAAAAAAAAD///8E8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC7vDshtns0K7N67zayeq288fq
|
||||
s//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/P7L7w0+zF29vv0Lrn8OKX8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8/PzfvPz8xUAAAAAAAAAAPX19TLx8fGC8fHxgvHx8YLx8fGC8fHxgt3u1KXF6rHzxOmv/8Xp
|
||||
sP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/M67v/zey8/87svf/S7MPj4u7Zp/Hx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8/PzVQAAAAAAAAAA8fHxavHx8YLx8fGC8fHxgvHx8YLf7defwuis/cPo
|
||||
rf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruv/M67v/zey8/87s
|
||||
vf/N67z/3e7SufHx8YLx8fGC8fHxgvHx8YLz8/N8////Bf///w3x8fGC8fHxgvHx8YLx8fGC8fHxgsXp
|
||||
sOnB6Kv/wuis/8Porf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vr
|
||||
uv/M67v/zey8/87svf/O67z96/Hoj/Hx8YLx8fGC8fHxgvHx8YLy8vIm8/PzK/Hx8YLx8fGC8fHxgvHx
|
||||
8YLg79icwOep/8Hoqv/B6Kv/wuis/8Porf/E6a7/wuit/73opP+76KL/u+eh/77opv/D6a3/yeu1/8nq
|
||||
tv/K67f/y+u5/8zruv/M67v/zey8/87svf/d7tSz8fHxgvHx8YLx8fGC8fHxgvHx8Tby8vI68fHxgvHx
|
||||
8YLx8fGC8fHxgtTrxre/56j/wOep/8Hoqv/B6Kv/uOad/53idv+V4Gn/leBq/5fhbP+Y4W//muFx/5vi
|
||||
c/+e4Xb/puWD/7PmlP/D6a3/y+u5/8zruv/M67v/zey8/9rtzsHx8fGC8fHxgvHx8YLx8fGC8/PzQfPz
|
||||
80Lx8fGC8fHxgvHx8YLx8fGC0OvAwr/nqP+/56j/wOep/8Hoqv+o44b/kd9k/5LfZv+U4Gj/leBq/5fh
|
||||
bf+Y4W//muFx/5vic/+d4nX/n+N3/7fnm//K67j/y+u5/8zruv/M67v/2u3QvPHx8YLx8fGC8fHxgvHx
|
||||
8YLy8vI98/PzP/Hx8YLx8fGC8fHxgvHx8YLQ6sK/v+eo/7/nqP+/56j/wOep/6jjhv+P3mL/kd9k/5Lf
|
||||
Zv+U4Gj/leBr/5fhbf+Y4W//muFx/5zic/+d4nX/v+mm/8nqt//K67j/y+u5/8zruv/f79au8fHxgvHx
|
||||
8YLx8fGC8fHxgvX19TLx8fE38fHxgvHx8YLx8fGC8fHxgtTrybO/56j/v+eo/7/nqP+/56j/sOSS/47e
|
||||
YP+P3mL/kd9k/5LfZv+U4Gj/leBr/5fhbf+Z4W//muJx/5/jd//H6bP/yeq2/8nqt//K67j/y+u5/+nv
|
||||
45Tx8fGC8fHxgvHx8YLx8fGC7+/vIPHx8SXx8fGC8fHxgvHx8YLx8fGC4e/Zm7/nqP+/56j/v+eo/7/n
|
||||
qP+956X/jt5h/47eYP+P3mL/kd9k/5LfZv+U4Gn/luBr/5fhbf+Z4W//q+aK/8fqs//I6rT/yeq2/8nq
|
||||
t//N7Lvw8fHxgvHx8YLx8fGC8fHxgvPz84D///8G6+vrDfHx8YLx8fGC8fHxgvHx8YLv8e+Dweis87/n
|
||||
qP+/56j/v+eo/7/nqP+d4XX/jN1e/47eYP+P3mL/kd9k/5PfZ/+U4Gn/luBr/5fhbf+86KP/xuqy/8fq
|
||||
s//I6rX/yeq2/9Tsx8nx8fGC8fHxgvHx8YLx8fGC8PDwaAAAAAAAAAAA8fHxbPHx8YLx8fGC8fHxgvHx
|
||||
8YLM6rrMv+eo/7/nqP+/56j/v+eo/7blmv+N3V//jN1e/47eYP+Q3mL/kd9k/5PfZ/+U4Gn/qeSH/8Xp
|
||||
sP/G6bH/xuqy/8fqs//I6rX/5fDem/Hx8YLx8fGC8fHxgvHx8YLz8/M/AAAAAAAAAADz8/NB8fHxgvHx
|
||||
8YLx8fGC8fHxgt3s06O/56j/v+eo/7/nqP+/56j/v+eo/7Xmmf+U32n/jN1e/47eYP+Q3mL/k99o/6zk
|
||||
i//D6a7/xemv/8XpsP/G6bH/xuqy/8vqu+jx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xUAAAAAAAAAAPT0
|
||||
9Bfx8fGC8fHxgvHx8YLx8fGC8fHvg8Tpsee/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+35pz/suWV/7Xm
|
||||
mf/A6Kj/wuit/8Porf/E6a7/xemv/8XpsP/G6bH/3e3UqvHx8YLx8fGC8fHxgvHx8YLw8PBmAAAAAAAA
|
||||
AAAAAAAAAAAAAPHx8W7x8fGC8fHxgvHx8YLx8fGC4u7cmMHnqvm/56j/v+eo/7/nqP+/56j/v+eo/7/n
|
||||
qP+/56j/wOep/8Hoqv/C6Kz/wuit/8Porf/E6a7/xemv/9Hrwszx8fGC8fHxgvHx8YLx8fGC8fHxgvX1
|
||||
9TEAAAAAAAAAAAAAAAAAAAAA7u7uO/Hx8YLx8fGC8fHxgvHx8YLx8fGC3e7SpMHoqfq/56j/v+eo/7/n
|
||||
qP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz/wuit/8Porf/O67zV8PHwhPHx8YLx8fGC8fHxgvHx
|
||||
8YLy8vJ2////BQAAAAAAAAAAAAAAAAAAAACqqqoD8PDwafHx8YLx8fGC8fHxgvHx8YLx8fGC4O/YnMTo
|
||||
ruy/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz90uvEwe/x74Px8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvPz8ykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/MW8fHxfPHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8PLuhdXtyLXF6bHlv+eo/7/nqP+/56j/v+eo/7/nqP/B6Kv0zeq8zOXv4JTx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADy8vIm8fHxgPHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLs8OmJ4e/Zm93u06Pf7def5+/hkvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxXf///wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AADy8vIo8/PzffHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8VnMzMwFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAD29vYb8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz83/v7+9BgICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8/PzQPLy8nnx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz84Hx8fFc9PT0GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////B/X19TLx8fFc8PDwevHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgPHx8Wv09PRE9PT0FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAA7+/vEPb29hvw8PAj7+/vH/T09Be/v78EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////8B///wAA//wAAD/wAAAP4AAAB+AA
|
||||
AAfAAAADwAAAA4AAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAADwAAAA8AAAAPAAAAH4AAAB+AA
|
||||
AA/wAAAP+AAAH/gAAD/+AAB//wAB///AA///+B////////////8oAAAAEAAAACAAAAABACAAAAAAAAAE
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CfDw8BH///8GAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAAu7u7i7x8fFe8PDwevHx8YLx8fGC8fHxgvDw
|
||||
8Hvx8fFs7+/vT/Dw8CMAAAABAAAAAAAAAAAAAAAA5ubmCvLy8l/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx
|
||||
8YLx8fGC8fHxgvHx8YLx8fGC8/PzZu7u7g8AAAAAAAAAAPHx8V3x8fGC8fHxgunv5o7Z7c200+vFytTs
|
||||
xc7W7cnH2+7QueLu2qbu8OyH8fHxgvHx8YLx8fFu////BfHx8STx8fGC8fHxgtrtzq3D6a/8xemw/8bp
|
||||
sv/I6rT/yeq2/8vruP/M67v/z+u++Nzu0bjx8fGC8fHxgu/v7zDx8fFI8fHxguzw6ojC56z3wuis/8Tp
|
||||
rv/E6q3/weiq/8fqsv/J6rb/y+u5/8zru//N67z/6/HpjfHx8YLy8vJN8fHxXPHx8YLg79icv+eo/8Ho
|
||||
qv+k4n//lOBo/5fhbf+a4XH/n+J5/7Pmlv/L67n/zOu7/+Xw353x8fGC8fHxXvHx8Vrx8fGC4O3Zm7/n
|
||||
qP+/56j/nuF3/5HfZP+U4Gj/l+Ft/5ricf+x5pL/yeq3/8vruf/r8emN8fHxgu/v70/x8fFK8fHxguzw
|
||||
6ojA6Kn8v+eo/6njiP+O3mD/kd9k/5Tgaf+X4W3/vuim/8jqtP/N67zr8fHxgvHx8YLy8vI68/PzK/Hx
|
||||
8YLx8fGCx+m03L/nqP++6Kb/meBw/47eYP+S32X/q+SL/8XpsP/G6rL/1+zLvvHx8YLz8/OB8PDwEdXV
|
||||
1Qbx8fF98fHxgt/t1Z/A56j9v+eo/7/nqP+656H/vuim/8Lorf/E6a7/yOq18Ovw6Yvx8fGC8vLyYwAA
|
||||
AAAAAAAA8fHxR/Hx8YLx8fGC2O3NrMDnqfq/56j/v+eo/7/nqP/B6Kv/xumy7OTu3Zfx8fGC8/PzgfLy
|
||||
8icAAAAAAAAAAP///wPz8/Nm8fHxgvHx8YLo7+SO0+zFuczquszM6bzJ1+zMru7w7Ibx8fGC8fHxgvHx
|
||||
8UcAAAAAAAAAAAAAAAAAAAAA4+PjCfHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgfPz
|
||||
80D///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8/PzK/Ly8mDz8/N+8fHxgvHx8YLy8vJ68vLyUezs
|
||||
7BsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAevr6w3j4+MJAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD8fwAA4AcAAMADAACAAQAAgAEAAIABAACAAQAAgAEAAIAB
|
||||
AADAAwAAwAMAAOAHAADwDwAA/n8AAP//AAA=
|
||||
</value>
|
||||
</data>
|
||||
</root>
|
3
extra/exe-builder/FodyWeavers.xml
Normal file
3
extra/exe-builder/FodyWeavers.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Costura />
|
||||
</Weavers>
|
141
extra/exe-builder/FodyWeavers.xsd
Normal file
141
extra/exe-builder/FodyWeavers.xsd
Normal file
@@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCompression" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCleanup" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
243
extra/exe-builder/Program.cs
Normal file
243
extra/exe-builder/Program.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
using UptimeKuma.Properties;
|
||||
|
||||
namespace UptimeKuma {
|
||||
static class Program {
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main(string[] args) {
|
||||
var cwd = Path.GetDirectoryName(Application.ExecutablePath);
|
||||
|
||||
if (cwd != null) {
|
||||
Environment.CurrentDirectory = cwd;
|
||||
}
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new UptimeKumaApplicationContext());
|
||||
}
|
||||
}
|
||||
|
||||
public class UptimeKumaApplicationContext : ApplicationContext
|
||||
{
|
||||
private static Mutex mutex = null;
|
||||
|
||||
const string appName = "Uptime Kuma";
|
||||
|
||||
private NotifyIcon trayIcon;
|
||||
private Process process;
|
||||
|
||||
private MenuItem statusMenuItem;
|
||||
private MenuItem runWhenStarts;
|
||||
private MenuItem openMenuItem;
|
||||
|
||||
private RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
|
||||
|
||||
|
||||
public UptimeKumaApplicationContext() {
|
||||
|
||||
// Single instance only
|
||||
bool createdNew;
|
||||
mutex = new Mutex(true, appName, out createdNew);
|
||||
if (!createdNew) {
|
||||
return;
|
||||
}
|
||||
|
||||
var startingText = "Starting server...";
|
||||
trayIcon = new NotifyIcon();
|
||||
trayIcon.Text = startingText;
|
||||
|
||||
runWhenStarts = new MenuItem("Run when system starts", RunWhenStarts);
|
||||
runWhenStarts.Checked = registryKey.GetValue(appName) != null;
|
||||
|
||||
statusMenuItem = new MenuItem(startingText);
|
||||
statusMenuItem.Enabled = false;
|
||||
|
||||
openMenuItem = new MenuItem("Open", Open);
|
||||
openMenuItem.Enabled = false;
|
||||
|
||||
trayIcon.Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location);
|
||||
trayIcon.ContextMenu = new ContextMenu(new MenuItem[] {
|
||||
statusMenuItem,
|
||||
openMenuItem,
|
||||
//new("Debug Console", DebugConsole),
|
||||
runWhenStarts,
|
||||
new("Check for Update...", CheckForUpdate),
|
||||
new("Visit GitHub...", VisitGitHub),
|
||||
new("About", About),
|
||||
new("Exit", Exit),
|
||||
});
|
||||
|
||||
trayIcon.MouseDoubleClick += new MouseEventHandler(Open);
|
||||
trayIcon.Visible = true;
|
||||
|
||||
var hasUpdateFile = File.Exists("update");
|
||||
|
||||
if (!hasUpdateFile && Directory.Exists("core") && Directory.Exists("node") && Directory.Exists("core/node_modules") && Directory.Exists("core/dist")) {
|
||||
// Go go go
|
||||
StartProcess();
|
||||
} else {
|
||||
DownloadFiles();
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadFiles() {
|
||||
var form = new DownloadForm();
|
||||
form.Closed += Exit;
|
||||
form.Show();
|
||||
}
|
||||
|
||||
private void RunWhenStarts(object sender, EventArgs e) {
|
||||
if (registryKey == null) {
|
||||
MessageBox.Show("Error: Unable to set startup registry key.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (runWhenStarts.Checked) {
|
||||
registryKey.DeleteValue(appName, false);
|
||||
runWhenStarts.Checked = false;
|
||||
} else {
|
||||
registryKey.SetValue(appName, Application.ExecutablePath);
|
||||
runWhenStarts.Checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
void StartProcess() {
|
||||
var startInfo = new ProcessStartInfo {
|
||||
FileName = "node/node.exe",
|
||||
Arguments = "server/server.js --data-dir=\"../data/\"",
|
||||
RedirectStandardOutput = false,
|
||||
RedirectStandardError = false,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = "core"
|
||||
};
|
||||
|
||||
process = new Process();
|
||||
process.StartInfo = startInfo;
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += ProcessExited;
|
||||
|
||||
try {
|
||||
process.Start();
|
||||
//Open(null, null);
|
||||
|
||||
// Async task to check if the server is ready
|
||||
Task.Run(() => {
|
||||
var runningText = "Server is running";
|
||||
using TcpClient tcpClient = new TcpClient();
|
||||
while (true) {
|
||||
try {
|
||||
tcpClient.Connect("127.0.0.1", 3001);
|
||||
statusMenuItem.Text = runningText;
|
||||
openMenuItem.Enabled = true;
|
||||
trayIcon.Text = runningText;
|
||||
break;
|
||||
} catch (Exception) {
|
||||
System.Threading.Thread.Sleep(2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
MessageBox.Show("Startup failed: " + e.Message, "Uptime Kuma Error");
|
||||
}
|
||||
}
|
||||
|
||||
void StopProcess() {
|
||||
process?.Kill();
|
||||
}
|
||||
|
||||
void Open(object sender, EventArgs e) {
|
||||
Process.Start("http://localhost:3001");
|
||||
}
|
||||
|
||||
void DebugConsole(object sender, EventArgs e) {
|
||||
|
||||
}
|
||||
|
||||
void CheckForUpdate(object sender, EventArgs e) {
|
||||
var needUpdate = false;
|
||||
|
||||
// Check version.json exists
|
||||
if (File.Exists("version.json")) {
|
||||
// Load version.json and compare with the latest version from GitHub
|
||||
var currentVersionObj = JsonConvert.DeserializeObject<Version>(File.ReadAllText("version.json"));
|
||||
|
||||
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
|
||||
var latestVersionObj = JsonConvert.DeserializeObject<Version>(versionJson);
|
||||
|
||||
// Compare version, if the latest version is newer, then update
|
||||
if (new System.Version(latestVersionObj.latest).CompareTo(new System.Version(currentVersionObj.latest)) > 0) {
|
||||
var result = MessageBox.Show("A new version is available. Do you want to update?", "Update", MessageBoxButtons.YesNo);
|
||||
if (result == DialogResult.Yes) {
|
||||
// Create a empty file `update`, so the app will download the core files again at startup
|
||||
File.Create("update").Close();
|
||||
|
||||
trayIcon.Visible = false;
|
||||
process?.Kill();
|
||||
|
||||
// Restart the app, it will download the core files again at startup
|
||||
Application.Restart();
|
||||
}
|
||||
} else {
|
||||
MessageBox.Show("You are using the latest version.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void VisitGitHub(object sender, EventArgs e)
|
||||
{
|
||||
Process.Start("https://github.com/louislam/uptime-kuma");
|
||||
}
|
||||
|
||||
void About(object sender, EventArgs e)
|
||||
{
|
||||
MessageBox.Show("Uptime Kuma Windows Runtime v1.0.0" + Environment.NewLine + "© 2023 Louis Lam", "Info");
|
||||
}
|
||||
|
||||
void Exit(object sender, EventArgs e)
|
||||
{
|
||||
// Hide tray icon, otherwise it will remain shown until user mouses over it
|
||||
trayIcon.Visible = false;
|
||||
process?.Kill();
|
||||
Application.Exit();
|
||||
}
|
||||
|
||||
void ProcessExited(object sender, EventArgs e) {
|
||||
|
||||
if (process.ExitCode != 0) {
|
||||
var line = "";
|
||||
while (!process.StandardOutput.EndOfStream)
|
||||
{
|
||||
line += process.StandardOutput.ReadLine();
|
||||
}
|
||||
|
||||
MessageBox.Show("Uptime Kuma exited unexpectedly. Exit code: " + process.ExitCode + " " + line);
|
||||
}
|
||||
|
||||
trayIcon.Visible = false;
|
||||
Application.Exit();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
36
extra/exe-builder/Properties/AssemblyInfo.cs
Normal file
36
extra/exe-builder/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Uptime Kuma")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Uptime Kuma")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[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")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// 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")]
|
62
extra/exe-builder/Properties/Resources.Designer.cs
generated
Normal file
62
extra/exe-builder/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,62 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace UptimeKuma.Properties {
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder",
|
||||
"4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance",
|
||||
"CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
|
||||
.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if ((resourceMan == null)) {
|
||||
global::System.Resources.ResourceManager temp =
|
||||
new global::System.Resources.ResourceManager("UptimeKuma.Properties.Resources",
|
||||
typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
|
||||
.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get { return resourceCulture; }
|
||||
set { resourceCulture = value; }
|
||||
}
|
||||
}
|
||||
}
|
117
extra/exe-builder/Properties/Resources.resx
Normal file
117
extra/exe-builder/Properties/Resources.resx
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
23
extra/exe-builder/Properties/Settings.Designer.cs
generated
Normal file
23
extra/exe-builder/Properties/Settings.Designer.cs
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace UptimeKuma.Properties {
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(
|
||||
"Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
private static Settings defaultInstance =
|
||||
((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get { return defaultInstance; }
|
||||
}
|
||||
}
|
||||
}
|
7
extra/exe-builder/Properties/Settings.settings
Normal file
7
extra/exe-builder/Properties/Settings.settings
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
</Profiles>
|
||||
<Settings />
|
||||
</SettingsFile>
|
212
extra/exe-builder/UptimeKuma.csproj
Normal file
212
extra/exe-builder/UptimeKuma.csproj
Normal file
@@ -0,0 +1,212 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="packages\Costura.Fody.5.7.0\build\Costura.Fody.props" Condition="Exists('packages\Costura.Fody.5.7.0\build\Costura.Fody.props')" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>UptimeKuma</RootNamespace>
|
||||
<AssemblyName>uptime-kuma</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ApplicationIcon>..\..\public\favicon.ico</ApplicationIcon>
|
||||
<LangVersion>9</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent>COPY "$(SolutionDir)bin\Debug\uptime-kuma.exe" "%UserProfile%\Desktop\uptime-kuma-win64\"</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Costura, Version=5.7.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Costura.Fody.5.7.0\lib\netstandard1.0\Costura.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Win32.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="mscorlib" />
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.AppContext, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.AppContext.4.3.0\lib\net463\System.AppContext.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.ComponentModel.Composition" />
|
||||
<Reference Include="System.Console, Version=4.0.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Console.4.3.1\lib\net46\System.Console.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Diagnostics.DiagnosticSource, Version=7.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Diagnostics.DiagnosticSource.7.0.1\lib\net462\System.Diagnostics.DiagnosticSource.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Diagnostics.Tracing, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Globalization.Calendars, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.IO, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.IO.4.3.0\lib\net462\System.IO.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.IO.Compression, Version=4.1.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Reference Include="System.IO.Compression.ZipFile, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.IO.FileSystem, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.IO.FileSystem.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Linq, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Linq.4.3.0\lib\net463\System.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Linq.Expressions, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Linq.Expressions.4.3.0\lib\net463\System.Linq.Expressions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Net.Http, Version=4.1.1.3, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Net.Sockets, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Numerics" />
|
||||
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Reflection, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.Extensions, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Runtime.Extensions.4.3.1\lib\net462\System.Runtime.Extensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.InteropServices, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Runtime.InteropServices.4.3.0\lib\net463\System.Runtime.InteropServices.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Security.Cryptography.Algorithms, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Security.Cryptography.Encoding, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Security.Cryptography.Primitives, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Security.Cryptography.X509Certificates, Version=4.1.1.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Text.RegularExpressions, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Text.RegularExpressions.4.3.1\lib\net463\System.Text.RegularExpressions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Deployment" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Xml.ReaderWriter, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="DownloadForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="DownloadForm.Designer.cs">
|
||||
<DependentUpon>DownloadForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Version.cs" />
|
||||
<EmbeddedResource Include="DownloadForm.resx">
|
||||
<DependentUpon>DownloadForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<SubType>Designer</SubType>
|
||||
</EmbeddedResource>
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<None Include="..\..\public\favicon.ico">
|
||||
<Link>favicon.ico</Link>
|
||||
</None>
|
||||
<None Include="packages.config" />
|
||||
<None Include="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
<Compile Include="Properties\Settings.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include=".gitignore" />
|
||||
<Content Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('packages\Costura.Fody.5.7.0\build\Costura.Fody.props')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Costura.Fody.5.7.0\build\Costura.Fody.props'))" />
|
||||
<Error Condition="!Exists('packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Costura.Fody.5.7.0\build\Costura.Fody.targets'))" />
|
||||
<Error Condition="!Exists('packages\Fody.6.6.4\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\Fody.6.6.4\build\Fody.targets'))" />
|
||||
<Error Condition="!Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets'))" />
|
||||
</Target>
|
||||
<Import Project="packages\Costura.Fody.5.7.0\build\Costura.Fody.targets" Condition="Exists('packages\Costura.Fody.5.7.0\build\Costura.Fody.targets')" />
|
||||
<Import Project="packages\Fody.6.6.4\build\Fody.targets" Condition="Exists('packages\Fody.6.6.4\build\Fody.targets')" />
|
||||
<Import Project="packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets" Condition="Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" />
|
||||
</Project>
|
16
extra/exe-builder/UptimeKuma.sln
Normal file
16
extra/exe-builder/UptimeKuma.sln
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UptimeKuma", "UptimeKuma.csproj", "{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
3
extra/exe-builder/UptimeKuma.sln.DotSettings.user
Normal file
3
extra/exe-builder/UptimeKuma.sln.DotSettings.user
Normal file
@@ -0,0 +1,3 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=UptimeKuma_002FProperties_002FResources/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean></wpf:ResourceDictionary>
|
9
extra/exe-builder/Version.cs
Normal file
9
extra/exe-builder/Version.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace UptimeKuma {
|
||||
public class Version {
|
||||
public string latest { get; set; }
|
||||
public string slow { get; set; }
|
||||
public string beta { get; set; }
|
||||
public string nodejs { get; set; }
|
||||
public string exe { get; set; }
|
||||
}
|
||||
}
|
28
extra/exe-builder/app.manifest
Normal file
28
extra/exe-builder/app.manifest
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- UAC Manifest Options
|
||||
If you want to change the Windows User Account Control level replace the
|
||||
requestedExecutionLevel node with one of the following.
|
||||
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||
|
||||
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
||||
Remove this element if your application requires this virtualization for backwards
|
||||
compatibility.
|
||||
-->
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
56
extra/exe-builder/packages.config
Normal file
56
extra/exe-builder/packages.config
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Costura.Fody" version="5.7.0" targetFramework="net472" developmentDependency="true" />
|
||||
<package id="Fody" version="6.6.4" targetFramework="net472" developmentDependency="true" />
|
||||
<package id="Microsoft.NETCore.Platforms" version="7.0.0" targetFramework="net472" />
|
||||
<package id="Microsoft.Win32.Primitives" version="4.3.0" targetFramework="net472" />
|
||||
<package id="NETStandard.Library" version="2.0.3" targetFramework="net472" />
|
||||
<package id="Newtonsoft.Json" version="13.0.2" targetFramework="net472" />
|
||||
<package id="System.AppContext" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Console" version="4.3.1" targetFramework="net472" />
|
||||
<package id="System.Diagnostics.DiagnosticSource" version="7.0.1" targetFramework="net472" />
|
||||
<package id="System.Net.Http" version="4.3.4" targetFramework="net472" />
|
||||
<package id="System.Runtime.Extensions" version="4.3.1" targetFramework="net472" />
|
||||
<package id="System.Security.Cryptography.Algorithms" version="4.3.1" targetFramework="net472" />
|
||||
<package id="System.Security.Cryptography.X509Certificates" version="4.3.2" targetFramework="net472" />
|
||||
<package id="System.Text.RegularExpressions" version="4.3.1" targetFramework="net472" />
|
||||
<package id="System.Xml.ReaderWriter" version="4.3.1" targetFramework="net472" />
|
||||
<package id="System.Memory" version="4.5.5" targetFramework="net472" />
|
||||
<package id="System.Net.Primitives" version="4.3.1" targetFramework="net472" />
|
||||
<package id="System.Runtime" version="4.3.1" targetFramework="net472" />
|
||||
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
|
||||
<package id="System.Collections" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Collections.Concurrent" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Diagnostics.Debug" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Diagnostics.Tools" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Diagnostics.Tracing" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Globalization" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Globalization.Calendars" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.IO" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.IO.Compression" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.IO.Compression.ZipFile" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.IO.FileSystem" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.IO.FileSystem.Primitives" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Linq" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Linq.Expressions" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Net.Sockets" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
|
||||
<package id="System.ObjectModel" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Reflection" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Reflection.Extensions" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Reflection.Primitives" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Resources.ResourceManager" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net472" />
|
||||
<package id="System.Runtime.Handles" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Runtime.InteropServices" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Runtime.Numerics" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Security.Cryptography.Encoding" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Text.Encoding" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Text.Encoding.Extensions" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Threading" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Threading.Tasks" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Threading.Timer" version="4.3.0" targetFramework="net472" />
|
||||
<package id="System.Xml.XDocument" version="4.3.0" targetFramework="net472" />
|
||||
</packages>
|
@@ -1,4 +1,8 @@
|
||||
/*
|
||||
* ⚠️ ⚠️ ⚠️ ⚠️ Due to the weird issue in Portainer that the healthcheck script is still pointing to this script for unknown reason.
|
||||
* IT CANNOT BE DROPPED, even though it looks like it is not used.
|
||||
* See more: https://github.com/louislam/uptime-kuma/issues/2774#issuecomment-1429092359
|
||||
*
|
||||
* ⚠️ 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.
|
||||
*/
|
||||
@@ -19,17 +23,17 @@ if (sslKey && sslCert) {
|
||||
|
||||
// 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 (::)
|
||||
let hostname = process.env.UPTIME_KUMA_SERVICE_HOST || process.env.UPTIME_KUMA_HOST || "::";
|
||||
let hostname = process.env.UPTIME_KUMA_HOST;
|
||||
|
||||
// Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD
|
||||
if (!hostname && !FBSD) {
|
||||
hostname = process.env.HOST;
|
||||
}
|
||||
|
||||
const port = parseInt(process.env.UPTIME_KUMA_SERVICE_PORT || process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
|
||||
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
|
||||
|
||||
let options = {
|
||||
host: hostname,
|
||||
host: hostname || "127.0.0.1",
|
||||
port: port,
|
||||
timeout: 28 * 1000,
|
||||
};
|
||||
|
22
extra/sort-contributors.js
Normal file
22
extra/sort-contributors.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const fs = require("fs");
|
||||
|
||||
// Read the file from private/sort-contributors.txt
|
||||
const file = fs.readFileSync("private/sort-contributors.txt", "utf8");
|
||||
|
||||
// Convert to an array of lines
|
||||
let lines = file.split("\n");
|
||||
|
||||
// Remove empty lines
|
||||
lines = lines.filter((line) => line !== "");
|
||||
|
||||
// Remove duplicates
|
||||
lines = [ ...new Set(lines) ];
|
||||
|
||||
// Remove @weblate and @UptimeKumaBot
|
||||
lines = lines.filter((line) => line !== "@weblate" && line !== "@UptimeKumaBot" && line !== "@louislam");
|
||||
|
||||
// Sort the lines
|
||||
lines = lines.sort();
|
||||
|
||||
// Output the lines, concat with " "
|
||||
console.log(lines.join(" "));
|
@@ -26,7 +26,8 @@ if (! exists) {
|
||||
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
// Also update package-lock.json
|
||||
childProcess.spawnSync("npm", [ "install" ]);
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
childProcess.spawnSync(npm, [ "install" ]);
|
||||
|
||||
commit(newVersion);
|
||||
tag(newVersion);
|
||||
|
17531
package-lock.json
generated
17531
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.20.0-beta.0",
|
||||
"version": "1.21.2-beta.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -39,7 +39,7 @@
|
||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||
"setup": "git checkout 1.19.6 && npm ci --production && npm run download-dist",
|
||||
"setup": "git checkout 1.21.1 && npm ci --production && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
@@ -63,12 +63,14 @@
|
||||
"cy:run": "npx cypress run --browser chrome --headless --config-file ./config/cypress.config.js",
|
||||
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js",
|
||||
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
||||
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go"
|
||||
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
|
||||
"depoly-demo-server": "node extra/deploy-demo-server.js",
|
||||
"sort-contributors": "node extra/sort-contributors.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "~1.7.3",
|
||||
"@louislam/ping": "~0.4.2-mod.1",
|
||||
"@louislam/sqlite3": "15.1.2",
|
||||
"@louislam/ping": "~0.4.4-mod.0",
|
||||
"@louislam/sqlite3": "15.1.6",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.27.0",
|
||||
"axios-ntlm": "1.3.0",
|
||||
@@ -83,6 +85,7 @@
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"compression": "~1.7.4",
|
||||
"croner": "^6.0.3",
|
||||
"dayjs": "~1.11.5",
|
||||
"dotenv": "~16.0.3",
|
||||
"express": "~4.17.3",
|
||||
@@ -98,10 +101,11 @@
|
||||
"jsonwebtoken": "~9.0.0",
|
||||
"jwt-decode": "~3.1.2",
|
||||
"limiter": "~2.1.0",
|
||||
"mongodb": "~4.13.0",
|
||||
"mongodb": "~4.14.0",
|
||||
"mqtt": "~4.3.7",
|
||||
"mssql": "~8.1.4",
|
||||
"mysql2": "~2.3.3",
|
||||
"nanoid": "^3.3.4",
|
||||
"node-cloudflared-tunnel": "~1.0.9",
|
||||
"node-radius-client": "~1.0.0",
|
||||
"nodemailer": "~6.6.5",
|
||||
@@ -112,6 +116,7 @@
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.1",
|
||||
"protobufjs": "~7.1.1",
|
||||
"qs": "~6.10.4",
|
||||
"redbean-node": "~0.2.0",
|
||||
"redis": "~4.5.1",
|
||||
"socket.io": "~4.5.3",
|
||||
@@ -142,6 +147,7 @@
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"concurrently": "^7.1.0",
|
||||
"core-js": "~3.26.1",
|
||||
"cronstrue": "~2.24.0",
|
||||
"cross-env": "~7.0.3",
|
||||
"cypress": "^10.1.0",
|
||||
"delay": "^5.0.0",
|
||||
@@ -150,8 +156,9 @@
|
||||
"eslint": "~8.14.0",
|
||||
"eslint-plugin-vue": "~8.7.1",
|
||||
"favico.js": "~0.3.10",
|
||||
"marked": "~4.2.5",
|
||||
"jest": "~27.2.5",
|
||||
"marked": "~4.2.5",
|
||||
"node-ssh": "~13.0.1",
|
||||
"postcss-html": "~1.5.0",
|
||||
"postcss-rtlcss": "~3.7.2",
|
||||
"postcss-scss": "~4.0.4",
|
||||
@@ -167,7 +174,7 @@
|
||||
"v-pagination-3": "~0.1.7",
|
||||
"vite": "~3.1.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "next",
|
||||
"vue": "~3.2.47",
|
||||
"vue-chart-3": "3.0.9",
|
||||
"vue-confirm-dialog": "~1.0.2",
|
||||
"vue-contenteditable": "~3.0.4",
|
||||
|
@@ -2,7 +2,9 @@ const basicAuth = require("express-basic-auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
const { setting } = require("./util-server");
|
||||
const { loginRateLimiter } = require("./rate-limiter");
|
||||
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
/**
|
||||
* Login to web app
|
||||
@@ -34,8 +36,36 @@ exports.login = async function (username, password) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback for myAuthorizer
|
||||
* @callback myAuthorizerCB
|
||||
* Validate a provided API key
|
||||
* @param {string} key API key to verify
|
||||
*/
|
||||
async function verifyAPIKey(key) {
|
||||
if (typeof key !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// uk prefix + key ID is before _
|
||||
let index = key.substring(2, key.indexOf("_"));
|
||||
let clear = key.substring(key.indexOf("_") + 1, key.length);
|
||||
|
||||
let hash = await R.findOne("api_key", " id=? ", [ index ]);
|
||||
|
||||
if (hash === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let current = dayjs();
|
||||
let expiry = dayjs(hash.expires);
|
||||
if (expiry.diff(current) < 0 || !hash.active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash && passwordHash.verify(clear, hash.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for basic auth authorizers
|
||||
* @callback authCallback
|
||||
* @param {any} err Any error encountered
|
||||
* @param {boolean} authorized Is the client authorized?
|
||||
*/
|
||||
@@ -44,9 +74,31 @@ exports.login = async function (username, password) {
|
||||
* Custom authorizer for express-basic-auth
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @param {myAuthorizerCB} callback
|
||||
* @param {authCallback} callback
|
||||
*/
|
||||
function myAuthorizer(username, password, callback) {
|
||||
function apiAuthorizer(username, password, callback) {
|
||||
// API Rate Limit
|
||||
apiRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
verifyAPIKey(password).then((valid) => {
|
||||
callback(null, valid);
|
||||
// Only allow a set number of api requests per minute
|
||||
// (currently set to 60)
|
||||
apiRateLimiter.removeTokens(1);
|
||||
});
|
||||
} else {
|
||||
callback(null, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom authorizer for express-basic-auth
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @param {authCallback} callback
|
||||
*/
|
||||
function userAuthorizer(username, password, callback) {
|
||||
// Login Rate Limit
|
||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
@@ -71,7 +123,7 @@ function myAuthorizer(username, password, callback) {
|
||||
*/
|
||||
exports.basicAuth = async function (req, res, next) {
|
||||
const middleware = basicAuth({
|
||||
authorizer: myAuthorizer,
|
||||
authorizer: userAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
@@ -84,3 +136,32 @@ exports.basicAuth = async function (req, res, next) {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Use use API Key if API keys enabled, else use basic auth
|
||||
* @param {express.Request} req Express request object
|
||||
* @param {express.Response} res Express response object
|
||||
* @param {express.NextFunction} next
|
||||
*/
|
||||
exports.apiAuth = async function (req, res, next) {
|
||||
if (!await Settings.get("disableAuth")) {
|
||||
let usingAPIKeys = await Settings.get("apiKeysEnabled");
|
||||
let middleware;
|
||||
if (usingAPIKeys) {
|
||||
middleware = basicAuth({
|
||||
authorizer: apiAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
} else {
|
||||
middleware = basicAuth({
|
||||
authorizer: userAuthorizer,
|
||||
authorizeAsync: true,
|
||||
challenge: true,
|
||||
});
|
||||
}
|
||||
middleware(req, res, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
@@ -113,6 +113,31 @@ async function sendProxyList(socket) {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit API key list to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendAPIKeyList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let result = [];
|
||||
const list = await R.find(
|
||||
"api_key",
|
||||
"user_id=?",
|
||||
[ socket.userID ],
|
||||
);
|
||||
|
||||
for (let bean of list) {
|
||||
result.push(bean.toPublicJSON());
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("apiKeyList", result);
|
||||
timeLogger.print("Sent API Key List");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the version information to the client.
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
@@ -157,6 +182,7 @@ module.exports = {
|
||||
sendImportantHeartbeatList,
|
||||
sendHeartbeatList,
|
||||
sendProxyList,
|
||||
sendAPIKeyList,
|
||||
sendInfo,
|
||||
sendDockerHostList
|
||||
};
|
||||
|
@@ -30,11 +30,6 @@ class Database {
|
||||
*/
|
||||
static patched = false;
|
||||
|
||||
/**
|
||||
* For Backup only
|
||||
*/
|
||||
static backupPath = null;
|
||||
|
||||
/**
|
||||
* Add patch filename in key
|
||||
* Values:
|
||||
@@ -70,6 +65,11 @@ class Database {
|
||||
"patch-maintenance-table2.sql": true,
|
||||
"patch-add-gamedig-monitor.sql": true,
|
||||
"patch-add-google-analytics-status-page-tag.sql": true,
|
||||
"patch-http-body-encoding.sql": true,
|
||||
"patch-add-description-monitor.sql": true,
|
||||
"patch-api-key-table.sql": true,
|
||||
"patch-monitor-tls.sql": true,
|
||||
"patch-maintenance-cron.sql": true,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -194,15 +194,7 @@ class Database {
|
||||
} else {
|
||||
log.info("db", "Database patch is needed");
|
||||
|
||||
try {
|
||||
this.backup(version);
|
||||
} catch (e) {
|
||||
log.error("db", e);
|
||||
log.error("db", "Unable to create a backup before patching the database. Please make sure you have enough space and permission.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Try catch anything here, if gone wrong, restore the backup
|
||||
// Try catch anything here
|
||||
try {
|
||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||
const sqlFile = `./db/patch${i}.sql`;
|
||||
@@ -218,7 +210,6 @@ class Database {
|
||||
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
||||
log.error("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -260,8 +251,6 @@ class Database {
|
||||
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
||||
log.error("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -363,8 +352,6 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
this.backup(dayjs().format("YYYYMMDDHHmmss"));
|
||||
|
||||
log.info("db", sqlFilename + " is patching");
|
||||
this.patched = true;
|
||||
await this.importSQLFile("./db/" + sqlFilename);
|
||||
@@ -446,90 +433,6 @@ class Database {
|
||||
process.removeListener("unhandledRejection", listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* One backup one time in this process.
|
||||
* Reset this.backupPath if you want to backup again
|
||||
* @param {string} version Version code of backup
|
||||
*/
|
||||
static backup(version) {
|
||||
if (! this.backupPath) {
|
||||
log.info("db", "Backing up the database");
|
||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, this.backupPath);
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
if (fs.existsSync(shmPath)) {
|
||||
this.backupShmPath = shmPath + ".bak" + version;
|
||||
fs.copyFileSync(shmPath, this.backupShmPath);
|
||||
}
|
||||
|
||||
const walPath = Database.path + "-wal";
|
||||
if (fs.existsSync(walPath)) {
|
||||
this.backupWalPath = walPath + ".bak" + version;
|
||||
fs.copyFileSync(walPath, this.backupWalPath);
|
||||
}
|
||||
|
||||
// Double confirm if all files actually backup
|
||||
if (!fs.existsSync(this.backupPath)) {
|
||||
throw new Error("Backup failed! " + this.backupPath);
|
||||
}
|
||||
|
||||
if (fs.existsSync(shmPath)) {
|
||||
if (!fs.existsSync(this.backupShmPath)) {
|
||||
throw new Error("Backup failed! " + this.backupShmPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(walPath)) {
|
||||
if (!fs.existsSync(this.backupWalPath)) {
|
||||
throw new Error("Backup failed! " + this.backupWalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore from most recent backup */
|
||||
static restore() {
|
||||
if (this.backupPath) {
|
||||
log.error("db", "Patching the database failed!!! Restoring the backup");
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
const walPath = Database.path + "-wal";
|
||||
|
||||
// Delete patch failed db
|
||||
try {
|
||||
if (fs.existsSync(Database.path)) {
|
||||
fs.unlinkSync(Database.path);
|
||||
}
|
||||
|
||||
if (fs.existsSync(shmPath)) {
|
||||
fs.unlinkSync(shmPath);
|
||||
}
|
||||
|
||||
if (fs.existsSync(walPath)) {
|
||||
fs.unlinkSync(walPath);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("db", "Restore failed; you may need to restore the backup manually");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Restore backup
|
||||
fs.copyFileSync(this.backupPath, Database.path);
|
||||
|
||||
if (this.backupShmPath) {
|
||||
fs.copyFileSync(this.backupShmPath, shmPath);
|
||||
}
|
||||
|
||||
if (this.backupWalPath) {
|
||||
fs.copyFileSync(this.backupWalPath, walPath);
|
||||
}
|
||||
|
||||
} else {
|
||||
log.info("db", "Nothing to restore");
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the size of the database */
|
||||
static getSize() {
|
||||
log.debug("db", "Database.getSize()");
|
||||
|
76
server/model/api_key.js
Normal file
76
server/model/api_key.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
class APIKey extends BeanModel {
|
||||
/**
|
||||
* Get the current status of this API key
|
||||
* @returns {string} active, inactive or expired
|
||||
*/
|
||||
getStatus() {
|
||||
let current = dayjs();
|
||||
let expiry = dayjs(this.expires);
|
||||
if (expiry.diff(current) < 0) {
|
||||
return "expired";
|
||||
}
|
||||
|
||||
return this.active ? "active" : "inactive";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
key: this.key,
|
||||
name: this.name,
|
||||
userID: this.user_id,
|
||||
createdDate: this.created_date,
|
||||
active: this.active,
|
||||
expires: this.expires,
|
||||
status: this.getStatus(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that ready to parse to JSON with sensitive fields
|
||||
* removed
|
||||
* @returns {Object}
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
userID: this.user_id,
|
||||
createdDate: this.created_date,
|
||||
active: this.active,
|
||||
expires: this.expires,
|
||||
status: this.getStatus(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new API Key and store it in the database
|
||||
* @param {Object} key Object sent by client
|
||||
* @param {int} userID ID of socket user
|
||||
* @returns {Promise<bean>}
|
||||
*/
|
||||
static async save(key, userID) {
|
||||
let bean;
|
||||
bean = R.dispense("api_key");
|
||||
|
||||
bean.key = key.key;
|
||||
bean.name = key.name;
|
||||
bean.user_id = userID;
|
||||
bean.active = key.active;
|
||||
bean.expires = key.expires;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = APIKey;
|
@@ -1,8 +1,10 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
|
||||
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
|
||||
const { parseTimeObject, parseTimeFromTimeObject, log } = require("../../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const dayjs = require("dayjs");
|
||||
const Cron = require("croner");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const apicache = require("../modules/apicache");
|
||||
|
||||
class Maintenance extends BeanModel {
|
||||
|
||||
@@ -15,16 +17,16 @@ class Maintenance extends BeanModel {
|
||||
|
||||
let dateRange = [];
|
||||
if (this.start_date) {
|
||||
dateRange.push(utcToLocal(this.start_date));
|
||||
dateRange.push(this.start_date);
|
||||
if (this.end_date) {
|
||||
dateRange.push(utcToLocal(this.end_date));
|
||||
dateRange.push(this.end_date);
|
||||
}
|
||||
}
|
||||
|
||||
let timeRange = [];
|
||||
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
|
||||
let startTime = parseTimeObject(this.start_time);
|
||||
timeRange.push(startTime);
|
||||
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
|
||||
let endTime = parseTimeObject(this.end_time);
|
||||
timeRange.push(endTime);
|
||||
|
||||
let obj = {
|
||||
@@ -39,12 +41,43 @@ class Maintenance extends BeanModel {
|
||||
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
||||
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
||||
timeslotList: [],
|
||||
cron: this.cron,
|
||||
duration: this.duration,
|
||||
durationMinutes: parseInt(this.duration / 60),
|
||||
timezone: await this.getTimezone(),
|
||||
timezoneOffset: await this.getTimezoneOffset(),
|
||||
status: await this.getStatus(),
|
||||
};
|
||||
|
||||
const timeslotList = await this.getTimeslotList();
|
||||
if (this.strategy === "manual") {
|
||||
// Do nothing, no timeslots
|
||||
} else if (this.strategy === "single") {
|
||||
obj.timeslotList.push({
|
||||
startDate: this.start_date,
|
||||
endDate: this.end_date,
|
||||
});
|
||||
} else {
|
||||
// Should be cron or recurring here
|
||||
if (this.beanMeta.job) {
|
||||
let runningTimeslot = this.getRunningTimeslot();
|
||||
|
||||
for (let timeslot of timeslotList) {
|
||||
obj.timeslotList.push(await timeslot.toPublicJSON());
|
||||
if (runningTimeslot) {
|
||||
obj.timeslotList.push(runningTimeslot);
|
||||
}
|
||||
|
||||
let nextRunDate = this.beanMeta.job.nextRun();
|
||||
if (nextRunDate) {
|
||||
let startDateDayjs = dayjs(nextRunDate);
|
||||
|
||||
let startDate = startDateDayjs.toISOString();
|
||||
let endDate = startDateDayjs.add(this.duration, "second").toISOString();
|
||||
|
||||
obj.timeslotList.push({
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(obj.weekdays)) {
|
||||
@@ -55,54 +88,9 @@ class Maintenance extends BeanModel {
|
||||
obj.daysOfMonth = [];
|
||||
}
|
||||
|
||||
// Maintenance Status
|
||||
if (!obj.active) {
|
||||
obj.status = "inactive";
|
||||
} else if (obj.strategy === "manual") {
|
||||
obj.status = "under-maintenance";
|
||||
} else if (obj.timeslotList.length > 0) {
|
||||
let currentTimestamp = dayjs().unix();
|
||||
|
||||
for (let timeslot of obj.timeslotList) {
|
||||
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
|
||||
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
|
||||
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
|
||||
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
|
||||
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
|
||||
|
||||
obj.status = "under-maintenance";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!obj.status) {
|
||||
obj.status = "scheduled";
|
||||
}
|
||||
} else if (obj.timeslotList.length === 0) {
|
||||
obj.status = "ended";
|
||||
} else {
|
||||
obj.status = "unknown";
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only get future or current timeslots only
|
||||
* @returns {Promise<[]>}
|
||||
*/
|
||||
async getTimeslotList() {
|
||||
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
|
||||
SELECT maintenance_timeslot.*
|
||||
FROM maintenance_timeslot, maintenance
|
||||
WHERE maintenance_timeslot.maintenance_id = maintenance.id
|
||||
AND maintenance.id = ?
|
||||
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
|
||||
`, [
|
||||
this.id
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @param {string} timezone If not specified, the timeRange will be in UTC
|
||||
@@ -126,7 +114,7 @@ class Maintenance extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get a list of days in month that maintenance is active for
|
||||
* @returns {number[]} Array of active days in month
|
||||
* @returns {number[]|string[]} Array of active days in month
|
||||
*/
|
||||
getDayOfMonthList() {
|
||||
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
||||
@@ -135,26 +123,10 @@ class Maintenance extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start date and time for maintenance
|
||||
* @returns {dayjs.Dayjs} Start date and time
|
||||
*/
|
||||
getStartDateTime() {
|
||||
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
|
||||
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
|
||||
|
||||
// Start Time
|
||||
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
|
||||
log.debug("timeslot", "startTime: " + startTimeSecond);
|
||||
|
||||
// Bake StartDate + StartTime = Start DateTime
|
||||
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duraction of maintenance in seconds
|
||||
* Get the duration of maintenance in seconds
|
||||
* @returns {number} Duration of maintenance
|
||||
*/
|
||||
getDuration() {
|
||||
calcDuration() {
|
||||
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
|
||||
// Add 24hours if it is across day
|
||||
if (duration < 0) {
|
||||
@@ -169,71 +141,266 @@ class Maintenance extends BeanModel {
|
||||
* @param {Object} obj Data to fill bean with
|
||||
* @returns {Bean} Filled bean
|
||||
*/
|
||||
static jsonToBean(bean, obj) {
|
||||
static async jsonToBean(bean, obj) {
|
||||
if (obj.id) {
|
||||
bean.id = obj.id;
|
||||
}
|
||||
|
||||
// Apply timezone offset to timeRange, as it cannot apply automatically.
|
||||
if (obj.timeRange[0]) {
|
||||
timeObjectToUTC(obj.timeRange[0]);
|
||||
if (obj.timeRange[1]) {
|
||||
timeObjectToUTC(obj.timeRange[1]);
|
||||
}
|
||||
}
|
||||
|
||||
bean.title = obj.title;
|
||||
bean.description = obj.description;
|
||||
bean.strategy = obj.strategy;
|
||||
bean.interval_day = obj.intervalDay;
|
||||
bean.timezone = obj.timezone;
|
||||
bean.active = obj.active;
|
||||
|
||||
if (obj.dateRange[0]) {
|
||||
bean.start_date = localToUTC(obj.dateRange[0]);
|
||||
bean.start_date = obj.dateRange[0];
|
||||
|
||||
if (obj.dateRange[1]) {
|
||||
bean.end_date = localToUTC(obj.dateRange[1]);
|
||||
bean.end_date = obj.dateRange[1];
|
||||
}
|
||||
}
|
||||
|
||||
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||
|
||||
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||
if (bean.strategy === "cron") {
|
||||
bean.duration = obj.durationMinutes * 60;
|
||||
bean.cron = obj.cron;
|
||||
this.validateCron(bean.cron);
|
||||
}
|
||||
|
||||
if (bean.strategy.startsWith("recurring-")) {
|
||||
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||
await bean.generateCron();
|
||||
this.validateCron(bean.cron);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL conditions for active maintenance
|
||||
* @returns {string}
|
||||
* Throw error if cron is invalid
|
||||
* @param cron
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static getActiveMaintenanceSQLCondition() {
|
||||
return `
|
||||
(
|
||||
(maintenance_timeslot.start_date <= DATETIME('now')
|
||||
AND maintenance_timeslot.end_date >= DATETIME('now')
|
||||
AND maintenance.active = 1)
|
||||
OR
|
||||
(maintenance.strategy = 'manual' AND active = 1)
|
||||
)
|
||||
`;
|
||||
static async validateCron(cron) {
|
||||
let job = new Cron(cron, () => {});
|
||||
job.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL conditions for active and future maintenance
|
||||
* @returns {string}
|
||||
* Run the cron
|
||||
*/
|
||||
static getActiveAndFutureMaintenanceSQLCondition() {
|
||||
return `
|
||||
(
|
||||
((maintenance_timeslot.end_date >= DATETIME('now')
|
||||
AND maintenance.active = 1)
|
||||
OR
|
||||
(maintenance.strategy = 'manual' AND active = 1))
|
||||
)
|
||||
`;
|
||||
async run(throwError = false) {
|
||||
if (this.beanMeta.job) {
|
||||
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
|
||||
this.stop();
|
||||
}
|
||||
|
||||
log.debug("maintenance", "Run maintenance id: " + this.id);
|
||||
|
||||
// 1.21.2 migration
|
||||
if (!this.cron) {
|
||||
await this.generateCron();
|
||||
if (!this.timezone) {
|
||||
this.timezone = "UTC";
|
||||
}
|
||||
if (this.cron) {
|
||||
await R.store(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.strategy === "manual") {
|
||||
// Do nothing, because it is controlled by the user
|
||||
} else if (this.strategy === "single") {
|
||||
this.beanMeta.job = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => {
|
||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
apicache.clear();
|
||||
});
|
||||
} else if (this.cron != null) {
|
||||
// Here should be cron or recurring
|
||||
try {
|
||||
this.beanMeta.status = "scheduled";
|
||||
|
||||
let startEvent = (customDuration = 0) => {
|
||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||
|
||||
this.beanMeta.status = "under-maintenance";
|
||||
clearTimeout(this.beanMeta.durationTimeout);
|
||||
|
||||
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
||||
let duration;
|
||||
|
||||
if (customDuration > 0) {
|
||||
duration = customDuration;
|
||||
} else if (this.end_date) {
|
||||
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
||||
if (d < this.duration) {
|
||||
duration = d * 1000;
|
||||
}
|
||||
} else {
|
||||
duration = this.duration * 1000;
|
||||
}
|
||||
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
|
||||
this.beanMeta.durationTimeout = setTimeout(() => {
|
||||
// End of maintenance for this timeslot
|
||||
this.beanMeta.status = "scheduled";
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// Create Cron
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
}, startEvent);
|
||||
|
||||
// Continue if the maintenance is still in the window
|
||||
let runningTimeslot = this.getRunningTimeslot();
|
||||
let current = dayjs();
|
||||
|
||||
if (runningTimeslot) {
|
||||
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
|
||||
log.debug("maintenance", "Maintenance id: " + this.id + " Remaining duration: " + duration + "ms");
|
||||
startEvent(duration);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
log.error("maintenance", "Error in maintenance id: " + this.id);
|
||||
log.error("maintenance", "Cron: " + this.cron);
|
||||
log.error("maintenance", e);
|
||||
|
||||
if (throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.error("maintenance", "Maintenance id: " + this.id + " has no cron");
|
||||
}
|
||||
}
|
||||
|
||||
getRunningTimeslot() {
|
||||
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").format("YYYY-MM-DD HH:mm:ss")));
|
||||
let end = start.add(this.duration, "second");
|
||||
let current = dayjs();
|
||||
|
||||
if (current.isAfter(start) && current.isBefore(end)) {
|
||||
return {
|
||||
startDate: start.toISOString(),
|
||||
endDate: end.toISOString(),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.beanMeta.job) {
|
||||
this.beanMeta.job.stop();
|
||||
delete this.beanMeta.job;
|
||||
}
|
||||
}
|
||||
|
||||
async isUnderMaintenance() {
|
||||
return (await this.getStatus()) === "under-maintenance";
|
||||
}
|
||||
|
||||
async getTimezone() {
|
||||
if (!this.timezone) {
|
||||
return await UptimeKumaServer.getInstance().getTimezone();
|
||||
}
|
||||
return this.timezone;
|
||||
}
|
||||
|
||||
async getTimezoneOffset() {
|
||||
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
if (!this.active) {
|
||||
return "inactive";
|
||||
}
|
||||
|
||||
if (this.strategy === "manual") {
|
||||
return "under-maintenance";
|
||||
}
|
||||
|
||||
// Check if the maintenance is started
|
||||
if (this.start_date && dayjs().isBefore(dayjs.tz(this.start_date, await this.getTimezone()))) {
|
||||
return "scheduled";
|
||||
}
|
||||
|
||||
// Check if the maintenance is ended
|
||||
if (this.end_date && dayjs().isAfter(dayjs.tz(this.end_date, await this.getTimezone()))) {
|
||||
return "ended";
|
||||
}
|
||||
|
||||
if (this.strategy === "single") {
|
||||
return "under-maintenance";
|
||||
}
|
||||
|
||||
if (!this.beanMeta.status) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return this.beanMeta.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Cron for recurring maintenance
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async generateCron() {
|
||||
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
|
||||
|
||||
if (this.strategy === "cron") {
|
||||
// Do nothing for cron
|
||||
} else if (!this.strategy.startsWith("recurring-")) {
|
||||
this.cron = "";
|
||||
} else if (this.strategy === "recurring-interval") {
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
|
||||
this.duration = this.calcDuration();
|
||||
log.debug("maintenance", "Cron: " + this.cron);
|
||||
log.debug("maintenance", "Duration: " + this.duration);
|
||||
} else if (this.strategy === "recurring-weekday") {
|
||||
let list = this.getDayOfWeekList();
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = minute + " " + hour + " * * " + list.join(",");
|
||||
this.duration = this.calcDuration();
|
||||
} else if (this.strategy === "recurring-day-of-month") {
|
||||
let list = this.getDayOfMonthList();
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
|
||||
let dayList = [];
|
||||
|
||||
for (let day of list) {
|
||||
if (typeof day === "string" && day.startsWith("lastDay")) {
|
||||
if (day === "lastDay1") {
|
||||
dayList.push("L");
|
||||
}
|
||||
// Unfortunately, lastDay2-4 is not supported by cron
|
||||
} else {
|
||||
dayList.push(day);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicate
|
||||
dayList = [ ...new Set(dayList) ];
|
||||
|
||||
this.cron = minute + " " + hour + " " + dayList.join(",") + " * *";
|
||||
this.duration = this.calcDuration();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,198 +0,0 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
const dayjs = require("dayjs");
|
||||
const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
|
||||
class MaintenanceTimeslot extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||
|
||||
const obj = {
|
||||
id: this.id,
|
||||
startDate: this.start_date,
|
||||
endDate: this.end_date,
|
||||
startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||
endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||
serverTimezoneOffset,
|
||||
};
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toJSON() {
|
||||
return await this.toPublicJSON();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Maintenance} maintenance
|
||||
* @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
|
||||
* @param {boolean} removeExist Remove existing timeslot before create
|
||||
* @returns {Promise<MaintenanceTimeslot>}
|
||||
*/
|
||||
static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
|
||||
if (removeExist) {
|
||||
await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
|
||||
maintenance.id
|
||||
]);
|
||||
}
|
||||
|
||||
if (maintenance.strategy === "manual") {
|
||||
log.debug("maintenance", "No need to generate timeslot for manual type");
|
||||
|
||||
} else if (maintenance.strategy === "single") {
|
||||
let bean = R.dispense("maintenance_timeslot");
|
||||
bean.maintenance_id = maintenance.id;
|
||||
bean.start_date = maintenance.start_date;
|
||||
bean.end_date = maintenance.end_date;
|
||||
bean.generated_next = true;
|
||||
return await R.store(bean);
|
||||
|
||||
} else if (maintenance.strategy === "recurring-interval") {
|
||||
// Prevent dead loop, in case interval_day is not set
|
||||
if (!maintenance.interval_day || maintenance.interval_day <= 0) {
|
||||
maintenance.interval_day = 1;
|
||||
}
|
||||
|
||||
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||
return startDateTime.add(maintenance.interval_day, "day");
|
||||
}, () => {
|
||||
return true;
|
||||
});
|
||||
|
||||
} else if (maintenance.strategy === "recurring-weekday") {
|
||||
let dayOfWeekList = maintenance.getDayOfWeekList();
|
||||
log.debug("timeslot", dayOfWeekList);
|
||||
|
||||
if (dayOfWeekList.length <= 0) {
|
||||
log.debug("timeslot", "No weekdays selected?");
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = (startDateTime) => {
|
||||
log.debug("timeslot", "nextDateTime: " + startDateTime);
|
||||
|
||||
let day = startDateTime.local().day();
|
||||
log.debug("timeslot", "nextDateTime.day(): " + day);
|
||||
|
||||
return dayOfWeekList.includes(day);
|
||||
};
|
||||
|
||||
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||
while (true) {
|
||||
startDateTime = startDateTime.add(1, "day");
|
||||
|
||||
if (isValid(startDateTime)) {
|
||||
return startDateTime;
|
||||
}
|
||||
}
|
||||
}, isValid);
|
||||
|
||||
} else if (maintenance.strategy === "recurring-day-of-month") {
|
||||
let dayOfMonthList = maintenance.getDayOfMonthList();
|
||||
if (dayOfMonthList.length <= 0) {
|
||||
log.debug("timeslot", "No day selected?");
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = (startDateTime) => {
|
||||
let day = parseInt(startDateTime.local().format("D"));
|
||||
|
||||
log.debug("timeslot", "day: " + day);
|
||||
|
||||
// Check 1-31
|
||||
if (dayOfMonthList.includes(day)) {
|
||||
return startDateTime;
|
||||
}
|
||||
|
||||
// Check "lastDay1","lastDay2"...
|
||||
let daysInMonth = startDateTime.daysInMonth();
|
||||
let lastDayList = [];
|
||||
|
||||
// Small first, e.g. 28 > 29 > 30 > 31
|
||||
for (let i = 4; i >= 1; i--) {
|
||||
if (dayOfMonthList.includes("lastDay" + i)) {
|
||||
lastDayList.push(daysInMonth - i + 1);
|
||||
}
|
||||
}
|
||||
log.debug("timeslot", lastDayList);
|
||||
return lastDayList.includes(day);
|
||||
};
|
||||
|
||||
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||
while (true) {
|
||||
startDateTime = startDateTime.add(1, "day");
|
||||
if (isValid(startDateTime)) {
|
||||
return startDateTime;
|
||||
}
|
||||
}
|
||||
}, isValid);
|
||||
} else {
|
||||
throw new Error("Unknown maintenance strategy");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a next timeslot for all recurring types
|
||||
* @param maintenance
|
||||
* @param minDate
|
||||
* @param {function} nextDayCallback The logic how to get the next possible day
|
||||
* @param {function} isValidCallback Check the day whether is matched the current strategy
|
||||
* @returns {Promise<null|MaintenanceTimeslot>}
|
||||
*/
|
||||
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
|
||||
let bean = R.dispense("maintenance_timeslot");
|
||||
|
||||
let duration = maintenance.getDuration();
|
||||
let startDateTime = maintenance.getStartDateTime();
|
||||
let endDateTime;
|
||||
|
||||
// Keep generating from the first possible date, until it is ok
|
||||
while (true) {
|
||||
log.debug("timeslot", "startDateTime: " + startDateTime.format());
|
||||
|
||||
// Handling out of effective date range
|
||||
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||
log.debug("timeslot", "Out of effective date range");
|
||||
return null;
|
||||
}
|
||||
|
||||
endDateTime = startDateTime.add(duration, "second");
|
||||
|
||||
// If endDateTime is out of effective date range, use the end datetime from effective date range
|
||||
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||
endDateTime = dayjs.utc(maintenance.end_date);
|
||||
}
|
||||
|
||||
// If minDate is set, the endDateTime must be bigger than it.
|
||||
// And the endDateTime must be bigger current time
|
||||
// Is valid under current recurring strategy
|
||||
if (
|
||||
(!minDate || endDateTime.diff(minDate) > 0) &&
|
||||
endDateTime.diff(dayjs()) > 0 &&
|
||||
isValidCallback(startDateTime)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
startDateTime = nextDayCallback(startDateTime);
|
||||
}
|
||||
|
||||
bean.maintenance_id = maintenance.id;
|
||||
bean.start_date = localToUTC(startDateTime);
|
||||
bean.end_date = localToUTC(endDateTime);
|
||||
bean.generated_next = false;
|
||||
return await R.store(bean);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MaintenanceTimeslot;
|
@@ -16,7 +16,6 @@ const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||
const { DockerHost } = require("../docker");
|
||||
const Maintenance = require("./maintenance");
|
||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const Gamedig = require("gamedig");
|
||||
|
||||
@@ -72,6 +71,7 @@ class Monitor extends BeanModel {
|
||||
let data = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
hostname: this.hostname,
|
||||
@@ -111,6 +111,7 @@ class Monitor extends BeanModel {
|
||||
radiusCalledStationId: this.radiusCalledStationId,
|
||||
radiusCallingStationId: this.radiusCallingStationId,
|
||||
game: this.game,
|
||||
httpBodyEncoding: this.httpBodyEncoding
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
@@ -131,6 +132,9 @@ class Monitor extends BeanModel {
|
||||
mqttPassword: this.mqttPassword,
|
||||
authWorkstation: this.authWorkstation,
|
||||
authDomain: this.authDomain,
|
||||
tlsCa: this.tlsCa,
|
||||
tlsCert: this.tlsCert,
|
||||
tlsKey: this.tlsKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -143,7 +147,7 @@ class Monitor extends BeanModel {
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
*/
|
||||
async getTags() {
|
||||
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 = ?", [ this.id ]);
|
||||
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 ]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +207,7 @@ class Monitor extends BeanModel {
|
||||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
|
||||
let prometheus = new Prometheus(this);
|
||||
this.prometheus = new Prometheus(this);
|
||||
|
||||
const beat = async () => {
|
||||
|
||||
@@ -272,17 +276,34 @@ class Monitor extends BeanModel {
|
||||
|
||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||
|
||||
let contentType = null;
|
||||
let bodyValue = null;
|
||||
|
||||
if (this.body && (typeof this.body === "string" && this.body.trim().length > 0)) {
|
||||
if (!this.httpBodyEncoding || this.httpBodyEncoding === "json") {
|
||||
try {
|
||||
bodyValue = JSON.parse(this.body);
|
||||
contentType = "application/json";
|
||||
} catch (e) {
|
||||
throw new Error("Your JSON body is invalid. " + e.message);
|
||||
}
|
||||
} else if (this.httpBodyEncoding === "xml") {
|
||||
bodyValue = this.body;
|
||||
contentType = "text/xml; charset=utf-8";
|
||||
}
|
||||
}
|
||||
|
||||
// Axios Options
|
||||
const options = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
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,
|
||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||
...(contentType ? { "Content-Type": contentType } : {}),
|
||||
...(basicAuthHeader),
|
||||
...(this.headers ? JSON.parse(this.headers) : {})
|
||||
},
|
||||
maxRedirects: this.maxredirects,
|
||||
validateStatus: (status) => {
|
||||
@@ -290,6 +311,10 @@ class Monitor extends BeanModel {
|
||||
},
|
||||
};
|
||||
|
||||
if (bodyValue) {
|
||||
options.data = bodyValue;
|
||||
}
|
||||
|
||||
if (this.proxy_id) {
|
||||
const proxy = await R.load("proxy", this.proxy_id);
|
||||
|
||||
@@ -308,6 +333,18 @@ class Monitor extends BeanModel {
|
||||
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
||||
}
|
||||
|
||||
if (this.auth_method === "mtls") {
|
||||
if (this.tlsCert !== null && this.tlsCert !== "") {
|
||||
options.httpsAgent.options.cert = Buffer.from(this.tlsCert);
|
||||
}
|
||||
if (this.tlsCa !== null && this.tlsCa !== "") {
|
||||
options.httpsAgent.options.ca = Buffer.from(this.tlsCa);
|
||||
}
|
||||
if (this.tlsKey !== null && this.tlsKey !== "") {
|
||||
options.httpsAgent.options.key = Buffer.from(this.tlsKey);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
|
||||
@@ -599,9 +636,7 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "mysql") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
|
||||
bean.msg = "";
|
||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "mongodb") {
|
||||
@@ -755,7 +790,7 @@ class Monitor extends BeanModel {
|
||||
await R.store(bean);
|
||||
|
||||
log.debug("monitor", `[${this.name}] prometheus.update`);
|
||||
prometheus.update(bean, tlsInfo);
|
||||
this.prometheus?.update(bean, tlsInfo);
|
||||
|
||||
previousBeat = bean;
|
||||
|
||||
@@ -813,7 +848,6 @@ class Monitor extends BeanModel {
|
||||
domain: this.authDomain,
|
||||
workstation: this.authWorkstation ? this.authWorkstation : undefined
|
||||
});
|
||||
|
||||
} else {
|
||||
res = await axios.request(options);
|
||||
}
|
||||
@@ -840,15 +874,15 @@ class Monitor extends BeanModel {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
|
||||
this.prometheus().remove();
|
||||
this.prometheus?.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new prometheus instance
|
||||
* @returns {Prometheus}
|
||||
* Get prometheus instance
|
||||
* @returns {Prometheus|undefined}
|
||||
*/
|
||||
prometheus() {
|
||||
return new Prometheus(this);
|
||||
getPrometheus() {
|
||||
return this.prometheus;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1210,7 +1244,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
if (notificationList.length > 0) {
|
||||
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
@@ -1228,7 +1262,7 @@ class Monitor extends BeanModel {
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
log.debug("monitor", "Sending to " + notification.name);
|
||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will be expired in ${daysRemaining} days`);
|
||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will expire in ${daysRemaining} days`);
|
||||
sent = true;
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send cert notification to " + notification.name);
|
||||
@@ -1268,18 +1302,19 @@ class Monitor extends BeanModel {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static async isUnderMaintenance(monitorID) {
|
||||
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||
const maintenance = await R.getRow(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM monitor_maintenance mm
|
||||
JOIN maintenance
|
||||
ON mm.maintenance_id = maintenance.id
|
||||
AND mm.monitor_id = ?
|
||||
LEFT JOIN maintenance_timeslot
|
||||
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||
WHERE ${activeCondition}
|
||||
LIMIT 1`, [ monitorID ]);
|
||||
return maintenance.count !== 0;
|
||||
const maintenanceIDList = await R.getCol(`
|
||||
SELECT maintenance_id FROM monitor_maintenance
|
||||
WHERE monitor_id = ?
|
||||
`, [ monitorID ]);
|
||||
|
||||
for (const maintenanceID of maintenanceIDList) {
|
||||
const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
||||
if (maintenance && await maintenance.isUnderMaintenance()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Make sure monitor interval is between bounds */
|
||||
|
@@ -3,7 +3,6 @@ const { R } = require("redbean-node");
|
||||
const cheerio = require("cheerio");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const jsesc = require("jsesc");
|
||||
const Maintenance = require("./maintenance");
|
||||
const googleAnalytics = require("../google-analytics");
|
||||
|
||||
class StatusPage extends BeanModel {
|
||||
@@ -60,8 +59,11 @@ class StatusPage extends BeanModel {
|
||||
}
|
||||
|
||||
// OG Meta Tags
|
||||
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
|
||||
head.append(`<meta property="og:description" content="${description155}" />`);
|
||||
let ogTitle = $("<meta property=\"og:title\" content=\"\" />").attr("content", statusPage.title);
|
||||
head.append(ogTitle);
|
||||
|
||||
let ogDescription = $("<meta property=\"og:description\" content=\"\" />").attr("content", description155);
|
||||
head.append(ogDescription);
|
||||
|
||||
// Preload data
|
||||
// Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
|
||||
@@ -287,21 +289,17 @@ class StatusPage extends BeanModel {
|
||||
try {
|
||||
const publicMaintenanceList = [];
|
||||
|
||||
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
|
||||
SELECT DISTINCT maintenance.*
|
||||
FROM maintenance
|
||||
JOIN maintenance_status_page
|
||||
ON maintenance_status_page.maintenance_id = maintenance.id
|
||||
AND maintenance_status_page.status_page_id = ?
|
||||
LEFT JOIN maintenance_timeslot
|
||||
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||
WHERE ${activeCondition}
|
||||
ORDER BY maintenance.end_date
|
||||
`, [ statusPageId ]));
|
||||
let maintenanceIDList = await R.getCol(`
|
||||
SELECT DISTINCT maintenance_id
|
||||
FROM maintenance_status_page
|
||||
WHERE status_page_id = ?
|
||||
`, [ statusPageId ]);
|
||||
|
||||
for (const bean of maintenanceBeanList) {
|
||||
publicMaintenanceList.push(await bean.toPublicJSON());
|
||||
for (const maintenanceID of maintenanceIDList) {
|
||||
let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
||||
if (maintenance && await maintenance.isUnderMaintenance()) {
|
||||
publicMaintenanceList.push(await maintenance.toPublicJSON());
|
||||
}
|
||||
}
|
||||
|
||||
return publicMaintenanceList;
|
||||
|
@@ -8,7 +8,12 @@ class LunaSea extends NotificationProvider {
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice;
|
||||
let lunaseaurl = "";
|
||||
if (notification.lunaseaTarget === "user") {
|
||||
lunaseaurl = "https://notify.lunasea.app/v1/custom/user/" + notification.lunaseaUserID;
|
||||
} else {
|
||||
lunaseaurl = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice;
|
||||
}
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
@@ -16,7 +21,7 @@ class LunaSea extends NotificationProvider {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": msg,
|
||||
};
|
||||
await axios.post(lunaseadevice, testdata);
|
||||
await axios.post(lunaseaurl, testdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@@ -25,7 +30,7 @@ class LunaSea extends NotificationProvider {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
};
|
||||
await axios.post(lunaseadevice, downdata);
|
||||
await axios.post(lunaseaurl, downdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@@ -34,7 +39,7 @@ class LunaSea extends NotificationProvider {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
};
|
||||
await axios.post(lunaseadevice, updata);
|
||||
await axios.post(lunaseaurl, updata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
|
97
server/notification-providers/opsgenie.js
Normal file
97
server/notification-providers/opsgenie.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP, DOWN } = require("../../src/util");
|
||||
|
||||
const opsgenieAlertsUrlEU = "https://api.eu.opsgenie.com/v2/alerts";
|
||||
const opsgenieAlertsUrlUS = "https://api.opsgenie.com/v2/alerts";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
class Opsgenie extends NotificationProvider {
|
||||
|
||||
name = "Opsgenie";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let opsgenieAlertsUrl;
|
||||
let priority = (notification.opsgeniePriority == "") ? 3 : notification.opsgeniePriority;
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
|
||||
try {
|
||||
switch (notification.opsgenieRegion) {
|
||||
case "US":
|
||||
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
|
||||
break;
|
||||
case "EU":
|
||||
opsgenieAlertsUrl = opsgenieAlertsUrlEU;
|
||||
break;
|
||||
default:
|
||||
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
|
||||
}
|
||||
|
||||
if (heartbeatJSON == null) {
|
||||
let notificationTestAlias = "uptime-kuma-notification-test";
|
||||
let data = {
|
||||
"message": msg,
|
||||
"alias": notificationTestAlias,
|
||||
"source": "Uptime Kuma",
|
||||
"priority": "P5"
|
||||
};
|
||||
|
||||
return this.post(notification, opsgenieAlertsUrl, data);
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
let data = {
|
||||
"message": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
||||
"alias": monitorJSON.name,
|
||||
"description": msg,
|
||||
"source": "Uptime Kuma",
|
||||
"priority": `P${priority}`
|
||||
};
|
||||
|
||||
return this.post(notification, opsgenieAlertsUrl, data);
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === UP) {
|
||||
let opsgenieAlertsCloseUrl = `${opsgenieAlertsUrl}/${encodeURIComponent(monitorJSON.name)}/close?identifierType=alias`;
|
||||
let data = {
|
||||
"source": "Uptime Kuma",
|
||||
};
|
||||
|
||||
return this.post(notification, opsgenieAlertsCloseUrl, data);
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {BeanModel} notification
|
||||
* @param {string} url Request url
|
||||
* @param {Object} data Request body
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async post(notification, url, data) {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `GenieKey ${notification.opsgenieApiKey}`,
|
||||
}
|
||||
};
|
||||
|
||||
let res = await axios.post(url, data, config);
|
||||
if (res.status == null) {
|
||||
return "Opsgenie notification failed with invalid response!";
|
||||
}
|
||||
if (res.status < 200 || res.status >= 300) {
|
||||
return `Opsgenie notification failed with status code ${res.status}`;
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Opsgenie;
|
91
server/notification-providers/pagertree.js
Normal file
91
server/notification-providers/pagertree.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||
const { setting } = require("../util-server");
|
||||
let successMessage = "Sent Successfully.";
|
||||
|
||||
class PagerTree extends NotificationProvider {
|
||||
name = "PagerTree";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
// general messages
|
||||
return this.postNotification(notification, msg, monitorJSON, heartbeatJSON);
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === UP && notification.pagertreeAutoResolve === "resolve") {
|
||||
return this.postNotification(notification, null, monitorJSON, heartbeatJSON, notification.pagertreeAutoResolve);
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
const title = `Uptime Kuma Monitor "${monitorJSON.name}" is DOWN`;
|
||||
return this.postNotification(notification, title, monitorJSON, heartbeatJSON);
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if result is successful, result code should be in range 2xx
|
||||
* @param {Object} result Axios response object
|
||||
* @throws {Error} The status code is not in range 2xx
|
||||
*/
|
||||
checkResult(result) {
|
||||
if (result.status == null) {
|
||||
throw new Error("PagerTree notification failed with invalid response!");
|
||||
}
|
||||
if (result.status < 200 || result.status >= 300) {
|
||||
throw new Error("PagerTree notification failed with status code " + result.status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the message
|
||||
* @param {BeanModel} notification Message title
|
||||
* @param {string} title Message title
|
||||
* @param {Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {?string} eventAction Action event for PagerTree (create, resolve)
|
||||
* @returns {string}
|
||||
*/
|
||||
async postNotification(notification, title, monitorJSON, heartbeatJSON, eventAction = "create") {
|
||||
|
||||
if (eventAction == null) {
|
||||
return "No action required";
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
url: notification.pagertreeIntegrationUrl,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
data: {
|
||||
event_type: eventAction,
|
||||
id: heartbeatJSON?.monitorID || "uptime-kuma",
|
||||
title: title,
|
||||
urgency: notification.pagertreeUrgency,
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON
|
||||
}
|
||||
};
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
if (baseURL && monitorJSON) {
|
||||
options.client = "Uptime Kuma";
|
||||
options.client_url = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||
}
|
||||
|
||||
let result = await axios.request(options);
|
||||
this.checkResult(result);
|
||||
if (result.statusText != null) {
|
||||
return "PagerTree notification succeed: " + result.statusText;
|
||||
}
|
||||
|
||||
return successMessage;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PagerTree;
|
@@ -42,7 +42,7 @@ class Slack extends NotificationProvider {
|
||||
const time = heartbeatJSON["time"];
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
let data = {
|
||||
"text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
||||
"text": `${textMsg}\n${msg}`,
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
|
@@ -9,11 +9,18 @@ class Telegram extends NotificationProvider {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let params = {
|
||||
chat_id: notification.telegramChatID,
|
||||
text: msg,
|
||||
disable_notification: notification.telegramSendSilently ?? false,
|
||||
protect_content: notification.telegramProtectContent ?? false,
|
||||
};
|
||||
if (notification.telegramMessageThreadID) {
|
||||
params.message_thread_id = notification.telegramMessageThreadID;
|
||||
}
|
||||
|
||||
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
|
||||
params: {
|
||||
chat_id: notification.telegramChatID,
|
||||
text: msg,
|
||||
},
|
||||
params: params,
|
||||
});
|
||||
return okMsg;
|
||||
|
||||
|
41
server/notification-providers/twilio.js
Normal file
41
server/notification-providers/twilio.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Twilio extends NotificationProvider {
|
||||
|
||||
name = "twilio";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
let accountSID = notification.twilioAccountSID;
|
||||
let authToken = notification.twilioAuthToken;
|
||||
|
||||
try {
|
||||
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
"Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"),
|
||||
}
|
||||
};
|
||||
|
||||
let data = new URLSearchParams();
|
||||
data.append("To", notification.twilioToNumber);
|
||||
data.append("From", notification.twilioFromNumber);
|
||||
data.append("Body", msg);
|
||||
|
||||
let url = "https://api.twilio.com/2010-04-01/Accounts/" + accountSID + "/Messages.json";
|
||||
|
||||
await axios.post(url, data, config);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Twilio;
|
@@ -23,7 +23,9 @@ const Mattermost = require("./notification-providers/mattermost");
|
||||
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 PagerTree = require("./notification-providers/pagertree");
|
||||
const PromoSMS = require("./notification-providers/promosms");
|
||||
const Pushbullet = require("./notification-providers/pushbullet");
|
||||
const PushDeer = require("./notification-providers/pushdeer");
|
||||
@@ -40,6 +42,7 @@ const Stackfield = require("./notification-providers/stackfield");
|
||||
const Teams = require("./notification-providers/teams");
|
||||
const TechulusPush = require("./notification-providers/techulus-push");
|
||||
const Telegram = require("./notification-providers/telegram");
|
||||
const Twilio = require("./notification-providers/twilio");
|
||||
const Splunk = require("./notification-providers/splunk");
|
||||
const Webhook = require("./notification-providers/webhook");
|
||||
const WeCom = require("./notification-providers/wecom");
|
||||
@@ -82,7 +85,9 @@ class Notification {
|
||||
new Ntfy(),
|
||||
new Octopush(),
|
||||
new OneBot(),
|
||||
new Opsgenie(),
|
||||
new PagerDuty(),
|
||||
new PagerTree(),
|
||||
new PromoSMS(),
|
||||
new Pushbullet(),
|
||||
new PushDeer(),
|
||||
@@ -101,6 +106,7 @@ class Notification {
|
||||
new Teams(),
|
||||
new TechulusPush(),
|
||||
new Telegram(),
|
||||
new Twilio(),
|
||||
new Splunk(),
|
||||
new Webhook(),
|
||||
new WeCom(),
|
||||
|
@@ -132,6 +132,9 @@ class Proxy {
|
||||
...httpAgentOptions,
|
||||
...httpsAgentOptions,
|
||||
...proxyOptions,
|
||||
tls: {
|
||||
rejectUnauthorized: httpsAgentOptions.rejectUnauthorized,
|
||||
},
|
||||
});
|
||||
|
||||
httpAgent = agent;
|
||||
|
@@ -54,6 +54,13 @@ const loginRateLimiter = new KumaRateLimiter({
|
||||
errorMessage: "Too frequently, try again later."
|
||||
});
|
||||
|
||||
const apiRateLimiter = new KumaRateLimiter({
|
||||
tokensPerInterval: 60,
|
||||
interval: "minute",
|
||||
fireImmediately: true,
|
||||
errorMessage: "Too frequently, try again later."
|
||||
});
|
||||
|
||||
const twoFaRateLimiter = new KumaRateLimiter({
|
||||
tokensPerInterval: 30,
|
||||
interval: "minute",
|
||||
@@ -63,5 +70,6 @@ const twoFaRateLimiter = new KumaRateLimiter({
|
||||
|
||||
module.exports = {
|
||||
loginRateLimiter,
|
||||
apiRateLimiter,
|
||||
twoFaRateLimiter,
|
||||
};
|
||||
|
@@ -1,5 +1,5 @@
|
||||
let express = require("express");
|
||||
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, send403 } = require("../util-server");
|
||||
const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, sendHttpError } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
@@ -7,6 +7,7 @@ const dayjs = require("dayjs");
|
||||
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = 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");
|
||||
|
||||
@@ -86,6 +87,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
await R.store(bean);
|
||||
|
||||
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
|
||||
UptimeCacheList.clearCache(monitor.id);
|
||||
Monitor.sendStats(io, monitor.id, monitor.user_id);
|
||||
|
||||
response.json({
|
||||
@@ -145,7 +147,11 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response
|
||||
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
|
||||
const state = overrideValue !== undefined ? overrideValue : heartbeat.status;
|
||||
|
||||
badgeValues.label = label ?? "Status";
|
||||
if (label === undefined) {
|
||||
badgeValues.label = "Status";
|
||||
} else {
|
||||
badgeValues.label = label;
|
||||
}
|
||||
switch (state) {
|
||||
case DOWN:
|
||||
badgeValues.color = downColor;
|
||||
@@ -175,7 +181,7 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,7 +228,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
|
||||
);
|
||||
|
||||
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
|
||||
const cleanUptime = parseFloat(uptime.toPrecision(4));
|
||||
const cleanUptime = (uptime * 100).toPrecision(4);
|
||||
|
||||
// use a given, custom color or calculate one based on the uptime value
|
||||
badgeValues.color = color ?? percentageToColor(uptime);
|
||||
@@ -233,7 +239,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
|
||||
labelPrefix,
|
||||
label ?? `Uptime (${requestedDuration}${labelSuffix})`,
|
||||
]);
|
||||
badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]);
|
||||
badgeValues.message = filterAndJoin([ prefix, cleanUptime, suffix ]);
|
||||
}
|
||||
|
||||
// build the SVG based on given values
|
||||
@@ -242,7 +248,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -303,7 +309,7 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -373,7 +379,7 @@ router.get("/api/badge/:id/avg-response/:duration?", cache("5 minutes"), async (
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -464,7 +470,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -536,7 +542,7 @@ router.get("/api/badge/:id/response", cache("5 minutes"), async (request, respon
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -2,7 +2,7 @@ let express = require("express");
|
||||
const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const { allowDevAllOrigin, send403 } = require("../util-server");
|
||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const Monitor = require("../model/monitor");
|
||||
|
||||
@@ -44,10 +44,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
|
||||
let statusPageData = await StatusPage.getStatusPageData(statusPage);
|
||||
|
||||
if (!statusPageData) {
|
||||
response.statusCode = 404;
|
||||
response.json({
|
||||
msg: "Not Found"
|
||||
});
|
||||
sendHttpError(response, "Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,7 +52,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
|
||||
response.json(statusPageData);
|
||||
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,7 +100,7 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -119,10 +116,7 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
|
||||
]);
|
||||
|
||||
if (!statusPage) {
|
||||
response.statusCode = 404;
|
||||
response.json({
|
||||
msg: "Not Found"
|
||||
});
|
||||
sendHttpError(response, "Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,7 +135,7 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -87,7 +87,7 @@ log.debug("server", "Importing Background Jobs");
|
||||
const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs");
|
||||
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
|
||||
|
||||
const { basicAuth } = require("./auth");
|
||||
const { apiAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
@@ -129,7 +129,7 @@ if (config.demoMode) {
|
||||
}
|
||||
|
||||
// Must be after io instantiation
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client");
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client");
|
||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||
const TwoFA = require("./2fa");
|
||||
@@ -138,10 +138,12 @@ const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudfl
|
||||
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
||||
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
|
||||
const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler");
|
||||
const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handler");
|
||||
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
|
||||
const { Settings } = require("./settings");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
|
||||
const apicache = require("./modules/apicache");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -229,7 +231,7 @@ let needSetup = false;
|
||||
|
||||
// Prometheus API metrics /metrics
|
||||
// With Basic Auth using the first user's username/password
|
||||
app.get("/metrics", basicAuth, prometheusAPIMetrics());
|
||||
app.get("/metrics", apiAuth, prometheusAPIMetrics());
|
||||
|
||||
app.use("/", expressStaticGzip("dist", {
|
||||
enableBrotli: true,
|
||||
@@ -677,10 +679,8 @@ let needSetup = false;
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
// Reset Prometheus labels
|
||||
server.monitorList[monitor.id]?.prometheus()?.remove();
|
||||
|
||||
bean.name = monitor.name;
|
||||
bean.description = monitor.description;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
bean.method = monitor.method;
|
||||
@@ -688,6 +688,9 @@ let needSetup = false;
|
||||
bean.headers = monitor.headers;
|
||||
bean.basic_auth_user = monitor.basic_auth_user;
|
||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||
bean.tlsCa = monitor.tlsCa;
|
||||
bean.tlsCert = monitor.tlsCert;
|
||||
bean.tlsKey = monitor.tlsKey;
|
||||
bean.interval = monitor.interval;
|
||||
bean.retryInterval = monitor.retryInterval;
|
||||
bean.resendInterval = monitor.resendInterval;
|
||||
@@ -729,6 +732,7 @@ let needSetup = false;
|
||||
bean.radiusCalledStationId = monitor.radiusCalledStationId;
|
||||
bean.radiusCallingStationId = monitor.radiusCallingStationId;
|
||||
bean.radiusSecret = monitor.radiusSecret;
|
||||
bean.httpBodyEncoding = monitor.httpBodyEncoding;
|
||||
|
||||
bean.validate();
|
||||
|
||||
@@ -884,6 +888,9 @@ let needSetup = false;
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
// Fix #2880
|
||||
apicache.clear();
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
@@ -1320,6 +1327,7 @@ let needSetup = false;
|
||||
let monitor = {
|
||||
// Define the new variable from earlier here
|
||||
name: monitorListData[i].name,
|
||||
description: monitorListData[i].description,
|
||||
type: monitorListData[i].type,
|
||||
url: monitorListData[i].url,
|
||||
method: monitorListData[i].method || "GET",
|
||||
@@ -1503,6 +1511,7 @@ let needSetup = false;
|
||||
proxySocketHandler(socket);
|
||||
dockerSocketHandler(socket);
|
||||
maintenanceSocketHandler(socket);
|
||||
apiKeySocketHandler(socket);
|
||||
generalSocketHandler(socket, server);
|
||||
pluginsHandler(socket, server);
|
||||
|
||||
@@ -1611,6 +1620,7 @@ async function afterLogin(socket, user) {
|
||||
sendNotificationList(socket);
|
||||
sendProxyList(socket);
|
||||
sendDockerHostList(socket);
|
||||
sendAPIKeyList(socket);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
|
150
server/socket-handlers/api-key-socket-handler.js
Normal file
150
server/socket-handlers/api-key-socket-handler.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const { checkLogin } = require("../util-server");
|
||||
const { log } = require("../../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const { nanoid } = require("nanoid");
|
||||
const passwordHash = require("../password-hash");
|
||||
const apicache = require("../modules/apicache");
|
||||
const APIKey = require("../model/api_key");
|
||||
const { Settings } = require("../settings");
|
||||
const { sendAPIKeyList } = require("../client");
|
||||
|
||||
/**
|
||||
* Handlers for Maintenance
|
||||
* @param {Socket} socket Socket.io instance
|
||||
*/
|
||||
module.exports.apiKeySocketHandler = (socket) => {
|
||||
// Add a new api key
|
||||
socket.on("addAPIKey", async (key, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let clearKey = nanoid(40);
|
||||
let hashedKey = passwordHash.generate(clearKey);
|
||||
key["key"] = hashedKey;
|
||||
let bean = await APIKey.save(key, socket.userID);
|
||||
|
||||
log.debug("apikeys", "Added API Key");
|
||||
log.debug("apikeys", key);
|
||||
|
||||
// Append key ID and prefix to start of key seperated by _, used to get
|
||||
// correct hash when validating key.
|
||||
let formattedKey = "uk" + bean.id + "_" + clearKey;
|
||||
await sendAPIKeyList(socket);
|
||||
|
||||
// Enable API auth if the user creates a key, otherwise only basic
|
||||
// auth will be used for API.
|
||||
await Settings.set("apiKeysEnabled", true);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
key: formattedKey,
|
||||
keyID: bean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getAPIKeyList", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await sendAPIKeyList(socket);
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("deleteAPIKey", async (keyID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
log.debug("apikeys", `Deleted API Key: ${keyID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("DELETE FROM api_key WHERE id = ? AND user_id = ? ", [
|
||||
keyID,
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
apicache.clear();
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
});
|
||||
|
||||
await sendAPIKeyList(socket);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("disableAPIKey", async (keyID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
log.debug("apikeys", `Disabled Key: ${keyID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("UPDATE api_key SET active = 0 WHERE id = ? ", [
|
||||
keyID,
|
||||
]);
|
||||
|
||||
apicache.clear();
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Disabled Successfully.",
|
||||
});
|
||||
|
||||
await sendAPIKeyList(socket);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("enableAPIKey", async (keyID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
log.debug("apikeys", `Enabled Key: ${keyID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("UPDATE api_key SET active = 1 WHERE id = ? ", [
|
||||
keyID,
|
||||
]);
|
||||
|
||||
apicache.clear();
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Enabled Successfully",
|
||||
});
|
||||
|
||||
await sendAPIKeyList(socket);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@@ -9,10 +9,10 @@ let gameList = null;
|
||||
|
||||
/**
|
||||
* Get a game list via GameDig
|
||||
* @returns {any[]}
|
||||
* @returns {Object[]} list of games supported by GameDig
|
||||
*/
|
||||
function getGameList() {
|
||||
if (!gameList) {
|
||||
if (gameList == null) {
|
||||
gameList = gameResolver._readGames().games.sort((a, b) => {
|
||||
if ( a.pretty < b.pretty ) {
|
||||
return -1;
|
||||
@@ -22,9 +22,8 @@ function getGameList() {
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
} else {
|
||||
return gameList;
|
||||
}
|
||||
return gameList;
|
||||
}
|
||||
|
||||
module.exports.generalSocketHandler = (socket, server) => {
|
||||
|
@@ -5,7 +5,6 @@ const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const Maintenance = require("../model/maintenance");
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
|
||||
|
||||
/**
|
||||
* Handlers for Maintenance
|
||||
@@ -19,10 +18,12 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
log.debug("maintenance", maintenance);
|
||||
|
||||
let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
|
||||
let bean = await Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
|
||||
bean.user_id = socket.userID;
|
||||
let maintenanceID = await R.store(bean);
|
||||
await MaintenanceTimeslot.generateTimeslot(bean);
|
||||
|
||||
server.maintenanceList[maintenanceID] = bean;
|
||||
await bean.run(true);
|
||||
|
||||
await server.sendMaintenanceList(socket);
|
||||
|
||||
@@ -45,17 +46,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
|
||||
let bean = server.getMaintenance(maintenance.id);
|
||||
|
||||
if (bean.user_id !== socket.userID) {
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
Maintenance.jsonToBean(bean, maintenance);
|
||||
|
||||
await Maintenance.jsonToBean(bean, maintenance);
|
||||
await R.store(bean);
|
||||
await MaintenanceTimeslot.generateTimeslot(bean, null, true);
|
||||
|
||||
await bean.run(true);
|
||||
await server.sendMaintenanceList(socket);
|
||||
|
||||
callback({
|
||||
@@ -236,6 +235,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||
|
||||
if (maintenanceID in server.maintenanceList) {
|
||||
server.maintenanceList[maintenanceID].stop();
|
||||
delete server.maintenanceList[maintenanceID];
|
||||
}
|
||||
|
||||
@@ -267,9 +267,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
|
||||
maintenanceID,
|
||||
]);
|
||||
let maintenance = server.getMaintenance(maintenanceID);
|
||||
|
||||
if (!maintenance) {
|
||||
throw new Error("Maintenance not found");
|
||||
}
|
||||
|
||||
maintenance.active = false;
|
||||
await R.store(maintenance);
|
||||
maintenance.stop();
|
||||
|
||||
apicache.clear();
|
||||
|
||||
@@ -294,9 +300,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
|
||||
maintenanceID,
|
||||
]);
|
||||
let maintenance = server.getMaintenance(maintenanceID);
|
||||
|
||||
if (!maintenance) {
|
||||
throw new Error("Maintenance not found");
|
||||
}
|
||||
|
||||
maintenance.active = true;
|
||||
await R.store(maintenance);
|
||||
await maintenance.run();
|
||||
|
||||
apicache.clear();
|
||||
|
||||
|
@@ -47,8 +47,6 @@ class UptimeKumaServer {
|
||||
*/
|
||||
indexHTML = "";
|
||||
|
||||
generateMaintenanceTimeslotsInterval = undefined;
|
||||
|
||||
/**
|
||||
* Plugins Manager
|
||||
* @type {PluginsManager}
|
||||
@@ -74,6 +72,7 @@ class UptimeKumaServer {
|
||||
// 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;
|
||||
|
||||
log.info("server", "Creating express and socket.io instance");
|
||||
this.app = express();
|
||||
@@ -81,7 +80,8 @@ class UptimeKumaServer {
|
||||
log.info("server", "Server Type: HTTPS");
|
||||
this.httpServer = https.createServer({
|
||||
key: fs.readFileSync(sslKey),
|
||||
cert: fs.readFileSync(sslCert)
|
||||
cert: fs.readFileSync(sslCert),
|
||||
passphrase: sslKeyPassphrase,
|
||||
}, this.app);
|
||||
} else {
|
||||
log.info("server", "Server Type: HTTP");
|
||||
@@ -110,8 +110,7 @@ class UptimeKumaServer {
|
||||
log.debug("DEBUG", "Timezone: " + process.env.TZ);
|
||||
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
|
||||
|
||||
await this.generateMaintenanceTimeslots();
|
||||
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
|
||||
await this.loadMaintenanceList();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,16 +172,33 @@ class UptimeKumaServer {
|
||||
*/
|
||||
async getMaintenanceJSONList(userID) {
|
||||
let result = {};
|
||||
for (let maintenanceID in this.maintenanceList) {
|
||||
result[maintenanceID] = await this.maintenanceList[maintenanceID].toJSON();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load maintenance list and run
|
||||
* @param userID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadMaintenanceList(userID) {
|
||||
let maintenanceList = await R.findAll("maintenance", " ORDER BY end_date DESC, title", [
|
||||
|
||||
let maintenanceList = await R.find("maintenance", " user_id = ? ORDER BY end_date DESC, title", [
|
||||
userID,
|
||||
]);
|
||||
|
||||
for (let maintenance of maintenanceList) {
|
||||
result[maintenance.id] = await maintenance.toJSON();
|
||||
this.maintenanceList[maintenance.id] = maintenance;
|
||||
maintenance.run(this);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
getMaintenance(maintenanceID) {
|
||||
if (this.maintenanceList[maintenanceID]) {
|
||||
return this.maintenanceList[maintenanceID];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,7 +254,7 @@ class UptimeKumaServer {
|
||||
* Attempt to get the current server timezone
|
||||
* If this fails, fall back to environment variables and then make a
|
||||
* guess.
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async getTimezone() {
|
||||
let timezone = await Settings.get("serverTimezone");
|
||||
@@ -269,23 +285,9 @@ class UptimeKumaServer {
|
||||
dayjs.tz.setDefault(timezone);
|
||||
}
|
||||
|
||||
/** Load the timeslots for maintenance */
|
||||
async generateMaintenanceTimeslots() {
|
||||
|
||||
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
|
||||
|
||||
for (let maintenanceTimeslot of list) {
|
||||
let maintenance = await maintenanceTimeslot.maintenance;
|
||||
await MaintenanceTimeslot.generateTimeslot(maintenance, maintenanceTimeslot.end_date, false);
|
||||
maintenanceTimeslot.generated_next = true;
|
||||
await R.store(maintenanceTimeslot);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Stop the server */
|
||||
async stop() {
|
||||
clearTimeout(this.generateMaintenanceTimeslotsInterval);
|
||||
|
||||
}
|
||||
|
||||
loadPlugins() {
|
||||
@@ -334,5 +336,4 @@ module.exports = {
|
||||
};
|
||||
|
||||
// Must be at the end
|
||||
const MaintenanceTimeslot = require("./model/maintenance_timeslot");
|
||||
const { MonitorType } = require("./monitor-types/monitor-type");
|
||||
|
@@ -87,7 +87,10 @@ exports.ping = async (hostname, size = 56) => {
|
||||
return await exports.pingAsync(hostname, false, size);
|
||||
} catch (e) {
|
||||
// If the host cannot be resolved, try again with ipv6
|
||||
if (e.message.includes("service not known")) {
|
||||
console.debug("ping", "IPv6 error message: " + e.message);
|
||||
|
||||
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
|
||||
if (!e.message) {
|
||||
return await exports.pingAsync(hostname, true, size);
|
||||
} else {
|
||||
throw e;
|
||||
@@ -292,14 +295,23 @@ exports.postgresQuery = function (connectionString, query) {
|
||||
client.end();
|
||||
} else {
|
||||
// Connected here
|
||||
client.query(query, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
try {
|
||||
// No query provided by user, use SELECT 1
|
||||
if (!query || (typeof query === "string" && query.trim() === "")) {
|
||||
query = "SELECT 1";
|
||||
}
|
||||
client.end();
|
||||
});
|
||||
|
||||
client.query(query, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
client.end();
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -310,21 +322,28 @@ 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
|
||||
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||
* @returns {Promise<(string)>}
|
||||
*/
|
||||
exports.mysqlQuery = function (connectionString, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = mysql.createConnection(connectionString);
|
||||
connection.promise().query(query)
|
||||
.then(res => {
|
||||
resolve(res);
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
connection.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
connection.query(query, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
})
|
||||
.finally(() => {
|
||||
connection.destroy();
|
||||
});
|
||||
} else {
|
||||
if (Array.isArray(res)) {
|
||||
resolve("Rows: " + res.length);
|
||||
} else {
|
||||
resolve("No Error, but the result is not an array. Type: " + typeof res);
|
||||
}
|
||||
}
|
||||
connection.destroy();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -396,6 +415,9 @@ exports.redisPingAsync = function (dsn) {
|
||||
});
|
||||
client.connect().then(() => {
|
||||
client.ping().then((res, err) => {
|
||||
if (client.isOpen) {
|
||||
client.disconnect();
|
||||
}
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
@@ -730,15 +752,27 @@ exports.filterAndJoin = (parts, connector = "") => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a 403 response
|
||||
* Send an Error response
|
||||
* @param {Object} res Express response object
|
||||
* @param {string} [msg=""] Message to send
|
||||
*/
|
||||
module.exports.send403 = (res, msg = "") => {
|
||||
res.status(403).json({
|
||||
"status": "fail",
|
||||
"msg": msg,
|
||||
});
|
||||
module.exports.sendHttpError = (res, msg = "") => {
|
||||
if (msg.includes("SQLITE_BUSY") || msg.includes("SQLITE_LOCKED")) {
|
||||
res.status(503).json({
|
||||
"status": "fail",
|
||||
"msg": msg,
|
||||
});
|
||||
} else if (msg.toLowerCase().includes("not found")) {
|
||||
res.status(404).json({
|
||||
"status": "fail",
|
||||
"msg": msg,
|
||||
});
|
||||
} else {
|
||||
res.status(403).json({
|
||||
"status": "fail",
|
||||
"msg": msg,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) {
|
||||
|
@@ -556,6 +556,31 @@ h5.settings-subheading::after {
|
||||
border-bottom: 1px solid $dark-border-color;
|
||||
}
|
||||
|
||||
|
||||
$shadow-box-padding: 20px;
|
||||
|
||||
.shadow-box-with-fixed-bottom-bar {
|
||||
padding-top: $shadow-box-padding;
|
||||
padding-bottom: 0;
|
||||
padding-right: $shadow-box-padding;
|
||||
padding-left: $shadow-box-padding;
|
||||
}
|
||||
|
||||
.fixed-bottom-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
margin-left: -$shadow-box-padding;
|
||||
margin-right: -$shadow-box-padding;
|
||||
z-index: 100;
|
||||
background-color: rgba(white, 0.2);
|
||||
backdrop-filter: blur(2px);
|
||||
border-radius: 0 0 10px 10px;
|
||||
|
||||
.dark & {
|
||||
background-color: rgba($dark-header-bg, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
// Localization
|
||||
|
||||
@import "localization.scss";
|
||||
|
229
src/components/APIKeyDialog.vue
Normal file
229
src/components/APIKeyDialog.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<div ref="keyaddmodal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{ $t("Add API Key") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Name -->
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">{{ $t("Name") }}</label>
|
||||
<input
|
||||
id="name" v-model="key.name" type="text" class="form-control"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Expiry -->
|
||||
<div class="my-3">
|
||||
<label class="form-label">{{ $t("Expiry date") }}</label>
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<div class="col-6">
|
||||
<Datepicker
|
||||
v-model="key.expires"
|
||||
:dark="$root.isDark"
|
||||
:monthChangeOnScroll="false"
|
||||
:minDate="minDate"
|
||||
format="yyyy-MM-dd HH:mm"
|
||||
modelType="yyyy-MM-dd HH:mm:ss"
|
||||
:required="!noExpire"
|
||||
:disabled="noExpire"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 ms-3">
|
||||
<div class="form-check mb-0">
|
||||
<input
|
||||
id="no-expire" v-model="noExpire" class="form-check-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label class="form-check-label" for="no-expire">{{
|
||||
$t("Don't expire")
|
||||
}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
{{ $t("Generate") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="keymodal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{ $t("Key Added") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
{{ $t("apiKeyAddedMsg") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<CopyableInput v-model="clearKey" disabled="disabled" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">
|
||||
{{ $t('Continue') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
import { useToast } from "vue-toastification";
|
||||
import dayjs from "dayjs";
|
||||
import Datepicker from "@vuepic/vue-datepicker";
|
||||
import CopyableInput from "./CopyableInput.vue";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CopyableInput,
|
||||
Datepicker
|
||||
},
|
||||
props: {},
|
||||
// emits: [ "added" ],
|
||||
data() {
|
||||
return {
|
||||
keyaddmodal: null,
|
||||
keymodal: null,
|
||||
processing: false,
|
||||
key: {},
|
||||
dark: (this.$root.theme === "dark"),
|
||||
minDate: this.$root.date(dayjs()) + " 00:00",
|
||||
clearKey: null,
|
||||
noExpire: false,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.keyaddmodal = new Modal(this.$refs.keyaddmodal);
|
||||
this.keymodal = new Modal(this.$refs.keymodal);
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show modal
|
||||
*/
|
||||
show() {
|
||||
this.id = null;
|
||||
this.key = {
|
||||
name: "",
|
||||
expires: this.minDate,
|
||||
active: 1,
|
||||
};
|
||||
|
||||
this.keyaddmodal.show();
|
||||
},
|
||||
|
||||
/** Submit data to server */
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
if (this.noExpire) {
|
||||
this.key.expires = null;
|
||||
}
|
||||
|
||||
this.$root.addAPIKey(this.key, async (res) => {
|
||||
this.keyaddmodal.hide();
|
||||
this.processing = false;
|
||||
if (res.ok) {
|
||||
this.clearKey = res.key;
|
||||
this.keymodal.show();
|
||||
this.clearForm();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** Clear Form inputs */
|
||||
clearForm() {
|
||||
this.key = {
|
||||
name: "",
|
||||
expires: this.minDate,
|
||||
active: 1,
|
||||
};
|
||||
this.noExpire = false;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.modal-dialog .form-text, .modal-dialog p {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.dark-calendar::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.weekday-picker {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
|
||||
.form-check-inline {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.day-picker {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
|
||||
.form-check-inline {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -4,7 +4,7 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
{{ $t("Confirm") }}
|
||||
{{ title || $t("Confirm") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@
|
||||
<button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
|
||||
{{ yesText }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @click="no">
|
||||
{{ noText }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -44,8 +44,13 @@ export default {
|
||||
type: String,
|
||||
default: "No",
|
||||
},
|
||||
/** Title to show on modal. Defaults to translated version of "Config" */
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
emits: [ "yes" ],
|
||||
emits: [ "yes", "no" ],
|
||||
data: () => ({
|
||||
modal: null,
|
||||
}),
|
||||
@@ -63,6 +68,12 @@ export default {
|
||||
yes() {
|
||||
this.$emit("yes");
|
||||
},
|
||||
/**
|
||||
* @emits string "no" Notify the parent when No is pressed
|
||||
*/
|
||||
no() {
|
||||
this.$emit("no");
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@@ -13,6 +13,9 @@
|
||||
:disabled="disabled"
|
||||
>
|
||||
|
||||
<!-- A hidden textarea for copying text on non-https -->
|
||||
<textarea ref="hiddenTextarea" style="position: fixed; left: -999999px; top: -999999px;"></textarea>
|
||||
|
||||
<a class="btn btn-outline-primary" @click="copyToClipboard(model)">
|
||||
<font-awesome-icon :icon="icon" />
|
||||
</a>
|
||||
@@ -111,24 +114,19 @@ export default {
|
||||
}, 3000);
|
||||
|
||||
// navigator clipboard api needs a secure context (https)
|
||||
// For http, use the text area method (else part)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// navigator clipboard api method'
|
||||
return navigator.clipboard.writeText(textToCopy);
|
||||
} else {
|
||||
// text area method
|
||||
let textArea = document.createElement("textarea");
|
||||
let textArea = this.$refs.hiddenTextarea;
|
||||
textArea.value = textToCopy;
|
||||
// make the textarea out of viewport
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-999999px";
|
||||
textArea.style.top = "-999999px";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
return new Promise((res, rej) => {
|
||||
// here the magic happens
|
||||
document.execCommand("copy") ? res() : rej();
|
||||
textArea.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -3,16 +3,23 @@
|
||||
<div v-if="maintenance.strategy === 'manual'" class="timeslot">
|
||||
{{ $t("Manual") }}
|
||||
</div>
|
||||
<div v-else-if="maintenance.timeslotList.length > 0" class="timeslot">
|
||||
{{ maintenance.timeslotList[0].startDateServerTimezone }}
|
||||
<span class="to">-</span>
|
||||
{{ maintenance.timeslotList[0].endDateServerTimezone }}
|
||||
(UTC{{ maintenance.timeslotList[0].serverTimezoneOffset }})
|
||||
<div v-else-if="maintenance.timeslotList.length > 0">
|
||||
<div class="timeslot">
|
||||
{{ startDateTime }}
|
||||
<span class="to">-</span>
|
||||
{{ endDateTime }}
|
||||
</div>
|
||||
<div class="timeslot">
|
||||
UTC{{ maintenance.timezoneOffset }} <span v-if="maintenance.timezone !== 'UTC'">{{ maintenance.timezone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import { SQL_DATETIME_FORMAT_WITHOUT_SECOND } from "../util.ts";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
maintenance: {
|
||||
@@ -20,6 +27,14 @@ export default {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
startDateTime() {
|
||||
return dayjs(this.maintenance.timeslotList[0].startDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||
},
|
||||
endDateTime() {
|
||||
return dayjs(this.maintenance.timeslotList[0].endDate).tz(this.maintenance.timezone).format(SQL_DATETIME_FORMAT_WITHOUT_SECOND);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -31,6 +46,7 @@ export default {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 20px;
|
||||
padding: 0 10px;
|
||||
margin-right: 5px;
|
||||
|
||||
.to {
|
||||
margin: 0 6px;
|
||||
|
@@ -19,7 +19,7 @@
|
||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||
</div>
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" :title="item.description">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
|
@@ -13,7 +13,10 @@
|
||||
<div class="mb-3">
|
||||
<label for="notification-type" class="form-label">{{ $t("Notification Type") }}</label>
|
||||
<select id="notification-type" v-model="notification.type" class="form-select">
|
||||
<option v-for="type in notificationTypes" :key="type" :value="type">{{ $t(type) }}</option>
|
||||
<option v-for="(name, type) in notificationNameList.regularList" :key="type" :value="type">{{ name }}</option>
|
||||
<optgroup :label="$t('notificationRegional')">
|
||||
<option v-for="(name, type) in notificationNameList.regionalList" :key="type" :value="type">{{ name }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +70,7 @@
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script>
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
import Confirm from "./Confirm.vue";
|
||||
@@ -103,7 +106,93 @@ export default {
|
||||
return null;
|
||||
}
|
||||
return NotificationFormList[this.notification.type];
|
||||
}
|
||||
},
|
||||
|
||||
notificationNameList() {
|
||||
let regularList = {
|
||||
"alerta": "Alerta",
|
||||
"AlertNow": "AlertNow",
|
||||
"apprise": this.$t("apprise"),
|
||||
"Bark": "Bark",
|
||||
"clicksendsms": "ClickSend SMS",
|
||||
"discord": "Discord",
|
||||
"GoogleChat": "Google Chat (Google Workspace)",
|
||||
"gorush": "Gorush",
|
||||
"gotify": "Gotify",
|
||||
"HomeAssistant": "Home Assistant",
|
||||
"Kook": "Kook",
|
||||
"line": "LINE Messenger",
|
||||
"LineNotify": "LINE Notify",
|
||||
"lunasea": "LunaSea",
|
||||
"matrix": "Matrix",
|
||||
"mattermost": "Mattermost",
|
||||
"ntfy": "Ntfy",
|
||||
"octopush": "Octopush",
|
||||
"OneBot": "OneBot",
|
||||
"Opsgenie": "Opsgenie",
|
||||
"PagerDuty": "PagerDuty",
|
||||
"pushbullet": "Pushbullet",
|
||||
"PushByTechulus": "Push by Techulus",
|
||||
"pushover": "Pushover",
|
||||
"pushy": "Pushy",
|
||||
"rocket.chat": "Rocket.Chat",
|
||||
"signal": "Signal",
|
||||
"slack": "Slack",
|
||||
"squadcast": "SquadCast",
|
||||
"SMSEagle": "SMSEagle",
|
||||
"smtp": this.$t("smtp"),
|
||||
"stackfield": "Stackfield",
|
||||
"teams": "Microsoft Teams",
|
||||
"telegram": "Telegram",
|
||||
"twilio": "Twilio",
|
||||
"Splunk": "Splunk",
|
||||
"webhook": "Webhook",
|
||||
"GoAlert": "GoAlert",
|
||||
"ZohoCliq": "ZohoCliq"
|
||||
};
|
||||
|
||||
// Put notifications here if it's not supported in most regions or its documentation is not in English
|
||||
let regionalList = {
|
||||
"AliyunSMS": "AliyunSMS (阿里云短信服务)",
|
||||
"DingDing": "DingDing (钉钉自定义机器人)",
|
||||
"Feishu": "Feishu (飞书)",
|
||||
"FreeMobile": "FreeMobile (mobile.free.fr)",
|
||||
"PushDeer": "PushDeer",
|
||||
"promosms": "PromoSMS",
|
||||
"serwersms": "SerwerSMS.pl",
|
||||
"SMSManager": "SmsManager (smsmanager.cz)",
|
||||
"WeCom": "WeCom (企业微信群机器人)",
|
||||
"ServerChan": "ServerChan (Server酱)",
|
||||
};
|
||||
|
||||
// Sort by notification name
|
||||
// No idea how, but it works
|
||||
// https://stackoverflow.com/questions/1069666/sorting-object-property-by-values
|
||||
let sort = (list2) => {
|
||||
return Object.entries(list2)
|
||||
.sort(([ , a ], [ , b ]) => a.localeCompare(b))
|
||||
.reduce((r, [ k, v ]) => ({
|
||||
...r,
|
||||
[k]: v
|
||||
}), {});
|
||||
};
|
||||
|
||||
return {
|
||||
regularList: sort(regularList),
|
||||
regionalList: sort(regionalList),
|
||||
};
|
||||
},
|
||||
|
||||
notificationFullNameList() {
|
||||
let list = {};
|
||||
for (let [ key, value ] of Object.entries(this.notificationNameList.regularList)) {
|
||||
list[key] = value;
|
||||
}
|
||||
for (let [ key, value ] of Object.entries(this.notificationNameList.regionalList)) {
|
||||
list[key] = value;
|
||||
}
|
||||
return list;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -203,11 +292,12 @@ export default {
|
||||
* @return {string}
|
||||
*/
|
||||
getUniqueDefaultName(notificationKey) {
|
||||
|
||||
let index = 1;
|
||||
let name = "";
|
||||
do {
|
||||
name = this.$t("defaultNotificationName", {
|
||||
notification: this.$t(notificationKey).replace(/\(.+\)/, "").trim(),
|
||||
notification: this.notificationFullNameList[notificationKey].replace(/\(.+\)/, "").trim(),
|
||||
number: index++
|
||||
});
|
||||
} while (this.$root.notificationList.find(it => it.name === name));
|
||||
|
@@ -18,9 +18,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* @typedef {import('./TagsManager.vue').Tag} Tag
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
/** Object representing tag */
|
||||
/** Object representing tag
|
||||
* @type {Tag}
|
||||
*/
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -32,7 +38,7 @@ export default {
|
||||
},
|
||||
/**
|
||||
* Size of tag
|
||||
* @values normal, small
|
||||
* @type {"normal" | "small"}
|
||||
*/
|
||||
size: {
|
||||
type: String,
|
||||
|
@@ -12,7 +12,17 @@
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="tag-name" class="form-label">{{ $t("Name") }}</label>
|
||||
<input id="tag-name" v-model="tag.name" type="text" class="form-control" required>
|
||||
<input
|
||||
id="tag-name"
|
||||
v-model="tag.name"
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{'is-invalid': nameInvalid}"
|
||||
required
|
||||
>
|
||||
<div class="invalid-feedback">
|
||||
{{ $t("Tag with this name already exist.") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -112,7 +122,11 @@ export default {
|
||||
updated: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
existingTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -132,6 +146,7 @@ export default {
|
||||
removingMonitor: [],
|
||||
addingMonitor: [],
|
||||
selectedAddMonitor: null,
|
||||
nameInvalid: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -160,11 +175,16 @@ export default {
|
||||
watch: {
|
||||
// Set color option to "Custom" when a unknown color is entered
|
||||
"tag.color"(to, from) {
|
||||
if (colorOptions(this).find(x => x.color === to) == null) {
|
||||
if (to !== "" && colorOptions(this).find(x => x.color === to) == null) {
|
||||
this.selectedColor.name = this.$t("Custom");
|
||||
this.selectedColor.color = to;
|
||||
}
|
||||
},
|
||||
"tag.name"(to, from) {
|
||||
if (to != null) {
|
||||
this.validate();
|
||||
}
|
||||
},
|
||||
selectedColor(to, from) {
|
||||
if (to != null) {
|
||||
this.tag.color = to.color;
|
||||
@@ -197,6 +217,35 @@ export default {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the editTag form
|
||||
*/
|
||||
reset() {
|
||||
this.selectedColor = null;
|
||||
this.tag = {
|
||||
id: null,
|
||||
name: "",
|
||||
color: "",
|
||||
};
|
||||
this.monitors = [];
|
||||
this.removingMonitor = [];
|
||||
this.addingMonitor = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Check for existing tags of the same name, set invalid input
|
||||
* @returns {boolean} True if editing tag is valid
|
||||
*/
|
||||
validate() {
|
||||
this.nameInvalid = false;
|
||||
const sameName = this.existingTags.find((existingTag) => existingTag.name === this.tag.name);
|
||||
if (sameName != null && sameName.id !== this.tag.id) {
|
||||
this.nameInvalid = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load tag information for display in the edit dialog
|
||||
* @param {Object} tag tag object to edit
|
||||
@@ -228,6 +277,27 @@ export default {
|
||||
this.processing = true;
|
||||
let editResult = true;
|
||||
|
||||
if (!this.validate()) {
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tag.id == null) {
|
||||
await this.addTagAsync(this.tag).then((res) => {
|
||||
if (!res.ok) {
|
||||
this.$root.toastRes(res.msg);
|
||||
editResult = false;
|
||||
} else {
|
||||
this.tag.id = res.tag.id;
|
||||
this.updated();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!editResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let addId of this.addingMonitor) {
|
||||
await this.addMonitorTagAsync(this.tag.id, addId, "").then((res) => {
|
||||
if (!res.ok) {
|
||||
@@ -263,9 +333,9 @@ export default {
|
||||
* Delete the editing tag from server
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteTag() {
|
||||
async deleteTag() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteTag", this.tag.id, (res) => {
|
||||
await this.deleteTagAsync(this.tag.id).then((res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
@@ -309,6 +379,28 @@ export default {
|
||||
return getMonitorRelativeURL(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a tag asynchronously
|
||||
* @param {Object} newTag Object representing new tag to add
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
addTagAsync(newTag) {
|
||||
return new Promise((resolve) => {
|
||||
this.$root.getSocket().emit("addTag", newTag, resolve);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a tag asynchronously
|
||||
* @param {number} tagId ID of tag to delete
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
deleteTagAsync(tagId) {
|
||||
return new Promise((resolve) => {
|
||||
this.$root.getSocket().emit("deleteTag", tagId, resolve);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a tag to a monitor asynchronously
|
||||
* @param {number} tagId ID of tag to add
|
||||
|
@@ -134,13 +134,27 @@ import { colorOptions } from "../util-frontend";
|
||||
import Tag from "../components/Tag.vue";
|
||||
const toast = useToast();
|
||||
|
||||
/**
|
||||
* @typedef Tag
|
||||
* @type {object}
|
||||
* @property {number | undefined} id
|
||||
* @property {number | undefined} monitor_id
|
||||
* @property {number | undefined} tag_id
|
||||
* @property {string} value
|
||||
* @property {string} name
|
||||
* @property {string} color
|
||||
* @property {boolean | undefined} new
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tag,
|
||||
VueMultiselect,
|
||||
},
|
||||
props: {
|
||||
/** Array of tags to be pre-selected */
|
||||
/** Array of tags to be pre-selected
|
||||
* @type {Tag[]}
|
||||
*/
|
||||
preSelectedTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
@@ -148,10 +162,14 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
/** @type {Modal | null} */
|
||||
modal: null,
|
||||
/** @type {Tag[]} */
|
||||
existingTags: [],
|
||||
processing: false,
|
||||
/** @type {Tag[]} */
|
||||
newTags: [],
|
||||
/** @type {Tag[]} */
|
||||
deleteTags: [],
|
||||
newDraftTag: {
|
||||
name: null,
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<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">{{ $t("iOS") }}</option>
|
||||
<option value="ios">iOS</option>
|
||||
<option value="android">{{ $t("Android") }}</option>
|
||||
<option value="huawei">{{ $t("Huawei") }}</option>
|
||||
</select>
|
||||
|
@@ -1,9 +1,33 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="lunasea-device" class="form-label">{{ $t("LunaSea Device ID") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="$parent.notification.lunaseaDevice" type="text" class="form-control" required>
|
||||
<label for="lunasea-notification-target" class="form-label">{{ $t("lunaseaTarget") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<div class="form-text">
|
||||
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
|
||||
<p>
|
||||
<select id="lunasea-notification-target" v-model="$parent.notification.lunaseaTarget" class="form-select" required>
|
||||
<option value="device">{{ $t("lunaseaDeviceID") }}</option>
|
||||
<option value="user">{{ $t("lunaseaUserID") }}</option>
|
||||
</select>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="$parent.notification.lunaseaTarget === 'device'">
|
||||
<label for="lunasea-device" class="form-label">{{ $t("lunaseaDeviceID") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="$parent.notification.lunaseaDevice" type="text" class="form-control">
|
||||
</div>
|
||||
<div v-if="$parent.notification.lunaseaTarget === 'user'">
|
||||
<label for="lunasea-device" class="form-label">{{ $t("lunaseaUserID") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="lunasea-device" v-model="$parent.notification.lunaseaUserID" type="text" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
if (typeof this.$parent.notification.lunaseaTarget === "undefined") {
|
||||
this.$parent.notification.lunaseaTarget = "device";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
36
src/components/notifications/Opsgenie.vue
Normal file
36
src/components/notifications/Opsgenie.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="opsgenie-region" class="form-label">{{ $t("Region") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<select id="opsgenie-region" v-model="$parent.notification.opsgenieRegion" class="form-select" required>
|
||||
<option value="us">
|
||||
US (Default)
|
||||
</option>
|
||||
<option value="eu">
|
||||
EU
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="opsgenie-apikey" class="form-label">{{ $t("API Key") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<HiddenInput id="opsgenie-apikey" v-model="$parent.notification.opsgenieApiKey" required="true" autocomplete="false"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="opsgenie-priority" class="form-label">{{ $t("Priority") }}</label>
|
||||
<input id="opsgenie-priority" v-model="$parent.notification.opsgeniePriority" type="number" class="form-control" min="1" max="5" step="1">
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
|
||||
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
|
||||
<a href="https://docs.opsgenie.com/docs/alert-api" target="_blank">https://docs.opsgenie.com/docs/alert-api</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
31
src/components/notifications/PagerTree.vue
Normal file
31
src/components/notifications/PagerTree.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="pagertree-integration-url" class="form-label">{{ $t("pagertreeIntegrationUrl") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="pagertree-integration-url" v-model="$parent.notification.pagertreeIntegrationUrl" type="text" class="form-control" autocomplete="false">
|
||||
<i18n-t tag="div" keypath="wayToGetPagerTreeIntegrationURL" class="form-text">
|
||||
<a href="https://pagertree.com/docs/integration-guides/introduction#copy-the-endpoint-url" target="_blank">{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pagertree-urgency" class="form-label">{{ $t("pagertreeUrgency") }}</label>
|
||||
<select id="pagertree-urgency" v-model="$parent.notification.pagertreeUrgency" class="form-select">
|
||||
<option value="silent">{{ $t("pagertreeSilent") }}</option>
|
||||
<option value="low">{{ $t("pagertreeLow") }}</option>
|
||||
<option value="medium" selected="selected">{{ $t("pagertreeMedium") }}</option>
|
||||
<option value="high">{{ $t("pagertreeHigh") }}</option>
|
||||
<option value="critical">{{ $t("pagertreeCritical") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pagertree-resolve" class="form-label">{{ $t("pagertreeResolve") }}</label>
|
||||
<select id="pagertree-resolve" v-model="$parent.notification.pagertreeAutoResolve" class="form-select">
|
||||
<option value="resolve" selected="selected">{{ $t("pagertreeResolve") }}</option>
|
||||
<option value="0">{{ $t("pagertreeDoNothing") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
};
|
||||
</script>
|
@@ -97,7 +97,6 @@
|
||||
(leave blank for default one)<br />
|
||||
{{NAME}}: Service Name<br />
|
||||
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
|
||||
{{URL}}: URL<br />
|
||||
{{STATUS}}: Status<br />
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -28,6 +28,30 @@
|
||||
<a :href="telegramGetUpdatesURL('withToken')" target="_blank" style="word-break: break-word;">{{ telegramGetUpdatesURL("masked") }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label for="message_thread_id" class="form-label">{{ $t("telegramMessageThreadID") }}</label>
|
||||
<input id="message_thread_id" v-model="$parent.notification.telegramMessageThreadID" type="text" class="form-control">
|
||||
<p class="form-text">{{ $t("telegramMessageThreadIDDescription") }}</p>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="$parent.notification.telegramSendSilently" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t("telegramSendSilently") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
{{ $t("telegramSendSilentlyDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="$parent.notification.telegramProtectContent" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t("telegramProtectContent") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
{{ $t("telegramProtectContentDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
27
src/components/notifications/Twilio.vue
Normal file
27
src/components/notifications/Twilio.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="twilio-account-sid" class="form-label">{{ $t("Account SID") }}</label>
|
||||
<input id="twilio-account-sid" v-model="$parent.notification.twilioAccountSID" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="twilio-auth-token" class="form-label">{{ $t("Auth Token") }}</label>
|
||||
<input id="twilio-auth-token" v-model="$parent.notification.twilioAuthToken" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="twilio-from-number" class="form-label">{{ $t("From Number") }}</label>
|
||||
<input id="twilio-from-number" v-model="$parent.notification.twilioFromNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="twilio-to-number" class="form-label">{{ $t("To Number") }}</label>
|
||||
<input id="twilio-to-number" v-model="$parent.notification.twilioToNumber" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
<a href="https://www.twilio.com/docs/sms" target="_blank">https://www.twilio.com/docs/sms</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
@@ -21,7 +21,9 @@ import Mattermost from "./Mattermost.vue";
|
||||
import Ntfy from "./Ntfy.vue";
|
||||
import Octopush from "./Octopush.vue";
|
||||
import OneBot from "./OneBot.vue";
|
||||
import Opsgenie from "./Opsgenie.vue";
|
||||
import PagerDuty from "./PagerDuty.vue";
|
||||
import PagerTree from "./PagerTree.vue";
|
||||
import PromoSMS from "./PromoSMS.vue";
|
||||
import Pushbullet from "./Pushbullet.vue";
|
||||
import PushDeer from "./PushDeer.vue";
|
||||
@@ -40,6 +42,7 @@ import STMP from "./SMTP.vue";
|
||||
import Teams from "./Teams.vue";
|
||||
import TechulusPush from "./TechulusPush.vue";
|
||||
import Telegram from "./Telegram.vue";
|
||||
import Twilio from "./Twilio.vue";
|
||||
import Webhook from "./Webhook.vue";
|
||||
import WeCom from "./WeCom.vue";
|
||||
import GoAlert from "./GoAlert.vue";
|
||||
@@ -75,7 +78,9 @@ const NotificationFormList = {
|
||||
"ntfy": Ntfy,
|
||||
"octopush": Octopush,
|
||||
"OneBot": OneBot,
|
||||
"Opsgenie": Opsgenie,
|
||||
"PagerDuty": PagerDuty,
|
||||
"PagerTree": PagerTree,
|
||||
"promosms": PromoSMS,
|
||||
"pushbullet": Pushbullet,
|
||||
"PushByTechulus": TechulusPush,
|
||||
@@ -93,6 +98,7 @@ const NotificationFormList = {
|
||||
"stackfield": Stackfield,
|
||||
"teams": Teams,
|
||||
"telegram": Telegram,
|
||||
"twilio": Twilio,
|
||||
"Splunk": Splunk,
|
||||
"webhook": Webhook,
|
||||
"WeCom": WeCom,
|
||||
|
257
src/components/settings/APIKeys.vue
Normal file
257
src/components/settings/APIKeys.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="add-btn">
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.apiKeyDialog.show()">
|
||||
<font-awesome-icon icon="plus" /> {{ $t("Add API Key") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span v-if="Object.keys(keyList).length === 0" class="d-flex align-items-center justify-content-center my-3">
|
||||
{{ $t("No API Keys") }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in keyList"
|
||||
:key="index"
|
||||
class="item"
|
||||
:class="item.status"
|
||||
>
|
||||
<div class="left-part">
|
||||
<div
|
||||
class="circle"
|
||||
></div>
|
||||
<div class="info">
|
||||
<div class="title">{{ item.name }}</div>
|
||||
<div class="status">
|
||||
{{ $t("apiKey-" + item.status) }}
|
||||
</div>
|
||||
<div class="date">
|
||||
{{ $t("Created") }}: {{ item.createdDate }}
|
||||
</div>
|
||||
<div class="date">
|
||||
{{ $t("Expires") }}: {{ item.expires || $t("Never") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<div class="btn-group" role="group">
|
||||
<button v-if="item.active" class="btn btn-normal" @click="disableDialog(item.id)">
|
||||
<font-awesome-icon icon="pause" /> {{ $t("Disable") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!item.active" class="btn btn-primary" @click="enableKey(item.id)">
|
||||
<font-awesome-icon icon="play" /> {{ $t("Enable") }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" @click="deleteDialog(item.id)">
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3" style="font-size: 13px;">
|
||||
<a href="https://github.com/louislam/uptime-kuma/wiki/API-Keys" target="_blank">{{ $t("Learn More") }}</a>
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disableKey">
|
||||
{{ $t("disableAPIKeyMsg") }}
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteKey">
|
||||
{{ $t("deleteAPIKeyMsg") }}
|
||||
</Confirm>
|
||||
|
||||
<APIKeyDialog ref="apiKeyDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import APIKeyDialog from "../../components/APIKeyDialog.vue";
|
||||
import Confirm from "../Confirm.vue";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
APIKeyDialog,
|
||||
Confirm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedKeyID: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
keyList() {
|
||||
let result = Object.values(this.$root.apiKeyList);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show dialog to confirm deletion
|
||||
* @param {number} keyID ID of monitor that is being deleted
|
||||
*/
|
||||
deleteDialog(keyID) {
|
||||
this.selectedKeyID = keyID;
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a key
|
||||
*/
|
||||
deleteKey() {
|
||||
this.$root.deleteAPIKey(this.selectedKeyID, (res) => {
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show dialog to confirm pause
|
||||
*/
|
||||
disableDialog(keyID) {
|
||||
this.selectedKeyID = keyID;
|
||||
this.$refs.confirmPause.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause maintenance
|
||||
*/
|
||||
disableKey() {
|
||||
this.$root.getSocket().emit("disableAPIKey", this.selectedKeyID, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Resume maintenance
|
||||
*/
|
||||
enableKey(id) {
|
||||
this.$root.getSocket().emit("enableAPIKey", id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../assets/vars.scss";
|
||||
|
||||
.mobile {
|
||||
.item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
min-height: 90px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.circle {
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
.circle {
|
||||
background-color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&.expired {
|
||||
.left-part {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.circle {
|
||||
background-color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.left-part {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.circle {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.btn-group {
|
||||
width: 310px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 20px;
|
||||
padding: 0 10px;
|
||||
width: fit-content;
|
||||
|
||||
.dark & {
|
||||
color: white;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="my-4">
|
||||
<div class="mx-4 pt-1 my-3">
|
||||
<button class="btn btn-primary" @click.stop="addTag"><font-awesome-icon icon="plus" /> {{ $t("Add New Tag") }}</button>
|
||||
</div>
|
||||
|
||||
<div class="tags-list my-3">
|
||||
<div v-for="(tag, index) in tagsList" :key="tag.id" class="d-flex align-items-center mx-4 py-1 tags-list-row" :disabled="processing" @click="editTag(index)">
|
||||
<div class="col-5 ps-1">
|
||||
@@ -19,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TagEditDialog ref="tagEditDialog" :updated="tagsUpdated" />
|
||||
<TagEditDialog ref="tagEditDialog" :updated="tagsUpdated" :existing-tags="tagsList" />
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteTag">
|
||||
{{ $t("confirmDeleteTagMsg") }}
|
||||
</Confirm>
|
||||
@@ -100,6 +104,15 @@ export default {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Show dialog for adding a new tag
|
||||
* @returns {void}
|
||||
*/
|
||||
addTag() {
|
||||
this.$refs.tagEditDialog.reset();
|
||||
this.$refs.tagEditDialog.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Show dialog for editing a tag
|
||||
* @param {number} index index of the tag to edit in the local tagsList
|
||||
@@ -149,10 +162,10 @@ export default {
|
||||
|
||||
.tags-list .tags-list-row {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.125);
|
||||
|
||||
.dark & {
|
||||
border-bottom: 1px solid $dark-border-color;
|
||||
border-top: 1px solid $dark-border-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -164,8 +177,4 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.tags-list .tags-list-row:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -15,7 +15,9 @@ const languageList = {
|
||||
"fa": "Farsi",
|
||||
"pt-PT": "Português (Portugal)",
|
||||
"pt-BR": "Português (Brasileiro)",
|
||||
"fi": "Suomi",
|
||||
"fr-FR": "Français (France)",
|
||||
"he-IL": "עברית",
|
||||
"hu": "Magyar",
|
||||
"hr-HR": "Hrvatski",
|
||||
"it-IT": "Italiano (Italian)",
|
||||
@@ -34,11 +36,13 @@ const languageList = {
|
||||
"et-EE": "eesti",
|
||||
"vi-VN": "Tiếng Việt",
|
||||
"zh-TW": "繁體中文 (台灣)",
|
||||
"uk-UA": "Український",
|
||||
"uk-UA": "Українська",
|
||||
"th-TH": "ไทย",
|
||||
"el-GR": "Ελληνικά",
|
||||
"yue": "繁體中文 (廣東話 / 粵語)",
|
||||
"ro": "Limba română",
|
||||
"ur": "Urdu",
|
||||
"ge": "ქართული"
|
||||
};
|
||||
|
||||
let messages = {
|
||||
@@ -51,7 +55,7 @@ for (let lang in languageList) {
|
||||
};
|
||||
}
|
||||
|
||||
const rtlLangs = [ "fa", "ar-SY" ];
|
||||
const rtlLangs = [ "fa", "ar-SY", "ur" ];
|
||||
|
||||
export const currentLocale = () => localStorage.locale
|
||||
|| languageList[navigator.language] && navigator.language
|
||||
|
@@ -3,6 +3,9 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
// Add Free Font Awesome Icons
|
||||
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
|
||||
// In order to add an icon, you have to:
|
||||
// 1) add the icon name in the import statement below;
|
||||
// 2) add the icon name to the library.add() statement below.
|
||||
import {
|
||||
faArrowAltCircleUp,
|
||||
faCog,
|
||||
@@ -45,6 +48,7 @@ import {
|
||||
faHeartbeat,
|
||||
faFilter,
|
||||
faInfoCircle,
|
||||
faClone,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
@@ -90,6 +94,7 @@ library.add(
|
||||
faHeartbeat,
|
||||
faFilter,
|
||||
faInfoCircle,
|
||||
faClone,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"languageName": "العربية",
|
||||
"languageName": "إنجليزي",
|
||||
"checkEverySecond": "تحقق من كل {0} ثانية",
|
||||
"retryCheckEverySecond": "أعد محاولة كل {0} ثانية",
|
||||
"resendEveryXTimes": "إعادة تقديم كل {0} مرات",
|
||||
@@ -15,10 +15,10 @@
|
||||
"statusMaintenance": "صيانة",
|
||||
"Schedule maintenance": "جدولة الصيانة",
|
||||
"Affected Monitors": "الشاشات المتأثرة",
|
||||
"Pick Affected Monitors...": "اختيار الشاشات المتأثرة ...",
|
||||
"Pick Affected Monitors...": "اختر الشاشات المتأثرة …",
|
||||
"Start of maintenance": "بداية الصيانة",
|
||||
"All Status Pages": "جميع صفحات الحالة",
|
||||
"Select status pages...": "حدد صفحات الحالة ...",
|
||||
"Select status pages...": "حدد صفحات الحالة …",
|
||||
"recurringIntervalMessage": "ركض مرة واحدة كل يوم | قم بالتشغيل مرة واحدة كل يوم {0}",
|
||||
"affectedMonitorsDescription": "حدد المراقبين المتأثرة بالصيانة الحالية",
|
||||
"affectedStatusPages": "إظهار رسالة الصيانة هذه على صفحات الحالة المحددة",
|
||||
@@ -90,7 +90,7 @@
|
||||
"Heartbeat Interval": "فاصل نبضات القلب",
|
||||
"Retries": "يحاول مجدداً",
|
||||
"Heartbeat Retry Interval": "الفاصل الزمني لإعادة محاكمة نبضات القلب",
|
||||
"Resend Notification if Down X times consequently": "إعادة تقديم الإخطار إذا انخفض x مرات بالتالي",
|
||||
"Resend Notification if Down X times consecutively": "إعادة تقديم الإخطار إذا انخفض x مرات بالتالي",
|
||||
"Advanced": "متقدم",
|
||||
"Upside Down Mode": "وضع أسفل أسفل",
|
||||
"Max. Redirects": "الأعلى. إعادة التوجيه",
|
||||
@@ -178,7 +178,7 @@
|
||||
"Token": "رمز",
|
||||
"Show URI": "أظهر URI",
|
||||
"Tags": "العلامات",
|
||||
"Add New below or Select...": "أضف جديدًا أدناه أو حدد ...",
|
||||
"Add New below or Select...": "إضافة جديد أدناه أو تحديد …",
|
||||
"Tag with this name already exist.": "علامة مع هذا الاسم موجود بالفعل.",
|
||||
"Tag with this value already exist.": "علامة مع هذه القيمة موجودة بالفعل.",
|
||||
"color": "اللون",
|
||||
@@ -192,7 +192,7 @@
|
||||
"Purple": "نفسجي",
|
||||
"Pink": "لون القرنفل",
|
||||
"Custom": "العادة",
|
||||
"Search...": "يبحث...",
|
||||
"Search...": "يبحث…",
|
||||
"Avg. Ping": "متوسط. بينغ",
|
||||
"Avg. Response": "متوسط. إجابة",
|
||||
"Entry Page": "صفحة الدخول",
|
||||
@@ -215,6 +215,7 @@
|
||||
"Bot Token": "رمز الروبوت",
|
||||
"wayToGetTelegramToken": "يمكنك الحصول على رمز من {0}.",
|
||||
"Chat ID": "معرف الدردشة",
|
||||
"telegramMessageThreadID": "معرف المواضيع",
|
||||
"supportTelegramChatID": "دعم الدردشة المباشرة / معرف الدردشة للقناة",
|
||||
"wayToGetTelegramChatID": "يمكنك الحصول على معرف الدردشة الخاص بك عن طريق إرسال رسالة إلى الروبوت والانتقال إلى عنوان URL هذا لعرض Chat_id",
|
||||
"YOUR BOT TOKEN HERE": "رمز الروبوت الخاص بك هنا",
|
||||
@@ -237,10 +238,10 @@
|
||||
"smtpBCC": "BCC",
|
||||
"discord": "خلاف",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
"wayToGetDiscordURL": "يمكنك الحصول على هذا عن طريق الانتقال إلى إعدادات الخادم -> التكامل -> إنشاء WebHook",
|
||||
"wayToGetDiscordURL": "يمكنك الحصول على هذا بالانتقال إلى إعدادات الخادم -> عمليات التكامل -> عرض الخطافات على الويب -> خطاف ويب جديد",
|
||||
"Bot Display Name": "اسم عرض الروبوت",
|
||||
"Prefix Custom Message": "بادئة رسالة مخصصة",
|
||||
"Hello @everyone is...": "مرحبًا {'@'} الجميع ...",
|
||||
"Hello @everyone is...": "مرحبًا {'@'} الجميع…",
|
||||
"teams": "فرق Microsoft",
|
||||
"Webhook URL": "Webhook URL",
|
||||
"wayToGetTeamsURL": "يمكنك معرفة كيفية إنشاء عنوان URL webhook {0}.",
|
||||
@@ -351,8 +352,8 @@
|
||||
"Security": "حماية",
|
||||
"Steam API Key": "مفتاح API Steam",
|
||||
"Shrink Database": "تقلص قاعدة البيانات",
|
||||
"Pick a RR-Type...": "اختر نوع RR ...",
|
||||
"Pick Accepted Status Codes...": "اختيار رموز الحالة المقبولة ...",
|
||||
"Pick a RR-Type...": "اختر نوع RR …",
|
||||
"Pick Accepted Status Codes...": "اختر أكواد الحالة المقبولة …",
|
||||
"Default": "تقصير",
|
||||
"HTTP Options": "خيارات HTTP",
|
||||
"Create Incident": "إنشاء حادث",
|
||||
@@ -596,7 +597,7 @@
|
||||
"Domain": "اِختِصاص",
|
||||
"Workstation": "محطة العمل",
|
||||
"disableCloudflaredNoAuthMsg": "أنت في وضع مصادقة لا توجد كلمة مرور غير مطلوبة.",
|
||||
"trustProxyDescription": "الثقة 'x-forward-*'. إذا كنت ترغب في الحصول على IP العميل الصحيح وكوما في الوقت المناسب مثل Nginx أو Apache ، فيجب عليك تمكين ذلك.",
|
||||
"trustProxyDescription": "ثق في رؤوس \"X-Forwarded- *\". إذا كنت ترغب في الحصول على عنوان IP الصحيح للعميل وكان Uptime Kuma خلف وكيل مثل Nginx أو Apache ، فيجب عليك تمكين هذا.",
|
||||
"wayToGetLineNotifyToken": "يمكنك الحصول على رمز الوصول من {0}",
|
||||
"Examples": "أمثلة",
|
||||
"Home Assistant URL": "Home Assistant URL",
|
||||
@@ -616,7 +617,7 @@
|
||||
"goAlertInfo": "الهدف هو تطبيق مفتوح المصدر لجدولة الجدولة التلقائية والإشعارات (مثل الرسائل القصيرة أو المكالمات الصوتية). إشراك الشخص المناسب تلقائيًا بالطريقة الصحيحة وفي الوقت المناسب! {0}",
|
||||
"goAlertIntegrationKeyInfo": "احصل على مفتاح تكامل API العام للخدمة في هذا التنسيق \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" عادةً قيمة المعلمة الرمزية لعنوان url المنسق.",
|
||||
"goAlert": "المرمى",
|
||||
"backupOutdatedWarning": "إهمال",
|
||||
"backupOutdatedWarning": "مهمل: نظرًا لأنه تمت إضافة الكثير من الميزات وأن ميزة النسخ الاحتياطي هذه لم يتم الحفاظ عليها قليلاً ، فلا يمكنها إنشاء نسخة احتياطية كاملة أو استعادتها.",
|
||||
"backupRecommend": "يرجى النسخ الاحتياطي لحجم الصوت أو مجلد البيانات (./data/) مباشرة بدلاً من ذلك.",
|
||||
"Optional": "اختياري",
|
||||
"squadcast": "القاء فريقي",
|
||||
@@ -680,5 +681,36 @@
|
||||
"Specific Monitor Type": "نوع شاشة محدد",
|
||||
"dataRetentionTimeError": "يجب أن تكون فترة الاستبقاء 0 أو أكبر",
|
||||
"infiniteRetention": "ضبط على 0 للاحتفاظ لا نهائي.",
|
||||
"confirmDeleteTagMsg": "هل أنت متأكد من أنك تريد حذف هذه العلامة؟ لن يتم حذف الشاشات المرتبطة بهذه العلامة."
|
||||
"confirmDeleteTagMsg": "هل أنت متأكد من أنك تريد حذف هذه العلامة؟ لن يتم حذف الشاشات المرتبطة بهذه العلامة.",
|
||||
"Custom Monitor Type": "نوع الشاشة المخصص",
|
||||
"Game": "لعبة",
|
||||
"Don't know how to get the token? Please read the guide:": "لا أعرف كيفية الحصول على الرمز المميز؟ يرجى قراءة الدليل:",
|
||||
"Subject:": "موضوع:",
|
||||
"Valid To:": "صالحة ل:",
|
||||
"Days Remaining:": "الأيام المتبقية:",
|
||||
"Issuer:": "المُصدر:",
|
||||
"Fingerprint:": "بصمة:",
|
||||
"Most likely causes:": "الأسباب المرجحة:",
|
||||
"Help": "يساعد",
|
||||
"Accept characters:": "قبول الأحرف:",
|
||||
"plugin": "البرنامج المساعد | الإضافات",
|
||||
"install": "ثَبَّتَ",
|
||||
"installing": "التثبيت",
|
||||
"uninstall": "الغاء التثبيت",
|
||||
"uninstalling": "إلغاء التثبيت",
|
||||
"loadingError": "لا يمكن جلب البيانات ، يرجى المحاولة مرة أخرى في وقت لاحق.",
|
||||
"Example:": "مثال: {0}",
|
||||
"Google Analytics ID": "معرف Google Analytics",
|
||||
"markdownSupported": "دعم صيغة Markdown",
|
||||
"Edit Tag": "تحرير العلامة",
|
||||
"Server Address": "عنوان المستقبل",
|
||||
"Learn More": "يتعلم أكثر",
|
||||
"Automations can optionally be triggered in Home Assistant:": "يمكن تشغيل الأتمتة اختياريًا في Home Assistant:",
|
||||
"Trigger type:": "نوع الزناد:",
|
||||
"Event type:": "نوع الحدث:",
|
||||
"Event data:": "بيانات الحدث:",
|
||||
"More info on:": "مزيد من المعلومات حول: {0}",
|
||||
"What you can try:": "ماذا تستطيع أن تجرب:",
|
||||
"Packet Size": "حجم الحزمة",
|
||||
"confirmUninstallPlugin": "هل أنت متأكد من أنك تريد إلغاء تثبيت هذا المكون الإضافي؟"
|
||||
}
|
||||
|
688
src/lang/ar.json
Normal file
688
src/lang/ar.json
Normal file
@@ -0,0 +1,688 @@
|
||||
{
|
||||
"Edit": "تعديل",
|
||||
"Delete": "حذف",
|
||||
"Current": "حالي",
|
||||
"Uptime": "مدة التشغيل",
|
||||
"Monitor": "مراقب | مراقبات",
|
||||
"day": "يوم | أيام",
|
||||
"-day": "-يوم",
|
||||
"hour": "ساعة",
|
||||
"-hour": "-ساعة",
|
||||
"Response": "استجاية",
|
||||
"Ping": "بينغ",
|
||||
"Monitor Type": "نوع المراقب",
|
||||
"Cert Exp.": "انتهاء صَلاحِيَة شهادة الأمان SSL",
|
||||
"Theme - Heartbeat Bar": "موضوع - بار نبضات",
|
||||
"Normal": "طبيعي",
|
||||
"Bottom": "الأسفل",
|
||||
"None": "لا أحد",
|
||||
"Current Password": "كلمة المرور الحالي",
|
||||
"New Password": "كلمة سر جديدة",
|
||||
"Repeat New Password": "كرر كلمة المرور الجديدة",
|
||||
"Update Password": "تطوير كلمة السر",
|
||||
"Disable Auth": "تعطيل المصادقة",
|
||||
"Enable Auth": "تمكين المصادقة",
|
||||
"disableauth.message1": "هل أنت متأكد من أن <strong> تعطيل المصادقة </strong>؟",
|
||||
"disableauth.message2": "تم تصميمه للسيناريوهات <strong> حيث تنوي تنفيذ مصادقة الطرف الثالث </strong> أمام كوما في وقت التشغيل مثل CloudFlare Access Authelia أو آليات المصادقة الأخرى.",
|
||||
"Please use this option carefully!": "الرجاء استخدام هذا الخيار بعناية!",
|
||||
"Logout": "تسجيل خروج",
|
||||
"Leave": "غادر",
|
||||
"I understand, please disable": "أنا أفهم من فضلك تعطيل",
|
||||
"Confirm": "يتأكد",
|
||||
"Yes": "نعم",
|
||||
"No": "رقم",
|
||||
"Username": "اسم المستخدم",
|
||||
"Password": "كلمة المرور",
|
||||
"Remember me": "تذكرنى",
|
||||
"Login": "تسجيل الدخول",
|
||||
"No Monitors, please": "لا شاشات من فضلك",
|
||||
"alertNoFile": "الرجاء تحديد ملف للاستيراد.",
|
||||
"Skip existing": "تخطي الموجود",
|
||||
"Search...": "يبحث…",
|
||||
"Avg. Ping": "متوسط. بينغ",
|
||||
"Avg. Response": "متوسط. إجابة",
|
||||
"Entry Page": "صفحة الدخول",
|
||||
"statusPageNothing": "لا شيء هنا الرجاء إضافة مجموعة أو شاشة.",
|
||||
"No Services": "لا توجد خدمات",
|
||||
"All Systems Operational": "جميع الأنظمة التشغيلية",
|
||||
"Partially Degraded Service": "الخدمة المتدهورة جزئيا",
|
||||
"Degraded Service": "خدمة متدهورة",
|
||||
"Add Group": "أضف مجموعة",
|
||||
"Add a monitor": "إضافة شاشة",
|
||||
"Edit Status Page": "تحرير صفحة الحالة",
|
||||
"Go to Dashboard": "الذهاب إلى لوحة القيادة",
|
||||
"Status Page": "صفحة الحالة",
|
||||
"Application Token": "رمز التطبيق",
|
||||
"Server URL": "عنوان URL الخادم",
|
||||
"Priority": "أولوية",
|
||||
"Read more": "قراءة المزيد",
|
||||
"topic": "عنوان",
|
||||
"Last Updated": "التحديث الاخير",
|
||||
"Unpin": "إلغاء",
|
||||
"Show Tags": "أضهر العلامات",
|
||||
"Add one": "أضف واحدا",
|
||||
"wayToGetCloudflaredURL": "(قم بتنزيل CloudFlared من {0})",
|
||||
"cloudflareWebsite": "موقع CloudFlare",
|
||||
"Message:": ":رسالة",
|
||||
"Don't know how to get the token? Please read the guide:": "لا أعرف كيفية الحصول على الرمز المميز؟ يرجى قراءة الدليل:",
|
||||
"telegramSendSilently": "أرسل بصمت",
|
||||
"telegramSendSilentlyDescription": "ترسل الرسالة بصمت ويتلقى المستخدمون إشعارا بدون صوت.",
|
||||
"Enable": "يُمكَِن",
|
||||
"notificationRegional": "إقليمي",
|
||||
"Clone": "استنسخ",
|
||||
"cloneOf": "مُستنسَخ من {0}",
|
||||
"grpcMethodDescription": "يتم تحويل اسم الطريقة إلى تنسيق Cammelcase مثل Sayhello Check وما إلى ذلك.",
|
||||
"acceptedStatusCodesDescription": "حدد رموز الحالة التي تعتبر استجابة ناجحة.",
|
||||
"deleteNotificationMsg": "هل أنت متأكد من حذف هذا الإشعار لجميع الشاشات؟",
|
||||
"dnsPortDescription": "منفذ خادم DNS. الافتراضيات إلى 53. يمكنك تغيير المنفذ في أي وقت.",
|
||||
"pauseMonitorMsg": "هل أنت متأكد من أن تتوقف مؤقتًا؟",
|
||||
"API Keys": "مفاتيح API",
|
||||
"Expiry": "نهاية الصلاحية",
|
||||
"Expiry date": "تاريخ نهاية الصلاحية",
|
||||
"Continue": "مواصلة",
|
||||
"Add Another": "إضافة آخر",
|
||||
"Add API Key": "أضف مفتاح API",
|
||||
"apiKey-active": "نشط",
|
||||
"apiKey-expired": "منتهي الصلاحية",
|
||||
"Generate": "توليد",
|
||||
"Settings": "الإعدادات",
|
||||
"Dashboard": "لوح التحكم",
|
||||
"Help": "المساعدة",
|
||||
"New Update": "تحديث جديد متوفر",
|
||||
"Language": "اللغة",
|
||||
"Appearance": "المظهر",
|
||||
"Theme": "الحُلة",
|
||||
"General": "العامة",
|
||||
"Version": "الإصدار",
|
||||
"Primary Base URL": "الرابط التشعبي الأساسي",
|
||||
"Check Update On GitHub": "التحقق من التحديث على GitHub",
|
||||
"Add New Monitor": "أضف شاشة جديدة",
|
||||
"Quick Stats": "إحصائيات سريعة",
|
||||
"Pending": "قيد الانتظار",
|
||||
"General Monitor Type": "نوع الشاشة العامة",
|
||||
"Passive Monitor Type": "نوع الشاشة السلبي",
|
||||
"Specific Monitor Type": "نوع شاشة محدد",
|
||||
"markdownSupported": "دعم صيغة Markdown",
|
||||
"pauseDashboardHome": "وقفة",
|
||||
"Pause": "إيقاف مؤقت",
|
||||
"Name": "الاسم",
|
||||
"Status": "الحالة",
|
||||
"DateTime": "الوقت والتاريخ",
|
||||
"Message": "الرسالة",
|
||||
"No important events": "لا توجد أحداث مهمة",
|
||||
"Resume": "استمرار",
|
||||
"Keyword": "كلمة مفتاحية",
|
||||
"Friendly Name": "اسم معروف",
|
||||
"URL": "عنوان URL",
|
||||
"Hostname": "اسم المضيف",
|
||||
"Port": "المنفذ",
|
||||
"Heartbeat Interval": "فاصل نبضات القلب",
|
||||
"Add": "إضافة",
|
||||
"Up": "متصل",
|
||||
"Down": "غير متصل",
|
||||
"Maintenance": "الصيانة",
|
||||
"Unknown": "مجهول",
|
||||
"Retries": "يحاول مجدداً",
|
||||
"Heartbeat Retry Interval": "الفاصل الزمني لإعادة محاكمة نبضات القلب",
|
||||
"Resend Notification if Down X times consecutively": "إعادة تقديم الإخطار إذا انخفض x مرات بالتالي",
|
||||
"Advanced": "متقدم",
|
||||
"checkEverySecond": "تحقق من كل {0} ثانية",
|
||||
"retryCheckEverySecond": "أعد محاولة كل {0} ثانية",
|
||||
"resendEveryXTimes": "إعادة تقديم كل {0} مرات",
|
||||
"resendDisabled": "إعادة الالتزام بالتعطيل",
|
||||
"retriesDescription": "الحد الأقصى لإعادة المحاولة قبل تمييز الخدمة على أنها لأسفل وإرسال إشعار",
|
||||
"ignoreTLSError": "تجاهل خطأ TLS/SSL لمواقع HTTPS",
|
||||
"upsideDownModeDescription": "اقلب الحالة رأسًا على عقب. إذا كانت الخدمة قابلة للوصول إلى أسفل.",
|
||||
"maxRedirectDescription": "الحد الأقصى لعدد إعادة التوجيه لمتابعة. ضبط على 0 لتعطيل إعادة التوجيه.",
|
||||
"Upside Down Mode": "وضع أسفل أسفل",
|
||||
"Max. Redirects": "الأعلى. إعادة التوجيه",
|
||||
"Accepted Status Codes": "رموز الحالة المقبولة",
|
||||
"Push URL": "دفع عنوان URL",
|
||||
"needPushEvery": "يجب عليك استدعاء عنوان URL هذا كل ثانية.",
|
||||
"pushOptionalParams": "المعلمات الاختيارية",
|
||||
"Save": "يحفظ",
|
||||
"Notifications": "إشعارات",
|
||||
"Not available, please setup.": "غير متوفر من فضلك الإعداد.",
|
||||
"Setup Notification": "إشعار الإعداد",
|
||||
"Light": "نور",
|
||||
"Dark": "داكن",
|
||||
"Auto": "آلي",
|
||||
"Timezone": "وحدة زمنية",
|
||||
"Search Engine Visibility": "محرك بحث الرؤية",
|
||||
"Allow indexing": "السماح الفهرسة",
|
||||
"Discourage search engines from indexing site": "تثبيط محركات البحث من موقع الفهرسة",
|
||||
"Change Password": "غير كلمة السر",
|
||||
"add one": "أضف واحدا",
|
||||
"Notification Type": "نوع إعلام",
|
||||
"Email": "بريد إلكتروني",
|
||||
"Test": "امتحان",
|
||||
"Certificate Info": "معلومات الشهادة",
|
||||
"Resolver Server": "خادم Resolver",
|
||||
"Resource Record Type": "نوع سجل الموارد",
|
||||
"Last Result": "اخر نتيجة",
|
||||
"Create your admin account": "إنشاء حساب المسؤول الخاص بك",
|
||||
"Repeat Password": "اعد كلمة السر",
|
||||
"Import Backup": "استيراد النسخ الاحتياطي",
|
||||
"Export Backup": "النسخ الاحتياطي تصدير",
|
||||
"Export": "يصدّر",
|
||||
"Import": "يستورد",
|
||||
"respTime": "resp. الوقت (MS)",
|
||||
"notAvailableShort": "ن/أ",
|
||||
"Default enabled": "التمكين الافتراضي",
|
||||
"Apply on all existing monitors": "تنطبق على جميع الشاشات الحالية",
|
||||
"Create": "خلق",
|
||||
"Clear Data": "امسح البيانات",
|
||||
"Events": "الأحداث",
|
||||
"Heartbeats": "نبضات القلب",
|
||||
"Schedule maintenance": "جدولة الصيانة",
|
||||
"Affected Monitors": "الشاشات المتأثرة",
|
||||
"Pick Affected Monitors...": "اختر الشاشات المتأثرة …",
|
||||
"Start of maintenance": "بداية الصيانة",
|
||||
"All Status Pages": "جميع صفحات الحالة",
|
||||
"Select status pages...": "حدد صفحات الحالة …",
|
||||
"alertWrongFileType": "الرجاء تحديد ملف JSON.",
|
||||
"Clear all statistics": "مسح جميع الإحصاءات",
|
||||
"Overwrite": "الكتابة فوق",
|
||||
"Options": "خيارات",
|
||||
"Keep both": "احتفظ بكليهما",
|
||||
"Verify Token": "تحقق من الرمز المميز",
|
||||
"Setup 2FA": "الإعداد 2FA",
|
||||
"Enable 2FA": "تمكين 2FA",
|
||||
"Disable 2FA": "تعطيل 2FA",
|
||||
"2FA Settings": "2FA إعدادات",
|
||||
"Two Factor Authentication": "توثيق ذو عاملين",
|
||||
"Active": "نشيط",
|
||||
"Inactive": "غير نشط",
|
||||
"Token": "رمز",
|
||||
"Show URI": "أظهر URI",
|
||||
"Tags": "العلامات",
|
||||
"Add New below or Select...": "إضافة جديد أدناه أو تحديد …",
|
||||
"Tag with this name already exist.": "علامة مع هذا الاسم موجود بالفعل.",
|
||||
"Tag with this value already exist.": "علامة مع هذه القيمة موجودة بالفعل.",
|
||||
"color": "اللون",
|
||||
"value (optional)": "القيمة (اختياري)",
|
||||
"Gray": "رمادي",
|
||||
"Red": "أحمر",
|
||||
"Orange": "البرتقالي",
|
||||
"Green": "لون أخضر",
|
||||
"Blue": "أزرق",
|
||||
"Indigo": "النيلي",
|
||||
"Purple": "نفسجي",
|
||||
"webhookAdditionalHeadersDesc": "يحدد رؤوس إضافية مرسلة مع webhook.",
|
||||
"Webhook URL": "Webhook URL",
|
||||
"Pink": "لون القرنفل",
|
||||
"Custom": "العادة",
|
||||
"Status Pages": "صفحات الحالة",
|
||||
"defaultNotificationName": "تنبيه {الإخطار} ({number})",
|
||||
"here": "هنا",
|
||||
"Required": "مطلوب",
|
||||
"Post URL": "بعد عنوان URL",
|
||||
"Content Type": "نوع المحتوى",
|
||||
"webhookJsonDesc": "{0} مفيد لأي خوادم HTTP الحديثة مثل Express.js",
|
||||
"webhookFormDataDesc": "{multipart} مفيد لـ PHP. سيحتاج JSON إلى تحليل {decodefunction}",
|
||||
"webhookAdditionalHeadersTitle": "رؤوس إضافية",
|
||||
"emojiCheatSheet": "ورقة الغش في الرموز التعبيرية",
|
||||
"appriseInstalled": "تم تثبيت Prosise.",
|
||||
"appriseNotInstalled": "الإبرام غير مثبت. {0}",
|
||||
"Method": "طريقة",
|
||||
"Body": "الجسم",
|
||||
"Headers": "الرؤوس",
|
||||
"PushUrl": "دفع عنوان URL",
|
||||
"HeadersInvalidFormat": "رؤوس الطلبات غير صالحة JSON ",
|
||||
"BodyInvalidFormat": "هيئة الطلب غير صالحة JSON ",
|
||||
"Monitor History": "مراقبة التاريخ",
|
||||
"clearDataOlderThan": "الحفاظ على بيانات سجل المراقبة للأيام {0}.",
|
||||
"PasswordsDoNotMatch": "كلمة المرور غير مطابقة.",
|
||||
"records": "السجلات",
|
||||
"One record": "سجل واحد",
|
||||
"steamApiKeyDescription": "لمراقبة خادم لعبة Steam ، تحتاج إلى مفتاح Steam Web-API. يمكنك تسجيل مفتاح API الخاص بك هنا ",
|
||||
"Current User": "المستخدم الحالي",
|
||||
"topicExplanation": "موضوع MQTT لرصد",
|
||||
"successMessage": "نجاح رسالة",
|
||||
"successMessageExplanation": "رسالة MQTT التي ستعتبر نجاحًا",
|
||||
"recent": "الأخيرة",
|
||||
"Done": "فعله",
|
||||
"Info": "معلومات",
|
||||
"Security": "حماية",
|
||||
"Steam API Key": "مفتاح API Steam",
|
||||
"Shrink Database": "تقلص قاعدة البيانات",
|
||||
"Pick a RR-Type...": "اختر نوع RR …",
|
||||
"Pick Accepted Status Codes...": "اختر أكواد الحالة المقبولة …",
|
||||
"Default": "تقصير",
|
||||
"HTTP Options": "خيارات HTTP",
|
||||
"Create Incident": "إنشاء حادث",
|
||||
"Title": "لقب",
|
||||
"Content": "المحتوى",
|
||||
"Style": "أسلوب",
|
||||
"info": "معلومات",
|
||||
"warning": "تحذير",
|
||||
"danger": "خطر",
|
||||
"error": "خطأ",
|
||||
"critical": "شديد الأهمية",
|
||||
"primary": "الأولية",
|
||||
"light": "نور",
|
||||
"dark": "ظلام",
|
||||
"Post": "بريد",
|
||||
"Please input title and content": "الرجاء إدخال العنوان والمحتوى",
|
||||
"Created": "مخلوق",
|
||||
"Switch to Light Theme": "التبديل إلى موضوع الضوء",
|
||||
"Switch to Dark Theme": "التبديل إلى موضوع الظلام",
|
||||
"Hide Tags": "إخفاء العلامات",
|
||||
"Description": "وصف",
|
||||
"No monitors available.": "لا شاشات المتاحة.",
|
||||
"No Monitors": "لا شاشات",
|
||||
"Untitled Group": "مجموعة بلا عنوان",
|
||||
"Services": "خدمات",
|
||||
"Discard": "تجاهل",
|
||||
"Cancel": "يلغي",
|
||||
"Powered by": "مشغل بواسطة",
|
||||
"shrinkDatabaseDescription": "تشغيل فراغ قاعدة البيانات لـ SQLite. إذا تم إنشاء قاعدة البيانات الخاصة بك بعد تمكين 1.10.0 AUTO_VACUUM بالفعل ولا يلزم هذا الإجراء.",
|
||||
"Customize": "يعدل أو يكيف",
|
||||
"Custom Footer": "تذييل مخصص",
|
||||
"Custom CSS": "لغة تنسيق ويب حسب الطلب",
|
||||
"deleteStatusPageMsg": "هل أنت متأكد من حذف صفحة الحالة هذه؟",
|
||||
"Proxies": "وكلاء",
|
||||
"default": "تقصير",
|
||||
"enabled": "تمكين",
|
||||
"setAsDefault": "تعيين كافتراضي",
|
||||
"deleteProxyMsg": "هل أنت متأكد من حذف هذا الوكيل لجميع الشاشات؟",
|
||||
"proxyDescription": "يجب تعيين الوكلاء إلى شاشة للعمل.",
|
||||
"enableProxyDescription": "لن يؤثر هذا الوكيل على طلبات الشاشة حتى يتم تنشيطه. يمكنك التحكم مؤقتًا في تعطيل الوكيل من جميع الشاشات حسب حالة التنشيط.",
|
||||
"setAsDefaultProxyDescription": "سيتم تمكين هذا الوكيل افتراضيًا للشاشات الجديدة. لا يزال بإمكانك تعطيل الوكيل بشكل منفصل لكل شاشة.",
|
||||
"Certificate Chain": "سلسلة الشهادة",
|
||||
"Valid": "صالح",
|
||||
"Invalid": "غير صالح",
|
||||
"User": "المستعمل",
|
||||
"Installed": "المثبتة",
|
||||
"Not installed": "غير مثبت",
|
||||
"Running": "جري",
|
||||
"Not running": "لا يعمل",
|
||||
"Remove Token": "إزالة الرمز المميز",
|
||||
"Start": "بداية",
|
||||
"Stop": "قف",
|
||||
"Add New Status Page": "أضف صفحة حالة جديدة",
|
||||
"Slug": "سبيكة",
|
||||
"Accept characters:": "قبول الأحرف:",
|
||||
"startOrEndWithOnly": "ابدأ أو ينتهي بـ {0} فقط",
|
||||
"No consecutive dashes": "لا شرطات متتالية",
|
||||
"Next": "التالي",
|
||||
"The slug is already taken. Please choose another slug.": "تم أخذ سبيكة بالفعل. الرجاء اختيار سبيكة أخرى.",
|
||||
"No Proxy": "لا الوكيل",
|
||||
"Authentication": "المصادقة",
|
||||
"HTTP Basic Auth": "HTTP الأساسي Auth",
|
||||
"New Status Page": "صفحة حالة جديدة",
|
||||
"Page Not Found": "الصفحة غير موجودة",
|
||||
"Reverse Proxy": "وكيل عكسي",
|
||||
"Backup": "دعم",
|
||||
"About": "عن",
|
||||
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "قد يضيع الاتصال الحالي إذا كنت تتصل حاليًا عبر نفق CloudFlare. هل أنت متأكد تريد إيقافها؟ اكتب كلمة المرور الحالية لتأكيدها.",
|
||||
"HTTP Headers": "رؤوس HTTP",
|
||||
"Trust Proxy": "الوكيل الثقة",
|
||||
"Other Software": "برامج أخرى",
|
||||
"For example: nginx, Apache and Traefik.": "على سبيل المثال: nginx و Apache و Traefik.",
|
||||
"Please read": "يرجى القراءة",
|
||||
"Subject:": "موضوع:",
|
||||
"Valid To:": "صالحة ل:",
|
||||
"Days Remaining:": "الأيام المتبقية:",
|
||||
"Issuer:": "المُصدر:",
|
||||
"Fingerprint:": "بصمة:",
|
||||
"No status pages": "لا صفحات الحالة",
|
||||
"Domain Name Expiry Notification": "اسم النطاق إشعار انتهاء الصلاحية",
|
||||
"Proxy": "الوكيل",
|
||||
"Date Created": "تاريخ الإنشاء",
|
||||
"Footer Text": "نص تذييل",
|
||||
"Show Powered By": "عرض مدعوم من قبل",
|
||||
"Domain Names": "أسماء المجال",
|
||||
"signedInDisp": "وقعت في {0}",
|
||||
"signedInDispDisabled": "معاق المصادقة.",
|
||||
"RadiusSecret": "سر نصف القطر",
|
||||
"RadiusSecretDescription": "السر المشترك بين العميل والخادم",
|
||||
"RadiusCalledStationId": "يسمى معرف المحطة",
|
||||
"RadiusCalledStationIdDescription": "معرف الجهاز المتصل",
|
||||
"RadiusCallingStationId": "معرف محطة الاتصال",
|
||||
"RadiusCallingStationIdDescription": "معرف جهاز الاتصال",
|
||||
"Certificate Expiry Notification": "إشعار انتهاء الصلاحية",
|
||||
"API Username": "اسم المستخدم API",
|
||||
"API Key": "مفتاح API",
|
||||
"Show update if available": "عرض التحديث إذا كان ذلك متاحًا",
|
||||
"Also check beta release": "تحقق أيضًا من الإصدار التجريبي",
|
||||
"Using a Reverse Proxy?": "باستخدام وكيل عكسي؟",
|
||||
"Check how to config it for WebSocket": "تحقق من كيفية تكوينه لـ WebSocket",
|
||||
"Steam Game Server": "خادم لعبة البخار",
|
||||
"Most likely causes:": "الأسباب المرجحة:",
|
||||
"The resource is no longer available.": "لم يعد المورد متاحًا.",
|
||||
"There might be a typing error in the address.": "قد يكون هناك خطأ مطبعي في العنوان.",
|
||||
"What you can try:": "ماذا تستطيع أن تجرب:",
|
||||
"Retype the address.": "اعد كتابة العنوان.",
|
||||
"Go back to the previous page.": "عد للصفحة السابقة.",
|
||||
"Coming Soon": "قريبا",
|
||||
"Connection String": "سلسلة الاتصال",
|
||||
"Query": "استفسار",
|
||||
"settingsCertificateExpiry": "شهادة TLS انتهاء الصلاحية",
|
||||
"certificationExpiryDescription": "شاشات HTTPS تضيء عندما تنتهي شهادة TLS في",
|
||||
"Setup Docker Host": "إعداد مضيف Docker",
|
||||
"Connection Type": "نوع الاتصال",
|
||||
"Docker Daemon": "Docker Daemon",
|
||||
"deleteDockerHostMsg": "هل أنت متأكد من حذف مضيف Docker لجميع الشاشات؟",
|
||||
"socket": "قابس كهرباء",
|
||||
"tcp": "TCP / HTTP",
|
||||
"Docker Container": "حاوية Docker",
|
||||
"Container Name / ID": "اسم الحاوية / معرف",
|
||||
"Docker Host": "مضيف Docker",
|
||||
"Docker Hosts": "مضيفي Docker",
|
||||
"Domain": "اِختِصاص",
|
||||
"Workstation": "محطة العمل",
|
||||
"Packet Size": "حجم الحزمة",
|
||||
"Bot Token": "رمز الروبوت",
|
||||
"wayToGetTelegramToken": "يمكنك الحصول على رمز من {0}.",
|
||||
"Chat ID": "معرف الدردشة",
|
||||
"telegramMessageThreadID": "معرف المواضيع",
|
||||
"supportTelegramChatID": "دعم الدردشة المباشرة / معرف الدردشة للقناة",
|
||||
"wayToGetTelegramChatID": "يمكنك الحصول على معرف الدردشة الخاص بك عن طريق إرسال رسالة إلى الروبوت والانتقال إلى عنوان URL هذا لعرض Chat_id",
|
||||
"YOUR BOT TOKEN HERE": "رمز الروبوت الخاص بك هنا",
|
||||
"chatIDNotFound": "لم يتم العثور على معرف الدردشة ؛ الرجاء إرسال رسالة إلى هذا الروبوت أولاً",
|
||||
"disableCloudflaredNoAuthMsg": "أنت في وضع مصادقة لا توجد كلمة مرور غير مطلوبة.",
|
||||
"trustProxyDescription": "ثق في رؤوس \"X-Forwarded- *\". إذا كنت ترغب في الحصول على عنوان IP الصحيح للعميل وكان Uptime Kuma خلف وكيل مثل Nginx أو Apache ، فيجب عليك تمكين هذا.",
|
||||
"wayToGetLineNotifyToken": "يمكنك الحصول على رمز الوصول من {0}",
|
||||
"Examples": "أمثلة",
|
||||
"Home Assistant URL": "Home Assistant URL",
|
||||
"Long-Lived Access Token": "الرمز المميز للوصول منذ فترة طويلة",
|
||||
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "يمكن إنشاء رمز الوصول منذ فترة طويلة عن طريق النقر على اسم ملف التعريف الخاص بك (أسفل اليسار) والتمرير إلى الأسفل ثم انقر فوق إنشاء الرمز المميز. ",
|
||||
"Notification Service": "خدمة الإخطار",
|
||||
"default: notify all devices": "الافتراضي: إخطار جميع الأجهزة",
|
||||
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "يمكن العثور على قائمة بخدمات الإخطار في المساعد المنزلي ضمن \"Developer Tools > Services\" ابحث عن \"notification\" للعثور على اسم جهازك/هاتفك.",
|
||||
"Automations can optionally be triggered in Home Assistant:": "يمكن تشغيل الأتمتة اختياريًا في Home Assistant:",
|
||||
"Trigger type:": "نوع الزناد:",
|
||||
"Event type:": "نوع الحدث:",
|
||||
"Event data:": "بيانات الحدث:",
|
||||
"Then choose an action, for example switch the scene to where an RGB light is red.": "ثم اختر إجراءً على سبيل المثال قم بتبديل المشهد إلى حيث يكون ضوء RGB أحمر.",
|
||||
"Frontend Version": "إصدار الواجهة الأمامية",
|
||||
"Frontend Version do not match backend version!": "إصدار Frontend لا يتطابق مع الإصدار الخلفي!",
|
||||
"backupOutdatedWarning": "مهمل: نظرًا لأنه تمت إضافة الكثير من الميزات وأن ميزة النسخ الاحتياطي هذه لم يتم الحفاظ عليها قليلاً ، فلا يمكنها إنشاء نسخة احتياطية كاملة أو استعادتها.",
|
||||
"backupRecommend": "يرجى النسخ الاحتياطي لحجم الصوت أو مجلد البيانات (./data/) مباشرة بدلاً من ذلك.",
|
||||
"Optional": "اختياري",
|
||||
"or": "أو",
|
||||
"recurringInterval": "فترة",
|
||||
"Recurring": "يتكرر",
|
||||
"strategyManual": "نشط/غير نشط يدويًا",
|
||||
"warningTimezone": "إنه يستخدم المنطقة الزمنية للخادم",
|
||||
"weekdayShortMon": "الاثنين",
|
||||
"weekdayShortTue": "الثلاثاء",
|
||||
"weekdayShortWed": "تزوج",
|
||||
"weekdayShortThu": "الخميس",
|
||||
"weekdayShortFri": "الجمعة",
|
||||
"No Maintenance": "لا صيانة",
|
||||
"weekdayShortSat": "جلس",
|
||||
"weekdayShortSun": "شمس",
|
||||
"dayOfWeek": "يوم من الأسبوع",
|
||||
"dayOfMonth": "يوم من الشهر",
|
||||
"lastDay": "بالأمس",
|
||||
"lastDay1": "آخر يوم من الشهر",
|
||||
"lastDay2": "الثاني في اليوم الأخير من الشهر",
|
||||
"lastDay3": "الثالث في اليوم الأخير من الشهر",
|
||||
"lastDay4": "الرابع في اليوم الأخير من الشهر",
|
||||
"pauseMaintenanceMsg": "هل أنت متأكد من أن تتوقف مؤقتًا؟",
|
||||
"maintenanceStatus-under-maintenance": "تحت الصيانة",
|
||||
"maintenanceStatus-inactive": "غير نشط",
|
||||
"maintenanceStatus-scheduled": "المقرر",
|
||||
"maintenanceStatus-ended": "انتهى",
|
||||
"maintenanceStatus-unknown": "مجهول",
|
||||
"Display Timezone": "عرض المنطقة الزمنية",
|
||||
"Server Timezone": "المنطقة الزمنية الخادم",
|
||||
"statusPageMaintenanceEndDate": "نهاية",
|
||||
"IconUrl": "url url icon",
|
||||
"Enable DNS Cache": "تمكين ذاكرة التخزين المؤقت DNS",
|
||||
"Disable": "إبطال",
|
||||
"dnsCacheDescription": "قد لا يعمل في بعض بيئات IPv6 تعطيله إذا واجهت أي مشكلات.",
|
||||
"Single Maintenance Window": "نافذة صيانة واحدة",
|
||||
"Maintenance Time Window of a Day": "نافذة وقت الصيانة لليوم",
|
||||
"Effective Date Range": "نطاق التاريخ السريع",
|
||||
"Schedule Maintenance": "جدولة الصيانة",
|
||||
"Date and Time": "التاريخ و الوقت",
|
||||
"DateTime Range": "نطاق DateTime",
|
||||
"loadingError": "لا يمكن جلب البيانات ، يرجى المحاولة مرة أخرى في وقت لاحق.",
|
||||
"plugin": "البرنامج المساعد | الإضافات",
|
||||
"install": "ثَبَّتَ",
|
||||
"installing": "التثبيت",
|
||||
"uninstall": "الغاء التثبيت",
|
||||
"uninstalling": "إلغاء التثبيت",
|
||||
"confirmUninstallPlugin": "هل أنت متأكد من أنك تريد إلغاء تثبيت هذا المكون الإضافي؟",
|
||||
"smtp": "البريد الإلكتروني (SMTP)",
|
||||
"secureOptionNone": "لا شيء / startTls (25 587)",
|
||||
"secureOptionTLS": "TLS (465)",
|
||||
"Ignore TLS Error": "تجاهل خطأ TLS",
|
||||
"From Email": "من البريد الإلكترونى",
|
||||
"emailCustomSubject": "موضوع مخصص",
|
||||
"To Email": "للبريد الإلكتروني",
|
||||
"smtpCC": "نسخة",
|
||||
"smtpBCC": "BCC",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
"wayToGetDiscordURL": "يمكنك الحصول على هذا بالانتقال إلى إعدادات الخادم -> عمليات التكامل -> عرض الخطافات على الويب -> خطاف ويب جديد",
|
||||
"Bot Display Name": "اسم عرض الروبوت",
|
||||
"Prefix Custom Message": "بادئة رسالة مخصصة",
|
||||
"Hello @everyone is...": "مرحبًا {'@'} الجميع…",
|
||||
"wayToGetTeamsURL": "يمكنك معرفة كيفية إنشاء عنوان URL webhook {0}.",
|
||||
"wayToGetZohoCliqURL": "يمكنك معرفة كيفية إنشاء عنوان URL webhook {0}.",
|
||||
"needSignalAPI": "تحتاج إلى وجود عميل إشارة مع REST API.",
|
||||
"wayToCheckSignalURL": "يمكنك التحقق من عنوان URL هذا لعرض كيفية إعداد واحد",
|
||||
"Number": "رقم",
|
||||
"Recipients": "المستلمين",
|
||||
"Access Token": "رمز وصول",
|
||||
"Channel access token": "قناة الوصول إلى الرمز",
|
||||
"Line Developers Console": "تحكم المطورين",
|
||||
"lineDevConsoleTo": "وحدة المطورين Line Console - {0}",
|
||||
"Basic Settings": "الإعدادات الأساسية",
|
||||
"confirmClearStatisticsMsg": "هل أنت متأكد من أنك تريد حذف جميع الإحصائيات؟",
|
||||
"importHandleDescription": "اختر 'تخطي موجود' إذا كنت تريد تخطي كل شاشة أو إشعار بنفس الاسم. 'الكتابة فوق' سوف يحذف كل شاشة وإخطار موجود.",
|
||||
"User ID": "معرف المستخدم",
|
||||
"Messaging API": "واجهة برمجة تطبيقات المراسلة",
|
||||
"wayToGetLineChannelToken": "قم أولاً بالوصول إلى {0} إنشاء مزود وقناة (واجهة برمجة تطبيقات المراسلة) ، ثم يمكنك الحصول على رمز الوصول إلى القناة ومعرف المستخدم من عناصر القائمة المذكورة أعلاه.",
|
||||
"Icon URL": "url url icon",
|
||||
"aboutIconURL": "يمكنك توفير رابط لصورة في \"Icon URL\" لتجاوز صورة الملف الشخصي الافتراضي. لن يتم استخدامه إذا تم تعيين رمز رمز رمز.",
|
||||
"aboutMattermostChannelName": "يمكنك تجاوز القناة الافتراضية التي تنشرها WebHook من خلال إدخال اسم القناة في \"Channel Name\" الحقل. يجب تمكين هذا في إعدادات Webhook Mattern. السابق",
|
||||
"dataRetentionTimeError": "يجب أن تكون فترة الاستبقاء 0 أو أكبر",
|
||||
"infiniteRetention": "ضبط على 0 للاحتفاظ لا نهائي.",
|
||||
"confirmDeleteTagMsg": "هل أنت متأكد من أنك تريد حذف هذه العلامة؟ لن يتم حذف الشاشات المرتبطة بهذه العلامة.",
|
||||
"enableGRPCTls": "السماح لإرسال طلب GRPC مع اتصال TLS",
|
||||
"deleteMonitorMsg": "هل أنت متأكد من حذف هذا الشاشة؟",
|
||||
"deleteMaintenanceMsg": "هل أنت متأكد من حذف هذه الصيانة؟",
|
||||
"resolverserverDescription": "CloudFlare هو الخادم الافتراضي. يمكنك تغيير خادم المحوّل في أي وقت.",
|
||||
"rrtypeDescription": "حدد نوع RR الذي تريد مراقبته",
|
||||
"enableDefaultNotificationDescription": "سيتم تمكين هذا الإشعار افتراضيًا للشاشات الجديدة. لا يزال بإمكانك تعطيل الإخطار بشكل منفصل لكل شاشة.",
|
||||
"clearEventsMsg": "هل أنت متأكد من حذف جميع الأحداث لهذا الشاشة؟",
|
||||
"clearHeartbeatsMsg": "هل أنت متأكد من حذف جميع دقات القلب لهذا الشاشة؟",
|
||||
"confirmImportMsg": "هل أنت متأكد من أنك تريد استيراد النسخ الاحتياطي؟ يرجى التحقق من أنك حددت خيار الاستيراد الصحيح.",
|
||||
"twoFAVerifyLabel": "الرجاء إدخال الرمز المميز الخاص بك للتحقق من 2FA",
|
||||
"pushoversounds pushover": "سداد (افتراضي)",
|
||||
"pushoversounds bike": "دراجة هوائية",
|
||||
"pushoversounds bugle": "بوق",
|
||||
"tokenValidSettingsMsg": "الرمز المميز صالح! يمكنك الآن حفظ إعدادات 2FA.",
|
||||
"confirmEnableTwoFAMsg": "هل أنت متأكد من أنك تريد تمكين 2FA؟",
|
||||
"confirmDisableTwoFAMsg": "هل أنت متأكد من أنك تريد تعطيل 2FA؟",
|
||||
"recurringIntervalMessage": "ركض مرة واحدة كل يوم | قم بالتشغيل مرة واحدة كل يوم {0}",
|
||||
"affectedMonitorsDescription": "حدد المراقبين المتأثرة بالصيانة الحالية",
|
||||
"affectedStatusPages": "إظهار رسالة الصيانة هذه على صفحات الحالة المحددة",
|
||||
"atLeastOneMonitor": "حدد شاشة واحدة على الأقل من المتأثرين",
|
||||
"passwordNotMatchMsg": "كلمة المرور المتكررة لا تتطابق.",
|
||||
"notificationDescription": "يجب تعيين الإخطارات إلى شاشة للعمل.",
|
||||
"keywordDescription": "ابحث في الكلمة الرئيسية في استجابة HTML العادية أو JSON. البحث حساس للحالة.",
|
||||
"backupDescription": "يمكنك النسخ الاحتياطي لجميع الشاشات والإشعارات في ملف JSON.",
|
||||
"backupDescription3": "يتم تضمين البيانات الحساسة مثل الرموز الإخطار في ملف التصدير ؛ يرجى تخزين التصدير بشكل آمن.",
|
||||
"endpoint": "نقطة النهاية",
|
||||
"octopushAPIKey": "\"API key\" from HTTP API بيانات اعتماد في لوحة التحكم",
|
||||
"octopushLogin": "\"Login\" من بيانات اعتماد API HTTP في لوحة التحكم",
|
||||
"promosmsLogin": "اسم تسجيل الدخول API",
|
||||
"promosmsPassword": "كلمة مرور API",
|
||||
"pushoversounds cashregister": "ماكينة تسجيل المدفوعات النقدية",
|
||||
"pushoversounds classical": "كلاسيكي",
|
||||
"pushoversounds cosmic": "كونية",
|
||||
"pushoversounds falling": "هبوط",
|
||||
"pushoversounds gamelan": "Gamelan",
|
||||
"pushoversounds incoming": "واردة",
|
||||
"pushoversounds intermission": "استراحة",
|
||||
"pushoversounds magic": "سحر",
|
||||
"pushoversounds mechanical": "ميكانيكي",
|
||||
"pushoversounds pianobar": "شريط البيانو",
|
||||
"pushoversounds siren": "صفارة إنذار",
|
||||
"pushoversounds spacealarm": "إنذار الفضاء",
|
||||
"pushoversounds tugboat": "قارب السحب",
|
||||
"pushoversounds alien": "إنذار أجنبي (طويل)",
|
||||
"pushoversounds climb": "تسلق (طويل)",
|
||||
"pushoversounds persistent": "مستمر (طويل)",
|
||||
"pushoversounds echo": "صدى مهووس (طويل)",
|
||||
"pushoversounds updown": "صعودا (طويلة)",
|
||||
"pushoversounds vibrate": "يهتز فقط",
|
||||
"pushoversounds none": "لا شيء (صامت)",
|
||||
"pushyAPIKey": "مفتاح API السري",
|
||||
"pushyToken": "رمز الجهاز",
|
||||
"apprise": "إبلاغ (دعم 50+ خدمات الإخطار)",
|
||||
"GoogleChat": "دردشة Google",
|
||||
"wayToGetKookBotToken": "قم بإنشاء تطبيق واحصل على رمز الروبوت الخاص بك على {0}",
|
||||
"wayToGetKookGuildID": "قم بتشغيل 'وضع المطور' في إعداد Kook وانقر بزر الماوس الأيمن على النقابة للحصول على معرفه",
|
||||
"Guild ID": "معرف النقابة",
|
||||
"User Key": "مفتاح المستخدم",
|
||||
"Device": "جهاز",
|
||||
"Message Title": "عنوان الرسالة",
|
||||
"Notification Sound": "صوت الإشعار",
|
||||
"More info on:": "مزيد من المعلومات حول: {0}",
|
||||
"pushoverDesc1": "أولوية الطوارئ (2) لها مهلة افتراضية 30 ثانية بين إعادة المحاولة وستنتهي صلاحيتها بعد ساعة واحدة.",
|
||||
"pushoverDesc2": "إذا كنت ترغب في إرسال إشعارات إلى أجهزة مختلفة ، قم بملء حقل الجهاز.",
|
||||
"SMS Type": "نوع الرسائل القصيرة",
|
||||
"octopushTypePremium": "قسط (سريع - موصى به للتنبيه)",
|
||||
"octopushTypeLowCost": "التكلفة المنخفضة (بطيئة - تم حظرها أحيانًا بواسطة المشغل)",
|
||||
"checkPrice": "تحقق من الأسعار {0}",
|
||||
"apiCredentials": "بيانات اعتماد API",
|
||||
"octopushLegacyHint": "هل تستخدم الإصدار القديم من Octopush (2011-2020) أو الإصدار الجديد؟",
|
||||
"Check octopush prices": "تحقق من أسعار Octopush {0}.",
|
||||
"AccessKeyId": "معرف AccessKey",
|
||||
"SecretAccessKey": "Accesskey Secret",
|
||||
"PhoneNumbers": "أرقام الهواتف",
|
||||
"octopushPhoneNumber": "رقم الهاتف (تنسيق intl على سبيل المثال ",
|
||||
"octopushSMSSender": "اسم مرسل الرسائل القصيرة",
|
||||
"LunaSea Device ID": "معرف جهاز Lunasea",
|
||||
"Apprise URL": "إبلاغ عنوان URL",
|
||||
"Example:": "مثال: {0}",
|
||||
"Read more:": "{0} :قراءة المزيد",
|
||||
"Status:": "{0} :حالة",
|
||||
"Strategy": "إستراتيجية",
|
||||
"Free Mobile User Identifier": "معرف مستخدم الهاتف المحمول المجاني",
|
||||
"Free Mobile API Key": "مفتاح واجهة برمجة تطبيقات مجانية للهاتف المحمول",
|
||||
"Enable TLS": "تمكين TLS",
|
||||
"Proto Service Name": "اسم خدمة البروتو",
|
||||
"Proto Method": "طريقة البروتو",
|
||||
"Proto Content": "محتوى proto",
|
||||
"Economy": "اقتصاد",
|
||||
"Lowcost": "تكلفة منخفضة",
|
||||
"high": "عالي",
|
||||
"SendKey": "Sendkey",
|
||||
"SMSManager API Docs": "مستندات SMSManager API ",
|
||||
"Gateway Type": "نوع البوابة",
|
||||
"You can divide numbers with": "يمكنك تقسيم الأرقام مع",
|
||||
"Base URL": "عنوان URL الأساسي",
|
||||
"goAlertInfo": "الهدف هو تطبيق مفتوح المصدر لجدولة الجدولة التلقائية والإشعارات (مثل الرسائل القصيرة أو المكالمات الصوتية). إشراك الشخص المناسب تلقائيًا بالطريقة الصحيحة وفي الوقت المناسب! {0}",
|
||||
"goAlertIntegrationKeyInfo": "احصل على مفتاح تكامل API العام للخدمة في هذا التنسيق \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" عادةً قيمة المعلمة الرمزية لعنوان url المنسق.",
|
||||
"TemplateCode": "TemplateCode",
|
||||
"SignName": "اسم تسجيل الدخول",
|
||||
"Sms template must contain parameters: ": "يجب أن يحتوي قالب الرسائل القصيرة على معلمات: ",
|
||||
"Bark Endpoint": "نقطة نهاية اللحاء",
|
||||
"Bark Group": "مجموعة اللحاء",
|
||||
"Bark Sound": "صوت اللحاء",
|
||||
"WebHookUrl": "webhookurl",
|
||||
"SecretKey": "Secretkey",
|
||||
"For safety, must use secret key": "للسلامة يجب استخدام المفتاح السري",
|
||||
"Device Token": "رمز الجهاز",
|
||||
"Platform": "منصة",
|
||||
"Android": "ذكري المظهر",
|
||||
"Huawei": "هواوي",
|
||||
"High": "عالٍ",
|
||||
"Retry": "إعادة المحاولة",
|
||||
"Topic": "عنوان",
|
||||
"WeCom Bot Key": "WECOM BOT KEY",
|
||||
"Setup Proxy": "وكيل الإعداد",
|
||||
"Proxy Protocol": "بروتوكول الوكيل",
|
||||
"Proxy Server": "مخدم بروكسي",
|
||||
"Proxy server has authentication": "خادم الوكيل لديه مصادقة",
|
||||
"promosmsTypeEco": "SMS Eco - رخيصة ولكن بطيئة وغالبًا ما تكون محملة. يقتصر فقط على المستفيدين البولنديين.",
|
||||
"promosmsTypeFlash": "SMS Flash - سيتم عرض الرسالة تلقائيًا على جهاز المستلم. يقتصر فقط على المستفيدين البولنديين.",
|
||||
"promosmsTypeFull": "SMS Full - Tier Premium SMS يمكنك استخدام اسم المرسل الخاص بك (تحتاج إلى تسجيل الاسم أولاً). موثوقة للتنبيهات.",
|
||||
"promosmsTypeSpeed": "سرعة الرسائل القصيرة - أولوية قصوى في النظام. سريع وموثوق للغاية ولكنه مكلف (حوالي مرتين من الرسائل القصيرة السعر الكامل).",
|
||||
"promosmsPhoneNumber": "رقم الهاتف (للمستلم البولندي ، يمكنك تخطي رموز المنطقة)",
|
||||
"matrixDesc2": "يوصى بشدة بإنشاء مستخدم جديد ولا تستخدم رمز الوصول إلى مستخدم Matrix الخاص بك لأنه سيتيح الوصول الكامل إلى حسابك وجميع الغرف التي انضمت إليها. بدلاً من ذلك ، قم بإنشاء مستخدم جديد ودعوته فقط إلى الغرفة التي تريد تلقيها الإشعار فيها. يمكنك الحصول على رمز الوصول عن طريق تشغيل {0}",
|
||||
"Channel Name": "اسم القناة",
|
||||
"promosmsSMSSender": "اسم مرسل الرسائل القصيرة",
|
||||
"promosmsAllowLongSMS": "السماح الرسائل القصيرة الطويلة",
|
||||
"Feishu WebHookUrl": "Feishu Webhookurl",
|
||||
"matrixHomeserverURL": "عنوان URL HomeServer (مع HTTP (S)",
|
||||
"Internal Room Id": "معرف الغرفة الداخلية",
|
||||
"matrixDesc1": "يمكنك العثور على معرف الغرفة الداخلي من خلال البحث في القسم المتقدم من إعدادات الغرفة في عميل Matrix الخاص بك. يجب أن تبدو مثل! QMDRCPUIFLWSFJXYE6",
|
||||
"Uptime Kuma URL": "UPTIME KUMA URL",
|
||||
"Icon Emoji": "أيقونة الرموز التعبيرية",
|
||||
"signalImportant": "مهم",
|
||||
"aboutWebhooks": "مزيد من المعلومات حول Webhooks ON",
|
||||
"aboutChannelName": "أدخل اسم القناة في حقل اسم القناة {0} إذا كنت تريد تجاوز قناة WebHook. السابق",
|
||||
"aboutKumaURL": "إذا تركت حقل URL في وقت التشغيل KUMA فارغًا ، فسيتم افتراضيًا إلى صفحة GitHub Project.",
|
||||
"smtpDkimSettings": "إعدادات DKIM",
|
||||
"smtpDkimDesc": "يرجى الرجوع إلى Nodemailer dkim {0} للاستخدام.",
|
||||
"documentation": "توثيق",
|
||||
"smtpDkimDomain": "اسم النطاق",
|
||||
"smtpDkimKeySelector": "المحدد الرئيسي",
|
||||
"smtpDkimPrivateKey": "مفتاح سري",
|
||||
"smtpDkimHashAlgo": "خوارزمية التجزئة (اختياري)",
|
||||
"smtpDkimheaderFieldNames": "مفاتيح الرأس للتوقيع (اختياري)",
|
||||
"smtpDkimskipFields": "مفاتيح الرأس لا توقيع (اختياري)",
|
||||
"wayToGetPagerDutyKey": "يمكنك الحصول على هذا عن طريق الانتقال إلى الخدمة -> دليل الخدمة -> (حدد خدمة) -> تكامل -> إضافة التكامل. هنا يمكنك البحث عن \"Events API V2\". مزيد من المعلومات {0}",
|
||||
"Integration Key": "مفتاح التكامل",
|
||||
"Integration URL": "URL تكامل",
|
||||
"do nothing": "لا تفعل شيئا",
|
||||
"alertaApiEndpoint": "نقطة نهاية API",
|
||||
"alertaEnvironment": "بيئة",
|
||||
"alertaApiKey": "مفتاح API",
|
||||
"alertaAlertState": "حالة التنبيه",
|
||||
"alertaRecoverState": "استعادة الدولة",
|
||||
"auto acknowledged": "",
|
||||
"auto resolve": "",
|
||||
"serwersmsAPIUser": "اسم مستخدم API (بما في ذلك بادئة WebAPI_)",
|
||||
"serwersmsAPIPassword": "كلمة مرور API",
|
||||
"serwersmsPhoneNumber": "رقم الهاتف",
|
||||
"serwersmsSenderName": "اسم مرسل الرسائل القصيرة (مسجل عبر بوابة العملاء)",
|
||||
"smseagleTo": "أرقام الهواتف)",
|
||||
"smseagleGroup": "اسم مجموعة كتب الهاتف (S)",
|
||||
"smseagleContact": "كتاب الاتصال اسم (S)",
|
||||
"smseagleRecipientType": "نوع المستلم",
|
||||
"smseagleRecipient": "المتلقي (المتلقيين) (يجب فصل المتعددة مع فاصلة)",
|
||||
"smseagleToken": "API وصول الرمز المميز",
|
||||
"smseagleUrl": "عنوان URL لجهاز SMSEGLE الخاص بك",
|
||||
"smseagleEncoding": "إرسال Unicode",
|
||||
"smseaglePriority": "أولوية الرسالة (0-9 افتراضي = 0)",
|
||||
"Recipient Number": "رقم المستلم",
|
||||
"From Name/Number": "من الاسم/الرقم",
|
||||
"Leave blank to use a shared sender number.": "اترك فارغًا لاستخدام رقم المرسل المشترك.",
|
||||
"Octopush API Version": "إصدار Octopush API",
|
||||
"Legacy Octopush-DM": "Legacy Octopush-DM",
|
||||
"ntfy Topic": "موضوع ntfy",
|
||||
"onebotHttpAddress": "OneBot HTTP عنوان",
|
||||
"onebotMessageType": "نوع رسالة OneBot",
|
||||
"onebotGroupMessage": "مجموعة",
|
||||
"onebotPrivateMessage": "خاص",
|
||||
"onebotUserOrGroupId": "معرف المجموعة/المستخدم",
|
||||
"onebotSafetyTips": "للسلامة يجب ضبط الرمز المميز للوصول",
|
||||
"PushDeer Key": "مفتاح PushDeer",
|
||||
"wayToGetClickSendSMSToken": "يمكنك الحصول على اسم مستخدم API ومفتاح API من {0}.",
|
||||
"Custom Monitor Type": "نوع الشاشة المخصص",
|
||||
"Google Analytics ID": "معرف Google Analytics",
|
||||
"Edit Tag": "تحرير العلامة",
|
||||
"Server Address": "عنوان المستقبل",
|
||||
"Learn More": "يتعلم أكثر",
|
||||
"apiKeyAddedMsg": "تمت إضافة مفتاح API خاص بك. يرجى تدوين ذلك لأنه لن يتم عرضه مرة أخرى.",
|
||||
"No API Keys": "لا توجد مفاتيح API",
|
||||
"apiKey-inactive": "غير نشط",
|
||||
"disableAPIKeyMsg": "هل أنت متأكد أنك تريد تعطيل مفتاح API هذا؟",
|
||||
"deleteAPIKeyMsg": "هل أنت متأكد أنك تريد حذف مفتاح API هذا؟",
|
||||
"Auto Get": "الحصول التلقائي",
|
||||
"Auto resolve or acknowledged": "",
|
||||
"backupDescription2": "ملحوظة",
|
||||
"languageName": "العربية",
|
||||
"Game": "الألعاب",
|
||||
"List": "قائمة",
|
||||
"statusMaintenance": "الصيانة"
|
||||
}
|
@@ -227,7 +227,7 @@
|
||||
"smtpCC": "Явно копие до имейл адрес:",
|
||||
"smtpBCC": "Скрито копие до имейл адрес:",
|
||||
"Discord Webhook URL": "Discord URL адрес на уеб кука",
|
||||
"wayToGetDiscordURL": "Може да създадете, от меню \"Настройки на сървъра\" -> \"Интеграции\" -> \"Уеб куки\" -> \"Нова уеб кука\"",
|
||||
"wayToGetDiscordURL": "Можете да създадете, от меню \"Настройки на сървъра\" -> \"Интеграции\" -> \"Уеб куки\" -> \"Нова уеб кука\"",
|
||||
"Bot Display Name": "Име на бота, което да се показва",
|
||||
"Prefix Custom Message": "Модифицирано обръщение",
|
||||
"Hello @everyone is...": "Здравейте, {'@'}everyone е…",
|
||||
@@ -236,8 +236,8 @@
|
||||
"Number": "Номер",
|
||||
"Recipients": "Получатели",
|
||||
"needSignalAPI": "Необходимо е да разполагате със Signal клиент с REST API.",
|
||||
"wayToCheckSignalURL": "Може да посетите този URL адрес, ако се нуждаете от помощ при настройването:",
|
||||
"signalImportant": "ВАЖНО: Не може да смесвате \"Групи\" и \"Номера\" в поле \"Получатели\"!",
|
||||
"wayToCheckSignalURL": "Можете да посетите този URL адрес, ако се нуждаете от помощ при настройването:",
|
||||
"signalImportant": "ВАЖНО: Не можете да смесвате \"Групи\" и \"Номера\" в поле \"Получатели\"!",
|
||||
"Application Token": "Токен код за приложението",
|
||||
"Server URL": "URL адрес на сървъра",
|
||||
"Priority": "Приоритет",
|
||||
@@ -278,21 +278,21 @@
|
||||
"Basic Settings": "Основни настройки",
|
||||
"User ID": "Потребител ID",
|
||||
"Messaging API": "API за съобщаване",
|
||||
"wayToGetLineChannelToken": "Необходимо е първо да посетите {0}, за да създадете (Messaging API) за доставчик и канал, след което може да вземете токен кода за канал и потребителско ID от споменатите по-горе елементи на менюто.",
|
||||
"wayToGetLineChannelToken": "Необходимо е първо да посетите {0}, за да създадете (Messaging API) за доставчик и канал, след което можете да вземете токен кода за канал и потребителско ID от споменатите по-горе елементи на менюто.",
|
||||
"Icon URL": "URL адрес за иконка",
|
||||
"aboutIconURL": "Може да предоставите линк към картинка в поле \"URL Адрес за иконка\" за да отмените картинката на профила по подразбиране. Няма да се използва, ако вече сте настроили емотикон.",
|
||||
"aboutMattermostChannelName": "Може да замените канала по подразбиране, към който публикува уеб куката, като въведете името на канала в полето \"Канал име\". Трябва да бъде активирано в настройките за уеб кука на Mattermost. Например: #other-channel",
|
||||
"aboutIconURL": "Можете да предоставите линк към картинка в поле \"URL Адрес за иконка\" за да отмените картинката на профила по подразбиране. Няма да се използва, ако вече сте настроили емотикон.",
|
||||
"aboutMattermostChannelName": "Можете да замените канала по подразбиране, към който публикува уеб куката, като въведете името на канала в полето \"Канал име\". Трябва да бъде активирано в настройките за уеб кука на Mattermost. Например: #other-channel",
|
||||
"matrix": "Matrix",
|
||||
"promosmsTypeEco": "SMS ECO - евтин, но бавен. Често е претоварен. Само за получатели от Полша.",
|
||||
"promosmsTypeFlash": "SMS FLASH - Съобщението автоматично се показва на устройството на получателя. Само за получатели от Полша.",
|
||||
"promosmsTypeFull": "SMS FULL - Високо ниво на SMS услуга. Може да използвате Вашето име като подател (Необходимо е първо да регистрирате името). Надежден метод за съобщения тип тревога.",
|
||||
"promosmsTypeFull": "SMS FULL - Високо ниво на SMS услуга. Можете да използвате Вашето име като подател (Необходимо е първо да регистрирате името). Надежден метод за съобщения тип тревога.",
|
||||
"promosmsTypeSpeed": "SMS SPEED - Най-висок приоритет в системата. Много бърза и надеждна, но същевременно скъпа услуга. (Около два пъти по-висока цена в сравнение с SMS FULL).",
|
||||
"promosmsPhoneNumber": "Телефонен номер (за получатели от Полша, може да пропуснете въвеждането на код за населено място)",
|
||||
"promosmsPhoneNumber": "Телефонен номер (за получатели от Полша, можете да пропуснете въвеждането на код за населено място)",
|
||||
"promosmsSMSSender": "SMS Подател име: Предварително регистрирано име или някое от имената по подразбиране: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
|
||||
"Feishu WebHookUrl": "Feishu URL адрес за уеб кука",
|
||||
"matrixHomeserverURL": "Сървър URL адрес (започва с http(s):// и порт по желание)",
|
||||
"Internal Room Id": "ID на вътрешна стая",
|
||||
"matrixDesc1": "Може да намерите \"ID на вътрешна стая\" в разширените настройки на стаята във вашия Matrix клиент. Примерен изглед: !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
"matrixDesc1": "Можете да намерите \"ID на вътрешна стая\" в разширените настройки на стаята във вашия Matrix клиент. Примерен изглед: !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
"matrixDesc2": "Силно препоръчваме да създадете НОВ потребител и да НЕ използвате токен кодът на вашия личен Matrix потребител, т.к. той позволява пълен достъп до вашия акаунт и всички стаи към които сте се присъединили. Вместо това създайте нов потребител и го поканете само в стаята, където желаете да получавате известията. Токен код за достъп ще получите изпълнявайки {0}",
|
||||
"Method": "Метод",
|
||||
"Body": "Съобщение",
|
||||
@@ -304,7 +304,7 @@
|
||||
"clearDataOlderThan": "Ще се съхранява за {0} дни.",
|
||||
"records": "записа",
|
||||
"One record": "Един запис",
|
||||
"steamApiKeyDescription": "За да мониторирате Steam Gameserver се нуждаете от Steam Web-API ключ. Може да регистрирате Вашия API ключ тук: ",
|
||||
"steamApiKeyDescription": "За да мониторирате Steam Game Server се нуждаете от Steam Web-API ключ. Можете да регистрирате Вашия API ключ тук: ",
|
||||
"clicksendsms": "ClickSend SMS",
|
||||
"apiCredentials": "API удостоверяване",
|
||||
"PasswordsDoNotMatch": "Паролите не съвпадат.",
|
||||
@@ -379,8 +379,8 @@
|
||||
"setAsDefault": "Зададен по подразбиране",
|
||||
"deleteProxyMsg": "Сигурни ли сте, че желаете да изтриете това прокси за всички монитори?",
|
||||
"proxyDescription": "За да функционират трябва да бъдат зададени към монитор.",
|
||||
"enableProxyDescription": "Това прокси няма да има ефект върху заявките за мониторинг, докато не бъде активирано. Може да контролирате временното деактивиране на проксито от всички монитори чрез статуса на активиране.",
|
||||
"setAsDefaultProxyDescription": "Това прокси ще бъде активно по подразбиране за новите монитори. Може да го изключите по отделно за всеки един монитор.",
|
||||
"enableProxyDescription": "Това прокси няма да има ефект върху заявките за мониторинг, докато не бъде активирано. Можете да контролирате временното деактивиране на проксито от всички монитори чрез статуса на активиране.",
|
||||
"setAsDefaultProxyDescription": "Това прокси ще бъде активно по подразбиране за новите монитори. Можете да го изключвате отделно за всеки един монитор.",
|
||||
"Certificate Chain": "Верига на сертификата",
|
||||
"Valid": "Валиден",
|
||||
"Invalid": "Невалиден",
|
||||
@@ -435,7 +435,7 @@
|
||||
"cloudflareWebsite": "Cloudflare уеб сайт",
|
||||
"Message:": "Съобщение:",
|
||||
"Don't know how to get the token? Please read the guide:": "Не знаете как да вземете токен? Моля, прочетете ръководството:",
|
||||
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Текущата връзка може да прекъсне ако в момента сте свързани чрез \"Cloudflare Tunnel\". Сигурни ли сте, че желаете да го спрете? Въведете Вашата текуща парола за да потвърдите.",
|
||||
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Текущата връзка може да прекъсне ако в момента сте свързани чрез \"Cloudflare Tunnel\". Сигурни ли сте, че желаете да го спрете? Моля, въведете Вашата текуща парола за да потвърдите.",
|
||||
"Other Software": "Друг софтуер",
|
||||
"For example: nginx, Apache and Traefik.": "Например: Nginx, Apache и Traefik.",
|
||||
"Please read": "Моля, прочетете",
|
||||
@@ -513,15 +513,15 @@
|
||||
"Most likely causes:": "Най-вероятни причини:",
|
||||
"The resource is no longer available.": "Ресурсът вече не е наличен.",
|
||||
"There might be a typing error in the address.": "Възможно е да е допусната грешка при изписването на адреса.",
|
||||
"What you can try:": "Може да опитате:",
|
||||
"What you can try:": "Какво можете да опитате:",
|
||||
"Retype the address.": "Повторно въвеждане на адреса.",
|
||||
"Go back to the previous page.": "Да се върнете към предишната страница.",
|
||||
"Coming Soon": "Очаквайте скоро",
|
||||
"wayToGetClickSendSMSToken": "Може да получите API потребителско име и API ключ от {0} .",
|
||||
"dnsPortDescription": "DNS порт на сървъра. По подразбиране е 53, но може да бъде променен по всяко време.",
|
||||
"wayToGetClickSendSMSToken": "Можете да получите API потребителско име и API ключ от {0} .",
|
||||
"dnsPortDescription": "DNS порт на сървъра. По подразбиране е 53. Можете да го промените по всяко време.",
|
||||
"error": "грешка",
|
||||
"critical": "критично",
|
||||
"wayToGetPagerDutyKey": "Може да го получите като посетите Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Тук трябва да потърсите \"Events API V2\". Повече информация {0}",
|
||||
"wayToGetPagerDutyKey": "Можете да го получите като посетите Service -> Service Directory -> (Select a service) -> Integrations -> Add integration. Тук трябва да потърсите \"Events API V2\". Повече информация {0}",
|
||||
"Integration Key": "Ключ за интегриране",
|
||||
"Integration URL": "URL адрес за интеграция",
|
||||
"Auto resolve or acknowledged": "Автоматично разрешаване или потвърждаване",
|
||||
@@ -536,10 +536,10 @@
|
||||
"Domain": "Домейн",
|
||||
"Workstation": "Работна станция",
|
||||
"disableCloudflaredNoAuthMsg": "Тъй като сте в режим \"No Auth mode\", парола не се изисква.",
|
||||
"wayToGetLineNotifyToken": "Може да получите токен код за достъп от {0}",
|
||||
"wayToGetLineNotifyToken": "Можете да получите токен код за достъп от {0}",
|
||||
"resendEveryXTimes": "Изпращай повторно на всеки {0} пъти",
|
||||
"resendDisabled": "Повторното изпращане е изключено",
|
||||
"Resend Notification if Down X times consequently": "Повторно изпращане на известие, ако е недостъпен X пъти последователно",
|
||||
"Resend Notification if Down X times consecutively": "Повторно изпращане на известие, ако е недостъпен X пъти последователно",
|
||||
"Bark Group": "Bark група",
|
||||
"Bark Sound": "Bark звук",
|
||||
"HTTP Headers": "HTTP хедъри",
|
||||
@@ -601,7 +601,7 @@
|
||||
"SMSManager API Docs": "SMSManager API Документация ",
|
||||
"Gateway Type": "Тип на шлюза",
|
||||
"SMSManager": "SMSManager",
|
||||
"You can divide numbers with": "Може да разделяте числата с",
|
||||
"You can divide numbers with": "Можете да разделяте числата с",
|
||||
"or": "или",
|
||||
"recurringInterval": "Интервал",
|
||||
"Recurring": "Повтаряне",
|
||||
@@ -676,12 +676,12 @@
|
||||
"wayToGetKookGuildID": "Превключете в 'Developer Mode' в 'Kook' настройките, след което десен клик върху 'guild' за да вземете неговото 'ID'",
|
||||
"Guild ID": "Guild ID",
|
||||
"Help": "Помощ",
|
||||
"Game": "игрови",
|
||||
"Game": "Игра",
|
||||
"Custom": "Потребителски",
|
||||
"infiniteRetention": "Задайте стойност 0 за безкрайно съхранение.",
|
||||
"Monitor": "Монитор | Монитори",
|
||||
"dataRetentionTimeError": "Периодът на съхранение трябва да е 0 или по-голям",
|
||||
"confirmDeleteTagMsg": "Сигурни ли сте, че желаете да изтриете този таг? Мониторите, свързани с него, няма да бъдат изтрити.",
|
||||
"confirmDeleteTagMsg": "Сигурни ли сте, че желаете да изтриете този етикет? Мониторите, свързани с него, няма да бъдат изтрити.",
|
||||
"promosmsAllowLongSMS": "Позволи дълъг SMS",
|
||||
"Packet Size": "Размер на пакет",
|
||||
"Custom Monitor Type": "Потребителски тип монитор",
|
||||
@@ -691,5 +691,52 @@
|
||||
"installing": "Инсталиране",
|
||||
"uninstall": "Деинсталирай",
|
||||
"uninstalling": "Деинсталиране",
|
||||
"confirmUninstallPlugin": "Сигурни ли сте, че желаете да деинсталирате този плъгин?"
|
||||
"confirmUninstallPlugin": "Сигурни ли сте, че желаете да деинсталирате този плъгин?",
|
||||
"markdownSupported": "Поддържа се Markdown синтаксис",
|
||||
"Google Analytics ID": "Google Analytics ID",
|
||||
"Edit Tag": "Редактиране на етикет",
|
||||
"Learn More": "Научете повече",
|
||||
"Server Address": "Сървър адрес",
|
||||
"notificationRegional": "Регионални",
|
||||
"Body Encoding": "Кодировка на тялото",
|
||||
"telegramMessageThreadID": "(По избор) Thread ID на съобщението",
|
||||
"telegramMessageThreadIDDescription": "Незадължителен уникален идентификатор за целевата нишка от съобщения (тема) на форума; само за форумни супергрупи",
|
||||
"telegramProtectContent": "Защита на препращане/записване",
|
||||
"telegramProtectContentDescription": "Ако е активирано, съобщенията от ботове в Telegram ще бъдат защитени от препращане и запазване.",
|
||||
"telegramSendSilentlyDescription": "Изпраща съобщението тихо. Потребителите ще получат известие без звук.",
|
||||
"telegramSendSilently": "Изпрати тихо",
|
||||
"Clone Monitor": "Клониране на монитор",
|
||||
"Clone": "Клонирай",
|
||||
"cloneOf": "Клонинг на {0}",
|
||||
"Expiry": "Валиден до",
|
||||
"Expiry date": "Дата на изтичане",
|
||||
"Add Another": "Добави друг",
|
||||
"Key Added": "Ключът е добавен",
|
||||
"Add API Key": "Добави API ключ",
|
||||
"No API Keys": "Няма API ключове",
|
||||
"apiKey-active": "Активен",
|
||||
"Expires": "Изтича на",
|
||||
"deleteAPIKeyMsg": "Сигурни ли сте, че желаете да изтриете този API ключ?",
|
||||
"Generate": "Генерирай",
|
||||
"API Keys": "API Ключове",
|
||||
"Don't expire": "Не изтича",
|
||||
"Continue": "Продължи",
|
||||
"apiKeyAddedMsg": "Вашият API ключ е добавен. Моля, запишете го, тъй като той няма да бъде показан отново.",
|
||||
"apiKey-expired": "Изтекъл",
|
||||
"apiKey-inactive": "Неактивен",
|
||||
"disableAPIKeyMsg": "Сигурни ли сте, че желаете да деактивирате този API ключ?",
|
||||
"pagertreeUrgency": "Спешност",
|
||||
"pagertreeSilent": "Тих",
|
||||
"pagertreeLow": "Ниска",
|
||||
"pagertreeHigh": "Висока",
|
||||
"pagertreeResolve": "Автоматично разрешаване",
|
||||
"pagertreeDoNothing": "Не прави нищо",
|
||||
"wayToGetPagerTreeIntegrationURL": "След като създадете интеграция на Uptime Kuma в PagerTree, копирайте крайната точка. За пълни подробности вижте {0}",
|
||||
"pagertreeIntegrationUrl": "URL Адрес за интеграция",
|
||||
"pagertreeMedium": "Средна",
|
||||
"pagertreeCritical": "Критична",
|
||||
"Add New Tag": "Добави нов етикет",
|
||||
"lunaseaTarget": "Цел",
|
||||
"lunaseaDeviceID": "ID на устройството",
|
||||
"lunaseaUserID": "ID на потребител"
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@
|
||||
"grpcMethodDescription": "Název metody se převede do cammelCase formátu jako je sayHello, check, aj.",
|
||||
"acceptedStatusCodesDescription": "Vyberte stavové kódy, které jsou považovány za úspěšnou odpověď.",
|
||||
"Maintenance": "Údržba",
|
||||
"statusMaintenance": "Údržba",
|
||||
"statusMaintenance": "V údržbě",
|
||||
"Schedule maintenance": "Naplánovat údržbu",
|
||||
"Affected Monitors": "Dotčené dohledy",
|
||||
"Pick Affected Monitors...": "Vyberte dotčené dohledy…",
|
||||
@@ -24,9 +24,9 @@
|
||||
"affectedStatusPages": "Zobrazit tuto zprávu o údržbě na vybraných stavových stránkách",
|
||||
"atLeastOneMonitor": "Vyberte alespoň jeden dotčený dohled",
|
||||
"passwordNotMatchMsg": "Hesla se neshodují.",
|
||||
"notificationDescription": "Pro zajištění funkčnosti oznámení je nutné jej přiřadit dohledu.",
|
||||
"notificationDescription": "Aby byla upozornění fungovalo, je nutné ho přiřadit k dohledu.",
|
||||
"keywordDescription": "Vyhledat klíčové slovo v prosté odpovědi HTML nebo JSON. Při hledání se rozlišuje velikost písmen.",
|
||||
"pauseDashboardHome": "Pozastaveno",
|
||||
"pauseDashboardHome": "Pauza",
|
||||
"deleteMonitorMsg": "Opravdu chcete odstranit tento dohled?",
|
||||
"deleteMaintenanceMsg": "Opravdu chcete odstranit tuto údržbu?",
|
||||
"deleteNotificationMsg": "Opravdu chcete odstranit toto oznámení pro všechny dohledy?",
|
||||
@@ -59,7 +59,7 @@
|
||||
"Add New Monitor": "Přidat nový dohled",
|
||||
"Quick Stats": "Rychlý přehled",
|
||||
"Up": "Běží",
|
||||
"Down": "Nedostupný",
|
||||
"Down": "Nedostupné",
|
||||
"Pending": "Čekám",
|
||||
"Unknown": "Neznámý",
|
||||
"Pause": "Pauza",
|
||||
@@ -74,7 +74,7 @@
|
||||
"Current": "Aktuální",
|
||||
"Uptime": "Doba provozu",
|
||||
"Cert Exp.": "Platnost certifikátu",
|
||||
"Monitor": "Dohled | Dohledy",
|
||||
"Monitor": "Dohled | Dohledů",
|
||||
"day": "den | dny/í",
|
||||
"-day": "-dní",
|
||||
"hour": "hodina",
|
||||
@@ -90,7 +90,7 @@
|
||||
"Heartbeat Interval": "Heartbeat interval",
|
||||
"Retries": "Počet pokusů",
|
||||
"Heartbeat Retry Interval": "Interval opakování heartbeatu",
|
||||
"Resend Notification if Down X times consequently": "Znovu zaslat oznámení, pokud je služba nedostupná Xkrát za sebou",
|
||||
"Resend Notification if Down X times consecutively": "Zaslat oznámení znovu, pokud je služba nedostupná Xkrát za sebou",
|
||||
"Advanced": "Rozšířené",
|
||||
"Upside Down Mode": "Inverzní režim",
|
||||
"Max. Redirects": "Max. přesměrování",
|
||||
@@ -212,7 +212,7 @@
|
||||
"Required": "Vyžadováno",
|
||||
"telegram": "Telegram",
|
||||
"ZohoCliq": "ZohoCliq",
|
||||
"Bot Token": "Token robota",
|
||||
"Bot Token": "Token bota",
|
||||
"wayToGetTelegramToken": "Token můžete získat od {0}.",
|
||||
"Chat ID": "ID chatu",
|
||||
"supportTelegramChatID": "Podpora přímého chatu / skupiny / ID chatu kanálu",
|
||||
@@ -237,7 +237,7 @@
|
||||
"smtpBCC": "Skrytá kopie",
|
||||
"discord": "Discord",
|
||||
"Discord Webhook URL": "URL Webhooku Discord",
|
||||
"wayToGetDiscordURL": "Získáte tak, že přejdete do Nastavení serveru - > Integrace - > Vytvořit Webhook",
|
||||
"wayToGetDiscordURL": "Získáte tak, že přejdete do Nastavení serveru - > Integrace - > Zobrazi webhooky -> Nový webhook",
|
||||
"Bot Display Name": "Zobrazované jméno robota",
|
||||
"Prefix Custom Message": "Předpona vlastní zprávy",
|
||||
"Hello @everyone is...": "Dobrý den, {'@'}všichni jsou…",
|
||||
@@ -332,7 +332,7 @@
|
||||
"Body": "Tělo",
|
||||
"Headers": "Hlavičky",
|
||||
"PushUrl": "Push URL",
|
||||
"HeadersInvalidFormat": "Hlaviča žádosti není platný JSON: ",
|
||||
"HeadersInvalidFormat": "Hlavičky žádosti nejsou platný JSON: ",
|
||||
"BodyInvalidFormat": "Text žádosti není platný JSON: ",
|
||||
"Monitor History": "Historie dohledu",
|
||||
"clearDataOlderThan": "Historie dohledu bude uchovávána po dobu {0} dní.",
|
||||
@@ -581,7 +581,7 @@
|
||||
"Connection String": "Připojovací řetězec",
|
||||
"Query": "Dotaz",
|
||||
"settingsCertificateExpiry": "Platnost TLS certifikátu",
|
||||
"certificationExpiryDescription": "Aktivovat oznámení nad HTTPS dohledy, pokud platnost TLS certifikátu vyprší za:",
|
||||
"certificationExpiryDescription": "HTTPS dohledy upozorní na vypršení platnosti certifikátu TLS nastavenou dobu dopředu:",
|
||||
"Setup Docker Host": "Nastavit Docker hostitele",
|
||||
"Connection Type": "Typ připojení",
|
||||
"Docker Daemon": "Démon Dockeru",
|
||||
@@ -620,8 +620,8 @@
|
||||
"backupRecommend": "Prosím, zálohujte si ručně celý svazek nebo datovou složku (./data/).",
|
||||
"Optional": "Volitelný",
|
||||
"squadcast": "Squadcast",
|
||||
"SendKey": "SendKey",
|
||||
"SMSManager API Docs": "Dokumentace SMSManager API ",
|
||||
"SendKey": "Klíč k odesílání",
|
||||
"SMSManager API Docs": "Dokumentace API služby SMSManager ",
|
||||
"Gateway Type": "Typ brány",
|
||||
"SMSManager": "SMSManager",
|
||||
"You can divide numbers with": "Čísla můžete oddělit pomocí",
|
||||
@@ -674,7 +674,7 @@
|
||||
"Proto Content": "Proto obsah",
|
||||
"Economy": "Úsporná",
|
||||
"Lowcost": "Nízkonákladová",
|
||||
"high": "high",
|
||||
"high": "vysoká",
|
||||
"General Monitor Type": "Obecný typ dohledu",
|
||||
"Passive Monitor Type": "Pasivní typ dohledu",
|
||||
"Specific Monitor Type": "Konkrétní typ dohledu",
|
||||
@@ -691,5 +691,53 @@
|
||||
"installing": "Instaluji",
|
||||
"uninstall": "Odinstalace",
|
||||
"uninstalling": "Odinstalování",
|
||||
"Packet Size": "Velikost paketu"
|
||||
"Packet Size": "Velikost paketu",
|
||||
"markdownSupported": "Markdown syntaxe podporována",
|
||||
"Google Analytics ID": "ID Google Analytics",
|
||||
"Edit Tag": "Upravit štítek",
|
||||
"Server Address": "Adresa serveru",
|
||||
"Learn More": "Zjistěte více",
|
||||
"notificationRegional": "Místní",
|
||||
"telegramMessageThreadID": "(Nepovinné) ID vlákna zprávy",
|
||||
"telegramMessageThreadIDDescription": "Nepovinný jedinečný identifikátor cílového vlákna zprávy (tématu) fóra; pouze pro nadskupiny fóra",
|
||||
"telegramProtectContentDescription": "Pokud je tato funkce povolena, budou zprávy bota v aplikaci Telegram chráněny před přeposíláním a ukládáním.",
|
||||
"Body Encoding": "Kódování těla zprávy",
|
||||
"telegramProtectContent": "Ochrana přeposílání/ukládání",
|
||||
"telegramSendSilently": "Odeslat potichu",
|
||||
"telegramSendSilentlyDescription": "Zprávu odešle tiše. Uživatelé obdrží oznámení bez zvuku.",
|
||||
"Clone": "Klonovat",
|
||||
"cloneOf": "Klonovat {0}",
|
||||
"Clone Monitor": "Klonovat dohled",
|
||||
"API Keys": "API klíče",
|
||||
"Expiry": "Platnost",
|
||||
"Don't expire": "Nevyprší",
|
||||
"Continue": "Pokračovat",
|
||||
"Add Another": "Přidat další",
|
||||
"Key Added": "Klíč byl přidán",
|
||||
"Expiry date": "Vyprší dne",
|
||||
"No API Keys": "Žàdné API klíče",
|
||||
"apiKey-active": "Aktivní",
|
||||
"apiKey-expired": "Vypršel",
|
||||
"Expires": "Vyprší",
|
||||
"disableAPIKeyMsg": "Jste si jistý, že chcete deaktivovat tento API klíč?",
|
||||
"Add API Key": "Přidat API klíč",
|
||||
"apiKey-inactive": "Neaktivní",
|
||||
"Generate": "Vygenerovat",
|
||||
"apiKeyAddedMsg": "Váš klíč API byl přidán. Poznamenejte si jej, protože se již nebude zobrazovat.",
|
||||
"deleteAPIKeyMsg": "Opravdu chcete tento klíč API odstranit?",
|
||||
"pagertreeUrgency": "Urgence",
|
||||
"pagertreeSilent": "Potichu",
|
||||
"pagertreeLow": "Slabě",
|
||||
"pagertreeCritical": "Kritické",
|
||||
"pagertreeResolve": "Automatické řešení",
|
||||
"pagertreeDoNothing": "Nedělej nic",
|
||||
"pagertreeIntegrationUrl": "Integrační URL",
|
||||
"pagertreeMedium": "Středně",
|
||||
"pagertreeHigh": "Nahlas",
|
||||
"wayToGetPagerTreeIntegrationURL": "Po vytvoření integrace Uptime Kuma v aplikaci PagerTree zkopírujte koncový bod. Zobrazit všechny podrobnosti {0}",
|
||||
"Add New Tag": "Přidat nový štítek",
|
||||
"lunaseaTarget": "Cíl",
|
||||
"lunaseaDeviceID": "ID zařízení",
|
||||
"lunaseaUserID": "ID uživatele",
|
||||
"statusPageRefreshIn": "Obnovení za: {0}"
|
||||
}
|
||||
|
@@ -23,13 +23,13 @@
|
||||
"Status": "Status",
|
||||
"DateTime": "Dato / Tid",
|
||||
"Message": "Beskeder",
|
||||
"No important events": "Inden vigtige begivenheder",
|
||||
"No important events": "Ingen vigtige begivenheder",
|
||||
"Resume": "Fortsæt",
|
||||
"Edit": "Rediger",
|
||||
"Delete": "Slet",
|
||||
"Current": "Aktuelt",
|
||||
"Uptime": "Oppetid",
|
||||
"Cert Exp.": "Certifikatets udløb",
|
||||
"Cert Exp.": "Certifikatets udløb.",
|
||||
"day": "Dag | Dage",
|
||||
"-day": "-Dage",
|
||||
"hour": "Timer",
|
||||
@@ -43,7 +43,7 @@
|
||||
"URL": "URL",
|
||||
"Hostname": "Hostname",
|
||||
"Port": "Port",
|
||||
"Heartbeat Interval": "Taktinterval",
|
||||
"Heartbeat Interval": "Hjerteslag interval",
|
||||
"Retries": "Gentagelser",
|
||||
"retriesDescription": "Maksimalt antal gentagelser, før tjenesten markeres som inaktiv og sender en meddelelse.",
|
||||
"Advanced": "Avanceret",
|
||||
@@ -152,7 +152,7 @@
|
||||
"Options": "Valgmuligheder",
|
||||
"Keep both": "Behold begge",
|
||||
"Tags": "Etiketter",
|
||||
"Add New below or Select...": "Tilføj Nyt nedenfor eller Vælg ...",
|
||||
"Add New below or Select...": "Tilføj Ny nedenfor eller Vælg…",
|
||||
"Tag with this name already exist.": "Et Tag med dette navn findes allerede.",
|
||||
"Tag with this value already exist.": "Et Tag med denne værdi findes allerede.",
|
||||
"color": "farve",
|
||||
@@ -165,7 +165,7 @@
|
||||
"Indigo": "Indigo",
|
||||
"Purple": "Lilla",
|
||||
"Pink": "Pink",
|
||||
"Search...": "Søg...",
|
||||
"Search...": "Søg…",
|
||||
"Avg. Ping": "Gns. Ping",
|
||||
"Avg. Response": "Gns. Respons",
|
||||
"Entry Page": "Entry Side",
|
||||
@@ -225,7 +225,7 @@
|
||||
"smtpCC": "CC",
|
||||
"smtpBCC": "BCC",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
"wayToGetDiscordURL": "Du kan få dette ved at gå til Serverindstillinger -> Integrationer -> Opret webhook ",
|
||||
"wayToGetDiscordURL": "Du kan få dette ved at gå til Serverindstillinger -> Integrationer -> Opret webhook",
|
||||
"Bot Display Name": "Bot Visningsnavn",
|
||||
"Prefix Custom Message": "Præfiks Brugerdefineret Besked",
|
||||
"Hello @everyone is...": "Hello {'@'}everyone is...",
|
||||
@@ -313,7 +313,7 @@
|
||||
"Security": "Sikkerhed",
|
||||
"Steam API Key": "Steam API-nøgle",
|
||||
"Shrink Database": "Krymp Database",
|
||||
"Pick a RR-Type...": "Vælg en RR-Type...",
|
||||
"Pick a RR-Type...": "Vælg en RR-Type…",
|
||||
"Pick Accepted Status Codes...": "Vælg Accepterede Statuskoder...",
|
||||
"Default": "Standard",
|
||||
"HTTP Options": "HTTP Valgmuligheder",
|
||||
@@ -350,5 +350,235 @@
|
||||
"serwersmsAPIUser": "API Brugernavn (inkl. webapi_ prefix)",
|
||||
"serwersmsAPIPassword": "API Adgangskode",
|
||||
"serwersmsPhoneNumber": "Telefonnummer",
|
||||
"serwersmsSenderName": "SMS Afsender Navn (registreret via kundeportal)"
|
||||
"serwersmsSenderName": "SMS Afsender Navn (registreret via kundeportal)",
|
||||
"statusMaintenance": "Vedligeholdelse",
|
||||
"Maintenance": "Vedligeholdelse",
|
||||
"No Maintenance": "Ingen vedligeholdelse",
|
||||
"Examples": "Eksempler",
|
||||
"High": "Høj",
|
||||
"Recipient Number": "Modtager Nummer",
|
||||
"From Name/Number": "Fra Navn/Nummer",
|
||||
"Help": "Hjælp",
|
||||
"Please use this option carefully!": "Brug venligst denne funktion med forsigtighed!",
|
||||
"disableauth.message1": "Er du sikker på, at du vil <strong>deaktivere authentication</strong>?",
|
||||
"successMessage": "Succesmeddelelse",
|
||||
"error": "fejl",
|
||||
"critical": "kritisk",
|
||||
"Customize": "Tilpas",
|
||||
"Custom Footer": "Brugerdefineret Footer",
|
||||
"Custom CSS": "Brugerdefineret CSS",
|
||||
"deleteStatusPageMsg": "Er du sikker på, at du vil slette denne statusside?",
|
||||
"Proxies": "Proxies",
|
||||
"default": "Standard",
|
||||
"enabled": "Aktiveret",
|
||||
"setAsDefault": "Indstil som standard",
|
||||
"Certificate Chain": "Certificate Chain",
|
||||
"Days Remaining:": "Dage tilbage:",
|
||||
"No status pages": "Ingen statussider",
|
||||
"Proxy": "Proxy",
|
||||
"default: notify all devices": "standard: underretter alle enheder",
|
||||
"Automations can optionally be triggered in Home Assistant:": "Automatiseringer kan valgfrit udløses i Home Assistant:",
|
||||
"Trigger type:": "Trigger type:",
|
||||
"Event type:": "Event type:",
|
||||
"Event data:": "Event data:",
|
||||
"Frontend Version": "Frontend Version",
|
||||
"or": "eller",
|
||||
"Notification Service": "Notifikationstjeneste",
|
||||
"Domain": "Domæne",
|
||||
"Google Analytics ID": "Google Analytics ID",
|
||||
"Edit Tag": "Ændre Tag",
|
||||
"Learn More": "Lær mere",
|
||||
"Schedule maintenance": "Planlæg vedligeholdelse",
|
||||
"Invalid": "Ugyldig",
|
||||
"User": "Bruger",
|
||||
"Installed": "Installeret",
|
||||
"Not installed": "Ikke installeret",
|
||||
"Running": "Køre",
|
||||
"Not running": "Køre ikke",
|
||||
"Remove Token": "Fjern Token",
|
||||
"Start": "Start",
|
||||
"Stop": "Stop",
|
||||
"Add New Status Page": "Tilføj ny statusside",
|
||||
"Next": "Næste",
|
||||
"No Proxy": "Ingen proxy",
|
||||
"New Status Page": "Ny statusside",
|
||||
"Page Not Found": "Side blev ikke fundet",
|
||||
"Reverse Proxy": "Reverse Proxy",
|
||||
"Backup": "Backup",
|
||||
"About": "Om",
|
||||
"cloudflareWebsite": "Cloudflare hjemmeside",
|
||||
"Message:": "Besked:",
|
||||
"HTTP Headers": "HTTP Headers",
|
||||
"Trust Proxy": "Trust Proxy",
|
||||
"For example: nginx, Apache and Traefik.": "For eksempel: nginx, Apache og Traefik.",
|
||||
"Please read": "Læs venligst",
|
||||
"Show Powered By": "Vis Powered By",
|
||||
"Domain Names": "Domænenavne",
|
||||
"signedInDisp": "Logget ind som {0}",
|
||||
"Certificate Expiry Notification": "Meddelelse om udløbsdato for certifikatet",
|
||||
"API Username": "API Brugernavn",
|
||||
"API Key": "API Key",
|
||||
"Steam Game Server": "Steam Game Server",
|
||||
"What you can try:": "Hvad du kan prøve:",
|
||||
"Go back to the previous page.": "Gå tilbage til forrige side.",
|
||||
"Coming Soon": "Kommer snart",
|
||||
"settingsCertificateExpiry": "Udløb af TLS-certifikat",
|
||||
"Setup Docker Host": "Opsæt Docker Host",
|
||||
"Connection Type": "Forbindelsestype",
|
||||
"Docker Daemon": "Docker Daemon",
|
||||
"socket": "Socket",
|
||||
"tcp": "TCP / HTTP",
|
||||
"Docker Container": "Docker Container",
|
||||
"Container Name / ID": "Container Navn / ID",
|
||||
"Packet Size": "Pakke størrelse",
|
||||
"Home Assistant URL": "Home Assistant URL",
|
||||
"Frontend Version do not match backend version!": "Frontend versionen stemmer ikke overens med backend versionen!",
|
||||
"Optional": "Valgfri",
|
||||
"HomeAssistant": "Home Assistant",
|
||||
"disableauth.message2": "Den er beregnet til scenarier <strong>hvor du har tænkt dig at implementere tredjepartsgodkendelse</strong> foran Uptime Kuma, f.eks. Cloudflare Access, Authelia eller andre godkendelsesmekanismer.",
|
||||
"deleteProxyMsg": "Er du sikker på, at du vil slette denne proxy for alle monitors?",
|
||||
"Valid": "Gyldig",
|
||||
"Don't know how to get the token? Please read the guide:": "Ved du ikke, hvordan du får fat i din Token? Læs venligst guiden:",
|
||||
"Subject:": "Emne:",
|
||||
"Footer Text": "Footer tekst",
|
||||
"Using a Reverse Proxy?": "Bruger du en Reverse Proxy?",
|
||||
"deleteDockerHostMsg": "Er du sikker på, at du vil slette denne docker host for alle monitors?",
|
||||
"Docker Host": "Docker Host",
|
||||
"Docker Hosts": "Docker Hosts",
|
||||
"loadingError": "Kan ikke hente dataene, prøv igen senere.",
|
||||
"Custom": "Brugerdefineret",
|
||||
"Monitor": "Monitor | Monitors",
|
||||
"Specific Monitor Type": "Specifik monitor-type",
|
||||
"topic": "Emne",
|
||||
"Fingerprint:": "Fingerprint:",
|
||||
"Issuer:": "Udsteder:",
|
||||
"dayOfWeek": "Ugedag",
|
||||
"dayOfMonth": "Dag i måneden",
|
||||
"lastDay": "Sidste dag",
|
||||
"lastDay1": "Sidste dag i måneden",
|
||||
"weekdayShortThu": "Tor",
|
||||
"weekdayShortFri": "Fre",
|
||||
"weekdayShortSat": "Lør",
|
||||
"weekdayShortSun": "Søn",
|
||||
"weekdayShortWed": "Ons",
|
||||
"lastDay2": "Anden sidste dag i måneden",
|
||||
"lastDay3": "Tredje sidste dag i måneden",
|
||||
"lastDay4": "Fjerde sidste dag i måneden",
|
||||
"maintenanceStatus-under-maintenance": "Under vedligeholdelse",
|
||||
"maintenanceStatus-inactive": "Inaktiv",
|
||||
"maintenanceStatus-scheduled": "Planlagt",
|
||||
"maintenanceStatus-ended": "Afsluttet",
|
||||
"maintenanceStatus-unknown": "Ukendt",
|
||||
"Display Timezone": "Vis tidszone",
|
||||
"Server Timezone": "Serverens tidszone",
|
||||
"IconUrl": "Ikon URL",
|
||||
"Enable DNS Cache": "Aktiver DNS Cache",
|
||||
"Enable": "Aktiver",
|
||||
"Disable": "Deaktiver",
|
||||
"dnsCacheDescription": "Det fungerer muligvis ikke i alle IPv6-miljøer, så deaktiver det, hvis du støder på problemer.",
|
||||
"Maintenance Time Window of a Day": "Tidsvindue for vedligeholdelse af en dag",
|
||||
"Schedule Maintenance": "Planlæg vedligeholdelse",
|
||||
"Date and Time": "Dato og klokkeslæt",
|
||||
"plugin": "Plugin | Plugins",
|
||||
"install": "Installer",
|
||||
"uninstall": "Afinstaller",
|
||||
"uninstalling": "Afinstallerer",
|
||||
"confirmUninstallPlugin": "Er du sikker på, at du vil afinstallere dette plugin?",
|
||||
"installing": "Installerer",
|
||||
"markdownSupported": "Markdown syntax understøttet",
|
||||
"Affected Monitors": "Berørte monitors",
|
||||
"All Status Pages": "Alle statussider",
|
||||
"Pick Affected Monitors...": "Vælg berørte monitors…",
|
||||
"Select status pages...": "Vælg statusside…",
|
||||
"proxyDescription": "Proxyer skal være tilknyttet en monitor for at fungere.",
|
||||
"Accept characters:": "Accepter tegn:",
|
||||
"Authentication": "Godkendelse",
|
||||
"wayToGetCloudflaredURL": "(Download cloudflared fra {0})",
|
||||
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "Den aktuelle forbindelse kan gå tabt, hvis du er forbundet via Cloudflare Tunnel. Er du sikker på, at du vil stoppe det? Indtast din nuværende adgangskode for at bekræfte den.",
|
||||
"Other Software": "Anden software",
|
||||
"Date Created": "Dato oprettet",
|
||||
"signedInDispDisabled": "Auth Deaktiveret.",
|
||||
"certificationExpiryDescription": "HTTPS Monitors sender en notifikation, når TLS-certifikatet udløber om:",
|
||||
"Also check beta release": "Se også betaudgivelsen",
|
||||
"Show update if available": "Vis opdatering, hvis tilgængelig",
|
||||
"wayToGetZohoCliqURL": "Du kan lære, hvordan du opretter et webhook URL {0}.",
|
||||
"recurringInterval": "Interval",
|
||||
"weekdayShortMon": "Man",
|
||||
"weekdayShortTue": "Tir",
|
||||
"dnsPortDescription": "DNS server port. Standardværdien er 53. Du kan altid ændre porten.",
|
||||
"Valid To:": "Gyldig til:",
|
||||
"Domain Name Expiry Notification": "Notifikation om udløb af domænenavn",
|
||||
"Custom Monitor Type": "Brugerdefineret overvågningstype",
|
||||
"API Keys": "API Nøgler",
|
||||
"Don't expire": "Udløb aldrig",
|
||||
"Continue": "Fortsæt",
|
||||
"Add Another": "Tilføj en mere",
|
||||
"Key Added": "Nøgle tilføjet",
|
||||
"Add API Key": "Tilføj API Nøgle",
|
||||
"No API Keys": "Ingen API nøgler",
|
||||
"apiKey-active": "Aktiv",
|
||||
"apiKey-expired": "Udløbet",
|
||||
"apiKey-inactive": "Inaktiv",
|
||||
"disableAPIKeyMsg": "Er du sikker på du vil deaktivere denne API nøgle?",
|
||||
"Generate": "Generér",
|
||||
"Game": "Spil",
|
||||
"General Monitor Type": "Generel Overvågningstype",
|
||||
"Clone Monitor": "Duplikér overvågning",
|
||||
"Clone": "Duplikér",
|
||||
"cloneOf": "Kopi af {0}",
|
||||
"promosmsLogin": "API Login Navn",
|
||||
"pushoversounds siren": "Sirene",
|
||||
"pushoversounds none": "Ingen (lydløs)",
|
||||
"smtpDkimSettings": "DKIM Indstillinger",
|
||||
"documentation": "dokumentation",
|
||||
"smtpDkimDomain": "Domænenavn",
|
||||
"smtpDkimPrivateKey": "Privat nøgle",
|
||||
"alertaApiEndpoint": "API Slutpunkt",
|
||||
"alertaApiKey": "API Nøgle",
|
||||
"smseagleEncoding": "Send som Unicode",
|
||||
"onebotHttpAddress": "OneBot HTTP Adresse",
|
||||
"onebotMessageType": "OneBot Meddelelse Type",
|
||||
"onebotGroupMessage": "Gruppe",
|
||||
"onebotPrivateMessage": "Privat",
|
||||
"onebotUserOrGroupId": "Gruppe/Bruger ID",
|
||||
"promosmsPassword": "API Adgangskode",
|
||||
"recurringIntervalMessage": "Kør hver dag | Kør hver {0}. dag",
|
||||
"smseagleTo": "Telefon numre",
|
||||
"pagertreeIntegrationUrl": "Integration URL",
|
||||
"pagertreeSilent": "Lydløs",
|
||||
"pagertreeLow": "Lav",
|
||||
"pagertreeMedium": "Mellem",
|
||||
"pagertreeHigh": "Høj",
|
||||
"pagertreeCritical": "Kritisk",
|
||||
"pushoversounds vibrate": "Kun Vibration",
|
||||
"Server Address": "Server Adresse",
|
||||
"pauseMaintenanceMsg": "Er du sikker på du vil pause?",
|
||||
"Recurring": "Tilbagevendende",
|
||||
"Enable TLS": "Aktivér TLS",
|
||||
"high": "høj",
|
||||
"Base URL": "Base URL",
|
||||
"Platform": "Platform",
|
||||
"Android": "Android",
|
||||
"Huawei": "Huawei",
|
||||
"Retry": "Forsøg igen",
|
||||
"Topic": "Emne",
|
||||
"Setup Proxy": "Opsæt Proxy",
|
||||
"Proxy Server": "Proxy Server",
|
||||
"wayToGetClickSendSMSToken": "Du kan få API brugernavn og API nøgle fra {0} .",
|
||||
"PushDeer Key": "PushDeer Nøgle",
|
||||
"The resource is no longer available.": "Denne ressource er ikke længere tilgængelig.",
|
||||
"Proxy Protocol": "Proxy Protokol",
|
||||
"Integration Key": "Integration Nøgle",
|
||||
"Integration URL": "Integration URL",
|
||||
"do nothing": "gør intet",
|
||||
"Passive Monitor Type": "Passiv Overvågningstype",
|
||||
"Most likely causes:": "Mest sandsynlige årsager:",
|
||||
"statusPageMaintenanceEndDate": "Slut",
|
||||
"pushoversounds magic": "Magisk",
|
||||
"pushoversounds mechanical": "Mekanisk",
|
||||
"pushyAPIKey": "Hemmelig API Nøgle",
|
||||
"Expiry date": "Udløbsdato",
|
||||
"Expires": "Udløber",
|
||||
"deleteAPIKeyMsg": "Er du sikker på du vil slette denne API nøgle?",
|
||||
"pagertreeDoNothing": "Gør intet"
|
||||
}
|
||||
|
@@ -102,7 +102,7 @@
|
||||
"deleteNotificationMsg": "Möchtest du diese Benachrichtigung wirklich für alle Monitore löschen?",
|
||||
"resolverserverDescription": "Cloudflare ist als der Standardserver festgelegt. Dieser kann jederzeit geändert werden.",
|
||||
"Resolver Server": "Auflösungsserver",
|
||||
"rrtypeDescription": "Wähle den RR-Typ aus, welchen du überwachen möchtest.",
|
||||
"rrtypeDescription": "Wähle den RR Typ aus, welchen du überwachen möchtest",
|
||||
"Last Result": "Letztes Ergebnis",
|
||||
"pauseMonitorMsg": "Bist du sicher, dass du den Monitor pausieren möchtest?",
|
||||
"clearEventsMsg": "Bist du sicher, dass du alle Ereignisse für diesen Monitor löschen möchtest?",
|
||||
@@ -135,7 +135,7 @@
|
||||
"Options": "Optionen",
|
||||
"confirmImportMsg": "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import-Option ausgewählt ist.",
|
||||
"Keep both": "Beide behalten",
|
||||
"twoFAVerifyLabel": "Bitte trage deinen Token ein, um zu verifizieren, dass 2FA funktioniert",
|
||||
"twoFAVerifyLabel": "Bitte trage deinen Token ein, um zu verifizieren, dass 2FA funktioniert:",
|
||||
"Verify Token": "Token verifizieren",
|
||||
"Setup 2FA": "2FA einrichten",
|
||||
"Enable 2FA": "2FA aktivieren",
|
||||
@@ -165,7 +165,7 @@
|
||||
"Pink": "Pink",
|
||||
"Search...": "Suchen…",
|
||||
"Heartbeat Retry Interval": "Überprüfungsintervall",
|
||||
"Resend Notification if Down X times consequently": "Benachrichtigung erneut senden, wenn Inaktiv X mal hintereinander",
|
||||
"Resend Notification if Down X times consecutively": "Benachrichtigung erneut senden, wenn Inaktiv X mal hintereinander",
|
||||
"retryCheckEverySecond": "Alle {0} Sekunden neu versuchen",
|
||||
"resendEveryXTimes": "Erneut versenden alle {0} mal",
|
||||
"resendDisabled": "Erneut versenden deaktiviert",
|
||||
@@ -206,7 +206,7 @@
|
||||
"mattermost": "Mattermost",
|
||||
"Primary Base URL": "Primär URL",
|
||||
"Push URL": "Push URL",
|
||||
"needPushEvery": "Du solltest diese URL alle {0} Sekunden aufrufen",
|
||||
"needPushEvery": "Du solltest diese URL alle {0} Sekunden aufrufen.",
|
||||
"pushOptionalParams": "Optionale Parameter: {0}",
|
||||
"defaultNotificationName": "Mein {notification} Alarm ({number})",
|
||||
"here": "hier",
|
||||
@@ -231,7 +231,7 @@
|
||||
"smtpCC": "CC",
|
||||
"smtpBCC": "BCC",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
"wayToGetDiscordURL": "Du kannst diese erhalten, indem du zu den Servereinstellungen gehst -> Integrationen -> Neuer Webhook",
|
||||
"wayToGetDiscordURL": "Du kannst diese erhalten, indem du zu den Servereinstellungen gehst -> Notifikationen -> Webhooks -> Neuer Webhook",
|
||||
"Bot Display Name": "Bot-Anzeigename",
|
||||
"Prefix Custom Message": "Benutzerdefinierter Nachrichten Präfix",
|
||||
"Hello @everyone is...": "Hallo {'@'}everyone ist…",
|
||||
@@ -276,10 +276,10 @@
|
||||
"appriseInstalled": "Apprise ist installiert.",
|
||||
"appriseNotInstalled": "Apprise ist nicht installiert. {0}",
|
||||
"Access Token": "Access Token",
|
||||
"Channel access token": "Channel access token",
|
||||
"Channel access token": "Channel Access Token",
|
||||
"Line Developers Console": "Line Developers Console",
|
||||
"lineDevConsoleTo": "Line Developers Console - {0}",
|
||||
"Basic Settings": "Basic Settings",
|
||||
"Basic Settings": "Grundeinstellungen",
|
||||
"User ID": "User ID",
|
||||
"Messaging API": "Messaging API",
|
||||
"wayToGetLineChannelToken": "Rufe zuerst {0} auf, erstelle dann einen Provider und Channel (Messaging API). Als nächstes kannst du den Channel access token und die User ID aus den oben genannten Menüpunkten abrufen.",
|
||||
@@ -298,7 +298,7 @@
|
||||
"Internal Room Id": "Interne Raum-ID",
|
||||
"matrixDesc1": "Die interne Raum-ID findest du im erweiterten Bereich der Raumeinstellungen im Matrix-Client. Es sollte aussehen wie z.B. !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
"matrixDesc2": "Es wird dringend empfohlen einen neuen Benutzer anzulegen und nicht den Zugriffstoken deines eigenen Matrix-Benutzers zu verwenden. Anderenfalls ermöglicht es vollen Zugriff auf dein Konto und alle Räume, denen du beigetreten bist. Erstelle stattdessen einen neuen Benutzer und lade ihn nur in den Raum ein, in dem du die Benachrichtigung erhalten möchtest. Du kannst den Zugriffstoken erhalten, indem du Folgendes ausführst {0}",
|
||||
"Method": "Method",
|
||||
"Method": "Methode",
|
||||
"Body": "Body",
|
||||
"Headers": "Headers",
|
||||
"PushUrl": "Push URL",
|
||||
@@ -348,7 +348,7 @@
|
||||
"Services": "Dienste",
|
||||
"Discard": "Verwerfen",
|
||||
"Cancel": "Abbrechen",
|
||||
"Powered by": "Powered by",
|
||||
"Powered by": "Erstellt mit",
|
||||
"shrinkDatabaseDescription": "Löse VACUUM für die SQLite Datenbank aus. Wenn die Datenbank nach 1.10.0 erstellt wurde, ist AUTO_VACUUM bereits aktiviert und diese Aktion ist nicht erforderlich.",
|
||||
"serwersms": "SerwerSMS.pl",
|
||||
"serwersmsAPIUser": "API Benutzername (inkl. webapi_ prefix)",
|
||||
@@ -533,7 +533,7 @@
|
||||
"Also check beta release": "Auch nach beta Versionen schauen",
|
||||
"Using a Reverse Proxy?": "Wird ein Reverse Proxy genutzt?",
|
||||
"Check how to config it for WebSocket": "Prüfen, wie er für die Nutzung mit WebSocket konfiguriert wird",
|
||||
"Steam Game Server": "Steam Game Server",
|
||||
"Steam Game Server": "Steam Spielserver",
|
||||
"Most likely causes:": "Wahrscheinliche Ursachen:",
|
||||
"The resource is no longer available.": "Die Quelle ist nicht mehr verfügbar.",
|
||||
"There might be a typing error in the address.": "Es gibt einen Tippfehler in der Adresse.",
|
||||
@@ -560,7 +560,7 @@
|
||||
"Domain": "Domain",
|
||||
"Workstation": "Workstation",
|
||||
"disableCloudflaredNoAuthMsg": "Du bist im nicht-authentifizieren Modus, ein Passwort wird nicht benötigt.",
|
||||
"trustProxyDescription": "Vertraue 'X-Forwarded-*' headern. Wenn man die richtige client IP haben möchte und Uptime Kuma hinter einem Proxy wie Nginx or Apache läuft, wollte dies aktiviert werden.",
|
||||
"trustProxyDescription": "Vertraue 'X-Forwarded-*' headern. Wenn man die richtige Client IP erhalten möchte und Uptime Kuma hinter einem Proxy wie Nginx oder Apache läuft, sollte dies aktiviert werden.",
|
||||
"wayToGetLineNotifyToken": "Du kannst hier ein Token erhalten: {0}",
|
||||
"Examples": "Beispiele",
|
||||
"Home Assistant URL": "Home Assistant URL",
|
||||
@@ -590,22 +590,22 @@
|
||||
"atLeastOneMonitor": "Wähle mindestens einen Monitor",
|
||||
"deleteMaintenanceMsg": "Möchtest du diese Wartung löschen?",
|
||||
"Base URL": "Basis URL",
|
||||
"goAlertInfo": "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Beauftragen Sie automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt. {0}",
|
||||
"goAlertInfo": "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Engagiere automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt! {0}",
|
||||
"goAlertIntegrationKeyInfo": "Bekommt einen generischen API Schlüssel in folgenden Format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\". Normalerweise entspricht dies dem Wert des Token aus der URL.",
|
||||
"goAlert": "GoAlert",
|
||||
"backupOutdatedWarning": "Veraltet: Eine menge Neuerungen sind eingeflossen und diese Funktion wurde etwas vernachlässigt worden. Es kann kein vollständiges Backup erstellt oder eingespielt werden.",
|
||||
"backupOutdatedWarning": "Veraltet: Da viele Funktionen hinzugefügt wurden und die Backupfunktion nicht mehr gepflegt wird, kann keine vollständige Sicherung erstellt oder wiederhergestellt werden.",
|
||||
"backupRecommend": "Bitte Backup das Volume oder den Ordner (./ data /) selbst.",
|
||||
"Optional": "Optional",
|
||||
"squadcast": "Squadcast",
|
||||
"SendKey": "SendKey",
|
||||
"SMSManager API Docs": "SMSManager API Dokumente",
|
||||
"Gateway Type": "Gateway Type",
|
||||
"SMSManager API Docs": "SMSManager API Dokumente ",
|
||||
"Gateway Type": "Gateway Typ",
|
||||
"SMSManager": "SMSManager",
|
||||
"You can divide numbers with": "Du kannst Zahlen teilen mit",
|
||||
"or": "oder",
|
||||
"recurringInterval": "Intervall",
|
||||
"Recurring": "Wiederkehrend",
|
||||
"strategyManual": "Active/Inactive Manually",
|
||||
"strategyManual": "Aktiv/Inaktiv Manuell",
|
||||
"warningTimezone": "Es wird die Zeitzone des Servers genutzt",
|
||||
"weekdayShortMon": "Mo",
|
||||
"weekdayShortTue": "Di",
|
||||
@@ -629,5 +629,116 @@
|
||||
"maintenanceStatus-ended": "Ende",
|
||||
"maintenanceStatus-unknown": "Unbekannt",
|
||||
"Display Timezone": "Zeitzone anzeigen",
|
||||
"Server Timezone": "Server Zeitzone"
|
||||
"Server Timezone": "Server Zeitzone",
|
||||
"telegramMessageThreadID": "(Optional) Nachrichten Thread ID",
|
||||
"telegramMessageThreadIDDescription": "Optionale eindeutige Kennung für den Ziel-Thread (Thema) des Forums; nur für Forum-Supergroups",
|
||||
"Enable": "Aktivieren",
|
||||
"telegramProtectContent": "Schütze gegen Weiterleiten/Speichern der Nachricht",
|
||||
"telegramProtectContentDescription": "Die Bot-Nachrichten in Telegram sind gegen Weiterleitung und Speichern geschützt.",
|
||||
"Disable": "Deaktivieren",
|
||||
"plugin": "Plugin | Plugins",
|
||||
"installing": "Installiere",
|
||||
"uninstall": "Deinstallieren",
|
||||
"uninstalling": "Deinstalliere",
|
||||
"confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
|
||||
"notificationRegional": "Regional",
|
||||
"Single Maintenance Window": "Einmaliges Wartungsfenster",
|
||||
"dnsCacheDescription": "In einigen IPv6-Umgebungen funktioniert es möglicherweise nicht. Deaktiviere es, wenn Probleme auftreten.",
|
||||
"Maintenance Time Window of a Day": "Wartungszeitfenster eines Tages",
|
||||
"Effective Date Range": "Gültigkeitsbereich",
|
||||
"Schedule Maintenance": "Wartung planen",
|
||||
"Date and Time": "Datum und Uhrzeit",
|
||||
"DateTime Range": "Datums- und Zeitbereich",
|
||||
"telegramSendSilently": "Stumm senden",
|
||||
"telegramSendSilentlyDescription": "Sende die Nachricht stumm. Nutzer bekommen eine Benachrichtigung ohne Ton.",
|
||||
"markdownSupported": "Markdown-Syntax unterstützt",
|
||||
"webhookAdditionalHeadersTitle": "Zusätzliche Header",
|
||||
"webhookAdditionalHeadersDesc": "Legt zusätzliche Kopfzeilen fest, die mit dem Webhook gesendet werden.",
|
||||
"Packet Size": "Paketgrösse",
|
||||
"IconUrl": "Symbol URL",
|
||||
"Enable DNS Cache": "DNS Cache aktivieren",
|
||||
"Help": "Hilfe",
|
||||
"Game": "Spiel",
|
||||
"General Monitor Type": "Allgemeiner Monitortyp",
|
||||
"Passive Monitor Type": "Passiver Monitortyp",
|
||||
"Specific Monitor Type": "Spezifischer Monitortyp",
|
||||
"Monitor": "Überwachung | Monitore",
|
||||
"Custom": "Benutzerdefiniert",
|
||||
"statusPageMaintenanceEndDate": "Ende",
|
||||
"loadingError": "Die Daten konnten nicht abgerufen werden, bitte später noch einmal versuchen.",
|
||||
"install": "Installieren",
|
||||
"Body Encoding": "Body Encoding",
|
||||
"Custom Monitor Type": "Benutzerdefinierter Monitortyp",
|
||||
"Expiry": "Ablauf",
|
||||
"Expiry date": "Ablaufdatum",
|
||||
"Don't expire": "Nicht ablaufen",
|
||||
"Add Another": "Hinzufügen",
|
||||
"Key Added": "Schlüssel hinzugefügt",
|
||||
"apiKeyAddedMsg": "API Schlüssel wurde hinzugefügt. Bitte notiere den Schlüssel, da er nicht erneut angezeigt wird.",
|
||||
"Add API Key": "API Schlüssel hinzufügen",
|
||||
"No API Keys": "Kein API Schlüssel",
|
||||
"apiKey-active": "Aktiv",
|
||||
"apiKey-expired": "Abgelaufen",
|
||||
"apiKey-inactive": "Inaktiv",
|
||||
"Expires": "Läuft ab",
|
||||
"disableAPIKeyMsg": "Bist du sicher, dass du diesen API Schlüssel deaktivieren willst?",
|
||||
"deleteAPIKeyMsg": "Bist du sicher, dass du diesen API Schlüssel löschen willst?",
|
||||
"Generate": "Generieren",
|
||||
"infiniteRetention": "Für unendliche Speicherung auf 0 setzen.",
|
||||
"dataRetentionTimeError": "Aufbewahrungsfrist muss grösser oder gleich 0 sein",
|
||||
"Clone Monitor": "Monitor klonen",
|
||||
"Clone": "Klonen",
|
||||
"cloneOf": "Klon von {0}",
|
||||
"wayToGetZohoCliqURL": "Wie eine Webhook URL erstellt werden kann, erfährst du {0}.",
|
||||
"enableGRPCTls": "Senden von gRPC Anforderungen mit TLS Verbindung zulassen",
|
||||
"grpcMethodDescription": "Der Name der Methode wird in das \"cammelCase\" Format konvertiert (z.B. sayHello, check, etc.)",
|
||||
"wayToGetKookGuildID": "Schalte den „Entwicklermodus“ in den Kook-Einstellungen ein und klicke mit der rechten Maustaste auf die Gilde, um die ID zu erhalten",
|
||||
"Guild ID": "Gilde ID",
|
||||
"Lowcost": "Kostengünstig",
|
||||
"high": "hoch",
|
||||
"Google Analytics ID": "Google Analytics ID",
|
||||
"Enable TLS": "TLS aktivieren",
|
||||
"Free Mobile API Key": "Kostenloser Mobile API Schlüssel",
|
||||
"Proto Service Name": "Proto Dienst Name",
|
||||
"Proto Method": "Proto Methode",
|
||||
"Proto Content": "Proto Inhalt",
|
||||
"Economy": "Economy",
|
||||
"pagertreeIntegrationUrl": "Integrations-URL",
|
||||
"pagertreeUrgency": "Dringlichkeit",
|
||||
"pagertreeSilent": "Stumm",
|
||||
"pagertreeLow": "Niedrig",
|
||||
"pagertreeMedium": "Mittel",
|
||||
"pagertreeHigh": "Hoch",
|
||||
"pagertreeCritical": "Kritisch",
|
||||
"pagertreeResolve": "Automatisch auflösen",
|
||||
"pagertreeDoNothing": "Nichts tun",
|
||||
"wayToGetPagerTreeIntegrationURL": "Nachdem du die Uptime Kuma Integration in PagerTree erstellt hast, kopiere den Endpunkt. Siehe details {0}",
|
||||
"Server Address": "Serveradresse",
|
||||
"Learn More": "Erfahre mehr",
|
||||
"Edit Tag": "Tag editieren",
|
||||
"promosmsAllowLongSMS": "Lange SMS erlauben",
|
||||
"smseagleRecipientType": "Empfängertyp",
|
||||
"smseagleToken": "API Zugriffstoken",
|
||||
"smseagleTo": "Telefonnummer(n)",
|
||||
"smseagleUrl": "Ihre SMSEagle Geräte URL",
|
||||
"smseagleEncoding": "Als Unicode senden",
|
||||
"smseaglePriority": "Nachrichtenpriorität (0-9, Standard = 0)",
|
||||
"smseagleContact": "Telefonbuch Kontaktname(n)",
|
||||
"confirmDeleteTagMsg": "Möchtest du dieses Tag wirklich löschen? Monitore, die mit diesem Tag verknüpft sind, werden nicht gelöscht.",
|
||||
"wayToGetKookBotToken": "Erstelle eine Anwendung und erhalte den Bot-Token unter {0}",
|
||||
"Strategy": "Strategie",
|
||||
"Free Mobile User Identifier": "Kostenlose mobile Benutzerkennung",
|
||||
"smseagleGroup": "Telefonbuch Gruppenname(n)",
|
||||
"smseagleRecipient": "Empfänger (mehrere müssen durch Komma getrennt werden)",
|
||||
"API Keys": "API Schlüssel",
|
||||
"Continue": "Weiter",
|
||||
"Add New Tag": "Neuen Tag hinzufügen",
|
||||
"lunaseaTarget": "Ziel",
|
||||
"lunaseaDeviceID": "Geräte-ID",
|
||||
"lunaseaUserID": "Benutzer-ID",
|
||||
"twilioAccountSID": "Account SID",
|
||||
"twilioFromNumber": "Absender",
|
||||
"twilioToNumber": "Empfänger",
|
||||
"twilioAuthToken": "Auth Token",
|
||||
"statusPageRefreshIn": "Aktualisierung in: {0}"
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"languageName": "Deutsch (Deutschland)",
|
||||
"Settings": "Einstellungen",
|
||||
"Dashboard": "Dashboard",
|
||||
"New Update": "Update verfügbar",
|
||||
"New Update": "Aktualisierung verfügbar",
|
||||
"Language": "Sprache",
|
||||
"Appearance": "Erscheinungsbild",
|
||||
"Theme": "Erscheinungsbild",
|
||||
@@ -135,7 +135,7 @@
|
||||
"Options": "Optionen",
|
||||
"confirmImportMsg": "Möchtest du das Backup wirklich importieren? Bitte stelle sicher, dass die richtige Import-Option ausgewählt ist.",
|
||||
"Keep both": "Beide behalten",
|
||||
"twoFAVerifyLabel": "Bitte trage deinen Token ein, um zu verifizieren, dass 2FA funktioniert",
|
||||
"twoFAVerifyLabel": "Bitte trage deinen Token ein, um zu verifizieren, dass 2FA funktioniert:",
|
||||
"Verify Token": "Token verifizieren",
|
||||
"Setup 2FA": "2FA einrichten",
|
||||
"Enable 2FA": "2FA aktivieren",
|
||||
@@ -165,7 +165,7 @@
|
||||
"Pink": "Pink",
|
||||
"Search...": "Suchen…",
|
||||
"Heartbeat Retry Interval": "Überprüfungsintervall",
|
||||
"Resend Notification if Down X times consequently": "Benachrichtigung erneut senden, wenn Inaktiv X mal hintereinander",
|
||||
"Resend Notification if Down X times consecutively": "Benachrichtigung erneut senden, wenn inaktiv X Mal hintereinander",
|
||||
"retryCheckEverySecond": "Alle {0} Sekunden neu versuchen",
|
||||
"resendEveryXTimes": "Erneut versenden alle {0} mal",
|
||||
"resendDisabled": "Erneut versenden deaktiviert",
|
||||
@@ -206,7 +206,7 @@
|
||||
"mattermost": "Mattermost",
|
||||
"Primary Base URL": "Primäre Basis-URL",
|
||||
"Push URL": "Push URL",
|
||||
"needPushEvery": "Du solltest diese URL alle {0} Sekunden aufrufen",
|
||||
"needPushEvery": "Du solltest diese URL alle {0} Sekunden aufrufen.",
|
||||
"pushOptionalParams": "Optionale Parameter: {0}",
|
||||
"defaultNotificationName": "Mein {notification} Alarm ({number})",
|
||||
"here": "hier",
|
||||
@@ -215,7 +215,7 @@
|
||||
"wayToGetTelegramToken": "Hier kannst du einen Token erhalten {0}.",
|
||||
"Chat ID": "Chat ID",
|
||||
"supportTelegramChatID": "Unterstützt Direkt Chat / Gruppe / Kanal Chat-ID's",
|
||||
"wayToGetTelegramChatID": "Du kannst die Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id: zu sehen.",
|
||||
"wayToGetTelegramChatID": "Du kannst deine Chat-ID erhalten, indem du eine Nachricht an den Bot sendest und zu dieser URL gehst, um die chat_id: zu sehen.",
|
||||
"YOUR BOT TOKEN HERE": "HIER DEIN BOT TOKEN",
|
||||
"chatIDNotFound": "Chat-ID wurde nicht gefunden: bitte sende zuerst eine Nachricht an diesen Bot",
|
||||
"Post URL": "Post URL",
|
||||
@@ -231,7 +231,7 @@
|
||||
"smtpCC": "CC",
|
||||
"smtpBCC": "BCC",
|
||||
"Discord Webhook URL": "Discord Webhook URL",
|
||||
"wayToGetDiscordURL": "Du kannst diese erhalten, indem du zu den Servereinstellungen gehst -> Integrationen -> Neuer Webhook",
|
||||
"wayToGetDiscordURL": "Du kannst diese erhalten, indem du zu den Servereinstellungen gehst -> Integrationen -> WebHooks anzeigen -> Neuer WebHook",
|
||||
"Bot Display Name": "Bot-Anzeigename",
|
||||
"Prefix Custom Message": "Benutzerdefinierter Nachrichten Präfix",
|
||||
"Hello @everyone is...": "Hallo {'@'}everyone ist…",
|
||||
@@ -275,11 +275,11 @@
|
||||
"Read more": "Weiterlesen",
|
||||
"appriseInstalled": "Apprise ist installiert.",
|
||||
"appriseNotInstalled": "Apprise ist nicht installiert. {0}",
|
||||
"Access Token": "Access Token",
|
||||
"Access Token": "Zugriffstoken",
|
||||
"Channel access token": "Channel access token",
|
||||
"Line Developers Console": "Line Developers Console",
|
||||
"lineDevConsoleTo": "Line Developers Console - {0}",
|
||||
"Basic Settings": "Basic Settings",
|
||||
"Line Developers Console": "Zeile Entwickler Konsole",
|
||||
"lineDevConsoleTo": "Line Entwicklerkonsole - {0}",
|
||||
"Basic Settings": "Grundeinstellungen",
|
||||
"User ID": "User ID",
|
||||
"Messaging API": "Messaging API",
|
||||
"wayToGetLineChannelToken": "Rufe zuerst {0} auf, erstelle dann einen Provider und Channel (Messaging API). Als nächstes kannst du den Channel access token und die User ID aus den oben genannten Menüpunkten abrufen.",
|
||||
@@ -298,9 +298,9 @@
|
||||
"Internal Room Id": "Interne Raum-ID",
|
||||
"matrixDesc1": "Die interne Raum-ID findest du im erweiterten Bereich der Raumeinstellungen im Matrix-Client. Es sollte aussehen wie z.B. !QMdRCpUIfLwsfjxye6:home.server.",
|
||||
"matrixDesc2": "Es wird dringend empfohlen einen neuen Benutzer anzulegen und nicht den Zugriffstoken deines eigenen Matrix-Benutzers zu verwenden. Anderenfalls ermöglicht es vollen Zugriff auf dein Konto und alle Räume, denen du beigetreten bist. Erstelle stattdessen einen neuen Benutzer und lade ihn nur in den Raum ein, in dem du die Benachrichtigung erhalten möchtest. Du kannst den Zugriffstoken erhalten, indem du Folgendes ausführst {0}",
|
||||
"Method": "Method",
|
||||
"Method": "Methode",
|
||||
"Body": "Body",
|
||||
"Headers": "Headers",
|
||||
"Headers": "Header",
|
||||
"PushUrl": "Push URL",
|
||||
"HeadersInvalidFormat": "Der Header ist kein gültiges JSON: ",
|
||||
"BodyInvalidFormat": "Der Body ist kein gültiges JSON: ",
|
||||
@@ -315,7 +315,7 @@
|
||||
"Done": "Fertig",
|
||||
"Info": "Info",
|
||||
"Security": "Sicherheit",
|
||||
"Steam API Key": "Steam API Key",
|
||||
"Steam API Key": "Steam API-Schlüssel",
|
||||
"Shrink Database": "Datenbank verkleinern",
|
||||
"Pick a RR-Type...": "Wähle ein RR-Typ aus…",
|
||||
"Pick Accepted Status Codes...": "Wähle akzeptierte Statuscodes aus…",
|
||||
@@ -348,7 +348,7 @@
|
||||
"Services": "Dienste",
|
||||
"Discard": "Verwerfen",
|
||||
"Cancel": "Abbrechen",
|
||||
"Powered by": "Powered by",
|
||||
"Powered by": "Erstellt mit",
|
||||
"shrinkDatabaseDescription": "Löse VACUUM für die SQLite Datenbank aus. Wenn die Datenbank nach 1.10.0 erstellt wurde, ist AUTO_VACUUM bereits aktiviert und diese Aktion ist nicht erforderlich.",
|
||||
"serwersms": "SerwerSMS.pl",
|
||||
"serwersmsAPIUser": "API Benutzername (inkl. webapi_ prefix)",
|
||||
@@ -388,7 +388,7 @@
|
||||
"Valid": "Gültig",
|
||||
"Invalid": "Ungültig",
|
||||
"AccessKeyId": "AccessKey ID",
|
||||
"SecretAccessKey": "AccessKey Secret",
|
||||
"SecretAccessKey": "Geheimer Zugangsschlüssel",
|
||||
"PhoneNumbers": "Telefonnummern",
|
||||
"TemplateCode": "Vorlagencode",
|
||||
"SignName": "Signaturname",
|
||||
@@ -533,7 +533,7 @@
|
||||
"Also check beta release": "Auch nach Beta Versionen schauen",
|
||||
"Using a Reverse Proxy?": "Wird ein Reverse Proxy genutzt?",
|
||||
"Check how to config it for WebSocket": "Prüfen, wie er für die Nutzung mit WebSocket konfiguriert wird",
|
||||
"Steam Game Server": "Steam Game Server",
|
||||
"Steam Game Server": "Steam Spielserver",
|
||||
"Most likely causes:": "Wahrscheinliche Ursachen:",
|
||||
"The resource is no longer available.": "Die Quelle ist nicht mehr verfügbar.",
|
||||
"There might be a typing error in the address.": "Es gibt einen Tippfehler in der Adresse.",
|
||||
@@ -553,14 +553,14 @@
|
||||
"socket": "Socket",
|
||||
"tcp": "TCP / HTTP",
|
||||
"Docker Container": "Docker Container",
|
||||
"Container Name / ID": "Container Name / ID",
|
||||
"Docker Host": "Docker Host",
|
||||
"Docker Hosts": "Docker Hosts",
|
||||
"Container Name / ID": "Container-Bezeichnung / ID",
|
||||
"Docker Host": "Docker-Host",
|
||||
"Docker Hosts": "Docker-Hosts",
|
||||
"ntfy Topic": "ntfy Thema",
|
||||
"Domain": "Domain",
|
||||
"Workstation": "Workstation",
|
||||
"disableCloudflaredNoAuthMsg": "Du bist im nicht-authentifizieren Modus, ein Passwort wird nicht benötigt.",
|
||||
"trustProxyDescription": "Vertraue 'X-Forwarded-*' headern. Wenn man die richtige client IP haben möchte und Uptime Kuma hinter einem Proxy wie Nginx or Apache läuft, wollte dies aktiviert werden.",
|
||||
"trustProxyDescription": "Vertraue 'X-Forwarded-*' headern. Wenn man die richtige Client IP haben möchte und Uptime Kuma hinter einem Proxy wie Nginx oder Apache läuft, sollte dies aktiviert werden.",
|
||||
"wayToGetLineNotifyToken": "Du kannst hier ein Token erhalten: {0}",
|
||||
"Examples": "Beispiele",
|
||||
"Home Assistant URL": "Home Assistant URL",
|
||||
@@ -574,7 +574,7 @@
|
||||
"Event type:": "Ereignistyp:",
|
||||
"Event data:": "Ereignis daten:",
|
||||
"Then choose an action, for example switch the scene to where an RGB light is red.": "Dann eine Aktion wählen, zum Beispiel eine Scene wählen in der ein RGB Licht rot ist.",
|
||||
"Frontend Version": "Frontend Version",
|
||||
"Frontend Version": "Frontend-Version",
|
||||
"Frontend Version do not match backend version!": "Die Frontend Version stimmt nicht mit der backend version überein!",
|
||||
"Maintenance": "Wartung",
|
||||
"statusMaintenance": "Wartung",
|
||||
@@ -590,22 +590,22 @@
|
||||
"atLeastOneMonitor": "Wähle mindestens einen Monitor",
|
||||
"deleteMaintenanceMsg": "Möchtest du diese Wartung löschen?",
|
||||
"Base URL": "Basis URL",
|
||||
"goAlertInfo": "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Beauftragen Sie automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt. {0}",
|
||||
"goAlertInfo": "GoAlert ist eine Open-Source Applikation für Rufbereitschaftsplanung, automatische Eskalation und Benachrichtigung (z.B. SMS oder Telefonanrufe). Engagiere automatisch die richtige Person, auf die richtige Art und Weise und zum richtigen Zeitpunkt! {0}",
|
||||
"goAlertIntegrationKeyInfo": "Bekommt einen generischen API Schlüssel in folgenden Format \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\". Normalerweise entspricht dies dem Wert des Token aus der URL.",
|
||||
"goAlert": "GoAlert",
|
||||
"backupOutdatedWarning": "Veraltet: Da viele Funktionen hinzugefügt wurden und diese Sicherungsfunktion nicht mehr gepflegt wird, kann sie keine vollständige Sicherung erstellen oder wiederherstellen.",
|
||||
"backupOutdatedWarning": "Veraltet: Da viele Funktionen hinzugefügt wurden und diese Sicherungsfunktion nicht mehr gepflegt wird, kann keine vollständige Sicherung erstellen oder wiederherstellen werden.",
|
||||
"backupRecommend": "Bitte sichere stattdessen das Volume oder den Datenordner (./data/) direkt.",
|
||||
"Optional": "Optional",
|
||||
"squadcast": "Squadcast",
|
||||
"SendKey": "SendKey",
|
||||
"SMSManager API Docs": "SMSManager API Dokumente",
|
||||
"Gateway Type": "Gateway Type",
|
||||
"Gateway Type": "Gateway Typ",
|
||||
"SMSManager": "SMSManager",
|
||||
"You can divide numbers with": "Du kannst Zahlen teilen mit",
|
||||
"or": "oder",
|
||||
"recurringInterval": "Intervall",
|
||||
"Recurring": "Wiederkehrend",
|
||||
"Single Maintenance Window": "Einzigartiges Wartungsfenster",
|
||||
"Single Maintenance Window": "Einmaliges Wartungsfenster",
|
||||
"Maintenance Time Window of a Day": "Zeitfenster für die Wartung",
|
||||
"Effective Date Range": "Bereich der Wirksamkeitsdaten",
|
||||
"strategyManual": "Aktiv/Inaktiv Manuell",
|
||||
@@ -641,7 +641,107 @@
|
||||
"Help": "Hilfe",
|
||||
"Game": "Spiel",
|
||||
"Custom": "Benutzerdefiniert",
|
||||
"Enable DNS Cache": "DNS Cache aktivieren",
|
||||
"Enable DNS Cache": "DNS-Cache aktivieren",
|
||||
"Enable": "Aktivieren",
|
||||
"Disable": "Deaktivieren"
|
||||
"Disable": "Deaktivieren",
|
||||
"Custom Monitor Type": "Benutzerdefinierter Monitortyp",
|
||||
"webhookAdditionalHeadersDesc": "Legt zusätzliche Header fest, die mit der Webhook gesendet wurden.",
|
||||
"dnsCacheDescription": "In einigen IPv6-Umgebungen funktioniert es möglicherweise nicht. Deaktiviere es, wenn Probleme auftreten.",
|
||||
"loadingError": "Die Daten konnten nicht abgerufen werden, bitte später noch einmal versuchen.",
|
||||
"confirmUninstallPlugin": "Möchtest du dieses Plugin wirklich deinstallieren?",
|
||||
"grpcMethodDescription": "Der Name der Methode wird in das \"cammelCase\"-Format konvertiert (z.B. sayHello, check, etc.)",
|
||||
"Passive Monitor Type": "Passiver Monitortyp",
|
||||
"Specific Monitor Type": "Spezifischer Monitortyp",
|
||||
"webhookAdditionalHeadersTitle": "Zusätzliche Header",
|
||||
"Packet Size": "Paketgröße",
|
||||
"IconUrl": "Symbol-URL",
|
||||
"wayToGetZohoCliqURL": "Wie eine Webhook URL erstellt werden kann, erfährst du {0}.",
|
||||
"dataRetentionTimeError": "Aufbewahrungszeit muss 0 oder größer sein",
|
||||
"infiniteRetention": "Für unendliche Aufbewahrung auf 0 setzen.",
|
||||
"confirmDeleteTagMsg": "Möchtest du dieses Tag wirklich löschen? Monitore, die mit diesem Tag verknüpft sind, werden nicht gelöscht.",
|
||||
"enableGRPCTls": "Senden von gRPC-Anforderungen mit TLS-Verbindung zulassen",
|
||||
"ZohoCliq": "ZohoCliq",
|
||||
"Monitor": "Überwachung | Monitore",
|
||||
"plugin": "Plugin | Plugins",
|
||||
"install": "Installieren",
|
||||
"installing": "Installiere",
|
||||
"uninstall": "Deinstallieren",
|
||||
"uninstalling": "Deinstallation",
|
||||
"markdownSupported": "Markdown-Syntax unterstützt",
|
||||
"wayToGetKookBotToken": "Erstelle eine Anwendung und erhalte den Bot-Token unter {0}",
|
||||
"wayToGetKookGuildID": "Schalte den „Entwicklermodus“ in den Kook-Einstellungen ein und klicke mit der rechten Maustaste auf die Gilde, um die ID zu erhalten",
|
||||
"Guild ID": "Guild-ID",
|
||||
"Free Mobile User Identifier": "Kostenlose mobile Benutzerkennung",
|
||||
"Free Mobile API Key": "Kostenloser Mobile API-Schlüssel",
|
||||
"Enable TLS": "Aktiviere TLS",
|
||||
"Proto Service Name": "Name des Proto-Dienstes",
|
||||
"Proto Method": "Proto-Methode",
|
||||
"Proto Content": "Proto-Inhalt",
|
||||
"Economy": "Wirtschaft",
|
||||
"Lowcost": "Kostengünstig",
|
||||
"high": "hoch",
|
||||
"promosmsAllowLongSMS": "Erlaube lange SMS",
|
||||
"General Monitor Type": "Allgemeiner Monitortyp",
|
||||
"smseagle": "SMSEagle",
|
||||
"smseagleTo": "Telefonnummer(n)",
|
||||
"smseagleGroup": "Telefonbuch Gruppenname(n)",
|
||||
"smseagleContact": "Telefonbuch Kontaktname(n)",
|
||||
"smseagleRecipientType": "Empfängertyp",
|
||||
"smseagleRecipient": "Empfänger (mehrere müssen durch Komma getrennt werden)",
|
||||
"smseagleToken": "API-Zugriffstoken",
|
||||
"smseagleUrl": "Ihre SMSEagle-Geräte-URL",
|
||||
"Kook": "Kook",
|
||||
"smseagleEncoding": "Als Unicode senden",
|
||||
"smseaglePriority": "Nachrichtenpriorität (0-9, Standard = 0)",
|
||||
"Google Analytics ID": "Google Analytics ID",
|
||||
"Edit Tag": "bearbeite Tag",
|
||||
"Server Address": "Server Adresse",
|
||||
"Learn More": "Erfahre mehr",
|
||||
"Body Encoding": "Körperkodierung",
|
||||
"Add API Key": "API Schlüssel hinzufügen",
|
||||
"apiKey-active": "Aktiv",
|
||||
"apiKey-expired": "Abgelaufen",
|
||||
"apiKey-inactive": "Inaktiv",
|
||||
"Expires": "Läuft ab",
|
||||
"deleteAPIKeyMsg": "Bist du sicher, dass du diesen API Schlüssel löschen willst?",
|
||||
"Generate": "Generieren",
|
||||
"API Keys": "API Schlüssel",
|
||||
"Expiry": "Ablauf",
|
||||
"Expiry date": "Ablaufdatum",
|
||||
"Don't expire": "Nicht ablaufen",
|
||||
"Continue": "Weiter",
|
||||
"Add Another": "Hinzufügen",
|
||||
"Clone Monitor": "Duplikat von",
|
||||
"Clone": "Duplizieren",
|
||||
"cloneOf": "Duplikat von {0}",
|
||||
"pagertreeIntegrationUrl": "Integrations URL",
|
||||
"pagertreeUrgency": "Dringlichkeit",
|
||||
"pagertreeSilent": "Leise",
|
||||
"pagertreeLow": "Niedrig",
|
||||
"pagertreeMedium": "Medium",
|
||||
"pagertreeHigh": "Hoch",
|
||||
"pagertreeCritical": "Kritisch",
|
||||
"pagertreeResolve": "Automatisch Auflösen",
|
||||
"No API Keys": "Keine API Schlüssel",
|
||||
"disableAPIKeyMsg": "Bist du sicher, dass du diesen API Schlüssel deaktivieren willst?",
|
||||
"pagertreeDoNothing": "Nichts tun",
|
||||
"wayToGetPagerTreeIntegrationURL": "Nachdem du die Uptime Kuma Integration in PagerTree erstellt hast, kopiere den Endpunkt. Siehe details {0}",
|
||||
"telegramProtectContent": "Schütze gegen Weiterleiten/Speichern der Nachricht",
|
||||
"telegramProtectContentDescription": "Die Bot-Nachrichten in Telegram sind gegen Weiterleitung und Speichern geschützt.",
|
||||
"notificationRegional": "Regional",
|
||||
"Key Added": "Schlüssel hinzugefügt",
|
||||
"apiKeyAddedMsg": "API Schlüssel wurde hinzugefügt. Bitte notiere den Schlüssel, da er nicht erneut angezeigt wird.",
|
||||
"telegramMessageThreadID": "(Optional) Nachrichten Thread ID",
|
||||
"telegramMessageThreadIDDescription": "Optionale eindeutige Kennung für den Ziel-Thread (Thema) des Forums; nur für Forum-Supergroups",
|
||||
"telegramSendSilently": "Stumm Senden",
|
||||
"telegramSendSilentlyDescription": "Sende die Nachricht stumm. Nutzer bekommen eine Benachrichtigung ohne Ton.",
|
||||
"Add New Tag": "Neuen Tag hinzufügen",
|
||||
"lunaseaDeviceID": "Geräte-ID",
|
||||
"lunaseaTarget": "Ziel",
|
||||
"lunaseaUserID": "Benutzer-ID",
|
||||
"twilioAccountSID": "Account SID",
|
||||
"twilioFromNumber": "Absender",
|
||||
"twilioToNumber": "Empfänger",
|
||||
"twilioAuthToken": "Auth Token",
|
||||
"statusPageRefreshIn": "Aktualisierung in: {0}"
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user