Merge branch 'master' into feature/#1891-set-ping-packet-size

pull/1892/head
Matthew Nickson 2 years ago committed by GitHub
commit c3d655afb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -23,7 +23,7 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec
## ⭐ Features ## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server. * Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
* Fancy, Reactive, Fast UI/UX. * Fancy, Reactive, Fast UI/UX.
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications). * Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
* 20 second intervals. * 20 second intervals.

@ -0,0 +1,18 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
CREATE TABLE docker_host (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
user_id INT NOT NULL,
docker_daemon VARCHAR(255),
docker_type VARCHAR(255),
name VARCHAR(255)
);
ALTER TABLE monitor
ADD docker_host INTEGER REFERENCES docker_host(id);
ALTER TABLE monitor
ADD docker_container VARCHAR(255);
COMMIT;

@ -0,0 +1,18 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD radius_username VARCHAR(255);
ALTER TABLE monitor
ADD radius_password VARCHAR(255);
ALTER TABLE monitor
ADD radius_calling_station_id VARCHAR(50);
ALTER TABLE monitor
ADD radius_called_station_id VARCHAR(50);
ALTER TABLE monitor
ADD radius_secret VARCHAR(255);
COMMIT

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

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

@ -11,7 +11,7 @@ WORKDIR /app
RUN apt update && \ 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 \ 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 && \ sqlite3 iputils-ping util-linux dumb-init && \
pip3 --no-cache-dir install apprise==0.9.9 && \ pip3 --no-cache-dir install apprise==1.0.0 && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove apt --yes autoremove

366
package-lock.json generated

@ -16,6 +16,7 @@
"badge-maker": "^3.3.1", "badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"bree": "~7.1.5", "bree": "~7.1.5",
"cacheable-lookup": "~6.0.4",
"chardet": "^1.3.0", "chardet": "^1.3.0",
"check-password-strength": "^2.0.5", "check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10", "cheerio": "^1.0.0-rc.10",
@ -38,15 +39,18 @@
"mqtt": "^4.2.8", "mqtt": "^4.2.8",
"mssql": "^8.1.0", "mssql": "^8.1.0",
"node-cloudflared-tunnel": "~1.0.9", "node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "^1.0.0",
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
"password-hash": "~1.2.2", "password-hash": "~1.2.2",
"pg": "^8.7.3",
"pg-connection-string": "^2.5.0",
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1", "prometheus-api-metrics": "~3.2.1",
"redbean-node": "0.1.4", "redbean-node": "0.1.4",
"socket.io": "~4.4.1", "socket.io": "~4.4.1",
"socket.io-client": "~4.4.1", "socket.io-client": "~4.4.1",
"socks-proxy-agent": "^6.1.1", "socks-proxy-agent": "6.1.1",
"tar": "^6.1.11", "tar": "^6.1.11",
"tcp-ping": "~0.1.1", "tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2" "thirty-two": "~1.0.2"
@ -4788,6 +4792,14 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
}, },
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"engines": {
"node": ">=4"
}
},
"node_modules/bulk-write-stream": { "node_modules/bulk-write-stream": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/bulk-write-stream/-/bulk-write-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/bulk-write-stream/-/bulk-write-stream-2.0.1.tgz",
@ -4806,6 +4818,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/cacheable-lookup": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
"integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==",
"engines": {
"node": ">=10.6.0"
}
},
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@ -8196,6 +8216,12 @@
"readable-stream": "^3.6.0" "readable-stream": "^3.6.0"
} }
}, },
"node_modules/hoek": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz",
"integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==",
"deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues."
},
"node_modules/homedir-polyfill": { "node_modules/homedir-polyfill": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@ -8896,6 +8922,17 @@
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"devOptional": true "devOptional": true
}, },
"node_modules/isemail": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
"integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
"dependencies": {
"punycode": "2.x.x"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -12132,6 +12169,32 @@
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true "dev": true
}, },
"node_modules/node-radius-client": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-radius-client/-/node-radius-client-1.0.0.tgz",
"integrity": "sha512-FkR9cMV5hNoX+kKDUTzuagvEixlLiaEJQ1/ywOdhahsihKrGDhVZmnCvmrCStA589MT3yuC/J2eKc6z68IGdBw==",
"dependencies": {
"joi": "^14.3.1",
"node-radius-utils": "^1.2.0",
"radius": "^1.1.4"
}
},
"node_modules/node-radius-client/node_modules/joi": {
"version": "14.3.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-14.3.1.tgz",
"integrity": "sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ==",
"deprecated": "This module has moved and is now available at @hapi/joi. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.",
"dependencies": {
"hoek": "6.x.x",
"isemail": "3.x.x",
"topo": "3.x.x"
}
},
"node_modules/node-radius-utils": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/node-radius-utils/-/node-radius-utils-1.2.0.tgz",
"integrity": "sha512-i3Sf6khnenl0aXumo0whAlfPWTaBqHxEnVBBxpu3dZ7q69NkPPv71rvPjlDZ5wkeKCTNNUTECljerS5kcYQxRw=="
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz",
@ -12479,6 +12542,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -12627,11 +12695,88 @@
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"optional": true "optional": true
}, },
"node_modules/pg": {
"version": "8.7.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz",
"integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.1",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"peerDependencies": {
"pg-native": ">=2.0.0"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-connection-string": { "node_modules/pg-connection-string": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
}, },
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz",
"integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/pgpass/node_modules/split2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz",
"integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -12887,6 +13032,41 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true "dev": true
}, },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -13293,6 +13473,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/radius": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/radius/-/radius-1.1.4.tgz",
"integrity": "sha512-UWuzdF6xf3NpsXFZZmUEkxtEalDXj8hdmMXgbGzn7vOk6zXNsiIY2I6SJ1euHt7PTQuMoz2qDEJB+AfJDJgQYw==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -14297,13 +14485,13 @@
} }
}, },
"node_modules/socks-proxy-agent": { "node_modules/socks-proxy-agent": {
"version": "6.2.1", "version": "6.1.1",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz",
"integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==",
"dependencies": { "dependencies": {
"agent-base": "^6.0.2", "agent-base": "^6.0.2",
"debug": "^4.3.3", "debug": "^4.3.1",
"socks": "^2.6.2" "socks": "^2.6.1"
}, },
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
@ -15125,6 +15313,15 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/topo": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
"integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
"deprecated": "This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.",
"dependencies": {
"hoek": "6.x.x"
}
},
"node_modules/toposort": { "node_modules/toposort": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
@ -20007,6 +20204,11 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
}, },
"buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
},
"bulk-write-stream": { "bulk-write-stream": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/bulk-write-stream/-/bulk-write-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/bulk-write-stream/-/bulk-write-stream-2.0.1.tgz",
@ -20022,6 +20224,11 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="
}, },
"cacheable-lookup": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
"integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A=="
},
"call-bind": { "call-bind": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@ -22495,6 +22702,11 @@
"readable-stream": "^3.6.0" "readable-stream": "^3.6.0"
} }
}, },
"hoek": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz",
"integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ=="
},
"homedir-polyfill": { "homedir-polyfill": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@ -22977,6 +23189,14 @@
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"devOptional": true "devOptional": true
}, },
"isemail": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
"integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
"requires": {
"punycode": "2.x.x"
}
},
"isexe": { "isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -25472,6 +25692,33 @@
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true "dev": true
}, },
"node-radius-client": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-radius-client/-/node-radius-client-1.0.0.tgz",
"integrity": "sha512-FkR9cMV5hNoX+kKDUTzuagvEixlLiaEJQ1/ywOdhahsihKrGDhVZmnCvmrCStA589MT3yuC/J2eKc6z68IGdBw==",
"requires": {
"joi": "^14.3.1",
"node-radius-utils": "^1.2.0",
"radius": "^1.1.4"
},
"dependencies": {
"joi": {
"version": "14.3.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-14.3.1.tgz",
"integrity": "sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ==",
"requires": {
"hoek": "6.x.x",
"isemail": "3.x.x",
"topo": "3.x.x"
}
}
}
},
"node-radius-utils": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/node-radius-utils/-/node-radius-utils-1.2.0.tgz",
"integrity": "sha512-i3Sf6khnenl0aXumo0whAlfPWTaBqHxEnVBBxpu3dZ7q69NkPPv71rvPjlDZ5wkeKCTNNUTECljerS5kcYQxRw=="
},
"node-releases": { "node-releases": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz",
@ -25722,6 +25969,11 @@
"p-timeout": "^3.0.0" "p-timeout": "^3.0.0"
} }
}, },
"packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"parent-module": { "parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -25831,11 +26083,67 @@
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"optional": true "optional": true
}, },
"pg": {
"version": "8.7.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz",
"integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==",
"requires": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.1",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
}
},
"pg-connection-string": { "pg-connection-string": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
}, },
"pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
},
"pg-pool": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz",
"integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ=="
},
"pg-protocol": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
},
"pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"requires": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
}
},
"pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"requires": {
"split2": "^4.1.0"
},
"dependencies": {
"split2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz",
"integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ=="
}
}
},
"picocolors": { "picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -26004,6 +26312,29 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true "dev": true
}, },
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
},
"postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="
},
"postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="
},
"postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"requires": {
"xtend": "^4.0.0"
}
},
"prelude-ls": { "prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -26302,6 +26633,11 @@
"integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
"dev": true "dev": true
}, },
"radius": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/radius/-/radius-1.1.4.tgz",
"integrity": "sha512-UWuzdF6xf3NpsXFZZmUEkxtEalDXj8hdmMXgbGzn7vOk6zXNsiIY2I6SJ1euHt7PTQuMoz2qDEJB+AfJDJgQYw=="
},
"range-parser": { "range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -27081,13 +27417,13 @@
} }
}, },
"socks-proxy-agent": { "socks-proxy-agent": {
"version": "6.2.1", "version": "6.1.1",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz",
"integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==",
"requires": { "requires": {
"agent-base": "^6.0.2", "agent-base": "^6.0.2",
"debug": "^4.3.3", "debug": "^4.3.1",
"socks": "^2.6.2" "socks": "^2.6.1"
} }
}, },
"sortablejs": { "sortablejs": {
@ -27737,6 +28073,14 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
}, },
"topo": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
"integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
"requires": {
"hoek": "6.x.x"
}
},
"toposort": { "toposort": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.17.1", "version": "1.18.0-beta.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -68,6 +68,7 @@
"badge-maker": "^3.3.1", "badge-maker": "^3.3.1",
"bcryptjs": "~2.4.3", "bcryptjs": "~2.4.3",
"bree": "~7.1.5", "bree": "~7.1.5",
"cacheable-lookup": "~6.0.4",
"chardet": "^1.3.0", "chardet": "^1.3.0",
"check-password-strength": "^2.0.5", "check-password-strength": "^2.0.5",
"cheerio": "^1.0.0-rc.10", "cheerio": "^1.0.0-rc.10",
@ -90,15 +91,18 @@
"mqtt": "^4.2.8", "mqtt": "^4.2.8",
"mssql": "^8.1.0", "mssql": "^8.1.0",
"node-cloudflared-tunnel": "~1.0.9", "node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "^1.0.0",
"nodemailer": "~6.6.5", "nodemailer": "~6.6.5",
"notp": "~2.0.3", "notp": "~2.0.3",
"password-hash": "~1.2.2", "password-hash": "~1.2.2",
"pg": "^8.7.3",
"pg-connection-string": "^2.5.0",
"prom-client": "~13.2.0", "prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.1", "prometheus-api-metrics": "~3.2.1",
"redbean-node": "0.1.4", "redbean-node": "0.1.4",
"socket.io": "~4.4.1", "socket.io": "~4.4.1",
"socket.io-client": "~4.4.1", "socket.io-client": "~4.4.1",
"socks-proxy-agent": "^6.1.1", "socks-proxy-agent": "6.1.1",
"tar": "^6.1.11", "tar": "^6.1.11",
"tcp-ping": "~0.1.1", "tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2" "thirty-two": "~1.0.2"

@ -0,0 +1,54 @@
const https = require("https");
const http = require("http");
const CacheableLookup = require("cacheable-lookup");
class CacheableDnsHttpAgent {
static cacheable = new CacheableLookup();
static httpAgentList = {};
static httpsAgentList = {};
/**
* Register cacheable to global agents
*/
static registerGlobalAgent() {
this.cacheable.install(http.globalAgent);
this.cacheable.install(https.globalAgent);
}
static install(agent) {
this.cacheable.install(agent);
}
/**
* @var {https.AgentOptions} agentOptions
* @return {https.Agent}
*/
static getHttpsAgent(agentOptions) {
let key = JSON.stringify(agentOptions);
if (!(key in this.httpsAgentList)) {
this.httpsAgentList[key] = new https.Agent(agentOptions);
this.cacheable.install(this.httpsAgentList[key]);
}
return this.httpsAgentList[key];
}
/**
* @var {http.AgentOptions} agentOptions
* @return {https.Agents}
*/
static getHttpAgent(agentOptions) {
let key = JSON.stringify(agentOptions);
if (!(key in this.httpAgentList)) {
this.httpAgentList[key] = new http.Agent(agentOptions);
this.cacheable.install(this.httpAgentList[key]);
}
return this.httpAgentList[key];
}
}
module.exports = {
CacheableDnsHttpAgent,
};

@ -125,10 +125,35 @@ async function sendInfo(socket) {
}); });
} }
/**
* Send list of docker hosts to client
* @param {Socket} socket Socket.io socket instance
* @returns {Promise<Bean[]>}
*/
async function sendDockerHostList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("docker_host", " user_id = ? ", [
socket.userID,
]);
for (let bean of list) {
result.push(bean.toJSON());
}
io.to(socket.userID).emit("dockerHostList", result);
timeLogger.print("Send Docker Host List");
return list;
}
module.exports = { module.exports = {
sendNotificationList, sendNotificationList,
sendImportantHeartbeatList, sendImportantHeartbeatList,
sendHeartbeatList, sendHeartbeatList,
sendProxyList, sendProxyList,
sendInfo, sendInfo,
sendDockerHostList
}; };

@ -53,6 +53,7 @@ class Database {
"patch-2fa-invalidate-used-token.sql": true, "patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true, "patch-notification_sent_history.sql": true,
"patch-monitor-basic-auth.sql": true, "patch-monitor-basic-auth.sql": true,
"patch-add-docker-columns.sql": true,
"patch-status-page.sql": true, "patch-status-page.sql": true,
"patch-proxy.sql": true, "patch-proxy.sql": true,
"patch-monitor-expiry-notification.sql": true, "patch-monitor-expiry-notification.sql": true,
@ -61,6 +62,8 @@ class Database {
"patch-add-clickable-status-page-link.sql": true, "patch-add-clickable-status-page-link.sql": true,
"patch-add-sqlserver-monitor.sql": true, "patch-add-sqlserver-monitor.sql": true,
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] }, "patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
"patch-add-radius-monitor.sql": true,
"patch-monitor-add-resend-interval.sql": true,
"patch-ping-packet-size.sql": true, "patch-ping-packet-size.sql": true,
}; };
@ -148,6 +151,9 @@ class Database {
await R.exec("PRAGMA cache_size = -12000"); await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = FULL"); await R.exec("PRAGMA auto_vacuum = FULL");
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
await R.exec("PRAGMA busy_timeout = 5000");
// This ensures that an operating system crash or power failure will not corrupt the database. // This ensures that an operating system crash or power failure will not corrupt the database.
// FULL synchronous is very safe, but it is also slower. // FULL synchronous is very safe, but it is also slower.
// Read more: https://sqlite.org/pragma.html#pragma_synchronous // Read more: https://sqlite.org/pragma.html#pragma_synchronous

@ -0,0 +1,106 @@
const axios = require("axios");
const { R } = require("redbean-node");
const version = require("../package.json").version;
const https = require("https");
class DockerHost {
/**
* Save a docker host
* @param {Object} dockerHost Docker host to save
* @param {?number} dockerHostID ID of the docker host to update
* @param {number} userID ID of the user who adds the docker host
* @returns {Promise<Bean>}
*/
static async save(dockerHost, dockerHostID, userID) {
let bean;
if (dockerHostID) {
bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
if (!bean) {
throw new Error("docker host not found");
}
} else {
bean = R.dispense("docker_host");
}
bean.user_id = userID;
bean.docker_daemon = dockerHost.dockerDaemon;
bean.docker_type = dockerHost.dockerType;
bean.name = dockerHost.name;
await R.store(bean);
return bean;
}
/**
* Delete a Docker host
* @param {number} dockerHostID ID of the Docker host to delete
* @param {number} userID ID of the user who created the Docker host
* @returns {Promise<void>}
*/
static async delete(dockerHostID, userID) {
let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
if (!bean) {
throw new Error("docker host not found");
}
// Delete removed proxy from monitors if exists
await R.exec("UPDATE monitor SET docker_host = null WHERE docker_host = ?", [ dockerHostID ]);
await R.trash(bean);
}
/**
* Fetches the amount of containers on the Docker host
* @param {Object} dockerHost Docker host to check for
* @returns {number} Total amount of containers on the host
*/
static async testDockerHost(dockerHost) {
const options = {
url: "/containers/json?all=true",
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: false,
}),
};
if (dockerHost.dockerType === "socket") {
options.socketPath = dockerHost.dockerDaemon;
} else if (dockerHost.dockerType === "tcp") {
options.baseURL = dockerHost.dockerDaemon;
}
let res = await axios.request(options);
if (Array.isArray(res.data)) {
if (res.data.length > 1) {
if ("ImageID" in res.data[0]) {
return res.data.length;
} else {
throw new Error("Invalid Docker response, is it Docker really a daemon?");
}
} else {
return res.data.length;
}
} else {
throw new Error("Invalid Docker response, is it Docker really a daemon?");
}
}
}
module.exports = {
DockerHost,
};

