Compare commits

..

2 Commits

Author SHA1 Message Date
Frank Elsinga
ff197839b3 Moved the DebugMonitorDialog into its own component 2024-10-09 04:53:35 +02:00
Frank Elsinga
c90aacc62c made sure that the debug output for editing the monitor is shell-escaped 2024-10-09 03:45:07 +02:00
39 changed files with 268 additions and 420 deletions

View File

@@ -1,6 +1,6 @@
# Project Info
First of all, I want to thank everyone who has submitted issues or shared pull requests for Uptime Kuma.
First of all, I want to thank everyone who have wrote issues or shared pull requests for Uptime Kuma.
I never thought the GitHub community would be so nice!
Because of this, I also never thought that other people would actually read and edit my code.
Parts of the code are not very well-structured or commented, sorry about that.
@@ -9,7 +9,7 @@ The project was created with `vite.js` and is written in `vue3`.
Our backend lives in the `server`-directory and mostly communicates via websockets.
Both frontend and backend share the same `package.json`.
For production, the frontend is built into the `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
For production, the frontend is build into `dist`-directory and the server (`express.js`) exposes the `dist` directory as the root of the endpoint.
For development, we run vite in development mode on another port.
## Directories
@@ -28,7 +28,7 @@ For development, we run vite in development mode on another port.
## Can I create a pull request for Uptime Kuma?
Yes or no, it depends on what you will try to do.
Both yours and our maintainers' time is precious, and we don't want to waste either.
Both your and our maintainers time is precious, and we don't want to waste both time.
If you have any questions about any process/.. is not clear, you are likely not alone => please ask them ^^
@@ -49,11 +49,11 @@ Different guidelines exist for different types of pull requests (PRs):
<p>
If you come across a bug and think you can solve, we appreciate your work.
Please make sure that you follow these rules:
Please make sure that you follow by these rules:
- keep the PR as small as possible, fix only one thing at a time => keeping it reviewable
- test that your code does what you claim it does.
- test that your code does what you came it does.
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>translations / internationalisation (i18n)</b></summary>
@@ -68,7 +68,7 @@ Different guidelines exist for different types of pull requests (PRs):
- language keys need to be **added to `en.json`** to be visible in weblate. If this has not happened, a PR is appreciated.
- **Adding a new language** requires a new file see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md)
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new notification providers</b></summary>
@@ -102,7 +102,7 @@ Different guidelines exist for different types of pull requests (PRs):
Therefore, making sure that they work is also really important.
Because testing notification providers is quite time intensive, we mostly offload this onto the person contributing a notification provider.
To make sure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
To make shure you have tested the notification provider, please include screenshots of the following events in the pull-request description:
- `UP`/`DOWN`
- Certificate Expiry via https://expired.badssl.com/
- Testing (the test button on the notification provider setup page)
@@ -117,7 +117,7 @@ Different guidelines exist for different types of pull requests (PRs):
| Testing | paste-image-here | paste-image-here |
```
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new monitoring types</b></summary>
@@ -138,14 +138,14 @@ Different guidelines exist for different types of pull requests (PRs):
-
<sub>Because maintainer time is precious, junior maintainers may merge uncontroversial PRs in this area.</sub>
<sub>Because maintainer time is precious junior maintainers may merge uncontroversial PRs in this area.</sub>
</p>
</details>
- <details><summary><b>new features/ major changes / breaking bugfixes</b></summary>
<p>
be sure to **create an empty draft pull request or open an issue, so we can have a discussion first**.
This is especially important for a large pull request or when you don't know if it will be merged or not.
This is especially important for a large pull request or you don't know if it will be merged or not.
<sub>Because of the large impact of this work, only senior maintainers may merge PRs in this area.</sub>
</p>
@@ -201,7 +201,7 @@ The rationale behind this is that we can align the direction and scope of the fe
## Project Styles
I personally do not like something that requires a lot of configuration before you can finally start the app.
I personally do not like something that requires so many configurations before you can finally start the app.
The goal is to make the Uptime Kuma installation as easy as installing a mobile app.
- Easy to install for non-Docker users
@@ -260,7 +260,7 @@ Port `3000` and port `3001` will be used.
npm run dev
```
But sometimes you may want to restart the server without restarting the frontend. In that case, you can run these commands in two terminals:
But sometimes, you would like to restart the server, but not the frontend, you can run these commands in two terminals:
```bash
npm run start-frontend-dev
@@ -409,7 +409,7 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc
### What is a maintainer and what are their roles?
This project has multiple maintainers who specialise in different areas.
This project has multiple maintainers which specialise in different areas.
Currently, there are 3 maintainers:
| Person | Role | Main Area |

View File