@ -0,0 +1,19 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
class DockerHost extends BeanModel {
/**
* Returns an object that ready to parse to JSON
* @returns {Object}
*/
toJSON() {
return {
id: this.id,
userID: this.user_id,
dockerDaemon: this.docker_daemon,
dockerType: this.docker_type,
name: this.name,
};
}
}
module.exports = DockerHost;

@ -7,7 +7,7 @@ dayjs.extend(timezone);
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mqttAsync, setSetting, httpNtlm } = require("../util-server"); const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification"); const { Notification } = require("../notification");
@ -16,6 +16,7 @@ const { demoMode } = require("../config");
const version = require("../../package.json").version; const version = require("../../package.json").version;
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server"); const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
/** /**
* status: * status:
@ -78,6 +79,7 @@ class Monitor extends BeanModel {
type: this.type, type: this.type,
interval: this.interval, interval: this.interval,
retryInterval: this.retryInterval, retryInterval: this.retryInterval,
resendInterval: this.resendInterval,
keyword: this.keyword, keyword: this.keyword,
expiryNotification: this.isEnabledExpiryNotification(), expiryNotification: this.isEnabledExpiryNotification(),
ignoreTls: this.getIgnoreTls(), ignoreTls: this.getIgnoreTls(),
@ -88,6 +90,9 @@ class Monitor extends BeanModel {
dns_resolve_type: this.dns_resolve_type, dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server, dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result, dns_last_result: this.dns_last_result,
pushToken: this.pushToken,
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id, proxyId: this.proxy_id,
notificationIDList, notificationIDList,
tags: tags, tags: tags,
@ -100,6 +105,11 @@ class Monitor extends BeanModel {
authMethod: this.authMethod, authMethod: this.authMethod,
authWorkstation: this.authWorkstation, authWorkstation: this.authWorkstation,
authDomain: this.authDomain, authDomain: this.authDomain,
radiusUsername: this.radiusUsername,
radiusPassword: this.radiusPassword,
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
radiusSecret: this.radiusSecret,
}; };
if (includeSensitiveData) { if (includeSensitiveData) {
@ -206,6 +216,7 @@ class Monitor extends BeanModel {
bean.monitor_id = this.id; bean.monitor_id = this.id;
bean.time = R.isoDateTimeMillis(dayjs.utc()); bean.time = R.isoDateTimeMillis(dayjs.utc());
bean.status = DOWN; bean.status = DOWN;
bean.downCount = previousBeat?.downCount || 0;
if (this.isUpsideDown()) { if (this.isUpsideDown()) {
bean.status = flipStatus(bean.status); bean.status = flipStatus(bean.status);
@ -441,10 +452,13 @@ class Monitor extends BeanModel {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
}, },
httpsAgent: new https.Agent({ httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(), rejectUnauthorized: !this.getIgnoreTls(),
}), }),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}),
maxRedirects: this.maxredirects, maxRedirects: this.maxredirects,
validateStatus: (status) => { validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes()); return checkStatusCode(status, this.getAcceptedStatuscodes());
@ -465,6 +479,35 @@ class Monitor extends BeanModel {
} else { } else {
throw new Error("Server not found on Steam"); throw new Error("Server not found on Steam");
} }
} else if (this.type === "docker") {
log.debug(`[${this.name}] Prepare Options for Axios`);
const dockerHost = await R.load("docker_host", this.docker_host);
const options = {
url: `/containers/${this.docker_container}/json`,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(),
}),
};
if (dockerHost._dockerType === "socket") {
options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") {
options.baseURL = dockerHost._dockerDaemon;
}
log.debug(`[${this.name}] Axios Request`);
let res = await axios.request(options);
if (res.data.State.Running) {
bean.status = UP;
bean.msg = "";
}
} else if (this.type === "mqtt") { } else if (this.type === "mqtt") {
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, { bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
port: this.port, port: this.port,
@ -481,6 +524,38 @@ class Monitor extends BeanModel {
bean.msg = ""; bean.msg = "";
bean.status = UP; bean.status = UP;
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "postgres") {
let startTime = dayjs().valueOf();
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "radius") {
let startTime = dayjs().valueOf();
try {
const resp = await radius(
this.hostname,
this.radiusUsername,
this.radiusPassword,
this.radiusCalledStationId,
this.radiusCallingStationId,
this.radiusSecret
);
if (resp.code) {
bean.msg = resp.code;
}
bean.status = UP;
} catch (error) {
bean.status = DOWN;
if (error.response?.code) {
bean.msg = error.response.code;
} else {
bean.msg = error.message;
}
}
bean.ping = dayjs().valueOf() - startTime;
} else { } else {
bean.msg = "Unknown Monitor Type"; bean.msg = "Unknown Monitor Type";
bean.status = PENDING; bean.status = PENDING;
@ -522,12 +597,27 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] sendNotification`); log.debug("monitor", `[${this.name}] sendNotification`);
await Monitor.sendNotification(isFirstBeat, this, bean); await Monitor.sendNotification(isFirstBeat, this, bean);
// Reset down count
bean.downCount = 0;
// Clear Status Page Cache // Clear Status Page Cache
log.debug("monitor", `[${this.name}] apicache clear`); log.debug("monitor", `[${this.name}] apicache clear`);
apicache.clear(); apicache.clear();
} else { } else {
bean.important = false; bean.important = false;
if (bean.status === DOWN && this.resendInterval > 0) {
++bean.downCount;
if (bean.downCount >= this.resendInterval) {
// Send notification again, because we are still DOWN
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
await Monitor.sendNotification(isFirstBeat, this, bean);
// Reset down count
bean.downCount = 0;
}
}
} }
if (bean.status === UP) { if (bean.status === UP) {
@ -538,7 +628,7 @@ class Monitor extends BeanModel {
} }
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`); log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
} else { } else {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`); log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
} }
log.debug("monitor", `[${this.name}] Send to socket`); log.debug("monitor", `[${this.name}] Send to socket`);

@ -0,0 +1,50 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
class AlertNow extends NotificationProvider {
name = "AlertNow";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let textMsg = "";
let status = "open";
let eventType = "ERROR";
let eventId = new Date().toISOString().slice(0, 10).replace(/-/g, "");
if (heartbeatJSON && heartbeatJSON.status === UP) {
textMsg = `[${heartbeatJSON.name}] ✅ Application is back online`;
status = "close";
eventType = "INFO";
eventId += `_${heartbeatJSON.name.replace(/\s/g, "")}`;
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
textMsg = `[${heartbeatJSON.name}] 🔴 Application went down`;
}
textMsg += ` - ${msg}`;
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
}
const data = {
"summary": textMsg,
"status": status,
"event_type": eventType,
"event_id": eventId,
};
await axios.post(notification.alertNowWebhookURL, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = AlertNow;

@ -12,9 +12,7 @@ const { default: axios } = require("axios");
// bark is an APN bridge that sends notifications to Apple devices. // bark is an APN bridge that sends notifications to Apple devices.
const barkNotificationGroup = "UptimeKuma";
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png"; const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
const barkNotificationSound = "telegraph";
const successMessage = "Successes!"; const successMessage = "Successes!";
class Bark extends NotificationProvider { class Bark extends NotificationProvider {
@ -50,13 +48,23 @@ class Bark extends NotificationProvider {
* @param {string} postUrl URL to append parameters to * @param {string} postUrl URL to append parameters to
* @returns {string} * @returns {string}
*/ */
appendAdditionalParameters(postUrl) { appendAdditionalParameters(notification, postUrl) {
// grouping all our notifications
postUrl += "?group=" + barkNotificationGroup;
// set icon to uptime kuma icon, 11kb should be fine // set icon to uptime kuma icon, 11kb should be fine
postUrl += "&icon=" + barkNotificationAvatar; postUrl += "&icon=" + barkNotificationAvatar;
// grouping all our notifications
if (notification.barkGroup != null) {
postUrl += "&group=" + notification.barkGroup;
} else {
// default name
postUrl += "&group=" + "UptimeKuma";
}
// picked a sound, this should follow system's mute status when arrival // picked a sound, this should follow system's mute status when arrival
postUrl += "&sound=" + barkNotificationSound; if (notification.barkSound != null) {
postUrl += "&sound=" + notification.barkSound;
} else {
// default sound
postUrl += "&sound=" + "telegraph";
}
return postUrl; return postUrl;
} }