@@ -1508,8 +1508,10 @@ class Monitor extends BeanModel {
return await R.getAll(`
SELECT monitor_notification.monitor_id, monitor_notification.notification_id
FROM monitor_notification
WHERE monitor_notification.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
WHERE monitor_notification.monitor_id IN (?)
`, [
monitorIDs,
]);
}
/**
@@ -1519,11 +1521,13 @@ class Monitor extends BeanModel {
*/
static async getMonitorTag(monitorIDs) {
return await R.getAll(`
SELECT monitor_tag.monitor_id, monitor_tag.tag_id, tag.name, tag.color
SELECT monitor_tag.monitor_id, tag.name, tag.color
FROM monitor_tag
JOIN tag ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
`, monitorIDs);
WHERE monitor_tag.monitor_id IN (?)
`, [
monitorIDs,
]);
}
/**
@@ -1563,7 +1567,6 @@ class Monitor extends BeanModel {
tagsMap.set(row.monitor_id, []);
}
tagsMap.get(row.monitor_id).push({
tag_id: row.tag_id,
name: row.name,
color: row.color
});

View File

@@ -1,35 +0,0 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Elks extends NotificationProvider {
name = "Elks";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.46elks.com/a1/sms";
try {
let data = new URLSearchParams();
data.append("from", notification.elksFromNumber);
data.append("to", notification.elksToNumber );
data.append("message", msg);
const config = {
headers: {
"Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64")
}
};
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Elks;

View File

@@ -87,6 +87,7 @@ class DingDing extends NotificationProvider {
* @returns {string} Status
*/
statusToString(status) {
// TODO: Move to notification-provider.js to avoid repetition in classes
switch (status) {
case DOWN:
return "DOWN";

View File

@@ -48,7 +48,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
value: this.extractAdress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,
@@ -85,7 +85,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
value: this.extractAdress(monitorJSON),
},
{
name: `Time (${heartbeatJSON["timezone"]})`,

View File

@@ -24,7 +24,7 @@ class NotificationProvider {
* @param {?object} monitorJSON Monitor details (For Up/Down only)
* @returns {string} The extracted address based on the monitor type.
*/
extractAddress(monitorJSON) {
extractAdress(monitorJSON) {
if (!monitorJSON) {
return "";
}

View File

@@ -32,7 +32,7 @@ class SevenIO extends NotificationProvider {
return okMsg;
}
let address = this.extractAddress(monitorJSON);
let address = this.extractAdress(monitorJSON);
if (address !== "") {
address = `(${address}) `;
}

View File

@@ -18,7 +18,7 @@ class SIGNL4 extends NotificationProvider {
msg,
// Source system
"X-S4-SourceSystem": "UptimeKuma",
monitorUrl: this.extractAddress(monitorJSON),
monitorUrl: this.extractAdress(monitorJSON),
};
const config = {

View File

@@ -48,7 +48,7 @@ class Slack extends NotificationProvider {
}
const address = this.extractAddress(monitorJSON);
const address = this.extractAdress(monitorJSON);
if (address) {
actions.push({
"type": "button",

View File

@@ -93,7 +93,7 @@ class SMTP extends NotificationProvider {
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
monitorHostnameOrURL = this.extractAddress(monitorJSON);
monitorHostnameOrURL = this.extractAdress(monitorJSON);
}
let serviceStatus = "⚠️ Test";

View File

@@ -34,7 +34,7 @@ class Squadcast extends NotificationProvider {
data.status = "resolve";
}
data.tags["AlertAddress"] = this.extractAddress(monitorJSON);
data.tags["AlertAddress"] = this.extractAdress(monitorJSON);
monitorJSON["tags"].forEach(tag => {
data.tags[tag["name"]] = {

View File

@@ -225,7 +225,7 @@ class Teams extends NotificationProvider {
const payload = this._notificationPayloadFactory({
heartbeatJSON: heartbeatJSON,
monitorName: monitorJSON.name,
monitorUrl: this.extractAddress(monitorJSON),
monitorUrl: this.extractAdress(monitorJSON),
dashboardUrl: dashboardUrl,
});

View File

@@ -10,22 +10,11 @@ class TechulusPush extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
let data = {
"title": notification?.pushTitle?.length ? notification.pushTitle : "Uptime-Kuma",
"body": msg,
"timeSensitive": notification.pushTimeSensitive ?? true,
};
if (notification.pushChannel) {
data.channel = notification.pushChannel;
}
if (notification.pushSound) {
data.sound = notification.pushSound;
}
try {
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, data);
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
"title": "Uptime-Kuma",
"body": msg,
});
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View File

@@ -85,7 +85,7 @@ class ZohoCliq extends NotificationProvider {
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: this.extractAddress(monitorJSON),
monitorUrl: this.extractAdress(monitorJSON),
status: heartbeatJSON.status
});

View File

@@ -11,7 +11,6 @@ const CallMeBot = require("./notification-providers/call-me-bot");
const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Elks = require("./notification-providers/46elks");
const Feishu = require("./notification-providers/feishu");
const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat");
@@ -96,7 +95,6 @@ class Notification {
new SMSC(),
new DingDing(),
new Discord(),
new Elks(),
new Feishu(),
new FreeMobile(),
new GoogleChat(),

View File

@@ -0,0 +1,201 @@
<template>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<div v-if="monitor?.type === 'http'">
<textarea id="curl-debug" v-model="curlCommand" class="form-control mb-3" readonly wrap="off"></textarea>
<button
id="debug-copy-btn" class="btn btn-outline-primary position-absolute top-0 end-0 mt-3 me-3 border-0"
type="button" @click.stop="copyToClipboard"
>
<font-awesome-icon icon="copy" />
</button>
<i18n-t keypath="CurlDebugInfo" tag="p" class="form-text">
<template #newiline>
<br>
</template>
<template #firewalls>
<a href="https://xkcd.com/2259/" target="_blank">{{ $t('firewalls') }}</a>
</template>
<template #dns_resolvers>
<a
href="https://www.reddit.com/r/sysadmin/comments/rxho93/thank_you_for_the_running_its_always_dns_joke_its/"
target="_blank"
>{{ $t('dns resolvers') }}</a>
</template>
<template #docker_networks>
<a href="https://youtu.be/bKFMS5C4CG0" target="_blank">{{ $t('docker networks') }}</a>
</template>
</i18n-t>
<div
v-if="monitor.authMethod === 'oauth2-cc'" class="alert alert-warning d-flex align-items-center gap-2"
role="alert"
>
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoOAuth2CCUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
<template #newline>
<br>
</template>
<template #oauth2_bearer>
<code>--oauth2-bearer TOKEN</code>
</template>
</i18n-t>
</div>
<div v-if="monitor.proxyId" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoProxiesUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
</i18n-t>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from "bootstrap";
import { version } from "../../package.json";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
name: "DebugMonitor",
props: {
/** Monitor this represents */
monitor: {
type: Object,
default: null,
},
},
data() {
return {
modal: null,
};
},
computed: {
curlCommand() {
if (this.monitor === null) {
return "";
}
let method = this.monitor.method;
if ([ "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS" ].indexOf(method) === -1) {
// set to a custom value => could lead to injections
method = this.escapeShell(method);
}
const command = [ "curl", "--verbose", "--head", "--request", method, "\\\n" ];
command.push("--user-agent", `'Uptime-Kuma/${version}'`, "\\\n");
if (this.monitor.ignoreTls) {
command.push("--insecure", "\\\n");
}
if (this.monitor.headers) {
try {
for (const [ key, value ] of Object.entries(JSON.parse(this.monitor.headers))) {
command.push("--header", `'${this.escapeShellNoQuotes(key)}: ${this.escapeShellNoQuotes(value)}'`, "\\\n");
}
} catch (e) {
command.push("--header", this.escapeShell(this.monitor.headers), "\\\n");
}
}
if (this.monitor.authMethod === "basic") {
command.push("--basic", "--user", `'${this.escapeShellNoQuotes(this.monitor.basic_auth_user)}:${this.escapeShellNoQuotes(this.monitor.basic_auth_pass)}'`, "\\\n");
} else if (this.monitor.authMethod === "mtls") {
command.push("--cacert", this.escapeShell(this.monitor.tlsCa), "\\\n");
command.push("--key", this.escapeShell(this.monitor.tlsKey), "\\\n");
command.push( "--cert", this.escapeShell(this.monitor.tlsCert), "\\\n");
} else if (this.monitor.authMethod === "ntlm") {
let domain = "";
if (this.monitor.authDomain) {
domain = `${this.monitor.authDomain}/`;
}
command.push("--ntlm", "--user", `'${this.escapeShellNoQuotes(domain)}${this.escapeShellNoQuotes(this.monitor.basic_auth_user)}:${this.escapeShellNoQuotes(this.monitor.basic_auth_pass)}'`, "\\\n");
}
if (this.monitor.body && this.monitor.httpBodyEncoding === "json") {
let json = "";
try {
// trying to parse the supplied data as json to trim whitespace
json = JSON.stringify(JSON.parse(this.monitor.body));
} catch (e) {
json = this.monitor.body;
}
command.push("--header", "'Content-Type: application/json'", "\\\n");
command.push("--data", this.escapeShell(json), "\\\n");
} else if (this.monitor.body && this.monitor.httpBodyEncoding === "xml") {
command.push("--headers", "'Content-Type: application/xml'", "\\\n");
command.push("--data", this.escapeShell(this.monitor.body), "\\\n");
}
if (this.monitor.maxredirects) {
command.push("--location", "--max-redirs", this.escapeShell(this.monitor.maxredirects), "\\\n");
}
if (this.monitor.timeout) {
command.push("--max-time", this.escapeShell(this.monitor.timeout), "\\\n");
}
if (this.monitor.maxretries) {
command.push("--retry", this.escapeShell(this.monitor.maxretries), "\\\n");
}
command.push("--url", this.escapeShell(this.monitor.url));
return command.join(" ");
},
},
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
/**
* Show the dialog
* @returns {void}
*/
show() {
this.modal.show();
},
/**
* Escape a string for use in a shell
* @param {string|number} s string to escape
* @returns {string} escaped, quoted string
*/
escapeShell(s) {
if (typeof s == "number") {
return s.toString();
}
return "'" + this.escapeShellNoQuotes(s) + "'";
},
/**
* Escape a string for use in a shell
* @param {string} s string to escape
* @returns {string} escaped string
*/
escapeShellNoQuotes(s) {
return s.replace(/(['"$`\\])/g, "\\$1");
},
/**
* Copies a value to the clipboard and shows toasts with the success/error
* @returns {void}
*/
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this.curlCommand);
toast.success(this.$t("CopyToClipboardSuccess"));
} catch (err) {
toast.error(this.$t("CopyToClipboardError", { error: err.message }));
}
},
}
};
</script>
<style lang="scss" scoped>
@import "../assets/vars";
textarea {
min-height: 200px;
}
#curl-debug {
font-family: monospace;
overflow: auto;
}
</style>

View File

@@ -118,7 +118,6 @@ export default {
"clicksendsms": "ClickSend SMS",
"CallMeBot": "CallMeBot (WhatsApp, Telegram Call, Facebook Messanger)",
"discord": "Discord",
"Elks": "46elks",
"GoogleChat": "Google Chat (Google Workspace)",
"gorush": "Gorush",
"gotify": "Gotify",

View File

@@ -33,7 +33,7 @@
<template #item="monitor">
<div class="item" data-testid="monitor">
<div class="row">
<div class="col-9 col-md-8 small-padding">
<div class="col-6 col-md-4 small-padding">
<div class="info">
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
@@ -71,7 +71,7 @@
</div>
</div>
</div>
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<div :key="$root.userHeartbeatBar" class="col-6 col-md-8">
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
</div>
</div>

View File

@@ -1,48 +0,0 @@
<template>
<div class="mb-3">
<label for="ElksUsername" class="form-label">{{ $t("Username") }}</label>
<input id="ElksUsername" v-model="$parent.notification.elksUsername" type="text" class="form-control" required>
<label for="ElksPassword" class="form-label">{{ $t("Password") }}</label>
</div>
<div class="form-text">
<HiddenInput id="ElksPassword" v-model="$parent.notification.elksAuthToken" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t tag="p" keypath="Can be found on:">
<a href="https://46elks.com/account" target="_blank">https://46elks.com/account</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="Elks-from-number" class="form-label">{{ $t("From") }}</label>
<input id="Elks-from-number" v-model="$parent.notification.elksFromNumber" type="text" class="form-control" required>
<div class="form-text">
{{ $t("Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.") }}
<i18n-t tag="p" keypath="More info on:">
<a href="https://46elks.se/kb/text-sender-id" target="_blank">https://46elks.se/kb/text-sender-id</a>
</i18n-t>
</div>
</div>
<div class="mb-3">
<label for="Elks-to-number" class="form-label">{{ $t("To Number") }}</label>
<input id="Elks-to-number" v-model="$parent.notification.elksToNumber" type="text" class="form-control" required>
<div class="form-text">
{{ $t("The phone number of the recipient in E.164 format.") }}
<i18n-t tag="p" keypath="More info on:">
<a href="https://46elks.se/kb/e164" target="_blank">https://46elks.se/kb/e164</a>
</i18n-t>
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://46elks.com/docs/send-sms" target="_blank">https://46elks.com/docs/send-sms</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View File

@@ -4,53 +4,6 @@
<HiddenInput id="push-api-key" v-model="$parent.notification.pushAPIKey" :required="true" autocomplete="new-password"></HiddenInput>
</div>
<div class="mb-3">
<label for="push-api-title" class="form-label">{{ $t("Title") }}</label>
<input id="push-api-title" v-model="$parent.notification.pushTitle" type="text" class="form-control">
</div>
<div class="mb-3">
<label for="push-api-channel" class="form-label">{{ $t("Notification Channel") }}</label>
<input id="push-api-channel" v-model="$parent.notification.pushChannel" type="text" class="form-control" patttern="[A-Za-z0-9-]+">
<div class="form-text">
{{ $t("Alphanumerical string and hyphens only") }}
</div>
</div>
<div class="mb-3">
<label for="push-api-sound" class="form-label">{{ $t("Sound") }}</label>
<select id="push-api-sound" v-model="$parent.notification.pushSound" class="form-select">
<option value="default">{{ $t("Default") }}</option>
<option value="arcade">{{ $t("Arcade") }}</option>
<option value="correct">{{ $t("Correct") }}</option>
<option value="fail">{{ $t("Fail") }}</option>
<option value="harp">{{ $t("Harp") }}</option>
<option value="reveal">{{ $t("Reveal") }}</option>
<option value="bubble">{{ $t("Bubble") }}</option>
<option value="doorbell">{{ $t("Doorbell") }}</option>
<option value="flute">{{ $t("Flute") }}</option>
<option value="money">{{ $t("Money") }}</option>
<option value="scifi">{{ $t("Scifi") }}</option>
<option value="clear">{{ $t("Clear") }}</option>
<option value="elevator">{{ $t("Elevator") }}</option>
<option value="guitar">{{ $t("Guitar") }}</option>
<option value="pop">{{ $t("Pop") }}</option>
</select>
<div class="form-text">
{{ $t("Custom sound to override default notification sound") }}
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input v-model="$parent.notification.pushTimeSensitive" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("Time Sensitive (iOS Only)") }}</label>
</div>
<div class="form-text">
{{ $t("Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.") }}
</div>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="https://docs.push.techulus.com" target="_blank">https://docs.push.techulus.com</a>
</i18n-t>
@@ -63,19 +16,5 @@ export default {
components: {
HiddenInput,
},
mounted() {
if (typeof this.$parent.notification.pushTitle === "undefined") {
this.$parent.notification.pushTitle = "Uptime-Kuma";
}
if (typeof this.$parent.notification.pushChannel === "undefined") {
this.$parent.notification.pushChannel = "uptime-kuma";
}
if (typeof this.$parent.notification.pushSound === "undefined") {
this.$parent.notification.pushSound = "default";
}
if (typeof this.$parent.notification.pushTimeSensitive === "undefined") {
this.$parent.notification.pushTimeSensitive = true;
}
},
};
</script>