@ -0,0 +1,38 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const defaultNotificationService = "notify";
class HomeAssistant extends NotificationProvider {
name = "HomeAssistant";
async send(notification, message, monitor = null, heartbeat = null) {
const notificationService = notification?.notificationService || defaultNotificationService;
try {
await axios.post(
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
{
title: "Uptime Kuma",
message,
...(notificationService !== "persistent_notification" && { data: {
name: monitor?.name,
status: heartbeat?.status,
} }),
},
{
headers: {
Authorization: `Bearer ${notification.longLivedAccessToken}`,
"Content-Type": "application/json",
},
}
);
return "Sent Successfully.";
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = HomeAssistant;

@ -0,0 +1,43 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const qs = require("qs");
const { DOWN, UP } = require("../../src/util");
class LineNotify extends NotificationProvider {
name = "LineNotify";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let lineAPIUrl = "https://notify-api.line.me/api/notify";
let config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer " + notification.lineNotifyAccessToken
}
};
if (heartbeatJSON == null) {
let testMessage = {
"message": msg,
};
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
} else if (heartbeatJSON["status"] === DOWN) {
let downMessage = {
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
};
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
} else if (heartbeatJSON["status"] === UP) {
let upMessage = {
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
};
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = LineNotify;

@ -1,40 +1,43 @@
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { log } = require("../src/util");
const Alerta = require("./notification-providers/alerta");
const AlertNow = require("./notification-providers/alertnow");
const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise"); const Apprise = require("./notification-providers/apprise");
const Bark = require("./notification-providers/bark");
const ClickSendSMS = require("./notification-providers/clicksendsms");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord"); const Discord = require("./notification-providers/discord");
const Feishu = require("./notification-providers/feishu");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Gotify = require("./notification-providers/gotify"); const Gotify = require("./notification-providers/gotify");
const Ntfy = require("./notification-providers/ntfy"); const HomeAssistant = require("./notification-providers/home-assistant");
const Line = require("./notification-providers/line"); const Line = require("./notification-providers/line");
const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea"); const LunaSea = require("./notification-providers/lunasea");
const Mattermost = require("./notification-providers/mattermost");
const Matrix = require("./notification-providers/matrix"); const Matrix = require("./notification-providers/matrix");
const Mattermost = require("./notification-providers/mattermost");
const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush"); const Octopush = require("./notification-providers/octopush");
const OneBot = require("./notification-providers/onebot");
const PagerDuty = require("./notification-providers/pagerduty");
const PromoSMS = require("./notification-providers/promosms"); const PromoSMS = require("./notification-providers/promosms");
const ClickSendSMS = require("./notification-providers/clicksendsms");
const Pushbullet = require("./notification-providers/pushbullet"); const Pushbullet = require("./notification-providers/pushbullet");
const PushDeer = require("./notification-providers/pushdeer");
const Pushover = require("./notification-providers/pushover"); const Pushover = require("./notification-providers/pushover");
const Pushy = require("./notification-providers/pushy"); const Pushy = require("./notification-providers/pushy");
const TechulusPush = require("./notification-providers/techulus-push");
const RocketChat = require("./notification-providers/rocket-chat"); const RocketChat = require("./notification-providers/rocket-chat");
const SerwerSMS = require("./notification-providers/serwersms");
const Signal = require("./notification-providers/signal"); const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack"); const Slack = require("./notification-providers/slack");
const SMTP = require("./notification-providers/smtp"); const SMTP = require("./notification-providers/smtp");
const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams"); const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push");
const Telegram = require("./notification-providers/telegram"); const Telegram = require("./notification-providers/telegram");
const Webhook = require("./notification-providers/webhook"); const Webhook = require("./notification-providers/webhook");
const Feishu = require("./notification-providers/feishu");
const AliyunSms = require("./notification-providers/aliyun-sms");
const DingDing = require("./notification-providers/dingding");
const Bark = require("./notification-providers/bark");
const { log } = require("../src/util");
const SerwerSMS = require("./notification-providers/serwersms");
const Stackfield = require("./notification-providers/stackfield");
const WeCom = require("./notification-providers/wecom"); const WeCom = require("./notification-providers/wecom");
const GoogleChat = require("./notification-providers/google-chat");
const PagerDuty = require("./notification-providers/pagerduty");
const Gorush = require("./notification-providers/gorush");
const Alerta = require("./notification-providers/alerta");
const OneBot = require("./notification-providers/onebot");
const PushDeer = require("./notification-providers/pushdeer");
class Notification { class Notification {
@ -47,41 +50,44 @@ class Notification {
this.providerList = {}; this.providerList = {};
const list = [ const list = [
new Apprise(), new Alerta(),
new AlertNow(),
new AliyunSms(), new AliyunSms(),
new Apprise(),
new Bark(),
new ClickSendSMS(),
new DingDing(), new DingDing(),
new Discord(), new Discord(),
new Teams(), new Feishu(),
new GoogleChat(),
new Gorush(),
new Gotify(), new Gotify(),
new Ntfy(), new HomeAssistant(),
new Line(), new Line(),
new LineNotify(),
new LunaSea(), new LunaSea(),
new Feishu(),
new Mattermost(),
new Matrix(), new Matrix(),
new Mattermost(),
new Ntfy(),
new Octopush(), new Octopush(),
new OneBot(),
new PagerDuty(),
new PromoSMS(), new PromoSMS(),
new ClickSendSMS(),
new Pushbullet(), new Pushbullet(),
new PushDeer(),
new Pushover(), new Pushover(),
new Pushy(), new Pushy(),
new TechulusPush(),
new RocketChat(), new RocketChat(),
new SerwerSMS(),
new Signal(), new Signal(),
new Slack(), new Slack(),
new SMTP(), new SMTP(),
new Stackfield(),
new Teams(),
new TechulusPush(),
new Telegram(), new Telegram(),
new Webhook(), new Webhook(),
new Bark(),
new SerwerSMS(),
new Stackfield(),
new WeCom(), new WeCom(),
new GoogleChat(),
new PagerDuty(),
new Gorush(),
new Alerta(),
new OneBot(),
new PushDeer(),
]; ];
for (let item of list) { for (let item of list) {

@ -118,13 +118,14 @@ if (config.demoMode) {
} }
// Must be after io instantiation // Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client"); const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler"); const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler"); const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const TwoFA = require("./2fa"); const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page"); const StatusPage = require("./model/status_page");
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler"); const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler"); const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
app.use(express.json()); app.use(express.json());
@ -164,12 +165,20 @@ let needSetup = false;
// Entry Page // Entry Page
app.get("/", async (request, response) => { app.get("/", async (request, response) => {
log.debug("entry", `Request Domain: ${request.hostname}`); let hostname = request.hostname;
if (await setting("trustProxy")) {
const proxy = request.headers["x-forwarded-host"];
if (proxy) {
hostname = proxy;
}
}
log.debug("entry", `Request Domain: ${hostname}`);
if (request.hostname in StatusPage.domainMappingList) { if (hostname in StatusPage.domainMappingList) {
log.debug("entry", "This is a status page domain"); log.debug("entry", "This is a status page domain");
let slug = StatusPage.domainMappingList[request.hostname]; let slug = StatusPage.domainMappingList[hostname];
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug); await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) { } else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
@ -246,7 +255,9 @@ let needSetup = false;
// *************************** // ***************************
socket.on("loginByToken", async (token, callback) => { socket.on("loginByToken", async (token, callback) => {
log.info("auth", `Login by token. IP=${getClientIp(socket)}`); const clientIP = await server.getClientIP(socket);
log.info("auth", `Login by token. IP=${clientIP}`);
try { try {
let decoded = jwt.verify(token, jwtSecret); let decoded = jwt.verify(token, jwtSecret);
@ -262,14 +273,14 @@ let needSetup = false;
afterLogin(socket, user); afterLogin(socket, user);
log.debug("auth", "afterLogin ok"); log.debug("auth", "afterLogin ok");
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${getClientIp(socket)}`); log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
callback({ callback({
ok: true, ok: true,
}); });
} else { } else {
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${getClientIp(socket)}`); log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`);
callback({ callback({
ok: false, ok: false,
@ -278,7 +289,7 @@ let needSetup = false;
} }
} catch (error) { } catch (error) {
log.error("auth", `Invalid token. IP=${getClientIp(socket)}`); log.error("auth", `Invalid token. IP=${clientIP}`);
callback({ callback({
ok: false, ok: false,
@ -289,7 +300,9 @@ let needSetup = false;
}); });
socket.on("login", async (data, callback) => { socket.on("login", async (data, callback) => {
log.info("auth", `Login by username + password. IP=${getClientIp(socket)}`); const clientIP = await server.getClientIP(socket);
log.info("auth", `Login by username + password. IP=${clientIP}`);
// Checking // Checking
if (typeof callback !== "function") { if (typeof callback !== "function") {
@ -302,7 +315,7 @@ let needSetup = false;
// Login Rate Limit // Login Rate Limit
if (! await loginRateLimiter.pass(callback)) { if (! await loginRateLimiter.pass(callback)) {
log.info("auth", `Too many failed requests for user ${data.username}. IP=${getClientIp(socket)}`); log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
return; return;
} }
@ -312,7 +325,7 @@ let needSetup = false;
if (user.twofa_status === 0) { if (user.twofa_status === 0) {
afterLogin(socket, user); afterLogin(socket, user);
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`); log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
callback({ callback({
ok: true, ok: true,
@ -324,7 +337,7 @@ let needSetup = false;
if (user.twofa_status === 1 && !data.token) { if (user.twofa_status === 1 && !data.token) {
log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`); log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`);
callback({ callback({
tokenRequired: true, tokenRequired: true,
@ -342,7 +355,7 @@ let needSetup = false;
socket.userID, socket.userID,
]); ]);
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`); log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
callback({ callback({
ok: true, ok: true,
@ -352,7 +365,7 @@ let needSetup = false;
}); });
} else { } else {
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${getClientIp(socket)}`); log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
callback({ callback({
ok: false, ok: false,
@ -362,7 +375,7 @@ let needSetup = false;
} }
} else { } else {
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${getClientIp(socket)}`); log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`);
callback({ callback({
ok: false, ok: false,
@ -434,6 +447,8 @@ let needSetup = false;
}); });
socket.on("save2FA", async (currentPassword, callback) => { socket.on("save2FA", async (currentPassword, callback) => {
const clientIP = await server.getClientIP(socket);
try { try {
if (! await twoFaRateLimiter.pass(callback)) { if (! await twoFaRateLimiter.pass(callback)) {
return; return;
@ -446,7 +461,7 @@ let needSetup = false;
socket.userID, socket.userID,
]); ]);
log.info("auth", `Saved 2FA token. IP=${getClientIp(socket)}`); log.info("auth", `Saved 2FA token. IP=${clientIP}`);
callback({ callback({
ok: true, ok: true,
@ -454,7 +469,7 @@ let needSetup = false;
}); });
} catch (error) { } catch (error) {
log.error("auth", `Error changing 2FA token. IP=${getClientIp(socket)}`); log.error("auth", `Error changing 2FA token. IP=${clientIP}`);
callback({ callback({
ok: false, ok: false,
@ -464,6 +479,8 @@ let needSetup = false;
}); });
socket.on("disable2FA", async (currentPassword, callback) => { socket.on("disable2FA", async (currentPassword, callback) => {
const clientIP = await server.getClientIP(socket);
try { try {
if (! await twoFaRateLimiter.pass(callback)) { if (! await twoFaRateLimiter.pass(callback)) {
return; return;
@ -473,7 +490,7 @@ let needSetup = false;
await doubleCheckPassword(socket, currentPassword); await doubleCheckPassword(socket, currentPassword);
await TwoFA.disable2FA(socket.userID); await TwoFA.disable2FA(socket.userID);
log.info("auth", `Disabled 2FA token. IP=${getClientIp(socket)}`); log.info("auth", `Disabled 2FA token. IP=${clientIP}`);
callback({ callback({
ok: true, ok: true,
@ -481,7 +498,7 @@ let needSetup = false;
}); });
} catch (error) { } catch (error) {
log.error("auth", `Error disabling 2FA token. IP=${getClientIp(socket)}`); log.error("auth", `Error disabling 2FA token. IP=${clientIP}`);
callback({ callback({
ok: false, ok: false,
@ -652,6 +669,7 @@ let needSetup = false;
bean.basic_auth_pass = monitor.basic_auth_pass; bean.basic_auth_pass = monitor.basic_auth_pass;
bean.interval = monitor.interval; bean.interval = monitor.interval;
bean.retryInterval = monitor.retryInterval; bean.retryInterval = monitor.retryInterval;
bean.resendInterval = monitor.resendInterval;
bean.hostname = monitor.hostname; bean.hostname = monitor.hostname;
bean.maxretries = monitor.maxretries; bean.maxretries = monitor.maxretries;
bean.port = parseInt(monitor.port); bean.port = parseInt(monitor.port);
@ -665,6 +683,8 @@ let needSetup = false;
bean.dns_resolve_type = monitor.dns_resolve_type; bean.dns_resolve_type = monitor.dns_resolve_type;
bean.dns_resolve_server = monitor.dns_resolve_server; bean.dns_resolve_server = monitor.dns_resolve_server;
bean.pushToken = monitor.pushToken; bean.pushToken = monitor.pushToken;
bean.docker_container = monitor.docker_container;
bean.docker_host = monitor.docker_host;
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null; bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
bean.mqttUsername = monitor.mqttUsername; bean.mqttUsername = monitor.mqttUsername;
bean.mqttPassword = monitor.mqttPassword; bean.mqttPassword = monitor.mqttPassword;
@ -675,6 +695,11 @@ let needSetup = false;
bean.authMethod = monitor.authMethod; bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation; bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain; bean.authDomain = monitor.authDomain;
bean.radiusUsername = monitor.radiusUsername;
bean.radiusPassword = monitor.radiusPassword;
bean.radiusCalledStationId = monitor.radiusCalledStationId;
bean.radiusCallingStationId = monitor.radiusCallingStationId;
bean.radiusSecret = monitor.radiusSecret;
await R.store(bean); await R.store(bean);
@ -1255,6 +1280,7 @@ let needSetup = false;
authDomain: monitorListData[i].authDomain, authDomain: monitorListData[i].authDomain,
interval: monitorListData[i].interval, interval: monitorListData[i].interval,
retryInterval: retryInterval, retryInterval: retryInterval,
resendInterval: monitorListData[i].resendInterval || 0,
hostname: monitorListData[i].hostname, hostname: monitorListData[i].hostname,
maxretries: monitorListData[i].maxretries, maxretries: monitorListData[i].maxretries,
port: monitorListData[i].port, port: monitorListData[i].port,
@ -1423,6 +1449,7 @@ let needSetup = false;
cloudflaredSocketHandler(socket); cloudflaredSocketHandler(socket);
databaseSocketHandler(socket); databaseSocketHandler(socket);
proxySocketHandler(socket); proxySocketHandler(socket);
dockerSocketHandler(socket);
log.debug("server", "added all socket handlers"); log.debug("server", "added all socket handlers");
@ -1523,6 +1550,7 @@ async function afterLogin(socket, user) {
let monitorList = await server.sendMonitorList(socket); let monitorList = await server.sendMonitorList(socket);
sendNotificationList(socket); sendNotificationList(socket);
sendProxyList(socket); sendProxyList(socket);
sendDockerHostList(socket);
await sleep(500); await sleep(500);
@ -1677,10 +1705,6 @@ async function shutdownFunction(signal) {
await cloudflaredStop(); await cloudflaredStop();
} }
function getClientIp(socket) {
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
}
/** Final function called before application exits */ /** Final function called before application exits */
function finalFunction() { function finalFunction() {
log.info("server", "Graceful shutdown successful!"); log.info("server", "Graceful shutdown successful!");

@ -0,0 +1,79 @@
const { sendDockerHostList } = require("../client");
const { checkLogin } = require("../util-server");
const { DockerHost } = require("../docker");
const { log } = require("../../src/util");
/**
* Handlers for docker hosts
* @param {Socket} socket Socket.io instance
*/
module.exports.dockerSocketHandler = (socket) => {
socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
try {
checkLogin(socket);
let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID);
await sendDockerHostList(socket);
callback({
ok: true,
msg: "Saved",
id: dockerHostBean.id,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("deleteDockerHost", async (dockerHostID, callback) => {
try {
checkLogin(socket);
await DockerHost.delete(dockerHostID, socket.userID);
await sendDockerHostList(socket);
callback({
ok: true,
msg: "Deleted",
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("testDockerHost", async (dockerHost, callback) => {
try {
checkLogin(socket);
let amount = await DockerHost.testDockerHost(dockerHost);
let msg;
if (amount > 1) {
msg = "Connected Successfully. Amount of containers: " + amount;
} else {
msg = "Connected Successfully, but there are no containers?";
}
callback({
ok: true,
msg,
});
} catch (e) {
log.error("docker", e);
callback({
ok: false,
msg: e.message,
});
}
});
};

@ -202,7 +202,11 @@ module.exports.statusPageSocketHandler = (socket) => {
relationBean.weight = monitorOrder++; relationBean.weight = monitorOrder++;
relationBean.group_id = groupBean.id; relationBean.group_id = groupBean.id;
relationBean.monitor_id = monitor.id; relationBean.monitor_id = monitor.id;
if (monitor.sendUrl !== undefined) {
relationBean.send_url = monitor.sendUrl; relationBean.send_url = monitor.sendUrl;
}
await R.store(relationBean); await R.store(relationBean);
} }

@ -7,6 +7,8 @@ const { R } = require("redbean-node");
const { log } = require("../src/util"); const { log } = require("../src/util");
const Database = require("./database"); const Database = require("./database");
const util = require("util"); const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
/** /**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue. * `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@ -49,7 +51,6 @@ class UptimeKumaServer {
log.info("server", "Creating express and socket.io instance"); log.info("server", "Creating express and socket.io instance");
this.app = express(); this.app = express();
if (sslKey && sslCert) { if (sslKey && sslCert) {
log.info("server", "Server Type: HTTPS"); log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({ this.httpServer = https.createServer({
@ -71,6 +72,8 @@ class UptimeKumaServer {
} }
} }
CacheableDnsHttpAgent.registerGlobalAgent();
this.io = new Server(this.httpServer); this.io = new Server(this.httpServer);
} }
@ -126,6 +129,22 @@ class UptimeKumaServer {
errorLogStream.end(); errorLogStream.end();
} }
async getClientIP(socket) {
let clientIP = socket.client.conn.remoteAddress;
if (clientIP === undefined) {
clientIP = "";
}
if (await Settings.get("trustProxy")) {
return socket.client.conn.request.headers["x-forwarded-for"]
|| socket.client.conn.request.headers["x-real-ip"]
|| clientIP.replace(/^.*:/, "");
} else {
return clientIP.replace(/^.*:/, "");
}
}
} }
module.exports = { module.exports = {

@ -11,8 +11,16 @@ const mqtt = require("mqtt");
const chroma = require("chroma-js"); const chroma = require("chroma-js");
const { badgeConstants } = require("./config"); const { badgeConstants } = require("./config");
const mssql = require("mssql"); const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const { NtlmClient } = require("axios-ntlm"); const { NtlmClient } = require("axios-ntlm");
const { Settings } = require("./settings"); const { Settings } = require("./settings");
const radiusClient = require("node-radius-client");
const {
dictionaries: {
rfc2865: { file, attributes },
},
} = require("node-radius-utils");
// From ping-lite // From ping-lite
exports.WIN = /^win/.test(process.platform); exports.WIN = /^win/.test(process.platform);
@ -241,10 +249,6 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
*/ */
exports.mssqlQuery = function (connectionString, query) { exports.mssqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
mssql.on("error", err => {
reject(err);
});
mssql.connect(connectionString).then(pool => { mssql.connect(connectionString).then(pool => {
return pool.request() return pool.request()
.query(query); .query(query);
@ -258,10 +262,67 @@ exports.mssqlQuery = function (connectionString, query) {
}); });
}; };
/**
* Run a query on Postgres
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.postgresQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const config = postgresConParse(connectionString);
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
return reject(new Error("Password is undefined."));
}
const client = new Client({ connectionString });
client.connect();
return client.query(query)
.then(res => {
resolve(res);
})
.catch(err => {
reject(err);
})
.finally(() => {
client.end();
});
});
};
exports.radius = function (
hostname,
username,
password,
calledStationId,
callingStationId,
secret,
) {
const client = new radiusClient({
host: hostname,
dictionaries: [ file ],
});
return client.accessRequest({
secret: secret,
attributes: [
[ attributes.USER_NAME, username ],
[ attributes.USER_PASSWORD, password ],
[ attributes.CALLING_STATION_ID, callingStationId ],
[ attributes.CALLED_STATION_ID, calledStationId ],
],
});
};
/** /**
* Retrieve value of setting based on key * Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve * @param {string} key Key of setting to retrieve
* @returns {Promise<any>} Value * @returns {Promise<any>} Value
* @deprecated Use await Settings.get(key)
*/ */
exports.setting = async function (key) { exports.setting = async function (key) {
return await Settings.get(key); return await Settings.get(key);
@ -387,7 +448,7 @@ exports.checkCertificate = function (res) {
/** /**
* Check if the provided status code is within the accepted ranges * Check if the provided status code is within the accepted ranges
* @param {string} status The status code to check * @param {number} status The status code to check
* @param {string[]} acceptedCodes An array of accepted status codes * @param {string[]} acceptedCodes An array of accepted status codes
* @returns {boolean} True if status code within range, false otherwise * @returns {boolean} True if status code within range, false otherwise
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string * @throws {Error} Will throw an error if the provided status code is not a valid range string or code string

@ -0,0 +1,177 @@
<template>
<form @submit.prevent="submit">
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title">
{{ $t("Setup Docker Host") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="mb-3">
<label for="docker-name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="docker-name" v-model="dockerHost.name" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="docker-type" class="form-label">{{ $t("Connection Type") }}</label>
<select id="docker-type" v-model="dockerHost.dockerType" class="form-select">
<option v-for="type in connectionTypes" :key="type" :value="type">{{ $t(type) }}</option>
</select>
</div>
<div class="mb-3">
<label for="docker-daemon" class="form-label">{{ $t("Docker Daemon") }}</label>
<input id="docker-daemon" v-model="dockerHost.dockerDaemon" type="text" class="form-control" required>
<div class="form-text">
{{ $t("Examples") }}:
<ul>
<li>/var/run/docker.sock</li>
<li>tcp://localhost:2375</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
{{ $t("Delete") }}
</button>
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
{{ $t("Test") }}
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Save") }}
</button>
</div>
</div>
</div>
</div>
</form>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteDockerHost">
{{ $t("deleteDockerHostMsg") }}
</Confirm>
</template>
<script lang="ts">
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
Confirm,
},
props: {},
emits: [ "added" ],
data() {
return {
model: null,
processing: false,
id: null,
connectionTypes: [ "socket", "tcp" ],
dockerHost: {
name: "",
dockerDaemon: "",
dockerType: "",
// Do not set default value here, please scroll to show()
}
};
},
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
show(dockerHostID) {
if (dockerHostID) {
let found = false;
this.id = dockerHostID;
for (let n of this.$root.dockerHostList) {
if (n.id === dockerHostID) {
this.dockerHost = n;
found = true;
break;
}
}
if (!found) {
toast.error("Docker Host not found!");
}
} else {
this.id = null;
this.dockerHost = {
name: "",
dockerType: "socket",
dockerDaemon: "/var/run/docker.sock",
};
}
this.modal.show();
},
submit() {
this.processing = true;
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
this.$root.toastRes(res);
this.processing = false;
if (res.ok) {
this.modal.hide();
// Emit added event, doesn't emit edit.
if (! this.id) {
this.$emit("added", res.id);
}
}
});
},
test() {
this.processing = true;
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
this.$root.toastRes(res);
this.processing = false;
});
},
deleteDockerHost() {
this.processing = true;
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {
this.$root.toastRes(res);
this.processing = false;
if (res.ok) {
this.modal.hide();
}
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dark {
.modal-dialog .form-text, .modal-dialog p {
color: $dark-font-color;
}
}
</style>

@ -0,0 +1,13 @@
<template>
<div class="mb-3">
<label for="alertnow-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="alertnow-webhook-url" v-model="$parent.notification.alertNowWebhookURL" type="text" class="form-control" required>
<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://service.opsnow.com/docs/alertnow/en/user-guide-alertnow-en.html#standard" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
</div>
</template>

@ -2,9 +2,6 @@
<div class="mb-3"> <div class="mb-3">
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label> <label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required> <input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
<div class="form-text">
<p><span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}</p>
</div>
<i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text"> <i18n-t tag="div" keypath="wayToGetTeamsURL" class="form-text">
<a <a
href="https://github.com/Finb/Bark" href="https://github.com/Finb/Bark"
@ -12,4 +9,45 @@
>{{ $t("here") }}</a> >{{ $t("here") }}</a>
</i18n-t> </i18n-t>
</div> </div>
<div class="mb-3">
<label for="Bark Group" class="form-label">{{ $t("Bark Group") }}</label>
<input id="Bark Group" v-model="$parent.notification.barkGroup" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="Bark Sound" class="form-label">{{ $t("Bark Sound") }}</label>
<select id="Bark Sound" v-model="$parent.notification.barkSound" class="form-select" required>
<option value="alarm">alarm</option>
<option value="anticipate">anticipate</option>
<option value="bell">bell</option>
<option value="birdsong">birdsong</option>
<option value="bloom">bloom</option>
<option value="calypso">calypso</option>
<option value="chime">chime</option>
<option value="choo">choo</option>
<option value="descent">descent</option>
<option value="electronic">electronic</option>
<option value="fanfare">fanfare</option>
<option value="glass">glass</option>
<option value="gotosleep">gotosleep</option>
<option value="healthnotification">healthnotification</option>
<option value="horn">horn</option>
<option value="ladder">ladder</option>
<option value="mailsent">mailsent</option>
<option value="minuet">minuet</option>
<option value="multiwayinvitation">multiwayinvitation</option>
<option value="newmail">newmail</option>
<option value="newsflash">newsflash</option>
<option value="noir">noir</option>
<option value="paymentsuccess">paymentsuccess</option>
<option value="shake">shake</option>
<option value="sherwoodforest">sherwoodforest</option>
<option value="silence">silence</option>
<option value="spell">spell</option>
<option value="suspense">suspense</option>
<option value="telegraph">telegraph</option>
<option value="tiptoes">tiptoes</option>
<option value="typewriters">typewriters</option>
<option value="update">update</option>
</select>
</div>
</template> </template>

@ -0,0 +1,40 @@
<template>
<div class="mb-3">
<label for="homeAssistantUrl" class="form-label">{{ $t("Home Assistant URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="homeAssistantUrl" v-model="$parent.notification.homeAssistantUrl" type="url" class="form-control" required>
</div>
<div class="mb-3">
<label for="longLivedAccessToken" class="form-label">{{ $t("Long-Lived Access Token") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="longLivedAccessToken" v-model="$parent.notification.longLivedAccessToken" type="text" class="form-control" required>
<div class="form-text">
<p>{{ $t("Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ") }}</p>
</div>
</div>
<div class="mb-3">
<label for="notificationService" class="form-label">{{ $t("Notification Service") }}</label>
<input id="notificationService" v-model="$parent.notification.notificationService" type="text" :placeholder="$t('default: notify all devices')" class="form-control">
<div class="form-text">
<p>{{ $t("A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.") }}</p>
<p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
<p>
{{ $t("Trigger type:") }} <code>Event</code><br />
{{ $t("Event type:") }} <code>call_service</code><br />
{{ $t("Event data:") }}
</p>
<pre>domain: notify
service: mobile_app_my_phone # change to your device name
service_data:
title: Uptime Kuma
data:
status: 0 # 0=down 1=up
# name: Optional Uptime Kuma Monitor Name to filter by</pre>
<p>
{{ $t("Then choose an action, for example switch the scene to where an RGB light is red.") }}
</p>
</div>
</div>
</template>

@ -0,0 +1,9 @@
<template>
<div class="mb-3">
<label for="line-notify-access-token" class="form-label">{{ $t("Access Token") }}</label>
<input id="line-notify-access-token" v-model="$parent.notification.lineNotifyAccessToken" type="text" class="form-control" :required="true">
</div>
<i18n-t tag="div" keypath="wayToGetLineNotifyToken" class="form-text" style="margin-top: 8px;">
<a href="https://notify-bot.line.me/" target="_blank">https://notify-bot.line.me/</a>
</i18n-t>
</template>

@ -1,38 +1,41 @@
import STMP from "./SMTP.vue"; import Alerta from "./Alerta.vue";
import Telegram from "./Telegram.vue"; import AlertNow from "./AlertNow.vue";
import AliyunSMS from "./AliyunSms.vue";
import Apprise from "./Apprise.vue";
import Bark from "./Bark.vue";
import ClickSendSMS from "./ClickSendSMS.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue"; import Discord from "./Discord.vue";
import Webhook from "./Webhook.vue"; import Feishu from "./Feishu.vue";
import Signal from "./Signal.vue"; import GoogleChat from "./GoogleChat.vue";
import Gorush from "./Gorush.vue";
import Gotify from "./Gotify.vue"; import Gotify from "./Gotify.vue";
import HomeAssistant from "./HomeAssistant.vue";
import Line from "./Line.vue";
import LineNotify from "./LineNotify.vue";
import LunaSea from "./LunaSea.vue";
import Matrix from "./Matrix.vue";
import Mattermost from "./Mattermost.vue";
import Ntfy from "./Ntfy.vue"; import Ntfy from "./Ntfy.vue";
import Slack from "./Slack.vue";
import RocketChat from "./RocketChat.vue";
import Teams from "./Teams.vue";
import Pushover from "./Pushover.vue";
import Pushy from "./Pushy.vue";
import TechulusPush from "./TechulusPush.vue";
import Octopush from "./Octopush.vue"; import Octopush from "./Octopush.vue";
import OneBot from "./OneBot.vue";
import PagerDuty from "./PagerDuty.vue";
import PromoSMS from "./PromoSMS.vue"; import PromoSMS from "./PromoSMS.vue";
import ClickSendSMS from "./ClickSendSMS.vue";
import LunaSea from "./LunaSea.vue";
import Feishu from "./Feishu.vue";
import Apprise from "./Apprise.vue";
import Pushbullet from "./Pushbullet.vue"; import Pushbullet from "./Pushbullet.vue";
import Line from "./Line.vue"; import PushDeer from "./PushDeer.vue";
import Mattermost from "./Mattermost.vue"; import Pushover from "./Pushover.vue";
import Matrix from "./Matrix.vue"; import Pushy from "./Pushy.vue";
import AliyunSMS from "./AliyunSms.vue"; import RocketChat from "./RocketChat.vue";
import DingDing from "./DingDing.vue";
import Bark from "./Bark.vue";
import SerwerSMS from "./SerwerSMS.vue"; import SerwerSMS from "./SerwerSMS.vue";
import Signal from "./Signal.vue";
import Slack from "./Slack.vue";
import Stackfield from "./Stackfield.vue"; import Stackfield from "./Stackfield.vue";
import STMP from "./SMTP.vue";
import Teams from "./Teams.vue";
import TechulusPush from "./TechulusPush.vue";
import Telegram from "./Telegram.vue";
import Webhook from "./Webhook.vue";
import WeCom from "./WeCom.vue"; import WeCom from "./WeCom.vue";
import GoogleChat from "./GoogleChat.vue";
import PagerDuty from "./PagerDuty.vue";
import Gorush from "./Gorush.vue";
import Alerta from "./Alerta.vue";
import OneBot from "./OneBot.vue";
import PushDeer from "./PushDeer.vue";
/** /**
* Manage all notification form. * Manage all notification form.
@ -40,41 +43,44 @@ import PushDeer from "./PushDeer.vue";
* @type { Record<string, any> } * @type { Record<string, any> }
*/ */
const NotificationFormList = { const NotificationFormList = {
"telegram": Telegram, "alerta": Alerta,
"webhook": Webhook, "AlertNow": AlertNow,
"smtp": STMP, "AliyunSMS": AliyunSMS,
"apprise": Apprise,
"Bark": Bark,
"clicksendsms": ClickSendSMS,
"DingDing": DingDing,
"discord": Discord, "discord": Discord,
"teams": Teams, "Feishu": Feishu,
"signal": Signal, "GoogleChat": GoogleChat,
"gorush": Gorush,
"gotify": Gotify, "gotify": Gotify,
"HomeAssistant": HomeAssistant,
"line": Line,
"LineNotify": LineNotify,
"lunasea": LunaSea,
"matrix": Matrix,
"mattermost": Mattermost,
"ntfy": Ntfy, "ntfy": Ntfy,
"slack": Slack,
"rocket.chat": RocketChat,
"pushover": Pushover,
"pushy": Pushy,
"PushByTechulus": TechulusPush,
"octopush": Octopush, "octopush": Octopush,
"OneBot": OneBot,
"PagerDuty": PagerDuty,
"promosms": PromoSMS, "promosms": PromoSMS,
"clicksendsms": ClickSendSMS,
"lunasea": LunaSea,
"Feishu": Feishu,
"AliyunSMS": AliyunSMS,
"apprise": Apprise,
"pushbullet": Pushbullet, "pushbullet": Pushbullet,
"line": Line, "PushByTechulus": TechulusPush,
"mattermost": Mattermost, "PushDeer": PushDeer,
"matrix": Matrix, "pushover": Pushover,
"DingDing": DingDing, "pushy": Pushy,
"Bark": Bark, "rocket.chat": RocketChat,
"serwersms": SerwerSMS, "serwersms": SerwerSMS,
"signal": Signal,
"slack": Slack,
"smtp": STMP,
"stackfield": Stackfield, "stackfield": Stackfield,
"teams": Teams,
"telegram": Telegram,
"webhook": Webhook,
"WeCom": WeCom, "WeCom": WeCom,
"GoogleChat": GoogleChat,
"PagerDuty": PagerDuty,
"gorush": Gorush,
"alerta": Alerta,
"OneBot": OneBot,
"PushDeer": PushDeer,
}; };
export default NotificationFormList; export default NotificationFormList;

@ -0,0 +1,48 @@
<template>
<div>
<div class="dockerHost-list my-4">
<p v-if="$root.dockerHostList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(dockerHost, index) in $root.dockerHostList" :key="index" class="list-group-item">
{{ dockerHost.name }}<br>
<a href="#" @click="$refs.dockerHostDialog.show(dockerHost.id)">{{ $t("Edit") }}</a>
</li>
</ul>
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
{{ $t("Setup Docker Host") }}
</button>
</div>
<DockerHostDialog ref="dockerHostDialog" />
</div>
</template>
<script>
import DockerHostDialog from "../../components/DockerHostDialog.vue";
export default {
components: {
DockerHostDialog,
},
data() {
return {};
},
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
}
};
</script>

@ -91,6 +91,51 @@
{{ $t("For example: nginx, Apache and Traefik.") }} <br /> {{ $t("For example: nginx, Apache and Traefik.") }} <br />
{{ $t("Please read") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy</a>. {{ $t("Please read") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy</a>.
</div> </div>
<h4 class="my-4">{{ $t("HTTP Headers") }}</h4>
<div class="my-3">
<label class="form-label">
{{ $t("Trust Proxy") }}
</label>
<div class="form-check">
<input
id="trustProxyYes"
v-model="settings.trustProxy"
class="form-check-input"
type="radio"
name="trustProxyYes"
:value="true"
required
/>
<label class="form-check-label" for="trustProxyYes">
{{ $t("Yes") }}
</label>
</div>
<div class="form-check">
<input
id="trustProxyNo"
v-model="settings.trustProxy"
class="form-check-input"
type="radio"
name="flexRadioDefault"
:value="false"
required
/>
<label class="form-check-label" for="trustProxyNo">
{{ $t("No") }}
</label>
</div>
<div class="form-text">
{{ $t("trustProxyDescription") }}
</div>
</div>
<div>
<button class="btn btn-primary" type="submit" @click="saveSettings()">
{{ $t("Save") }}
</button>
</div>
</div> </div>
</template> </template>
@ -113,6 +158,12 @@ export default {
settings() { settings() {
return this.$parent.$parent.$parent.settings; return this.$parent.$parent.$parent.settings;
}, },
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
}, },
watch: { watch: {

@ -11,6 +11,7 @@ const languageList = {
"es-ES": "Español", "es-ES": "Español",
"eu": "Euskara", "eu": "Euskara",
"fa": "Farsi", "fa": "Farsi",
"pt-PT": "Português (Portugal)",
"pt-BR": "Português (Brasileiro)", "pt-BR": "Português (Brasileiro)",
"fr-FR": "Français (France)", "fr-FR": "Français (France)",
"hu": "Magyar", "hu": "Magyar",

@ -536,4 +536,5 @@ export default {
Domain: "Домейн", Domain: "Домейн",
Workstation: "Работна станция", Workstation: "Работна станция",
disableCloudflaredNoAuthMsg: "Тъй като сте в режим \"No Auth mode\", парола не се изисква.", disableCloudflaredNoAuthMsg: "Тъй като сте в режим \"No Auth mode\", парола не се изисква.",
wayToGetLineNotifyToken: "Може да получите токен код за достъп от {0}",
}; };

@ -165,7 +165,10 @@ export default {
Pink: "Pink", Pink: "Pink",
"Search...": "Suchen...", "Search...": "Suchen...",
"Heartbeat Retry Interval": "Überprüfungsintervall", "Heartbeat Retry Interval": "Überprüfungsintervall",
"Resend Notification if Down X times consequently": "Benachrichtigung erneut senden, wenn Inaktiv X mal hintereinander",
retryCheckEverySecond: "Alle {0} Sekunden neu versuchen", retryCheckEverySecond: "Alle {0} Sekunden neu versuchen",
resendEveryXTimes: "Erneut versenden alle {0} mal",
resendDisabled: "Erneut versenden deaktiviert",
"Import Backup": "Backup importieren", "Import Backup": "Backup importieren",
"Export Backup": "Backup exportieren", "Export Backup": "Backup exportieren",
"Avg. Ping": "Durchschn. Ping", "Avg. Ping": "Durchschn. Ping",

@ -2,6 +2,8 @@ export default {
languageName: "English", languageName: "English",
checkEverySecond: "Check every {0} seconds", checkEverySecond: "Check every {0} seconds",
retryCheckEverySecond: "Retry every {0} seconds", retryCheckEverySecond: "Retry every {0} seconds",
resendEveryXTimes: "Resend every {0} times",
resendDisabled: "Resend disabled",
retriesDescription: "Maximum retries before the service is marked as down and a notification is sent", retriesDescription: "Maximum retries before the service is marked as down and a notification is sent",
ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites", ignoreTLSError: "Ignore TLS/SSL error for HTTPS websites",
upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.", upsideDownModeDescription: "Flip the status upside down. If the service is reachable, it is DOWN.",
@ -72,6 +74,7 @@ export default {
"Heartbeat Interval": "Heartbeat Interval", "Heartbeat Interval": "Heartbeat Interval",
Retries: "Retries", Retries: "Retries",
"Heartbeat Retry Interval": "Heartbeat Retry Interval", "Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Resend Notification if Down X times consequently": "Resend Notification if Down X times consequently",
Advanced: "Advanced", Advanced: "Advanced",
"Upside Down Mode": "Upside Down Mode", "Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "Max. Redirects", "Max. Redirects": "Max. Redirects",
@ -408,6 +411,8 @@ export default {
SignName: "SignName", SignName: "SignName",
"Sms template must contain parameters: ": "Sms template must contain parameters: ", "Sms template must contain parameters: ": "Sms template must contain parameters: ",
"Bark Endpoint": "Bark Endpoint", "Bark Endpoint": "Bark Endpoint",
"Bark Group": "Bark Group",
"Bark Sound": "Bark Sound",
WebHookUrl: "WebHookUrl", WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey", SecretKey: "SecretKey",
"For safety, must use secret key": "For safety, must use secret key", "For safety, must use secret key": "For safety, must use secret key",
@ -453,6 +458,8 @@ export default {
"Message:": "Message:", "Message:": "Message:",
"Don't know how to get the token? Please read the guide:": "Don't know how to get the token? Please read the guide:", "Don't know how to get the token? Please read the guide:": "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.": "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.", "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.": "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.",
"HTTP Headers": "HTTP Headers",
"Trust Proxy": "Trust Proxy",
"Other Software": "Other Software", "Other Software": "Other Software",
"For example: nginx, Apache and Traefik.": "For example: nginx, Apache and Traefik.", "For example: nginx, Apache and Traefik.": "For example: nginx, Apache and Traefik.",
"Please read": "Please read", "Please read": "Please read",
@ -465,6 +472,7 @@ export default {
"Domain Name Expiry Notification": "Domain Name Expiry Notification", "Domain Name Expiry Notification": "Domain Name Expiry Notification",
Proxy: "Proxy", Proxy: "Proxy",
"Date Created": "Date Created", "Date Created": "Date Created",
HomeAssistant: "Home Assistant",
onebotHttpAddress: "OneBot HTTP Address", onebotHttpAddress: "OneBot HTTP Address",
onebotMessageType: "OneBot Message Type", onebotMessageType: "OneBot Message Type",
onebotGroupMessage: "Group", onebotGroupMessage: "Group",
@ -477,6 +485,12 @@ export default {
"Domain Names": "Domain Names", "Domain Names": "Domain Names",
signedInDisp: "Signed in as {0}", signedInDisp: "Signed in as {0}",
signedInDispDisabled: "Auth Disabled.", signedInDispDisabled: "Auth Disabled.",
RadiusSecret: "Radius Secret",
RadiusSecretDescription: "Shared Secret between client and server",
RadiusCalledStationId: "Called Station Id",
RadiusCalledStationIdDescription: "Identifier of the called device",
RadiusCallingStationId: "Calling Station Id",
RadiusCallingStationIdDescription: "Identifier of the calling device",
"Certificate Expiry Notification": "Certificate Expiry Notification", "Certificate Expiry Notification": "Certificate Expiry Notification",
"API Username": "API Username", "API Username": "API Username",
"API Key": "API Key", "API Key": "API Key",
@ -485,7 +499,7 @@ export default {
"Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.", "Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.",
"Octopush API Version": "Octopush API Version", "Octopush API Version": "Octopush API Version",
"Legacy Octopush-DM": "Legacy Octopush-DM", "Legacy Octopush-DM": "Legacy Octopush-DM",
"endpoint": "endpoint", endpoint: "endpoint",
octopushAPIKey: "\"API key\" from HTTP API credentials in control panel", octopushAPIKey: "\"API key\" from HTTP API credentials in control panel",
octopushLogin: "\"Login\" from HTTP API credentials in control panel", octopushLogin: "\"Login\" from HTTP API credentials in control panel",
promosmsLogin: "API Login Name", promosmsLogin: "API Login Name",
@ -529,12 +543,24 @@ export default {
"Coming Soon": "Coming Soon", "Coming Soon": "Coming Soon",
wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .", wayToGetClickSendSMSToken: "You can get API Username and API Key from {0} .",
"Connection String": "Connection String", "Connection String": "Connection String",
"Query": "Query", Query: "Query",
settingsCertificateExpiry: "TLS Certificate Expiry", settingsCertificateExpiry: "TLS Certificate Expiry",
certificationExpiryDescription: "HTTPS Monitors trigger notification when TLS certificate expires in:", certificationExpiryDescription: "HTTPS Monitors trigger notification when TLS certificate expires in:",
"Setup Docker Host": "Setup Docker Host",
"Connection Type": "Connection Type",
"Docker Daemon": "Docker Daemon",
deleteDockerHostMsg: "Are you sure want to delete this docker host for all monitors?",
socket: "Socket",
tcp: "TCP / HTTP",
"Docker Container": "Docker Container",
"Container Name / ID": "Container Name / ID",
"Docker Host": "Docker Host",
"Docker Hosts": "Docker Hosts",
"ntfy Topic": "ntfy Topic", "ntfy Topic": "ntfy Topic",
"Domain": "Domain", "Domain": "Domain",
"Workstation": "Workstation", "Workstation": "Workstation",
disableCloudflaredNoAuthMsg: "You are in No Auth mode, password is not require.", disableCloudflaredNoAuthMsg: "You are in No Auth mode, password is not require.",
"Packet Size": "Packet Size", "Packet Size": "Packet Size",
trustProxyDescription: "Trust 'X-Forwarded-*' headers. If you want to get the correct client IP and your Uptime Kuma is behind such as Nginx or Apache, you should enable this.",
wayToGetLineNotifyToken: "You can get an access token from {0}",
}; };

@ -7,8 +7,8 @@ export default {
maxRedirectDescription: "Número máximo de direcciones a seguir. Establecer a 0 para deshabilitar.", maxRedirectDescription: "Número máximo de direcciones a seguir. Establecer a 0 para deshabilitar.",
acceptedStatusCodesDescription: "Seleccionar los códigos de estado que se consideran como respuesta exitosa.", acceptedStatusCodesDescription: "Seleccionar los códigos de estado que se consideran como respuesta exitosa.",
passwordNotMatchMsg: "La contraseña repetida no coincide.", passwordNotMatchMsg: "La contraseña repetida no coincide.",
notificationDescription: "Por favor asigne una notificación a el/los monitor(es) para hacerlos funcional(es).", notificationDescription: "Por favor asigna una notificación a el/los monitor(es) para hacerlos funcional(es).",
keywordDescription: "Palabra clave en HTML plano o respuesta JSON y es sensible a mayúsculas", keywordDescription: "Palabra clave en HTML plano o respuesta JSON, es sensible a mayúsculas",
pauseDashboardHome: "Pausado", pauseDashboardHome: "Pausado",
deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?", deleteMonitorMsg: "¿Seguro que quieres eliminar este monitor?",
deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?", deleteNotificationMsg: "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
@ -35,7 +35,7 @@ export default {
Pause: "Pausar", Pause: "Pausar",
Name: "Nombre", Name: "Nombre",
Status: "Estado", Status: "Estado",
DateTime: "Fecha y Hora", DateTime: "Fecha y hora",
Message: "Mensaje", Message: "Mensaje",
"No important events": "No hay eventos importantes", "No important events": "No hay eventos importantes",
Resume: "Reanudar", Resume: "Reanudar",
@ -50,7 +50,7 @@ export default {
"-hour": "-hora", "-hour": "-hora",
Response: "Respuesta", Response: "Respuesta",
Ping: "Ping", Ping: "Ping",
"Monitor Type": "Tipo de Monitor", "Monitor Type": "Tipo de monitor",
Keyword: "Palabra clave", Keyword: "Palabra clave",
"Friendly Name": "Nombre sencillo", "Friendly Name": "Nombre sencillo",
URL: "URL", URL: "URL",
@ -60,11 +60,11 @@ export default {
Retries: "Reintentos", Retries: "Reintentos",
Advanced: "Avanzado", Advanced: "Avanzado",
"Upside Down Mode": "Modo invertido", "Upside Down Mode": "Modo invertido",
"Max. Redirects": "Redirecciones Máximas", "Max. Redirects": "Redirecciones máximas",
"Accepted Status Codes": "Códigos de estado aceptados", "Accepted Status Codes": "Códigos de estado aceptados",
Save: "Guardar", Save: "Guardar",
Notifications: "Notificaciones", Notifications: "Notificaciones",
"Not available, please setup.": "No disponible, por favor configúrelo.", "Not available, please setup.": "No disponible, por favor configúralo.",
"Setup Notification": "Configurar notificación", "Setup Notification": "Configurar notificación",
Light: "Claro", Light: "Claro",
Dark: "Oscuro", Dark: "Oscuro",
@ -82,8 +82,8 @@ export default {
"New Password": "Nueva contraseña", "New Password": "Nueva contraseña",
"Repeat New Password": "Repetir nueva contraseña", "Repeat New Password": "Repetir nueva contraseña",
"Update Password": "Actualizar contraseña", "Update Password": "Actualizar contraseña",
"Disable Auth": "Deshabilitar Autenticación", "Disable Auth": "Deshabilitar autenticación",
"Enable Auth": "Habilitar Autenticación", "Enable Auth": "Habilitar autenticación",
"disableauth.message1": "Seguro que deseas <strong>deshabilitar la autenticación</strong>?", "disableauth.message1": "Seguro que deseas <strong>deshabilitar la autenticación</strong>?",
"disableauth.message2": "Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.", "disableauth.message2": "Es para <strong>quien implementa autenticación de terceros</strong> ante Uptime Kuma como por ejemplo Cloudflare Access.",
"Please use this option carefully!": "Por favor usar con cuidado.", "Please use this option carefully!": "Por favor usar con cuidado.",
@ -104,32 +104,32 @@ export default {
Test: "Test", Test: "Test",
"Certificate Info": "Información del certificado", "Certificate Info": "Información del certificado",
"Resolver Server": "Servidor de resolución", "Resolver Server": "Servidor de resolución",
"Resource Record Type": "Tipo de Registro", "Resource Record Type": "Tipo de registro",
"Last Result": "Último resultado", "Last Result": "Último resultado",
"Create your admin account": "Crea tu cuenta de administrador", "Create your admin account": "Crea tu cuenta de administrador",
"Repeat Password": "Repetir contraseña", "Repeat Password": "Repetir contraseña",
respTime: "Tiempo de resp. (ms)", respTime: "Tiempo de resp. (ms)",
notAvailableShort: "N/A", notAvailableShort: "N/A",
Create: "Crear", Create: "Crear",
clearEventsMsg: "¿Está seguro de que desea eliminar todos los eventos de este monitor?", clearEventsMsg: "¿Estás seguro de que deseas eliminar todos los eventos de este monitor?",
clearHeartbeatsMsg: "¿Está seguro de que desea eliminar todos los latidos de este monitor?", clearHeartbeatsMsg: "¿Estás seguro de que deseas eliminar todos los latidos de este monitor?",
confirmClearStatisticsMsg: "¿Está seguro de que desea eliminar TODAS las estadísticas?", confirmClearStatisticsMsg: "¿Estás seguro de que deseas eliminar TODAS las estadísticas?",
"Clear Data": "Borrar Datos", "Clear Data": "Borrar datos",
Events: "Eventos", Events: "Eventos",
Heartbeats: "Latidos", Heartbeats: "Latidos",
"Auto Get": "Obtener automáticamente", "Auto Get": "Obtener automáticamente",
enableDefaultNotificationDescription: "Para cada nuevo monitor, esta notificación estará habilitada de forma predeterminada. Aún puede deshabilitar la notificación por separado para cada monitor.", enableDefaultNotificationDescription: "Para cada nuevo monitor, esta notificación estará habilitada de forma predeterminada. Aún puedes deshabilitar la notificación por separado para cada monitor.",
"Default enabled": "Habilitado por defecto", "Default enabled": "Habilitado por defecto",
"Also apply to existing monitors": "También se aplica a monitores existentes", "Also apply to existing monitors": "También se aplica a monitores existentes",
Export: "Exportar", Export: "Exportar",
Import: "Importar", Import: "Importar",
backupDescription: "Puede hacer una copia de seguridad de todos los monitores y todas las notificaciones en un archivo JSON.", backupDescription: "Puedes hacer una copia de seguridad de todos los monitores y todas las notificaciones en un archivo JSON.",
backupDescription2: "PD: el historial y los datos de eventos no están incluidos.", backupDescription2: "PD: el historial y los datos de eventos no están incluidos.",
backupDescription3: "Los datos confidenciales, como los tokens de notificación, se incluyen en el archivo de exportación. Guárdelo con cuidado.", backupDescription3: "Los datos confidenciales, como los tokens de notificación, se incluyen en el archivo de exportación. Guárdalo con cuidado.",
alertNoFile: "Seleccione un archivo para importar.", alertNoFile: "Selecciona un archivo para importar.",
alertWrongFileType: "Seleccione un archivo JSON.", alertWrongFileType: "Selecciona un archivo JSON.",
twoFAVerifyLabel: "Ingrese su token para verificar que 2FA está funcionando", twoFAVerifyLabel: "Ingresa tu token para verificar que 2FA está funcionando",
tokenValidSettingsMsg: "¡El token es válido! Ahora puede guardar la configuración de 2FA.", tokenValidSettingsMsg: "¡El token es válido! Ahora puedes guardar la configuración de 2FA.",
confirmEnableTwoFAMsg: "¿Estás seguro de que quieres habilitar 2FA?", confirmEnableTwoFAMsg: "¿Estás seguro de que quieres habilitar 2FA?",
confirmDisableTwoFAMsg: "¿Estás seguro de que quieres desactivar 2FA?", confirmDisableTwoFAMsg: "¿Estás seguro de que quieres desactivar 2FA?",
"Apply on all existing monitors": "Aplicar en todos los monitores existentes", "Apply on all existing monitors": "Aplicar en todos los monitores existentes",
@ -145,19 +145,19 @@ export default {
"Show URI": "Mostrar URI", "Show URI": "Mostrar URI",
"Clear all statistics": "Borrar todas las estadísticas", "Clear all statistics": "Borrar todas las estadísticas",
retryCheckEverySecond: "Reintentar cada {0} segundo.", retryCheckEverySecond: "Reintentar cada {0} segundo.",
importHandleDescription: "Elija 'Omitir existente' si desea omitir todos los monitores o notificaciones con el mismo nombre. 'Sobrescribir' eliminará todos los monitores y notificaciones existentes.", importHandleDescription: "Elige 'Omitir existente' si deseas omitir todos los monitores o notificaciones con el mismo nombre. 'Sobrescribir' eliminará todos los monitores y notificaciones existentes.",
confirmImportMsg: "¿Estás seguro de importar la copia de seguridad? Asegúrese de haber seleccionado la opción de importación correcta.", confirmImportMsg: "¿Estás seguro de importar la copia de seguridad? Asegúrate de haber seleccionado la opción de importación correcta.",
"Heartbeat Retry Interval": "Intervalo de reintento de latido", "Heartbeat Retry Interval": "Intervalo de reintento de latido",
"Import Backup": "Importar copia de seguridad", "Import Backup": "Importar copia de seguridad",
"Export Backup": "Exportar copia de seguridad", "Export Backup": "Exportar copia de seguridad",
"Skip existing": "Omitir existente", "Skip existing": "Omitir existente",
Overwrite: "Sobrescribir", Overwrite: "Sobrescribir",
Options: "Opciones", Options: "Opciones",
"Keep both": "Mantén ambos", "Keep both": "Manténer ambos",
Tags: "Etiquetas", Tags: "Etiquetas",
"Add New below or Select...": "Agregar nuevo a continuación o Seleccionar...", "Add New below or Select...": "Agregar nuevo a continuación o seleccionar...",
"Tag with this name already exist.": "La etiqueta con este nombre ya existe.", "Tag with this name already exist.": "Una etiqueta con este nombre ya existe.",
"Tag with this value already exist.": "La etiqueta con este valor ya existe.", "Tag with this value already exist.": "Una etiqueta con este valor ya existe.",
color: "color", color: "color",
"value (optional)": "valor (opcional)", "value (optional)": "valor (opcional)",
Gray: "Gris", Gray: "Gris",
@ -172,17 +172,17 @@ export default {
"Avg. Ping": "Ping promedio", "Avg. Ping": "Ping promedio",
"Avg. Response": "Respuesta promedio", "Avg. Response": "Respuesta promedio",
"Entry Page": "Página de entrada", "Entry Page": "Página de entrada",
statusPageNothing: "No hay nada aquí, agregue un grupo o un monitor.", statusPageNothing: "No hay nada aquí, agrega un grupo o un monitor.",
"No Services": "Sin servicio", "No Services": "Sin servicio",
"All Systems Operational": "Todos los sistemas están operativos", "All Systems Operational": "Todos los sistemas están operativos",
"Partially Degraded Service": "Servicio parcialmente degradado", "Partially Degraded Service": "Servicio parcialmente degradado",
"Degraded Service": "Servicio degradado", "Degraded Service": "Servicio degradado",
"Add Group": "Agregar Grupo", "Add Group": "Agregar grupo",
"Add a monitor": "Agregar un monitor", "Add a monitor": "Agregar un monitor",
"Edit Status Page": "Editar página de estado", "Edit Status Page": "Editar página de estado",
"Go to Dashboard": "Ir al panel de control", "Go to Dashboard": "Ir al panel de control",
"Status Page": "Página de estado", "Status Page": "Página de estado",
"Status Pages": "Página de estado", "Status Pages": "Páginas de estado",
telegram: "Telegram", telegram: "Telegram",
webhook: "Webhook", webhook: "Webhook",
smtp: "Email (SMTP)", smtp: "Email (SMTP)",
@ -205,5 +205,5 @@ export default {
clearDataOlderThan: "Mantener los datos del historial del monitor durante {0} días.", clearDataOlderThan: "Mantener los datos del historial del monitor durante {0} días.",
records: "registros", records: "registros",
"One record": "Un registro", "One record": "Un registro",
steamApiKeyDescription: "Para monitorear un servidor de juegos de Steam, necesita una clave Steam Web-API. Puede registrar su clave API aquí: ", steamApiKeyDescription: "Para monitorear un servidor de juegos de Steam, necesitas una clave Steam Web-API. Puedes registrar tu clave API aquí: ",
}; };

@ -3,7 +3,7 @@ export default {
checkEverySecond: "{0}초마다 확인해요.", checkEverySecond: "{0}초마다 확인해요.",
retryCheckEverySecond: "{0}초마다 다시 확인해요.", retryCheckEverySecond: "{0}초마다 다시 확인해요.",
retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수", retriesDescription: "서비스가 중단된 후 알림을 보내기 전 최대 재시도 횟수",
ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 에러 무시하기", ignoreTLSError: "HTTPS 웹사이트에서 TLS/SSL 오류 무시하기",
upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거예요.", upsideDownModeDescription: "서버 상태를 반대로 표시해요. 서버가 작동하면 오프라인으로 표시할 거예요.",
maxRedirectDescription: "최대 리다이렉트 횟수예요. 0을 입력하면 리다이렉트를 꺼요.", maxRedirectDescription: "최대 리다이렉트 횟수예요. 0을 입력하면 리다이렉트를 꺼요.",
acceptedStatusCodesDescription: "응답 성공으로 간주할 상태 코드를 정해요.", acceptedStatusCodesDescription: "응답 성공으로 간주할 상태 코드를 정해요.",
@ -30,7 +30,7 @@ export default {
Dashboard: "대시보드", Dashboard: "대시보드",
"New Update": "새로운 업데이트", "New Update": "새로운 업데이트",
Language: "언어", Language: "언어",
Appearance: "외형", Appearance: "디스플레이",
Theme: "테마", Theme: "테마",
General: "일반", General: "일반",
Version: "버전", Version: "버전",
@ -78,7 +78,7 @@ export default {
Notifications: "알림", Notifications: "알림",
"Not available, please setup.": "존재하지 않아요, 새로운 거 하나 만드는 건 어때요?", "Not available, please setup.": "존재하지 않아요, 새로운 거 하나 만드는 건 어때요?",
"Setup Notification": "알림 설정", "Setup Notification": "알림 설정",
Light: "이트", Light: "이트",
Dark: "다크", Dark: "다크",
Auto: "자동", Auto: "자동",
"Theme - Heartbeat Bar": "테마 - 하트비트 바", "Theme - Heartbeat Bar": "테마 - 하트비트 바",
@ -91,7 +91,7 @@ export default {
"Discourage search engines from indexing site": "검색 엔진 인덱싱 거부", "Discourage search engines from indexing site": "검색 엔진 인덱싱 거부",
"Change Password": "비밀번호 변경", "Change Password": "비밀번호 변경",
"Current Password": "기존 비밀번호", "Current Password": "기존 비밀번호",
"New Password": "새로운 비밀번호", "New Password": "새 비밀번호",
"Repeat New Password": "새로운 비밀번호 재입력", "Repeat New Password": "새로운 비밀번호 재입력",
"Update Password": "비밀번호 변경", "Update Password": "비밀번호 변경",
"Disable Auth": "인증 비활성화", "Disable Auth": "인증 비활성화",
@ -109,14 +109,14 @@ export default {
Password: "비밀번호", Password: "비밀번호",
"Remember me": "비밀번호 기억하기", "Remember me": "비밀번호 기억하기",
Login: "로그인", Login: "로그인",
"No Monitors, please": "모니터링이 없어요,", "No Monitors, please": "모니터링이 현재 없어요,",
"add one": "하나 추가해봐요", "add one": "한번 추가해보실레요?",
"Notification Type": "알림 종류", "Notification Type": "알림 종류",
Email: "이메일", Email: "이메일",
Test: "테스트", Test: "테스트",
"Certificate Info": "인증서 정보", "Certificate Info": "인증서 정보",
"Resolver Server": "Resolver 서버", "Resolver Server": "Resolver 서버",
"Resource Record Type": "자원 레코드 유형", "Resource Record Type": "리소스 레코드 유형",
"Last Result": "최근 결과", "Last Result": "최근 결과",
"Create your admin account": "관리자 계정 만들기", "Create your admin account": "관리자 계정 만들기",
"Repeat Password": "비밀번호 재입력", "Repeat Password": "비밀번호 재입력",
@ -208,19 +208,19 @@ export default {
smtpBCC: "숨은 참조", smtpBCC: "숨은 참조",
discord: "Discord", discord: "Discord",
"Discord Webhook URL": "Discord Webhook URL", "Discord Webhook URL": "Discord Webhook URL",
wayToGetDiscordURL: "서버 설정 -> 연동 -> 웹후크 보기 -> 새 웹후크에서 얻을 수 있어요.", wayToGetDiscordURL: "서버 설정 -> 연동 -> 웹후크 보기 -> 새 웹후크에서 얻을 수 있어요!",
"Bot Display Name": "표시 이름", "Bot Display Name": "표시 이름",
"Prefix Custom Message": "접두사 메시지", "Prefix Custom Message": "접두사 메시지",
"Hello @everyone is...": "{'@'}everyone 서버 상태 알림이에요...", "Hello @everyone is...": "{'@'}everyone 서버 상태 알림이에요...",
teams: "Microsoft Teams", teams: "Microsoft Teams",
"Webhook URL": "Webhook URL", "Webhook URL": "Webhook URL",
wayToGetTeamsURL: "{0}에서 Webhook을 어떻게 만드는지 알아봐요.", wayToGetTeamsURL: "{0}에서 Webhook을 어떻게 만드는지 알아보세요!",
signal: "Signal", signal: "Signal",
Number: "숫자", Number: "숫자",
Recipients: "받는 사람", Recipients: "받는 사람",
needSignalAPI: "REST API를 사용하는 Signal 클라이언트가 있어야 해요.", needSignalAPI: "REST API를 사용하는 Signal 클라이언트가 있어야 해요.",
wayToCheckSignalURL: "밑에 URL을 확인해 URL 설정 방법을 볼 수 있어요.", wayToCheckSignalURL: "밑에 URL을 확인해 URL 설정 방법을 볼 수 있어요.",
signalImportant: "중요: 받는 사람의 그룹과 숫자는 섞을 수 없어요!", signalImportant: "경고: 받는 사람의 그룹과 숫자는 섞을 수 없어요!",
gotify: "Gotify", gotify: "Gotify",
"Application Token": "애플리케이션 토큰", "Application Token": "애플리케이션 토큰",
"Server URL": "서버 URL", "Server URL": "서버 URL",
@ -230,8 +230,8 @@ export default {
"Channel Name": "채널 이름", "Channel Name": "채널 이름",
"Uptime Kuma URL": "Uptime Kuma URL", "Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Webhook에 대한 설명: {0}", aboutWebhooks: "Webhook에 대한 설명: {0}",
aboutChannelName: "Webhook 채널을 우회하려면 {0} 채널 이름칸에 채널 이름을 입력해주세요. 예: #기타-채널", aboutChannelName: "Webhook 채널을 무시하려면 {0} 채널 이름칸에 채널 이름을 입력해주세요. 예: #기타-채널",
aboutKumaURL: "Uptime Kuma URL칸을 공백으로 두면 기본적으로 Project Github 페이지로 설정해요.", aboutKumaURL: "Uptime Kuma URL칸을 공백으로 두면 기본적으로 Github Project 페이지로 설정해요.",
emojiCheatSheet: "이모지 목록 시트: {0}", emojiCheatSheet: "이모지 목록 시트: {0}",
"rocket.chat": "Rocket.chat", "rocket.chat": "Rocket.chat",
pushover: "Pushover", pushover: "Pushover",
@ -243,8 +243,8 @@ export default {
pushbullet: "Pushbullet", pushbullet: "Pushbullet",
line: "Line Messenger", line: "Line Messenger",
mattermost: "Mattermost", mattermost: "Mattermost",
"User Key": "사용자 키", "User Key": "유저 키",
Device: "장치", Device: "디바이스",
"Message Title": "메시지 제목", "Message Title": "메시지 제목",
"Notification Sound": "알림음", "Notification Sound": "알림음",
"More info on:": "자세한 정보: {0}", "More info on:": "자세한 정보: {0}",
@ -254,7 +254,7 @@ export default {
octopushTypePremium: "프리미엄 (빠름) - 알림 기능에 적합해요)", octopushTypePremium: "프리미엄 (빠름) - 알림 기능에 적합해요)",
octopushTypeLowCost: "저렴한 요금 (느림) - 가끔 차단될 수 있어요)", octopushTypeLowCost: "저렴한 요금 (느림) - 가끔 차단될 수 있어요)",
"Check octopush prices": "{0}에서 Octopush 가격을 확인할 수 있어요.", "Check octopush prices": "{0}에서 Octopush 가격을 확인할 수 있어요.",
octopushPhoneNumber: "휴대전화 번호 (intl format, eg : +33612345678) ", octopushPhoneNumber: "휴대전화 번호 (intl format, 예시: +821023456789) ",
octopushSMSSender: "보내는 사람 이름 : 3-11개의 영숫자 및 여백공간 (a-z, A-Z, 0-9)", octopushSMSSender: "보내는 사람 이름 : 3-11개의 영숫자 및 여백공간 (a-z, A-Z, 0-9)",
"LunaSea Device ID": "LunaSea 장치 ID", "LunaSea Device ID": "LunaSea 장치 ID",
"Apprise URL": "Apprise URL", "Apprise URL": "Apprise URL",
@ -324,17 +324,17 @@ export default {
Content: "내용", Content: "내용",
Style: "스타일", Style: "스타일",
info: "정보", info: "정보",
warning: "경고", warning: "주의",
danger: "위험", danger: "경고",
primary: "기본", primary: "기본",
light: "이트", light: "이트",
dark: "다크", dark: "다크",
Post: "올리기", Post: "게시",
"Please input title and content": "제목과 내용을 작성해주세요.", "Please input title and content": "제목과 내용을 작성해주세요.",
Created: "생성 날짜", Created: "생성 날짜",
"Last Updated": "마지막 업데이트", "Last Updated": "마지막 업데이트",
Unpin: "제거", Unpin: "제거",
"Switch to Light Theme": "이트 테마로 전환", "Switch to Light Theme": "이트 테마로 전환",
"Switch to Dark Theme": "다크 테마로 전환", "Switch to Dark Theme": "다크 테마로 전환",
"Show Tags": "태그 보이기", "Show Tags": "태그 보이기",
"Hide Tags": "태그 숨기기", "Hide Tags": "태그 숨기기",
@ -361,8 +361,8 @@ export default {
topicExplanation: "모니터링할 MQTT Topic", topicExplanation: "모니터링할 MQTT Topic",
successMessage: "성공 메시지", successMessage: "성공 메시지",
successMessageExplanation: "성공으로 간주되는 MQTT 메시지", successMessageExplanation: "성공으로 간주되는 MQTT 메시지",
error: "error", error: "오류",
critical: "critical", critical: "크리티컬",
Customize: "커스터마이즈", Customize: "커스터마이즈",
"Custom Footer": "커스텀 Footer", "Custom Footer": "커스텀 Footer",
"Custom CSS": "커스텀 CSS", "Custom CSS": "커스텀 CSS",
@ -406,7 +406,7 @@ export default {
PhoneNumbers: "휴대전화 번호", PhoneNumbers: "휴대전화 번호",
TemplateCode: "템플릿 코드", TemplateCode: "템플릿 코드",
SignName: "SignName", SignName: "SignName",
"Sms template must contain parameters: ": "Sms 템플릿은 다음과 같은 파라미터가 포함되어야 해요:", "Sms template must contain parameters: ": "SMS 템플릿은 다음과 같은 파라미터가 포함되어야 해요:",
"Bark Endpoint": "Bark Endpoint", "Bark Endpoint": "Bark Endpoint",
WebHookUrl: "웹훅 URL", WebHookUrl: "웹훅 URL",
SecretKey: "Secret Key", SecretKey: "Secret Key",
@ -518,14 +518,14 @@ export default {
"Show update if available": "사용 가능한 경우에 업데이트 표시", "Show update if available": "사용 가능한 경우에 업데이트 표시",
"Also check beta release": "베타 릴리즈 확인", "Also check beta release": "베타 릴리즈 확인",
"Using a Reverse Proxy?": "리버스 프록시를 사용하시나요?", "Using a Reverse Proxy?": "리버스 프록시를 사용하시나요?",
"Check how to config it for WebSocket": "웹소켓 대한 설정 방법 확인", "Check how to config it for WebSocket": "웹소켓 대한 설정 방법",
"Steam Game Server": "스팀 게임 서버", "Steam Game Server": "스팀 게임 서버",
"Most likely causes:": "원인:", "Most likely causes:": "원인:",
"The resource is no longer available.": "더이상 사용할 수 없어요.", "The resource is no longer available.": "더 이상 사용할 수 없어요...",
"There might be a typing error in the address.": "주소에 오탈자가 있을 수 있어요.", "There might be a typing error in the address.": "주소에 오탈자가 있을 수 있어요.",
"What you can try:": "해결 방법:", "What you can try:": "해결 방법:",
"Retype the address.": "주소 다시 입력하기", "Retype the address.": "주소 다시 입력하기",
"Go back to the previous page.": "이전 페이지로 돌아가기", "Go back to the previous page.": "이전 페이지로 돌아가기",
"Coming Soon": "Coming Soon", "Coming Soon": "Coming Soon...",
wayToGetClickSendSMSToken: "{0}에서 API 사용자 이름과 키를 얻을 수 있어요.", wayToGetClickSendSMSToken: "{0}에서 API 사용자 이름과 키를 얻을 수 있어요.",
}; };

@ -0,0 +1,203 @@
export default {
languageName: "Português (Portugal)",
checkEverySecond: "Verificar a cada {0} segundos.",
retryCheckEverySecond: "Tentar novamente a cada {0} segundos.",
retriesDescription: "Máximo de tentativas antes que o serviço seja marcado como inativo e uma notificação seja enviada",
ignoreTLSError: "Ignorar erros TLS/SSL para sites HTTPS",
upsideDownModeDescription: "Inverte o status de cabeça para baixo. Se o serviço estiver acessível, ele está OFFLINE.",
maxRedirectDescription: "Número máximo de redirecionamentos a seguir. Define como 0 para desativar redirecionamentos.",
acceptedStatusCodesDescription: "Seleciona os códigos de status que são considerados uma resposta bem-sucedida.",
passwordNotMatchMsg: "A senha repetida não corresponde.",
notificationDescription: "Atribuir uma notificação ao (s) monitor (es) para que funcione.",
keywordDescription: "Pesquisa a palavra-chave em HTML simples ou resposta JSON e diferencia maiúsculas de minúsculas",
pauseDashboardHome: "Pausa",
deleteMonitorMsg: "Tens a certeza de que queres excluir este monitor?",
deleteNotificationMsg: "Tens a certeza de que queres excluir esta notificação para todos os monitores?",
resolverserverDescription: "A Cloudflare é o servidor padrão, podes alterar o servidor 'resolvedor' a qualquer momento.",
rrtypeDescription: "Seleciona o RR-Type que queres monitorizar",
pauseMonitorMsg: "Tens a certeza que queres fazer uma pausa?",
enableDefaultNotificationDescription: "Para cada monitor novo esta notificação vai estar activa por padrão. Podes também desativar a notificação separadamente para cada monitor.",
clearEventsMsg: "Tens a certeza que queres excluir todos os eventos deste monitor?",
clearHeartbeatsMsg: "Tens a certeza de que queres excluir todos os heartbeats deste monitor?",
confirmClearStatisticsMsg: "Tens a certeza que queres excluir TODAS as estatísticas?",
importHandleDescription: "Escolhe 'Ignorar existente' se quiseres ignorar todos os monitores ou notificações com o mesmo nome. 'Substituir' excluirá todos os monitores e notificações existentes.",
confirmImportMsg: "Tens a certeza que queres importar o backup? Certifica-te que selecionaste a opção de importação correta.",
twoFAVerifyLabel: "Insire o teu token para verificares se o 2FA está a funcionar",
tokenValidSettingsMsg: "O token é válido! Agora podes salvar as configurações do 2FA.",
confirmEnableTwoFAMsg: "Tens a certeza de que queres habilitar 2FA?",
confirmDisableTwoFAMsg: "Tens a certeza de que queres desativar 2FA?",
Settings: "Configurações",
Dashboard: "Dashboard",
"New Update": "Nova Atualização",
Language: "Linguagem",
Appearance: "Aparência",
Theme: "Tema",
General: "Geral",
Version: "Versão",
"Check Update On GitHub": "Verificar atualização no Github",
List: "Lista",
Add: "Adicionar",
"Add New Monitor": "Adicionar novo monitor",
"Quick Stats": "Estatísticas rápidas",
Up: "On",
Down: "Off",
Pending: "Pendente",
Unknown: "Desconhecido",
Pause: "Pausa",
Name: "Nome",
Status: "Status",
DateTime: "Data hora",
Message: "Mensagem",
"No important events": "Nenhum evento importante",
Resume: "Resumo",
Edit: "Editar",
Delete: "Apagar",
Current: "Atual",
Uptime: "Tempo de atividade",
"Cert Exp.": "Cert Exp.",
day: "dia | dias",
"-day": "-dia",
hour: "hora",
"-hour": "-hora",
Response: "Resposta",
Ping: "Ping",
"Monitor Type": "Tipo de Monitor",
Keyword: "Palavra-Chave",
"Friendly Name": "Nome Amigável",
URL: "URL",
Hostname: "Hostname",
Port: "Porta",
"Heartbeat Interval": "Intervalo de Heartbeats",
Retries: "Novas tentativas",
"Heartbeat Retry Interval": "Intervalo de repetição de Heartbeats",
Advanced: "Avançado",
"Upside Down Mode": "Modo de cabeça para baixo",
"Max. Redirects": "Redirecionamento Máx.",
"Accepted Status Codes": "Status Code Aceitáveis",
Save: "Guardar",
Notifications: "Notificações",
"Not available, please setup.": "Não disponível, por favor configura.",
"Setup Notification": "Configurar Notificação",
Light: "Claro",
Dark: "Escuro",
Auto: "Auto",
"Theme - Heartbeat Bar": "Tema - Barra de Heartbeat",
Normal: "Normal",
Bottom: "Inferior",
None: "Nenhum",
Timezone: "Fuso horário",
"Search Engine Visibility": "Visibilidade do mecanismo de pesquisa",
"Allow indexing": "Permitir Indexação",
"Discourage search engines from indexing site": "Desencorajar que motores de busca indexem o site",
"Change Password": "Mudar senha",
"Current Password": "Senha atual",
"New Password": "Nova Senha",
"Repeat New Password": "Repetir Nova Senha",
"Update Password": "Atualizar Senha",
"Disable Auth": "Desativar Autenticação",
"Enable Auth": "Ativar Autenticação",
"disableauth.message1": "Tens a certeza que queres <strong>desativar a autenticação</strong>?",
"disableauth.message2": "Isso é para <strong>alguém que tem autenticação de terceiros</strong> em frente ao 'UpTime Kuma' como o Cloudflare Access.",
"Please use this option carefully!": "Por favor, utiliza esta opção com cuidado.",
Logout: "Logout",
Leave: "Sair",
"I understand, please disable": "Eu entendo, por favor desativa.",
Confirm: "Confirmar",
Yes: "Sim",
No: "Não",
Username: "Utilizador",
Password: "Senha",
"Remember me": "Lembra-me",
Login: "Autenticar",
"No Monitors, please": "Nenhum monitor, por favor",
"add one": "adicionar um",
"Notification Type": "Tipo de Notificação",
Email: "Email",
Test: "Testar",
"Certificate Info": "Info. do Certificado ",
"Resolver Server": "Resolver Servidor",
"Resource Record Type": "Tipo de registro de aplicação",
"Last Result": "Último resultado",
"Create your admin account": "Cria a tua conta de admin",
"Repeat Password": "Repete a senha",
"Import Backup": "Importar Backup",
"Export Backup": "Exportar Backup",
Export: "Exportar",
Import: "Importar",
respTime: "Tempo de Resp. (ms)",
notAvailableShort: "N/A",
"Default enabled": "Padrão habilitado",
"Apply on all existing monitors": "Aplicar em todos os monitores existentes",
Create: "Criar",
"Clear Data": "Limpar Dados",
Events: "Eventos",
Heartbeats: "Heartbeats",
"Auto Get": "Obter Automático",
backupDescription: "Podes fazer backup de todos os monitores e todas as notificações num arquivo JSON.",
backupDescription2: "OBS: Os dados do histórico e do evento não estão incluídos.",
backupDescription3: "Dados confidenciais, como tokens de notificação, estão incluídos no arquivo de exportação, mantem-no com cuidado.",
alertNoFile: "Seleciona um arquivo para importar.",
alertWrongFileType: "Seleciona um arquivo JSON.",
"Clear all statistics": "Limpar todas as estatísticas",
"Skip existing": "Saltar existente",
Overwrite: "Sobrescrever",
Options: "Opções",
"Keep both": "Manter os dois",
"Verify Token": "Verificar Token",
"Setup 2FA": "Configurar 2FA",
"Enable 2FA": "Ativar 2FA",
"Disable 2FA": "Desativar 2FA",
"2FA Settings": "Configurações do 2FA ",
"Two Factor Authentication": "Autenticação de Dois Fatores",
Active: "Ativo",
Inactive: "Inativo",
Token: "Token",
"Show URI": "Mostrar URI",
Tags: "Tag",
"Add New below or Select...": "Adicionar Novo abaixo ou Selecionar ...",
"Tag with this name already exist.": "Já existe uma etiqueta com este nome.",
"Tag with this value already exist.": "Já existe uma etiqueta com este valor.",
color: "cor",
"value (optional)": "valor (opcional)",
Gray: "Cinza",
Red: "Vermelho",
Orange: "Laranja",
Green: "Verde",
Blue: "Azul",
Indigo: "Índigo",
Purple: "Roxo",
Pink: "Rosa",
"Search...": "Pesquisa...",
"Avg. Ping": "Ping Médio.",
"Avg. Response": "Resposta Média. ",
"Status Page": "Página de Status",
"Status Pages": "Página de Status",
"Entry Page": "Página de entrada",
statusPageNothing: "Nada aqui, por favor, adiciona um grupo ou monitor.",
"No Services": "Nenhum Serviço",
"All Systems Operational": "Todos os Serviços Operacionais",
"Partially Degraded Service": "Serviço parcialmente degradados",
"Degraded Service": "Serviço Degradado",
"Add Group": "Adicionar Grupo",
"Add a monitor": "Adicionar um monitor",
"Edit Status Page": "Editar Página de Status",
"Go to Dashboard": "Ir para o dashboard",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
};

@ -518,4 +518,5 @@ export default {
"Go back to the previous page.": "กลับไปที่หน้าก่อนหน้า", "Go back to the previous page.": "กลับไปที่หน้าก่อนหน้า",
"Coming Soon": "เร็ว ๆ นี้", "Coming Soon": "เร็ว ๆ นี้",
wayToGetClickSendSMSToken: "คุณสามารถรับ API Username และ API Key ได้จาก {0}", wayToGetClickSendSMSToken: "คุณสามารถรับ API Username และ API Key ได้จาก {0}",
wayToGetLineNotifyToken: "คุณสามารถรับ access token ได้จาก {0}",
}; };

@ -1,5 +1,5 @@
export default { export default {
languageName: "Український", languageName: "Українська",
checkEverySecond: "Перевірка кожні {0} секунд", checkEverySecond: "Перевірка кожні {0} секунд",
retriesDescription: "Максимальна кількість спроб перед позначенням сервісу як недоступного та надсиланням повідомлення", retriesDescription: "Максимальна кількість спроб перед позначенням сервісу як недоступного та надсиланням повідомлення",
ignoreTLSError: "Ігнорувати помилку TLS/SSL для сайтів HTTPS", ignoreTLSError: "Ігнорувати помилку TLS/SSL для сайтів HTTPS",
@ -7,11 +7,11 @@ export default {
maxRedirectDescription: "Максимальна кількість перенаправлень. Поставте 0, щоб вимкнути перенаправлення.", maxRedirectDescription: "Максимальна кількість перенаправлень. Поставте 0, щоб вимкнути перенаправлення.",
acceptedStatusCodesDescription: "Виберіть коди статусів для визначення доступності сервісу.", acceptedStatusCodesDescription: "Виберіть коди статусів для визначення доступності сервісу.",
passwordNotMatchMsg: "Повторення паролю не збігається.", passwordNotMatchMsg: "Повторення паролю не збігається.",
notificationDescription: "Прив'яжіть повідомлення до моніторів.", notificationDescription: "Прив'яжіть сповіщення до моніторів.",
keywordDescription: "Пошук слова в чистому HTML або JSON-відповіді (чутливо до регістру)", keywordDescription: "Пошук слова в чистому HTML або JSON-відповіді (чутливо до регістру)",
pauseDashboardHome: "Пауза", pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Ви дійсно хочете видалити цей монітор?", deleteMonitorMsg: "Ви дійсно хочете видалити цей монітор?",
deleteNotificationMsg: "Ви дійсно хочете видалити це повідомлення для всіх моніторів?", deleteNotificationMsg: "Ви дійсно хочете видалити це сповіщення для всіх моніторів?",
resolverserverDescription: "Cloudflare є сервером за замовчуванням. Ви завжди можете змінити цей сервер.", resolverserverDescription: "Cloudflare є сервером за замовчуванням. Ви завжди можете змінити цей сервер.",
rrtypeDescription: "Виберіть тип ресурсного запису, який ви хочете відстежувати", rrtypeDescription: "Виберіть тип ресурсного запису, який ви хочете відстежувати",
pauseMonitorMsg: "Ви дійсно хочете поставити на паузу?", pauseMonitorMsg: "Ви дійсно хочете поставити на паузу?",
@ -54,7 +54,7 @@ export default {
Keyword: "Ключове слово", Keyword: "Ключове слово",
"Friendly Name": "Ім'я", "Friendly Name": "Ім'я",
URL: "URL", URL: "URL",
Hostname: "Ім'я хоста", Hostname: "Адреса хоста",
Port: "Порт", Port: "Порт",
"Heartbeat Interval": "Частота опитування", "Heartbeat Interval": "Частота опитування",
Retries: "Спроб", Retries: "Спроб",
@ -63,7 +63,7 @@ export default {
"Max. Redirects": "Макс. кількість перенаправлень", "Max. Redirects": "Макс. кількість перенаправлень",
"Accepted Status Codes": "Припустимі коди статусу", "Accepted Status Codes": "Припустимі коди статусу",
Save: "Зберегти", Save: "Зберегти",
Notifications: "Повідомлення", Notifications: "Сповіщення",
"Not available, please setup.": "Доступних сповіщень немає, необхідно створити.", "Not available, please setup.": "Доступних сповіщень немає, необхідно створити.",
"Setup Notification": "Створити сповіщення", "Setup Notification": "Створити сповіщення",
Light: "Світла", Light: "Світла",
@ -100,7 +100,7 @@ export default {
"No Monitors, please": "Моніторів немає, будь ласка", "No Monitors, please": "Моніторів немає, будь ласка",
"No Monitors": "Монітори відсутні", "No Monitors": "Монітори відсутні",
"add one": "створіть новий", "add one": "створіть новий",
"Notification Type": "Тип повідомлення", "Notification Type": "Тип сповіщення",
Email: "Пошта", Email: "Пошта",
Test: "Перевірка", Test: "Перевірка",
"Certificate Info": "Інформація про сертифікат", "Certificate Info": "Інформація про сертифікат",
@ -119,7 +119,7 @@ export default {
Events: "Події", Events: "Події",
Heartbeats: "Опитування", Heartbeats: "Опитування",
"Auto Get": "Авто-отримання", "Auto Get": "Авто-отримання",
enableDefaultNotificationDescription: "Для кожного нового монітора це повідомлення буде включено за замовчуванням. Ви все ще можете відключити повідомлення в кожному моніторі окремо.", enableDefaultNotificationDescription: "Для кожного нового монітора це сповіщення буде включено за замовчуванням. Ви все ще можете відключити сповіщення в кожному моніторі окремо.",
"Default enabled": "Використовувати за промовчанням", "Default enabled": "Використовувати за промовчанням",
"Also apply to existing monitors": "Застосувати до існуючих моніторів", "Also apply to existing monitors": "Застосувати до існуючих моніторів",
Export: "Експорт", Export: "Експорт",
@ -170,7 +170,7 @@ export default {
Purple: "Пурпурний", Purple: "Пурпурний",
Pink: "Рожевий", Pink: "Рожевий",
"Search...": "Пошук...", "Search...": "Пошук...",
"Avg. Ping": "Середнє значення пінгу", "Avg. Ping": "Середній пінг",
"Avg. Response": "Середній час відповіді", "Avg. Response": "Середній час відповіді",
"Entry Page": "Головна сторінка", "Entry Page": "Головна сторінка",
statusPageNothing: "Тут порожньо. Додайте групу або монітор.", statusPageNothing: "Тут порожньо. Додайте групу або монітор.",
@ -210,7 +210,7 @@ export default {
"Push URL": "URL пуша", "Push URL": "URL пуша",
needPushEvery: "До цієї URL необхідно звертатися кожні {0} секунд", needPushEvery: "До цієї URL необхідно звертатися кожні {0} секунд",
pushOptionalParams: "Опціональні параметри: {0}", pushOptionalParams: "Опціональні параметри: {0}",
defaultNotificationName: "Моє повідомлення {notification} ({number})", defaultNotificationName: "Моє сповіщення {notification} ({number})",
here: "тут", here: "тут",
Required: "Потрібно", Required: "Потрібно",
"Bot Token": "Токен бота", "Bot Token": "Токен бота",
@ -257,7 +257,7 @@ export default {
"User Key": "Ключ користувача", "User Key": "Ключ користувача",
Device: "Пристрій", Device: "Пристрій",
"Message Title": "Заголовок повідомлення", "Message Title": "Заголовок повідомлення",
"Notification Sound": "Звук повідомлення", "Notification Sound": "Звук сповіщення",
"More info on:": "Більше інформації: {0}", "More info on:": "Більше інформації: {0}",
pushoverDesc1: "Екстренний пріоритет (2) має таймуут повтору за замовчуванням 30 секунд і закінчується через 1 годину.", pushoverDesc1: "Екстренний пріоритет (2) має таймуут повтору за замовчуванням 30 секунд і закінчується через 1 годину.",
pushoverDesc2: "Якщо ви бажаєте надсилати повідомлення різним пристроям, необхідно заповнити поле Пристрій.", pushoverDesc2: "Якщо ви бажаєте надсилати повідомлення різним пристроям, необхідно заповнити поле Пристрій.",
@ -354,7 +354,7 @@ export default {
"No consecutive dashes --": "Заборонено використовувати тире --", "No consecutive dashes --": "Заборонено використовувати тире --",
"HTTP Options": "HTTP Опції", "HTTP Options": "HTTP Опції",
Authentication: "Аутентифікація", Authentication: "Аутентифікація",
"HTTP Basic Auth": "HTTP Авторизація", "HTTP Basic Auth": "Базова HTTP",
PushByTechulus: "Push by Techulus", PushByTechulus: "Push by Techulus",
clicksendsms: "ClickSend SMS", clicksendsms: "ClickSend SMS",
GoogleChat: "Google Chat (тільки Google Workspace)", GoogleChat: "Google Chat (тільки Google Workspace)",
@ -392,4 +392,139 @@ export default {
alertaAlertState: "Стан алерту", alertaAlertState: "Стан алерту",
alertaRecoverState: "Стан відновлення", alertaRecoverState: "Стан відновлення",
deleteStatusPageMsg: "Дійсно хочете видалити цю сторінку статусів?", deleteStatusPageMsg: "Дійсно хочете видалити цю сторінку статусів?",
Proxies: "Проксі",
default: "За замовчуванням",
enabled: "Активно",
setAsDefault: "Встановити за замовчуванням",
deleteProxyMsg: "Ви впевнені, що хочете видалити цей проксі для всіх моніторів?",
proxyDescription: "Щоб функціонувати, монітору потрібно призначити проксі.",
enableProxyDescription: "Цей проксі не впливатиме на запити моніторингу, доки його не буде активовано. Ви можете контролювати тимчасове відключення проксі з усіх моніторів за статусом активації.",
setAsDefaultProxyDescription: "Цей проксі буде ввімкнено за умовчанням для нових моніторів. Ви все одно можете вимкнути проксі окремо для кожного монітора.",
Invalid: "Недійсний",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Шаблон смс повинен містити параметри: ",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "Для безпеки необхідно використовувати секретний ключ",
"Device Token": "Токен пристрою",
Platform: "Платформа",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "Високий",
Retry: "Повтор",
Topic: "Тема",
"WeCom Bot Key": "WeCom Bot ключ",
"Setup Proxy": "Налаштувати проксі",
"Proxy Protocol": "Протокол проксі",
"Proxy Server": "Проксі-сервер",
"Proxy server has authentication": "Проксі-сервер має аутентифікацію",
User: "Користувач",
Installed: "Встановлено",
"Not installed": "Не встановлено",
Running: "Запущено",
"Not running": "Не запущено",
"Remove Token": "Видалити токен",
Start: "Запустити",
Stop: "Зупинити",
"Uptime Kuma": "Uptime Kuma",
Slug: "Slug",
"Accept characters:": "Прийняти символи:",
startOrEndWithOnly: "Починається або закінчується лише {0}",
"No consecutive dashes": "Немає послідовних тире",
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
"No Proxy": "Без проксі",
"Page Not Found": "Сторінку не знайдено",
"Reverse Proxy": "Реверсивний проксі",
wayToGetCloudflaredURL: "(Завантажити Cloudflare з {0})",
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. Ви дійсно хочете зробити це? Для підтвердження введіть поточний пароль.",
"Other Software": "Інше програмне забезпечення",
"For example: nginx, Apache and Traefik.": "Наприклад: nginx, Apache and Traefik.",
"Please read": "Будь ласка, прочитайте",
"Subject:": "Тема:",
"Valid To:": "Дійсний до:",
"Days Remaining:": "Залишилось днів:",
"Issuer:": "Емітент:",
"Fingerprint:": "Відбиток:",
"No status pages": "Немає сторінок статусу",
"Domain Name Expiry Notification": "Сповіщення про закінчення терміну дії доменного імені",
Proxy: "Проксі",
"Date Created": "Дата створення",
onebotHttpAddress: "OneBot адреса HTTP",
onebotMessageType: "OneBot тип повідомлення",
onebotGroupMessage: "Група",
onebotPrivateMessage: "Приватне",
onebotUserOrGroupId: "Група/Користувач ID",
onebotSafetyTips: "Для безпеки необхідно встановити маркер доступу",
"PushDeer Key": "PushDeer ключ",
"Footer Text": "Текст нижнього колонтитула",
"Show Powered By": "Показувати платформу",
"Domain Names": "Доменні імена",
signedInDisp: "Ви ввійшли як {0}",
signedInDispDisabled: "Авторизація вимкнена.",
"Certificate Expiry Notification": "Сповіщення про закінчення терміну дії сертифіката",
"API Username": "Користувач API",
"API Key": "Ключ API",
"Recipient Number": "Номер одержувача",
"From Name/Number": "Від Ім'я/Номер",
"Leave blank to use a shared sender number.": "Залиште поле порожнім, щоб використовувати спільний номер відправника.",
"Octopush API Version": "Octopush API версія",
"Legacy Octopush-DM": "Legacy Octopush-DM",
"endpoint": "кінцева точка",
octopushAPIKey: "\"Ключ API\" з облікових даних HTTP API в панелі керування",
octopushLogin: "\"Ім'я користувача\" з облікових даних HTTP API на панелі керування",
promosmsLogin: "API Логін",
promosmsPassword: "API Пароль",
"pushoversounds pushover": "Pushover (по замовчуванню)",
"pushoversounds bike": "Bike",
"pushoversounds bugle": "Bugle",
"pushoversounds cashregister": "Cash Register",
"pushoversounds classical": "Classical",
"pushoversounds cosmic": "Cosmic",
"pushoversounds falling": "Falling",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Incoming",
"pushoversounds intermission": "Intermission",
"pushoversounds magic": "Magic",
"pushoversounds mechanical": "Mechanical",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "Tug Boat",
"pushoversounds alien": "Alien Alarm (long)",
"pushoversounds climb": "Climb (long)",
"pushoversounds persistent": "Persistent (long)",
"pushoversounds echo": "Pushover Echo (long)",
"pushoversounds updown": "Up Down (long)",
"pushoversounds vibrate": "Vibrate Only",
"pushoversounds none": "None (silent)",
pushyAPIKey: "Секретний ключ API",
pushyToken: "Токен пристрою",
"Using a Reverse Proxy?": "Використовувати зворотній проксі?",
"Check how to config it for WebSocket": "Перевірте, як налаштувати його для WebSocket",
"Steam Game Server": "Ігровий сервер Steam",
"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": "Незабаром",
wayToGetClickSendSMSToken: "Ви можете отримати ім’я користувача API та ключ API з {0} .",
"Connection String": "Рядок підключення",
"Query": "Запит",
settingsCertificateExpiry: "Закінчення терміну дії сертифіката TLS",
certificationExpiryDescription: "Запуск сповіщення для HTTPS моніторів коли до закінчення терміну дії TLS сертифіката:",
"ntfy Topic": "ntfy Тема",
"Domain": "Домен",
"Workstation": "Робоча станція",
disableCloudflaredNoAuthMsg: "Ви перебуваєте в режимі без авторизації, пароль не потрібен.",
}; };

@ -404,6 +404,8 @@ export default {
TemplateCode: "TemplateCode", TemplateCode: "TemplateCode",
SignName: "SignName", SignName: "SignName",
"Bark Endpoint": "Bark 接入点", "Bark Endpoint": "Bark 接入点",
"Bark Group": "Bark 群组",
"Bark Sound": "Bark 铃声",
"Device Token": "Apple Device Token", "Device Token": "Apple Device Token",
Platform: "平台", Platform: "平台",
iOS: "iOS", iOS: "iOS",

@ -13,6 +13,7 @@ export default {
pauseDashboardHome: "暫停", pauseDashboardHome: "暫停",
deleteMonitorMsg: "您確定要刪除此監測器嗎?", deleteMonitorMsg: "您確定要刪除此監測器嗎?",
deleteNotificationMsg: "您確定要為所有監測器刪除此通知嗎?", deleteNotificationMsg: "您確定要為所有監測器刪除此通知嗎?",
dnsPortDescription: "DNS 伺服器連接埠。預設為 53。您可以隨時變更連接埠。",
resolverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。", resolverserverDescription: "Cloudflare 為預設伺服器。您可以隨時更換解析伺服器。",
rrtypeDescription: "選擇您想要監測的資源記錄類型", rrtypeDescription: "選擇您想要監測的資源記錄類型",
pauseMonitorMsg: "您確定要暫停嗎?", pauseMonitorMsg: "您確定要暫停嗎?",
@ -332,6 +333,8 @@ export default {
info: "資訊", info: "資訊",
warning: "警告", warning: "警告",
danger: "危險", danger: "危險",
error: "錯誤",
critical: "嚴重",
primary: "主要", primary: "主要",
light: "淺色", light: "淺色",
dark: "暗色", dark: "暗色",
@ -372,6 +375,13 @@ export default {
smtpDkimHashAlgo: "雜湊演算法 (選填)", smtpDkimHashAlgo: "雜湊演算法 (選填)",
smtpDkimheaderFieldNames: "要簽署的郵件標頭 (選填)", smtpDkimheaderFieldNames: "要簽署的郵件標頭 (選填)",
smtpDkimskipFields: "不簽署的郵件標頭 (選填)", smtpDkimskipFields: "不簽署的郵件標頭 (選填)",
wayToGetPagerDutyKey: "您可以前往服務 -> 服務目錄 -> (選取服務) -> 整合 -> 新增整合以取得。您可以搜尋 \"Events API V2\"。詳細資訊 {0}",
"Integration Key": "整合金鑰",
"Integration URL": "整合網址",
"Auto resolve or acknowledged": "自動解決或認可",
"do nothing": "不進行任何操作",
"auto acknowledged": "自動認可",
"auto resolve": "自動解決",
gorush: "Gorush", gorush: "Gorush",
alerta: "Alerta", alerta: "Alerta",
alertaApiEndpoint: "API 端點", alertaApiEndpoint: "API 端點",
@ -398,6 +408,8 @@ export default {
SignName: "SignName", SignName: "SignName",
"Sms template must contain parameters: ": "Sms 範本必須包含參數:", "Sms template must contain parameters: ": "Sms 範本必須包含參數:",
"Bark Endpoint": "Bark 端點", "Bark Endpoint": "Bark 端點",
"Bark Group": "Bark 群組",
"Bark Sound": "Bark 鈴聲",
WebHookUrl: "WebHookUrl", WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey", SecretKey: "SecretKey",
"For safety, must use secret key": "為了安全起見,必須使用秘密金鑰", "For safety, must use secret key": "為了安全起見,必須使用秘密金鑰",
@ -465,4 +477,65 @@ export default {
"Footer Text": "頁尾文字", "Footer Text": "頁尾文字",
"Show Powered By": "顯示技術支援文字", "Show Powered By": "顯示技術支援文字",
"Domain Names": "網域名稱", "Domain Names": "網域名稱",
signedInDisp: "以 {0} 身分登入",
signedInDispDisabled: "驗證已停用。",
"Certificate Expiry Notification": "憑證到期通知",
"API Username": "API 使用者名稱",
"API Key": "API 金鑰",
"Recipient Number": "收件者號碼",
"From Name/Number": "來自名字/號碼",
"Leave blank to use a shared sender number.": "留空以使用共享寄件人號碼。",
"Octopush API Version": "Octopush API 版本",
"Legacy Octopush-DM": "舊版 Octopush-DM",
"endpoint": "端",
octopushAPIKey: "\"API key\" from HTTP API credentials in control panel",
octopushLogin: "\"Login\" from HTTP API credentials in control panel",
promosmsLogin: "API 登入名稱",
promosmsPassword: "API 密碼",
"pushoversounds pushover": "Pushover (預設)",
"pushoversounds bike": "車鈴",
"pushoversounds bugle": "號角",
"pushoversounds cashregister": "收銀機",
"pushoversounds classical": "古典",
"pushoversounds cosmic": "宇宙",
"pushoversounds falling": "下落",
"pushoversounds gamelan": "甘美朗",
"pushoversounds incoming": "來電",
"pushoversounds intermission": "中場休息",
"pushoversounds magic": "魔法",
"pushoversounds mechanical": "機械",
"pushoversounds pianobar": "Piano Bar",
"pushoversounds siren": "Siren",
"pushoversounds spacealarm": "Space Alarm",
"pushoversounds tugboat": "汽笛",
"pushoversounds alien": "外星鬧鐘 (長)",
"pushoversounds climb": "爬升 (長)",
"pushoversounds persistent": "持續 (長)",
"pushoversounds echo": "Pushover 回音 (長)",
"pushoversounds updown": "上下 (長)",
"pushoversounds vibrate": "僅震動",
"pushoversounds none": "無 (靜音)",
pushyAPIKey: "API 密鑰",
pushyToken: "裝置權杖",
"Show update if available": "顯示可用更新",
"Also check beta release": "檢查 Beta 版",
"Using a Reverse Proxy?": "正在使用反向代理?",
"Check how to config it for WebSocket": "查看如何為 WebSocket 設定",
"Steam Game Server": "Steam 遊戲伺服器",
"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": "即將推出",
wayToGetClickSendSMSToken: "您可以從 {0} 取得 API 使用者名稱和金鑰。",
"Connection String": "連線字串",
"Query": "查詢",
settingsCertificateExpiry: "TLS 憑證到期",
certificationExpiryDescription: "TLS 將於 X 天後到期時觸發 HTTPS 監測器通知:",
"ntfy Topic": "ntfy 主題",
"Domain": "網域",
"Workstation": "工作站",
disableCloudflaredNoAuthMsg: "您處於無驗證模式。無須輸入密碼。",
}; };

@ -77,7 +77,7 @@
<!-- Mobile Only --> <!-- Mobile Only -->
<div v-if="$root.isMobile" style="width: 100%; height: 60px;" /> <div v-if="$root.isMobile" style="width: 100%; height: 60px;" />
<nav v-if="$root.isMobile" class="bottom-nav"> <nav v-if="$root.isMobile && $root.loggedIn" class="bottom-nav">
<router-link to="/dashboard" class="nav-link"> <router-link to="/dashboard" class="nav-link">
<div><font-awesome-icon icon="tachometer-alt" /></div> <div><font-awesome-icon icon="tachometer-alt" /></div>
{{ $t("Dashboard") }} {{ $t("Dashboard") }}

@ -39,6 +39,7 @@ export default {
uptimeList: { }, uptimeList: { },
tlsInfoList: {}, tlsInfoList: {},
notificationList: [], notificationList: [],
dockerHostList: [],
statusPageListLoaded: false, statusPageListLoaded: false,
statusPageList: [], statusPageList: [],
proxyList: [], proxyList: [],
@ -147,6 +148,10 @@ export default {
}); });
}); });
socket.on("dockerHostList", (data) => {
this.dockerHostList = data;
});
socket.on("heartbeat", (data) => { socket.on("heartbeat", (data) => {
if (! (data.monitorID in this.heartbeatList)) { if (! (data.monitorID in this.heartbeatList)) {
this.heartbeatList[data.monitorID] = []; this.heartbeatList[data.monitorID] = [];

@ -27,6 +27,9 @@
<option value="dns"> <option value="dns">
DNS DNS
</option> </option>
<option value="docker">
{{ $t("Docker Container") }}
</option>
</optgroup> </optgroup>
<optgroup label="Passive Monitor Type"> <optgroup label="Passive Monitor Type">
@ -45,6 +48,12 @@
<option value="sqlserver"> <option value="sqlserver">
SQL Server SQL Server
</option> </option>
<option value="postgres">
PostgreSQL
</option>
<option value="radius">
Radius
</option>
</optgroup> </optgroup>
</select> </select>
</div> </div>
@ -81,8 +90,8 @@
</div> </div>
<!-- Hostname --> <!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam / MQTT only --> <!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt'" class="my-3"> <div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label> <label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required>
</div> </div>
@ -138,6 +147,34 @@
</div> </div>
</template> </template>
<!-- Docker Container Name / ID -->
<!-- For Docker Type -->
<div v-if="monitor.type === 'docker'" class="my-3">
<label for="docker_container" class="form-label">{{ $t("Container Name / ID") }}</label>
<input id="docker_container" v-model="monitor.docker_container" type="text" class="form-control" required>
</div>
<!-- Docker Host -->
<!-- For Docker Type -->
<div v-if="monitor.type === 'docker'" class="my-3">
<h2 class="mb-2">{{ $t("Docker Host") }}</h2>
<p v-if="$root.dockerHostList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<div v-else class="mb-3">
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
<select id="docket-host" v-model="monitor.docker_host" class="form-select">
<option v-for="host in $root.dockerHostList" :key="host.id" :value="host.id">{{ host.name }}</option>
</select>
<a href="#" @click="$refs.dockerHostDialog.show(monitor.docker_host)">{{ $t("Edit") }}</a>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
{{ $t("Setup Docker Host") }}
</button>
</div>
<!-- MQTT --> <!-- MQTT -->
<!-- For MQTT Type --> <!-- For MQTT Type -->
<template v-if="monitor.type === 'mqtt'"> <template v-if="monitor.type === 'mqtt'">
@ -168,15 +205,51 @@
</div> </div>
</template> </template>
<!-- SQL Server --> <template v-if="monitor.type === 'radius'">
<template v-if="monitor.type === 'sqlserver'"> <div class="my-3">
<label for="radius_username" class="form-label">Radius {{ $t("Username") }}</label>
<input id="radius_username" v-model="monitor.radiusUsername" type="text" class="form-control" required />
</div>
<div class="my-3"> <div class="my-3">
<label for="sqlserverConnectionString" class="form-label">SQL Server {{ $t("Connection String") }}</label> <label for="radius_password" class="form-label">Radius {{ $t("Password") }}</label>
<input id="sqlserverConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control"> <input id="radius_password" v-model="monitor.radiusPassword" type="password" class="form-control" required />
</div> </div>
<div class="my-3"> <div class="my-3">
<label for="sqlserverQuery" class="form-label">SQL Server {{ $t("Query") }}</label> <label for="radius_secret" class="form-label">{{ $t("RadiusSecret") }}</label>
<textarea id="sqlserverQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea> <input id="radius_secret" v-model="monitor.radiusSecret" type="password" class="form-control" required />
<div class="form-text"> {{ $t( "RadiusSecretDescription") }} </div>
</div>
<div class="my-3">
<label for="radius_called_station_id" class="form-label">{{ $t("RadiusCalledStationId") }}</label>
<input id="radius_called_station_id" v-model="monitor.radiusCalledStationId" type="text" class="form-control" required />
<div class="form-text"> {{ $t( "RadiusCalledStationIdDescription") }} </div>
</div>
<div class="my-3">
<label for="radius_calling_station_id" class="form-label">{{ $t("RadiusCallingStationId") }}</label>
<input id="radius_calling_station_id" v-model="monitor.radiusCallingStationId" type="text" class="form-control" required />
<div class="form-text"> {{ $t( "RadiusCallingStationIdDescription") }} </div>
</div>
</template>
<!-- SQL Server and PostgreSQL -->
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres'">
<div class="my-3">
<label for="sqlConnectionString" class="form-label">{{ $t("Connection String") }}</label>
<template v-if="monitor.type === 'sqlserver'">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>">
</template>
<template v-if="monitor.type === 'postgres'">
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="postgres://username:password@host:port/database">
</template>
</div>
<div class="my-3">
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea>
</div> </div>
</template> </template>
@ -202,6 +275,15 @@
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required min="20" step="1"> <input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required min="20" step="1">
</div> </div>
<div class="my-3">
<label for="resend-interval" class="form-label">
{{ $t("Resend Notification if Down X times consequently") }}
<span v-if="monitor.resendInterval > 0">({{ $t("resendEveryXTimes", [ monitor.resendInterval ]) }})</span>
<span v-else>({{ $t("resendDisabled") }})</span>
</label>
<input id="resend-interval" v-model="monitor.resendInterval" type="number" class="form-control" required min="0" step="1">
</div>
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2> <h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
@ -421,6 +503,7 @@
</form> </form>
<NotificationDialog ref="notificationDialog" @added="addedNotification" /> <NotificationDialog ref="notificationDialog" @added="addedNotification" />
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
<ProxyDialog ref="proxyDialog" @added="addedProxy" /> <ProxyDialog ref="proxyDialog" @added="addedProxy" />
</div> </div>
</transition> </transition>
@ -431,6 +514,7 @@ import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import CopyableInput from "../components/CopyableInput.vue"; import CopyableInput from "../components/CopyableInput.vue";
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import DockerHostDialog from "../components/DockerHostDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue"; import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue"; import TagsManager from "../components/TagsManager.vue";
import { genSecret, isDev } from "../util.ts"; import { genSecret, isDev } from "../util.ts";
@ -442,6 +526,7 @@ export default {
ProxyDialog, ProxyDialog,
CopyableInput, CopyableInput,
NotificationDialog, NotificationDialog,
DockerHostDialog,
TagsManager, TagsManager,
VueMultiselect, VueMultiselect,
}, },
@ -590,7 +675,7 @@ export default {
method: "GET", method: "GET",
interval: 60, interval: 60,
retryInterval: this.interval, retryInterval: this.interval,
databaseConnectionString: "Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>", resendInterval: 0,
maxretries: 0, maxretries: 0,
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,
@ -601,6 +686,8 @@ export default {
accepted_statuscodes: [ "200-299" ], accepted_statuscodes: [ "200-299" ],
dns_resolve_type: "A", dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1", dns_resolve_server: "1.1.1.1",
docker_container: "",
docker_host: null,
proxyId: null, proxyId: null,
mqttUsername: "", mqttUsername: "",
mqttPassword: "", mqttPassword: "",
@ -728,6 +815,12 @@ export default {
addedProxy(id) { addedProxy(id) {
this.monitor.proxyId = id; this.monitor.proxyId = id;
}, },
// Added a Docker Host Event
// Enable it if the Docker Host is added in EditMonitor.vue
addedDockerHost(id) {
this.monitor.docker_host = id;
}
}, },
}; };
</script> </script>

@ -89,6 +89,9 @@ export default {
"monitor-history": { "monitor-history": {
title: this.$t("Monitor History"), title: this.$t("Monitor History"),
}, },
"docker-hosts": {
title: this.$t("Docker Hosts"),
},
security: { security: {
title: this.$t("Security"), title: this.$t("Security"),
}, },
@ -153,6 +156,10 @@ export default {
this.settings.tlsExpiryNotifyDays = [ 7, 14, 21 ]; this.settings.tlsExpiryNotifyDays = [ 7, 14, 21 ];
} }
if (this.settings.trustProxy === undefined) {
this.settings.trustProxy = false;
}
this.settingsLoaded = true; this.settingsLoaded = true;
}); });
}, },

@ -25,6 +25,7 @@ const Security = () => import("./components/settings/Security.vue");
import Proxies from "./components/settings/Proxies.vue"; import Proxies from "./components/settings/Proxies.vue";
import Backup from "./components/settings/Backup.vue"; import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue"; import About from "./components/settings/About.vue";
import DockerHosts from "./components/settings/Docker.vue";
const routes = [ const routes = [
{ {
@ -95,6 +96,10 @@ const routes = [
path: "monitor-history", path: "monitor-history",
component: MonitorHistory, component: MonitorHistory,
}, },
{
path: "docker-hosts",
component: DockerHosts,
},
{ {
path: "security", path: "security",
component: Security, component: Security,

Loading…
Cancel
Save