View File

@@ -9,7 +9,6 @@ import CallMeBot from "./CallMeBot.vue";
import SMSC from "./SMSC.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
import Elks from "./46elks.vue";
import Feishu from "./Feishu.vue";
import FreeMobile from "./FreeMobile.vue";
import GoogleChat from "./GoogleChat.vue";
@@ -83,7 +82,6 @@ const NotificationFormList = {
"smsc": SMSC,
"DingDing": DingDing,
"discord": Discord,
"Elks": Elks,
"Feishu": Feishu,
"FreeMobile": FreeMobile,
"GoogleChat": GoogleChat,

View File

@@ -1 +0,0 @@
{}

View File

@@ -1011,45 +1011,5 @@
"OAuth2: Client Credentials": "OAuth2: přihlašovací údaje klienta",
"Authentication Method": "Metoda ověřování",
"Authorization Header": "Hlavička autorizace",
"Form Data Body": "Tělo formuláře s daty",
"threemaRecipientTypePhoneFormat": "E.164, bez počátečního +",
"jsonQueryDescription": "Pro zpracování a získání konkrétních dat z JSON odpovědi serveru použijte JSON dotaz - případně \"$\" pro zdrojovou (raw) odpověď, pokud neočekáváte JSON výstup. Výsledek bude následně porovnán jako řetězec vůči očekávaní hodnotě. Dokumentaci naleznete na {0} a pro testování dotazů můžete využít {1}.",
"shrinkDatabaseDescriptionSqlite": "Podmínka spuštění příkazu {vacuum} nad SQLite databází. Příkaz {auto_vacuum} je již zapnutý, ale nedochází k defragmentaci databáze ani k přebalení jednotlivých stránek databáze tak, jak to dělá příkaz {vacuum}.",
"Community String": "Řetězec komunity",
"Host Onesender": "Onesender hostitel",
"Token Onesender": "Onesender token",
"snmpOIDHelptext": "Zadejte OID senzoru nebo stavu, který chcete monitorovat. Pokud si nejste jisti identifikátorem OID, použijte nástroje pro správu sítě, jako jsou prohlížeče MIB nebo SNMP software.",
"snmpCommunityStringHelptext": "Tento řetězec slouží jako heslo pro ověřování a řízení přístupu k zařízením podporujícím protokol SNMP. Shodujte se s konfigurací zařízení SNMP.",
"record": "záznam",
"Go back to home page.": "Vrátit se domovskou stránku.",
"No tags found.": "Nenalezeny žádné štítky.",
"Lost connection to the socket server.": "Ztraceno socketové spojení se serverem.",
"Cannot connect to the socket server.": "Nelze navázat socketové spojení se serverem.",
"SIGNL4": "SIGNL4",
"SIGNL4 Webhook URL": "URL adresa webhooku SIGNL4",
"signl4Docs": "Další informace související s konfigurací SIGNL4 a postup jak získat URL webhooku SIGNL4 naleznete na {0}.",
"Conditions": "Podmínky",
"conditionAdd": "Přidat podmínku",
"conditionDelete": "Vymazat podmínku",
"conditionAddGroup": "Přidat skupinu",
"conditionDeleteGroup": "Smazat skupinu",
"conditionValuePlaceholder": "Hodnota",
"equals": "rovná se",
"not equals": "nerovná se",
"contains": "obsahuje",
"not contains": "neobsahuje",
"starts with": "začíná na",
"not starts with": "nezačíná na",
"ends with": "končí na",
"not ends with": "nekončí na",
"less than": "menší než",
"greater than": "větší než",
"less than or equal to": "menší nebo rovno",
"greater than or equal to": "větší nebo rovno",
"groupOnesenderDesc": "Ujistěte se, že jste zadali platné GroupID. Pro odeslání zprávy do skupiny zadejte například 628123456789-342345",
"OAuth Token URL": "URL OAuth tokenu",
"Client ID": "ID klienta",
"Client Secret": "Tajemství klienta",
"OAuth Scope": "OAuth rozsah",
"Optional: Space separated list of scopes": "Volitelné: seznam rozsahů oddělte mezerami"
"Form Data Body": "Tělo formuláře s daty"
}

View File

@@ -1,7 +1,7 @@
{
"languageName": "Deutsch (Schweiz)",
"Settings": "Einstellungen",
"Dashboard": "Überblick",
"Dashboard": "Dashboard",
"New Update": "Update verfügbar",
"Language": "Sprache",
"Appearance": "Erscheinungsbild",
@@ -1047,6 +1047,5 @@
"greater than": "mehr als",
"less than or equal to": "kleiner als oder gleich",
"greater than or equal to": "grösser als oder gleich",
"record": "Eintrag",
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut."
"record": "Eintrag"
}

View File

@@ -1,7 +1,7 @@
{
"languageName": "Deutsch",
"Settings": "Einstellungen",
"Dashboard": "Überblick",
"Dashboard": "Dashboard",
"New Update": "Aktualisierung verfügbar",
"Language": "Sprache",
"Appearance": "Erscheinungsbild",
@@ -1050,6 +1050,5 @@
"less than": "weniger als",
"less than or equal to": "kleiner als oder gleich",
"greater than or equal to": "größer als oder gleich",
"record": "Eintrag",
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut."
"record": "Eintrag"
}

View File

@@ -1027,29 +1027,5 @@
"greater than": "greater than",
"less than or equal to": "less than or equal to",
"greater than or equal to": "greater than or equal to",
"record": "record",
"Notification Channel": "Notification Channel",
"Sound": "Sound",
"Alphanumerical string and hyphens only": "Alphanumerical string and hyphens only",
"Arcade": "Arcade",
"Correct": "Correct",
"Fail":"Fail",
"Harp":"Harp",
"Reveal":"Reveal",
"Bubble":"Bubble",
"Doorbell":"Doorbell",
"Flute":"Flute",
"Money":"Money",
"Scifi":"Scifi",
"Clear":"Clear",
"Elevator":"Elevator",
"Guitar":"Guitar",
"Pop":"Pop",
"Custom sound to override default notification sound": "Custom sound to override default notification sound",
"Time Sensitive (iOS Only)": "Time Sensitive (iOS Only)",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.",
"From":"From",
"Can be found on:": "Can be found on: {0}",
"The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies."
"record": "record"
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -106,7 +106,7 @@
"disableauth.message2": "Egoera jakin batzuetarako diseinatuta dago, Uptime Kumaren {intendThirdPartyAuth} (Cloudflare Access, Authelia edo beste autentifikazio-mekanismo batzuk).",
"where you intend to implement third-party authentication": "aurrean hirugarrengo autentifikazio batzuek jartzeko",
"Please use this option carefully!": "Mesedez, kontuz erabili aukera hau!",
"Logout": "Itxi saioa",
"Logout": "Saioa amaitu",
"Leave": "Utzi",
"I understand, please disable": "Ulertzen dut, mesedez desgaitu",
"Confirm": "Baieztatu",
@@ -115,7 +115,7 @@
"Username": "Erabiltzailea",
"Password": "Pasahitza",
"Remember me": "Gogora nazazu",
"Login": "Hasi saioa",
"Login": "Saioa hasi",
"No Monitors, please": "Monitorizaziorik ez, mesedez",
"add one": "gehitu bat",
"Notification Type": "Jakinarazpen mota",
@@ -164,11 +164,11 @@
"Add New below or Select...": "Gehitu beste bat behean edo hautatu…",
"Tag with this name already exist.": "Izen hau duen etiketa dagoeneko badago.",
"Tag with this value already exist.": "Balio hau duen etiketa dagoeneko badago.",
"color": "Kolorea",
"color": "kolorea",
"value (optional)": "balioa (hautazkoa)",
"Gray": "Grisa",
"Red": "Gorria",
"Orange": "Laranja",
"Orange": "Naranja",
"Green": "Berdea",
"Blue": "Urdina",
"Indigo": "Indigo",
@@ -190,7 +190,7 @@
"Status Page": "Egoera orria",
"Status Pages": "Egoera orriak",
"defaultNotificationName": "Nire {notification} Alerta ({number})",
"here": "hemen",
"here": "Hemen",
"Required": "Beharrezkoa",
"telegram": "Telegram",
"ZohoCliq": "ZohoCliq",
@@ -582,10 +582,6 @@
"Mechanism": "Mekanismoa",
"Home": "Hasiera",
"filterActive": "Aktibo",
"filterActivePaused": "Pausatua",
"Expected Value": "Esperotako balioa",
"statusPageRefreshIn": "{0} barru freskatuko da.",
"now": "orain",
"time ago": "duela {0}",
"-year": "-urte"
"filterActivePaused": "Geldituta",
"Expected Value": "Esperotako balioa"
}

View File

@@ -1050,6 +1050,5 @@
"greater than": "supérieur à",
"less than or equal to": "inférieur ou égal à",
"greater than or equal to": "supérieur ou égal à",
"record": "enregistrer",
"shrinkDatabaseDescriptionSqlite": "Déclencher la commande {vacuum} pour la base de données SQLite. {auto_vacuum} est déjà activé, mais cela ne défragmente pas la base de données ni ne réorganise les pages individuelles de la base de données de la même manière que la commande {vacuum}."
"record": "enregistrer"
}

View File

@@ -1015,6 +1015,5 @@
"less than": "níos lú ná",
"greater than": "níos mó ná",
"less than or equal to": "níos lú ná nó cothrom le",
"record": "taifead",
"shrinkDatabaseDescriptionSqlite": "Bunachar sonraí truicear {vacuum} le haghaidh SQLite. Tá {auto_vacuum} cumasaithe cheana féin ach ní dhéanann sé seo scoilt ar an mbunachar sonraí ná athphacáil leathanaigh aonair an bhunachair sonraí mar a dhéanann an t-ordú {vacuum}."
"record": "taifead"
}

View File

@@ -1045,6 +1045,5 @@
"New Group": "Grup Baru",
"Group Name": "Nama Grup",
"OAuth2: Client Credentials": "OAuth2: Kredensial Klien",
"Authentication Method": "Metode Autentikasi",
"shrinkDatabaseDescriptionSqlite": "Memicu pangkalan data {vacuum} untuk SQLite. {auto_vacuum} sudah diaktifkan, tetapi tidak mendefragmentasi pangkalan data atau mengemas ulang halaman individual dari pangkalan data seperti yang dilakukan oleh perintah {vacuum}."
"Authentication Method": "Metode Autentikasi"
}

View File

@@ -989,9 +989,5 @@
"wayToGetThreemaGateway": "Możesz zarejestrować się w Threema Gateway {0}.",
"threemaSenderIdentityFormat": "8 znaków, zwykle zaczyna się od *",
"threemaBasicModeInfo": "Uwaga: Ta integracja korzysta z Threema Gateway w trybie podstawowym (szyfrowanie po stronie serwera). Więcej szczegółów można znaleźć {0}.",
"apiKeysDisabledMsg": "Klucze API są wyłączone, ponieważ wyłączone jest uwierzytelnianie.",
"-year": "-rok",
"and": "i",
"now": "teraz",
"cacheBusterParam": "Dodaj parametr {0}"
"apiKeysDisabledMsg": "Klucze API są wyłączone, ponieważ wyłączone jest uwierzytelnianie."
}

View File

@@ -1050,6 +1050,5 @@
"less than": "daha küçük",
"greater than or equal to": "büyük veya eşit",
"record": "kayıt",
"jsonQueryDescription": "JSON sorgusunu kullanarak sunucunun JSON yanıtından belirli verileri ayrıştırın ve çıkarın. JSON beklemiyorsanız ham yanıt için \"$\" sembolünü kullanın. Sonuç daha sonra metin olarak beklenen değerle karşılaştırılır. Belgeler için {0}'a bakın ve sorgularla denemeler yapmak için {1}'i kullanın.",
"shrinkDatabaseDescriptionSqlite": "SQLite için {vacuum} veritabanını tetikle. {auto_vacuum} zaten etkin ancak bu, {vacuum} komutunun yaptığı gibi veritabanını birleştirmez veya tek tek veritabanı sayfalarını yeniden paketlemez."
"jsonQueryDescription": "JSON sorgusunu kullanarak sunucunun JSON yanıtından belirli verileri ayrıştırın ve çıkarın. JSON beklemiyorsanız ham yanıt için \"$\" sembolünü kullanın. Sonuç daha sonra metin olarak beklenen değerle karşılaştırılır. Belgeler için {0}'a bakın ve sorgularla denemeler yapmak için {1}'i kullanın."
}

View File

@@ -1 +0,0 @@
{}

View File

@@ -1056,6 +1056,5 @@
"greater than": "більше, ніж",
"less than or equal to": "менше або дорівнює",
"greater than or equal to": "більше або дорівнює",
"record": "запис",
"shrinkDatabaseDescriptionSqlite": "Запускає команду {vacuum} для бази даних SQLite. Команда {auto_vacuum} вже увімкнена, але вона не дефрагментує базу даних і не перепаковує окремі сторінки бази даних так, як це робить команда {vacuum}."
"record": "запис"
}

View File

@@ -1052,6 +1052,5 @@
"less than or equal to": "不多于",
"greater than or equal to": "不少于",
"record": "记录",
"jsonQueryDescription": "使用 JSON 查询解析并提取服务器 JSON 响应中的特定数据,或者,如果不期望得到 JSON 响应,则可使用 \"$\" 获取原始响应。然后将结果转为字符串并与期望值进行字符串比较。有关更多文档,请参阅 {0},亦可使用 {1} 来尝试查询。",
"shrinkDatabaseDescriptionSqlite": "触发 SQLite 数据库的 {vacuum} 命令。{auto_vacuum} 已经启用,但它不会像 {vacuum} 命令那样对数据库进行碎片整理,也不会重新打包各个数据库页面。"
"jsonQueryDescription": "使用 JSON 查询解析并提取服务器 JSON 响应中的特定数据,或者,如果不期望得到 JSON 响应,则可使用 \"$\" 获取原始响应。然后将结果转为字符串并与期望值进行字符串比较。有关更多文档,请参阅 {0},亦可使用 {1} 来尝试查询。"
}

View File

@@ -995,7 +995,7 @@
class="btn btn-outline-primary"
type="button"
:disabled="processing"
@click.stop="modal.show()"
@click="$refs.debugMonitorDialog.show()"
>
{{ $t("Debug") }}
</button>
@@ -1010,58 +1010,10 @@
<RemoteBrowserDialog ref="remoteBrowserDialog" />
</div>
</transition>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<textarea id="curl-debug" v-model="curlCommand" class="form-control mb-3" readonly wrap="off"></textarea>
<button id="debug-copy-btn" class="btn btn-outline-primary position-absolute top-0 end-0 mt-3 me-3 border-0" type="button" @click.stop="copyToClipboard">
<font-awesome-icon icon="copy" />
</button>
<i18n-t keypath="CurlDebugInfo" tag="p" class="form-text">
<template #newiline>
<br>
</template>
<template #firewalls>
<a href="https://xkcd.com/2259/" target="_blank">{{ $t('firewalls') }}</a>
</template>
<template #dns_resolvers>
<a href="https://www.reddit.com/r/sysadmin/comments/rxho93/thank_you_for_the_running_its_always_dns_joke_its/" target="_blank">{{ $t('dns resolvers') }}</a>
</template>
<template #docker_networks>
<a href="https://youtu.be/bKFMS5C4CG0" target="_blank">{{ $t('docker networks') }}</a>
</template>
</i18n-t>
<div v-if="monitor.authMethod === 'oauth2-cc'" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoOAuth2CCUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
<template #newline>
<br>
</template>
<template #oauth2_bearer>
<code>--oauth2-bearer TOKEN</code>
</template>
</i18n-t>
</div>
<div v-if="monitor.proxyId" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoProxiesUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
</i18n-t>
</div>
</div>
</div>
</div>
</div>
<DebugMonitorDialog ref="debugMonitorDialog" :monitor="monitor" />
</template>
<script>
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification";
import ActionSelect from "../components/ActionSelect.vue";
@@ -1076,8 +1028,7 @@ import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } fro
import { hostNameRegexPattern } from "../util-frontend";
import HiddenInput from "../components/HiddenInput.vue";
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
import { version } from "../../package.json";
const userAgent = `'Uptime-Kuma/${version}'`;
import DebugMonitorDialog from "../components/DebugMonitorDialog.vue";
const toast = useToast();
@@ -1127,6 +1078,7 @@ const monitorDefaults = {
export default {
components: {
DebugMonitorDialog,
HiddenInput,
ActionSelect,
ProxyDialog,
@@ -1169,58 +1121,9 @@ export default {
},
computed: {
curlCommand() {
const command = [ "curl", "--verbose", "--head", "--request", this.monitor.method, "\\\n", "--user-agent", userAgent, "\\\n" ];
if (this.monitor.ignoreTls) {
command.push("--insecure", "\\\n");
}
if (this.monitor.headers) {
try {
// trying to parse the supplied data as json to trim whitespace
for (const [ key, value ] of Object.entries(JSON.parse(this.monitor.headers))) {
command.push("--header", `'${key}: ${value}'`, "\\\n");
}
} catch (e) {
command.push("--header", `'${this.monitor.headers}'`, "\\\n");
}
}
if (this.monitor.authMethod === "basic") {
command.push("--user", `${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}`, "--basic", "\\\n");
} else if (this.monitor.authmethod === "mtls") {
command.push("--cacert", `'${this.monitor.tlsCa}'`, "\\\n", "--key", `'${this.monitor.tlsKey}'`, "\\\n", "--cert", `'${this.monitor.tlsCert}'`, "\\\n");
} else if (this.monitor.authMethod === "ntlm") {
command.push("--user", `'${this.monitor.authDomain ? `${this.monitor.authDomain}/` : ""}${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}'`, "--ntlm", "\\\n");
}
if (this.monitor.body && this.monitor.httpBodyEncoding === "json") {
let json = "";
try {
// trying to parse the supplied data as json to trim whitespace
json = JSON.stringify(JSON.parse(this.monitor.body));
} catch (e) {
json = this.monitor.body;
}
command.push("--header", "'Content-Type: application/json'", "\\\n", "--data", `'${json}'`, "\\\n");
} else if (this.monitor.body && this.monitor.httpBodyEncoding === "xml") {
command.push("--headers", "'Content-Type: application/xml'", "\\\n", "--data", `'${this.monitor.body}'`, "\\\n");
}
if (this.monitor.maxredirects) {
command.push("--location", "--max-redirs", this.monitor.maxredirects, "\\\n");
}
if (this.monitor.timeout) {
command.push("--max-time", this.monitor.timeout, "\\\n");
}
if (this.monitor.maxretries) {
command.push("--retry", this.monitor.maxretries, "\\\n");
}
command.push("--url", this.monitor.url);
return command.join(" ");
},
ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300)
if (! isDev) {
if (!isDev) {
return this.ipRegexPattern;
}
return null;
@@ -1573,7 +1476,6 @@ message HealthCheckResponse {
},
},
mounted() {
this.modal = new Modal(this.$refs.modal);
this.init();
let acceptedStatusCodeOptions = [
@@ -1614,14 +1516,6 @@ message HealthCheckResponse {
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
},
methods: {
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this.curlCommand);
toast.success(this.$t("CopyToClipboardSuccess"));
} catch (err) {
toast.error(this.$t("CopyToClipboardError", { error: err.message }));
}
},
/**
* Initialize the edit monitor form
* @returns {void}
@@ -1910,8 +1804,4 @@ message HealthCheckResponse {
min-height: 200px;
}
#curl-debug {
font-family: monospace;
overflow: auto;
}
</style>

View File

@@ -120,11 +120,7 @@ export const badgeConstants = {
defaultCertExpireDownDays: "7"
};
/**
* Flip the status of s between UP and DOWN if this is possible
* @param s {number} status
* @returns {number} flipped status
*/
/** Flip the status of s */
export function flipStatus(s: number) {
if (s === UP) {
return DOWN;