Merge branch 'master' into #1209-Logout-button-in-navbar

This commit is contained in:
Louis Lam
2022-04-18 20:39:24 +08:00
148 changed files with 6585 additions and 1624 deletions

View File

@@ -1,12 +1,12 @@
<template>
<router-view />
<router-view />
</template>
<script>
import { setPageLocale } from "./util-frontend";
export default {
created() {
setPageLocale();
},
created() {
setPageLocale();
},
};
</script>

View File

@@ -22,6 +22,18 @@ textarea.form-control {
width: 10px;
}
.list-group {
border-radius: 0.75rem;
.dark & {
.list-group-item {
background-color: $dark-bg;
color: $dark-font-color;
border-color: $dark-border-color;
}
}
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 20px;
@@ -412,6 +424,10 @@ textarea.form-control {
background-color: rgba(239, 239, 239, 0.7);
border-radius: 8px;
&.no-bg {
background-color: transparent !important;
}
&:focus {
outline: 0 solid #eee;
background-color: rgba(245, 245, 245, 0.9);
@@ -453,6 +469,10 @@ textarea.form-control {
color: $primary;
}
.prism-editor__textarea {
outline: none !important;
}
// Localization
@import "localization.scss";

View File

@@ -11,23 +11,23 @@
<table class="text-start">
<tbody>
<tr class="my-3">
<td class="px-3">Subject:</td>
<td class="px-3">{{ $t("Subject:") }}</td>
<td>{{ formatSubject(cert.subject) }}</td>
</tr>
<tr class="my-3">
<td class="px-3">Valid To:</td>
<td class="px-3">{{ $t("Valid To:") }}</td>
<td><Datetime :value="cert.validTo" /></td>
</tr>
<tr class="my-3">
<td class="px-3">Days Remaining:</td>
<td class="px-3">{{ $t("Days Remaining:") }}</td>
<td>{{ cert.daysRemaining }}</td>
</tr>
<tr class="my-3">
<td class="px-3">Issuer:</td>
<td class="px-3">{{ $t("Issuer:") }}</td>
<td>{{ formatSubject(cert.issuer) }}</td>
</tr>
<tr class="my-3">
<td class="px-3">Fingerprint:</td>
<td class="px-3">{{ $t("Fingerprint:") }}</td>
<td>{{ cert.fingerprint }}</td>
</tr>
</tbody>

View File

@@ -25,7 +25,7 @@
</template>
<script>
import { Modal } from "bootstrap"
import { Modal } from "bootstrap";
export default {
props: {
@@ -42,19 +42,20 @@ export default {
default: "No",
},
},
emits: [ "yes" ],
data: () => ({
modal: null,
}),
mounted() {
this.modal = new Modal(this.$refs.modal)
this.modal = new Modal(this.$refs.modal);
},
methods: {
show() {
this.modal.show()
this.modal.show();
},
yes() {
this.$emit("yes");
},
},
}
};
</script>

View File

@@ -57,6 +57,7 @@ export default {
default: undefined,
},
},
emits: [ "update:modelValue" ],
data() {
return {
visibility: "password",

View File

@@ -5,12 +5,12 @@
<script lang="ts">
import { sleep } from "../util.ts"
import { sleep } from "../util.ts";
export default {
props: {
value: [String, Number],
value: [ String, Number ],
time: {
type: Number,
default: 0.3,
@@ -25,12 +25,12 @@ export default {
return {
output: "",
frameDuration: 30,
}
};
},
computed: {
isNum() {
return typeof this.value === "number"
return typeof this.value === "number";
},
},
@@ -45,7 +45,7 @@ export default {
} else {
for (let i = 1; i < frames; i++) {
this.output += step;
await sleep(15)
await sleep(15);
}
}
@@ -59,5 +59,5 @@ export default {
methods: {},
}
};
</script>

View File

@@ -4,12 +4,12 @@
<script>
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"
import utc from "dayjs/plugin/utc"
import timezone from "dayjs/plugin/timezone" // dependent on utc plugin
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(relativeTime)
import relativeTime from "dayjs/plugin/relativeTime";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone"; // dependent on utc plugin
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
export default {
props: {
@@ -29,5 +29,5 @@ export default {
}
},
},
}
};
</script>

View File

@@ -38,7 +38,7 @@ export default {
beatMargin: 4,
move: false,
maxBeat: -1,
}
};
},
computed: {
@@ -69,12 +69,12 @@ export default {
if (start < 0) {
// Add empty placeholder
for (let i = start; i < 0; i++) {
placeholders.push(0)
placeholders.push(0);
}
start = 0;
}
return placeholders.concat(this.beatList.slice(start))
return placeholders.concat(this.beatList.slice(start));
},
wrapStyle() {
@@ -84,7 +84,7 @@ export default {
return {
padding: `${topBottom}px ${leftRight}px`,
width: "100%",
}
};
},
barStyle() {
@@ -94,12 +94,12 @@ export default {
return {
transition: "all ease-in-out 0.25s",
transform: `translateX(${width}px)`,
}
};
}
return {
transform: "translateX(0)",
}
};
},
@@ -109,7 +109,7 @@ export default {
height: this.beatHeight + "px",
margin: this.beatMargin + "px",
"--hover-scale": this.hoverScale,
}
};
},
},
@@ -120,7 +120,7 @@ export default {
setTimeout(() => {
this.move = false;
}, 300)
}, 300);
},
deep: true,
},
@@ -162,15 +162,15 @@ export default {
methods: {
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
}
},
getBeatTitle(beat) {
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : ``);
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
}
},
}
};
</script>
<style lang="scss" scoped>

View File

@@ -48,18 +48,19 @@ export default {
default: undefined,
},
},
emits: [ "update:modelValue" ],
data() {
return {
visibility: "password",
}
};
},
computed: {
model: {
get() {
return this.modelValue
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value)
this.$emit("update:modelValue", value);
}
}
},
@@ -74,5 +75,5 @@ export default {
this.visibility = "password";
},
}
}
};
</script>

View File

@@ -9,7 +9,9 @@
<a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
<font-awesome-icon icon="times" />
</a>
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" />
<form>
<input v-model="searchText" class="form-control search-input" :placeholder="$t('Search...')" autocomplete="off" />
</form>
</div>
</div>
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
@@ -19,7 +21,7 @@
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info">
<Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }}
@@ -34,7 +36,7 @@
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12">
<div class="col-12 bottom-style">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
@@ -170,7 +172,7 @@ export default {
.dark {
.footer {
// background-color: $dark-bg;
// background-color: $dark-bg;
}
}
@@ -196,14 +198,21 @@ export default {
max-width: 15em;
}
.monitorItem {
.monitor-item {
width: 100%;
}
.tags {
padding-left: 62px;
margin-top: 4px;
padding-left: 67px;
display: flex;
flex-wrap: wrap;
gap: 0;
}
.bottom-style {
padding-left: 67px;
margin-top: 5px;
}
</style>

View File

@@ -69,7 +69,6 @@
<script lang="ts">
import { Modal } from "bootstrap";
import { ucfirst } from "../util.ts";
import Confirm from "./Confirm.vue";
import NotificationFormList from "./notifications";
@@ -79,7 +78,7 @@ export default {
Confirm,
},
props: {},
emits: ["added"],
emits: [ "added" ],
data() {
return {
model: null,

View File

@@ -24,7 +24,7 @@ import timezone from "dayjs/plugin/timezone";
import "chartjs-adapter-dayjs";
import { LineChart } from "vue-chart-3";
import { useToast } from "vue-toastification";
import { UP, DOWN, PENDING } from "../util.ts";
import { DOWN } from "../util.ts";
dayjs.extend(utc);
dayjs.extend(timezone);
@@ -220,6 +220,7 @@ export default {
if (newPeriod == "0") {
newPeriod = null;
this.heartbeatList = null;
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
} else {
this.loading = true;
@@ -228,6 +229,7 @@ export default {
toast.error(res.msg);
} else {
this.heartbeatList = res.data;
this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
}
this.loading = false;
});
@@ -248,6 +250,12 @@ export default {
},
{ deep: true }
);
// Load chart period from storage if saved
let period = this.$root.storage()[`chart-period-${this.monitorId}`];
if (period != null) {
this.chartPeriodHrs = Math.min(period, 6);
}
}
};
</script>
@@ -278,7 +286,7 @@ export default {
.dropdown-item {
border-radius: 0.3rem;
padding: 2px 16px 4px 16px;
padding: 2px 16px 4px;
.dark & {
background: $dark-bg;
@@ -286,6 +294,7 @@ export default {
.dark &:hover {
background: $dark-font-color;
color: $dark-font-color2;
}
}

View File

@@ -0,0 +1,206 @@
<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 Proxy") }}
</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="proxy-protocol" class="form-label">{{ $t("Proxy Protocol") }}</label>
<select id="proxy-protocol" v-model="proxy.protocol" class="form-select">
<option value="https">HTTPS</option>
<option value="http">HTTP</option>
<option value="socks">SOCKS</option>
<option value="socks5">SOCKS v5</option>
<option value="socks4">SOCKS v4</option>
</select>
</div>
<div class="mb-3">
<label for="proxy-host" class="form-label">{{ $t("Proxy Server") }}</label>
<div class="d-flex">
<input id="proxy-host" v-model="proxy.host" type="text" class="form-control" required :placeholder="$t('Server Address')">
<input v-model="proxy.port" type="number" class="form-control ms-2" style="width: 100px;" required min="1" max="65535" :placeholder="$t('Port')">
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input id="mark-auth" v-model="proxy.auth" class="form-check-input" type="checkbox">
<label for="mark-auth" class="form-check-label">{{ $t("Proxy server has authentication") }}</label>
</div>
</div>
<div v-if="proxy.auth" class="mb-3">
<label for="proxy-username" class="form-label">{{ $t("User") }}</label>
<input id="proxy-username" v-model="proxy.username" type="text" class="form-control" required>
</div>
<div v-if="proxy.auth" class="mb-3">
<label for="proxy-password" class="form-label">{{ $t("Password") }}</label>
<input id="proxy-password" v-model="proxy.password" type="password" class="form-control" required>
</div>
<div class="mb-3 mt-4">
<hr class="dropdown-divider mb-4">
<div class="form-check form-switch">
<input id="mark-active" v-model="proxy.active" class="form-check-input" type="checkbox">
<label for="mark-active" class="form-check-label">{{ $t("enabled") }}</label>
</div>
<div class="form-text">
{{ $t("enableProxyDescription") }}
</div>
<br />
<div class="form-check form-switch">
<input id="mark-default" v-model="proxy.default" class="form-check-input" type="checkbox">
<label for="mark-default" class="form-check-label">{{ $t("setAsDefault") }}</label>
</div>
<div class="form-text">
{{ $t("setAsDefaultProxyDescription") }}
</div>
<br />
<div class="form-check form-switch">
<input id="apply-existing" v-model="proxy.applyExisting" class="form-check-input" type="checkbox">
<label class="form-check-label" for="apply-existing">{{ $t("Apply on all existing monitors") }}</label>
</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="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="deleteProxy">
{{ $t("deleteProxyMsg") }}
</Confirm>
</template>
<script lang="ts">
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
export default {
components: {
Confirm,
},
props: {},
emits: [ "added" ],
data() {
return {
model: null,
processing: false,
id: null,
proxy: {
protocol: null,
host: null,
port: null,
auth: false,
username: null,
password: null,
active: false,
default: false,
applyExisting: false,
}
};
},
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
show(proxyID) {
if (proxyID) {
this.id = proxyID;
for (let proxy of this.$root.proxyList) {
if (proxy.id === proxyID) {
this.proxy = proxy;
break;
}
}
} else {
this.id = null;
this.proxy = {
protocol: "https",
host: null,
port: null,
auth: false,
username: null,
password: null,
active: true,
default: false,
applyExisting: false,
};
}
this.modal.show();
},
submit() {
this.processing = true;
this.$root.getSocket().emit("addProxy", this.proxy, 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);
}
}
});
},
deleteProxy() {
this.processing = true;
this.$root.getSocket().emit("deleteProxy", 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>

View File

@@ -145,7 +145,7 @@ export default {
.mobile {
.item {
padding: 13px 0 10px 0;
padding: 13px 0 10px;
}
}

View File

@@ -41,7 +41,7 @@ export default {
}
}
}
}
};
</script>
<style lang="scss" scoped>

View File

@@ -49,7 +49,7 @@ export default {
<style lang="scss" scoped>
@import "../assets/vars.scss";
h5:after {
h5::after {
content: "";
display: block;
width: 50%;

View File

@@ -19,6 +19,19 @@
</div>
<p v-if="showURI && twoFAStatus == false" class="text-break mt-2">{{ uri }}</p>
<div v-if="!(uri && twoFAStatus == false)" class="mb-3">
<label for="current-password" class="form-label">
{{ $t("Current Password") }}
</label>
<input
id="current-password"
v-model="currentPassword"
type="password"
class="form-control"
required
/>
</div>
<button v-if="uri == null && twoFAStatus == false" class="btn btn-primary" type="button" @click="prepare2FA()">
{{ $t("Enable 2FA") }}
</button>
@@ -33,7 +46,7 @@
<input v-model="token" type="text" maxlength="6" class="form-control">
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">{{ $t("Verify Token") }}</button>
</div>
<p v-show="tokenValid" class="mt-2" style="color: green">{{ $t("tokenValidSettingsMsg") }}</p>
<p v-show="tokenValid" class="mt-2" style="color: green;">{{ $t("tokenValidSettingsMsg") }}</p>
</div>
</div>
</div>
@@ -59,11 +72,11 @@
</template>
<script lang="ts">
import { Modal } from "bootstrap"
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
import VueQrcode from "vue-qrcode"
import { useToast } from "vue-toastification"
const toast = useToast()
import VueQrcode from "vue-qrcode";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@@ -73,35 +86,36 @@ export default {
props: {},
data() {
return {
currentPassword: "",
processing: false,
uri: null,
tokenValid: false,
twoFAStatus: null,
token: null,
showURI: false,
}
};
},
mounted() {
this.modal = new Modal(this.$refs.modal)
this.modal = new Modal(this.$refs.modal);
this.getStatus();
},
methods: {
show() {
this.modal.show()
this.modal.show();
},
confirmEnableTwoFA() {
this.$refs.confirmEnableTwoFA.show()
this.$refs.confirmEnableTwoFA.show();
},
confirmDisableTwoFA() {
this.$refs.confirmDisableTwoFA.show()
this.$refs.confirmDisableTwoFA.show();
},
prepare2FA() {
this.processing = true;
this.$root.getSocket().emit("prepare2FA", (res) => {
this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => {
this.processing = false;
if (res.ok) {
@@ -109,49 +123,51 @@ export default {
} else {
toast.error(res.msg);
}
})
});
},
save2FA() {
this.processing = true;
this.$root.getSocket().emit("save2FA", (res) => {
this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => {
this.processing = false;
if (res.ok) {
this.$root.toastRes(res)
this.$root.toastRes(res);
this.getStatus();
this.currentPassword = "";
this.modal.hide();
} else {
toast.error(res.msg);
}
})
});
},
disable2FA() {
this.processing = true;
this.$root.getSocket().emit("disable2FA", (res) => {
this.$root.getSocket().emit("disable2FA", this.currentPassword, (res) => {
this.processing = false;
if (res.ok) {
this.$root.toastRes(res)
this.$root.toastRes(res);
this.getStatus();
this.currentPassword = "";
this.modal.hide();
} else {
toast.error(res.msg);
}
})
});
},
verifyToken() {
this.$root.getSocket().emit("verifyToken", this.token, (res) => {
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
if (res.ok) {
this.tokenValid = res.valid;
} else {
toast.error(res.msg);
}
})
});
},
getStatus() {
@@ -161,10 +177,10 @@ export default {
} else {
toast.error(res.msg);
}
})
});
},
},
}
};
</script>
<style lang="scss" scoped>

View File

@@ -22,33 +22,33 @@ export default {
return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%";
}
return this.$t("notAvailableShort")
return this.$t("notAvailableShort");
},
color() {
if (this.lastHeartBeat.status === 0) {
return "danger"
return "danger";
}
if (this.lastHeartBeat.status === 1) {
return "primary"
return "primary";
}
if (this.lastHeartBeat.status === 2) {
return "warning"
return "warning";
}
return "secondary"
return "secondary";
},
lastHeartBeat() {
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id]
return this.$root.lastHeartbeatList[this.monitor.id];
}
return {
status: -1,
}
};
},
className() {
@@ -59,7 +59,7 @@ export default {
return "";
},
},
}
};
</script>
<style>

View File

@@ -28,5 +28,5 @@ export default {
this.$parent.notification.gotifyPriority = 8;
}
},
}
};
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="mb-3">
<label for="mattermost-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color:red;"><sup>*</sup></span></label>
<label for="mattermost-webhook-url" class="form-label">{{ $t("Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="mattermost-webhook-url" v-model="$parent.notification.mattermostWebhookUrl" type="text" class="form-control" required>
<label for="mattermost-username" class="form-label">{{ $t("Username") }}</label>
<input id="mattermost-username" v-model="$parent.notification.mattermostusername" type="text" class="form-control">
@@ -11,7 +11,7 @@
<label for="mattermost-channel" class="form-label">{{ $t("Channel Name") }}</label>
<input id="mattermost-channel-name" v-model="$parent.notification.mattermostchannel" type="text" class="form-control">
<div class="form-text">
<span style="color:red;"><sup>*</sup></span>{{ $t("Required") }}
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">
<a href="https://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">https://docs.mattermost.com/developer/webhooks-incoming.html</a>
</i18n-t>

View File

@@ -0,0 +1,34 @@
<template>
<div class="mb-3">
<div class="mb-3">
<label for="onebot-http-addr" class="form-label">{{ $t("onebotHttpAddress") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="HttpUrl" v-model="$parent.notification.httpAddr" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="onebot-access-token" class="form-label">AccessToken<span style="color: red;"><sup>*</sup></span></label>
<input id="HttpUrl" v-model="$parent.notification.accessToken" type="text" class="form-control" required>
<div class="form-text">
<p>{{ $t("onebotSafetyTips") }}</p>
</div>
</div>
<div class="mb-3">
<label for="onebot-msg-type" class="form-label">{{ $t("onebotMessageType") }}</label>
<select id="onebot-msg-type" v-model="$parent.notification.msgType" class="form-select">
<option value="group">{{ $t("onebotGroupMessage") }}</option>
<option value="private">{{ $t("onebotPrivateMessage") }}</option>
</select>
</div>
<div class="mb-3">
<label for="onebot-reciever-id" class="form-label">{{ $t("onebotUserOrGroupId") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="secretKey" v-model="$parent.notification.recieverId" type="text" class="form-control" required>
</div>
<div class="form-text">
<i18n-t tag="p" keypath="Read more:">
<a href="https://github.com/botuniverse/onebot-11" target="_blank">https://github.com/botuniverse/onebot-11</a>
</i18n-t>
</div>
</div>
</template>

View File

@@ -0,0 +1,19 @@
<template>
<div class="mb-3">
<label for="pushdeer-key" class="form-label">{{ $t("PushDeer Key") }}</label>
<HiddenInput id="pushdeer-key" v-model="$parent.notification.pushdeerKey" :required="true" autocomplete="one-time-code" placeholder="PDUxxxx"></HiddenInput>
</div>
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
<a href="http://www.pushdeer.com/" rel="noopener noreferrer" target="_blank">http://www.pushdeer.com/</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View File

@@ -63,5 +63,5 @@ export default {
components: {
HiddenInput,
},
}
};
</script>

View File

@@ -24,11 +24,13 @@ import AliyunSMS from "./AliyunSms.vue";
import DingDing from "./DingDing.vue";
import Bark from "./Bark.vue";
import SerwerSMS from "./SerwerSMS.vue";
import Stackfield from './Stackfield.vue';
import Stackfield from "./Stackfield.vue";
import WeCom from "./WeCom.vue";
import GoogleChat from "./GoogleChat.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.
@@ -67,6 +69,8 @@ const NotificationFormList = {
"GoogleChat": GoogleChat,
"gorush": Gorush,
"alerta": Alerta,
"OneBot": OneBot,
"PushDeer": PushDeer,
};
export default NotificationFormList;

View File

@@ -44,6 +44,7 @@ export default {
.logo {
margin: 4em 1em;
}
.update-link {
font-size: 0.9em;
}

View File

@@ -69,7 +69,7 @@
<div class="mb-2">
<input
id="importBackup"
id="import-backend"
type="file"
class="form-control"
accept="application/json"
@@ -94,7 +94,7 @@
<div
v-if="importAlert"
class="alert alert-danger mt-3"
style="padding: 6px 16px"
style="padding: 6px 16px;"
>
{{ importAlert }}
</div>
@@ -159,7 +159,7 @@ export default {
importBackup() {
this.processing = true;
let uploadItem = document.getElementById("importBackup").files;
let uploadItem = document.getElementById("import-backend").files;
if (uploadItem.length <= 0) {
this.processing = false;
@@ -198,7 +198,7 @@ export default {
@import "../../assets/vars.scss";
.dark {
#importBackup {
#import-backend {
&::file-selector-button {
color: $primary;
background-color: $dark-bg;

View File

@@ -189,4 +189,3 @@ export default {
};
</script>
<style></style>

View File

@@ -52,7 +52,7 @@
<script>
import Confirm from "../../components/Confirm.vue";
import { debug } from "../../util.ts";
import { log } from "../../util.ts";
import { useToast } from "vue-toastification";
const toast = useToast();
@@ -91,13 +91,13 @@ export default {
methods: {
loadDatabaseSize() {
debug("load database size");
log.debug("monitorhistory", "load database size");
this.$root.getSocket().emit("getDatabaseSize", (res) => {
if (res.ok) {
this.databaseSize = res.size;
debug("database size: " + res.size);
log.debug("monitorhistory", "database size: " + res.size);
} else {
debug(res);
log.debug("monitorhistory", res);
}
});
},
@@ -108,7 +108,7 @@ export default {
this.loadDatabaseSize();
toast.success("Done");
} else {
debug(res);
log.debug("monitorhistory", res);
}
});
},
@@ -129,5 +129,3 @@ export default {
},
};
</script>
<style></style>

View File

@@ -0,0 +1,48 @@
<template>
<div>
<!-- Proxies -->
<div class="proxy-list my-4">
<p v-if="$root.proxyList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<p v-else>
{{ $t("proxyDescription") }}
</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(proxy, index) in $root.proxyList" :key="index" class="list-group-item">
{{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }})
<span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("Default") }}</span><br>
<a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a>
</li>
</ul>
<button class="btn btn-primary me-2" type="button" @click="$refs.proxyDialog.show()">
{{ $t("Setup Proxy") }}
</button>
</div>
<ProxyDialog ref="proxyDialog" />
</div>
</template>
<script>
import ProxyDialog from "../../components/ProxyDialog.vue";
export default {
components: {
ProxyDialog
},
};
</script>
<style lang="scss" scoped>
@import "../../assets/vars.scss";
.dark {
.list-group-item {
background-color: $dark-bg2;
color: $dark-font-color;
}
}
</style>

View File

@@ -0,0 +1,144 @@
<template>
<div>
<h4 class="mt-4">Cloudflare Tunnel</h4>
<div class="my-3">
<div>
cloudflared:
<span v-if="installed === true" class="text-primary">{{ $t("Installed") }}</span>
<span v-else-if="installed === false" class="text-danger">{{ $t("Not installed") }}</span>
</div>
<div>
{{ $t("Status") }}:
<span v-if="running" class="text-primary">{{ $t("Running") }}</span>
<span v-else-if="!running" class="text-danger">{{ $t("Not running") }}</span>
</div>
<div v-if="false">
{{ message }}
</div>
<div v-if="errorMessage" class="mt-3">
{{ $t("Message:") }}
<textarea v-model="errorMessage" class="form-control" readonly></textarea>
</div>
<i18n-t v-if="installed === false" tag="p" keypath="wayToGetCloudflaredURL">
<a
href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/"
target="_blank"
>{{ $t("cloudflareWebsite") }}</a>
</i18n-t>
</div>
<!-- If installed show token input -->
<div v-if="installed" class="mb-2">
<div class="mb-4">
<label class="form-label" for="cloudflareTunnelToken">
Cloudflare Tunnel {{ $t("Token") }}
</label>
<HiddenInput
id="cloudflareTunnelToken"
v-model="cloudflareTunnelToken"
autocomplete="one-time-code"
:readonly="running"
/>
<div class="form-text">
<div v-if="cloudflareTunnelToken" class="mb-3">
<span v-if="!running" class="remove-token" @click="removeToken">{{ $t("Remove Token") }}</span>
</div>
{{ $t("Don't know how to get the token? Please read the guide:") }}<br />
<a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel" target="_blank">
https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel
</a>
</div>
</div>
<div>
<button v-if="!running" class="btn btn-primary" type="submit" @click="start">
{{ $t("Start") }} cloudflared
</button>
<button v-if="running" class="btn btn-danger" type="submit" @click="$refs.confirmStop.show();">
{{ $t("Stop") }} cloudflared
</button>
<Confirm ref="confirmStop" btn-style="btn-danger" :yes-text="$t('Stop') + ' cloudflared'" :no-text="$t('Cancel')" @yes="stop">
{{ $t("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.") }}
<div class="mt-3">
<label for="current-password2" class="form-label">
{{ $t("Current Password") }}
</label>
<input
id="current-password2"
v-model="currentPassword"
type="password"
class="form-control"
required
/>
</div>
</Confirm>
</div>
</div>
<h4 class="mt-4">{{ $t("Other Software") }}</h4>
<div>
{{ $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>.
</div>
</div>
</template>
<script>
import HiddenInput from "../../components/HiddenInput.vue";
import Confirm from "../Confirm.vue";
const prefix = "cloudflared_";
export default {
components: {
HiddenInput,
Confirm
},
data() {
// See /src/mixins/socket.js
return this.$root.cloudflared;
},
computed: {
},
watch: {
},
created() {
this.$root.getSocket().emit(prefix + "join");
},
unmounted() {
this.$root.getSocket().emit(prefix + "leave");
},
methods: {
start() {
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
},
stop() {
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
this.$root.toastRes(res);
});
},
removeToken() {
this.$root.getSocket().emit(prefix + "removeToken");
this.cloudflareTunnelToken = "";
}
}
};
</script>
<style lang="scss" scoped>
.remove-token {
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@@ -192,6 +192,12 @@
<p>Пожалуйста, используйте с осторожностью.</p>
</template>
<template v-else-if="$i18n.locale === 'uk-UA' ">
<p>Ви впевнені, що бажаєте <strong>вимкнути авторизацію</strong>?</p>
<p>Це підходить для <strong>тих, у кого встановлена інша авторизація</strong> пееред відкриттям Uptime Kuma, наприклад Cloudflare Access.</p>
<p>Будь ласка, використовуйте з обережністю.</p>
</template>
<template v-else-if="$i18n.locale === 'fa' ">
<p>آیا مطمئن هستید که میخواهید <strong>احراز هویت را غیر فعال کنید</strong>?</p>
<p>این ویژگی برای کسانی است که <strong> لایه امنیتی شخص ثالث دیگر بر روی این آدرس فعال کردهاند</strong>، مانند Cloudflare Access.</p>
@@ -215,14 +221,14 @@
<p>Dette er for <strong>de som har tredjepartsautorisering</strong> foran Uptime Kuma, for eksempel Cloudflare Access.</p>
<p>Vennligst vær forsiktig.</p>
</template>
<template v-else-if="$i18n.locale === 'cs-CZ' ">
<p>Opravdu chcete <strong>deaktivovat autentifikaci</strong>?</p>
<p>Tato možnost je určena pro případy, kdy <strong>máte autentifikaci zajištěnou třetí stranou</strong> ještě před přístupem do Uptime Kuma, například prostřednictvím Cloudflare Access.</p>
<p>Používejte ji prosím s rozmyslem.</p>
</template>
<template v-else-if="$i18n.locale === 'vi-VN' ">
<template v-else-if="$i18n.locale === 'vi-VN' ">
<p>Bạn muốn <strong>TẮT XÁC THỰC</strong> không?</p>
<p>Điều này rất nguy hiểm<strong>BẤT KỲ AI</strong> cũng thể truy cập cướp quyền điều khiển.</p>
<p>Vui lòng <strong>cẩn thận</strong>.</p>
@@ -234,6 +240,19 @@
<p>It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.</p>
<p>Please use this option carefully!</p>
</template>
<div class="mb-3">
<label for="current-password2" class="form-label">
{{ $t("Current Password") }}
</label>
<input
id="current-password2"
v-model="password.currentPassword"
type="password"
class="form-control"
required
/>
</div>
</Confirm>
</div>
</template>
@@ -310,7 +329,12 @@ export default {
disableAuth() {
this.settings.disableAuth = true;
this.saveSettings();
// Need current password to disable auth
// Set it to empty if done
this.saveSettings(() => {
this.password.currentPassword = "";
}, this.password.currentPassword);
},
enableAuth() {
@@ -331,7 +355,7 @@ export default {
<style lang="scss" scoped>
@import "../../assets/vars.scss";
h5:after {
h5::after {
content: "";
display: block;
width: 50%;

View File

@@ -29,7 +29,8 @@ const languageList = {
"pl": "Polski",
"et-EE": "eesti",
"vi-VN": "Tiếng Việt",
"zh-TW": "繁體中文 (台灣)"
"zh-TW": "繁體中文 (台灣)",
"uk-UA": "Український",
};
let messages = {
@@ -42,7 +43,7 @@ for (let lang in languageList) {
};
}
const rtlLangs = ["fa"];
const rtlLangs = [ "fa" ];
export const currentLocale = () => localStorage.locale
|| languageList[navigator.language] && navigator.language

View File

@@ -38,6 +38,8 @@ import {
faPen,
faExternalLinkSquareAlt,
faSpinner,
faUndo,
faPlusCircle,
} from "@fortawesome/free-solid-svg-icons";
library.add(
@@ -75,6 +77,8 @@ library.add(
faPen,
faExternalLinkSquareAlt,
faSpinner,
faUndo,
faPlusCircle,
);
export { FontAwesomeIcon };

View File

@@ -197,7 +197,7 @@ export default {
line: "Line Messenger",
mattermost: "Mattermost",
"Status Page": "Статус страница",
"Status Pages": "Статус страница",
"Status Pages": "Статус страници",
"Primary Base URL": "Основен базов URL адрес",
"Push URL": "Генериран Push URL адрес",
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
@@ -343,7 +343,7 @@ export default {
"No Monitors": "Няма монитори",
"Untitled Group": "Група без заглавие",
Services: "Услуги",
Discard: "Премахни",
Discard: "Отмени",
Cancel: "Отмени",
"Powered by": "Създадено чрез",
serwersms: "SerwerSMS.pl",
@@ -371,4 +371,75 @@ export default {
alertaAlertState: "Състояние на тревога",
alertaRecoverState: "Състояние на възстановяване",
deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?",
Proxies: "Проксита",
default: "По подразбиране",
enabled: "Включено",
setAsDefault: "Зададен по подразбиране",
deleteProxyMsg: "Сигурни ли сте, че желаете да изтриете това прокси за всички монитори?",
proxyDescription: "Прокситата трябва да бъдат зададени към монитор за да функционират.",
enableProxyDescription: "Това прокси няма да има ефект върху заявките за мониторинг, докато не бъде активирано. Може да контролирате временното деактивиране на проксито от всички монитори чрез статуса на активиране.",
setAsDefaultProxyDescription: "Това проки ще бъде включено по подразбиране за новите монитори. Може да го изключите по отделно за всеки един монитор.",
"Certificate Chain": "Верига на сертификата",
Valid: "Валиден",
Invalid: "Невалиден",
AccessKeyId: "ID на ключ за достъп",
SecretAccessKey: "Тайна на ключа за достъп",
PhoneNumbers: "Телефонни номера",
TemplateCode: "Шаблон Код",
SignName: "Знак име",
"Sms template must contain parameters: ": "SMS шаблонът трябва да съдържа следните параметри: ",
"Bark Endpoint": "Bark крайна точка",
WebHookUrl: "URL адрес на уеб кука",
SecretKey: "Таен ключ",
"For safety, must use secret key": "За сигурност, трябва да се използва таен ключ",
"Device Token": "Токен за устройство",
Platform: "Платформа",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "Висок",
Retry: "Повтори",
Topic: "Тема",
"WeCom Bot Key": "WeCom бот ключ",
"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",
"Add New Status Page": "Добави нова статус страница",
Slug: "Слъг",
"Accept characters:": "Приеми символи:",
startOrEndWithOnly: "Започва или завършва само с {0}",
"No consecutive dashes": "Без последователни тирета",
Next: "Следващ",
"The slug is already taken. Please choose another slug.": "Този слъг вече се използва. Моля изберете друг.",
"No Proxy": "Без прокси",
"HTTP Basic Auth": "HTTP основно удостоверяване",
"New Status Page": "Нова статус страница",
"Page Not Found": "Страницата не е открита",
"Reverse Proxy": "Ревърс прокси",
Backup: "Архивиране",
About: "Относно",
wayToGetCloudflaredURL: "(Свалете \"cloudflared\" от {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 и Traefik.",
"Please read": "Моля, прочетете",
"Subject:": "Тема:",
"Valid To:": "Валиден до:",
"Days Remaining:": "Оставащи дни:",
"Issuer:": "Издател:",
"Fingerprint:": "Пръстов отпечатък:",
"No status pages": "Няма статус страници",
};

View File

@@ -337,7 +337,7 @@ export default {
"Hide Tags": "Tags ausblenden",
Description: "Beschreibung",
"No monitors available.": "Keine Monitore verfügbar.",
"Add one": "Füge eins hinzu",
"Add one": "Hinzufügen",
"No Monitors": "Keine Monitore",
"Untitled Group": "Gruppe ohne Titel",
Services: "Dienste",
@@ -350,7 +350,7 @@ export default {
serwersmsAPIPassword: "API Passwort",
serwersmsPhoneNumber: "Telefonnummer",
serwersmsSenderName: "Name des SMS-Absenders (über Kundenportal registriert)",
"stackfield": "Stackfield",
stackfield: "Stackfield",
clicksendsms: "ClickSend SMS",
apiCredentials: "API Zugangsdaten",
smtpDkimSettings: "DKIM Einstellungen",
@@ -362,4 +362,87 @@ export default {
smtpDkimHashAlgo: "Hash-Algorithmus (Optional)",
smtpDkimheaderFieldNames: "Zu validierende Header-Schlüssel (optional)",
smtpDkimskipFields: "Zu ignorierende Header Schlüssel (optional)",
PushByTechulus: "Push by Techulus",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpunkt",
alertaEnvironment: "Umgebung",
alertaApiKey: "API Schlüssel",
alertaAlertState: "Alarmstatus",
alertaRecoverState: "Wiederherstellungsstatus",
deleteStatusPageMsg: "Bist du sicher, dass du diese Status-Seite löschen willst?",
Proxies: "Proxies",
default: "Standard",
enabled: "Aktiviert",
setAsDefault: "Als Standard setzen",
deleteProxyMsg: "Bist du sicher, dass du diesen Proxy für alle Monitore löschen willst?",
proxyDescription: "Proxies müssen einem Monitor zugewiesen werden, um zu funktionieren.",
enableProxyDescription: "Dieser Proxy wird keinen Effekt auf Monitor-Anfragen haben, bis er aktiviert ist. Du kannst ihn temporär von allen Monitoren nach Aktivierungsstatus deaktivieren.",
setAsDefaultProxyDescription: "Dieser Proxy wird standardmäßig für alle neuen Monitore aktiviert sein. Du kannst den Proxy immernoch für jeden Monitor einzeln deaktivieren.",
"Certificate Chain": "Zertifikatskette",
Valid: "Gültig",
Invalid: "Ungültig",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "Telefonnummern",
TemplateCode: "Vorlagencode",
SignName: "Signaturname",
"Sms template must contain parameters: ": "SMS Vorlage muss folgende Parameter enthalten: ",
"Bark Endpoint": "Bark Endpunkt",
WebHookUrl: "Webhook URL",
SecretKey: "Geheimer Schlüssel",
"For safety, must use secret key": "Zur Sicherheit muss ein geheimer Schlüssel verwendet werden",
"Device Token": "Gerätetoken",
Platform: "Platform",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "Hoch",
Retry: "Wiederholungen",
Topic: "Thema",
"WeCom Bot Key": "WeCom Bot Schlüssel",
"Setup Proxy": "Proxy einrichten",
"Proxy Protocol": "Proxy Protokoll",
"Proxy Server": "Proxy Server",
"Proxy server has authentication": "Proxy server hat Authentifizierung",
User: "Benutzer",
Installed: "Installiert",
"Not installed": "Nicht installiert",
Running: "Läuft",
"Not running": "Gestoppt",
"Remove Token": "Token entfernen",
Start: "Start",
Stop: "Stop",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Neue Status-Seite hinzufügen",
Slug: "Slug",
"Accept characters:": "Akzeptierte Zeichen:",
startOrEndWithOnly: "Nur mit {0} anfangen und enden",
"No consecutive dashes": "Keine aufeinanderfolgenden Bindestriche",
Next: "Weiter",
"The slug is already taken. Please choose another slug.": "Der Slug ist bereits in Verwendung. Bitte wähle einen anderen.",
"No Proxy": "Kein Proxy",
"HTTP Basic Auth": "HTTP Basisauthentifizierung",
"New Status Page": "Neue Status-Seite",
"Page Not Found": "Seite nicht gefunden",
"Reverse Proxy": "Reverse Proxy",
Backup: "Sicherung",
About: "Über",
wayToGetCloudflaredURL: "(Lade cloudflared von {0} herunter)",
cloudflareWebsite: "Cloudflare Website",
"Message:": "Nachricht:",
"Don't know how to get the token? Please read the guide:": "Du weißt nicht, wie man den Token bekommt? Lies die Anleitung dazu:",
"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.": "Die aktuelle Verbindung kann unterbrochen werden, wenn du aktuell über Cloudflare Tunnel verbunden bist. Bist du sicher, dass du es stoppen willst? Gib zur Bestätigung dein aktuelles Passwort ein.",
"Other Software": "Andere Software",
"For example: nginx, Apache and Traefik.": "Zum Beispiel: nginx, Apache und Traefik.",
"Please read": "Bitte lesen",
"Subject:": "Betreff:",
"Valid To:": "Gültig bis:",
"Days Remaining:": "Tage verbleibend:",
"Issuer:": "Aussteller:",
"Fingerprint:": "Fingerabdruck:",
"No status pages": "Keine Status-Seiten",
Customize: "Anpassen",
"Custom Footer": "Eigener Footer",
"Custom CSS": "Eigenes CSS",
};

View File

@@ -309,6 +309,10 @@ export default {
"One record": "One record",
steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ",
"Current User": "Current User",
topic: "Topic",
topicExplanation: "MQTT topic to monitor",
successMessage: "Success Message",
successMessageExplanation: "MQTT message that will be considered as success",
recent: "Recent",
Done: "Done",
Info: "Info",
@@ -331,21 +335,21 @@ export default {
dark: "dark",
Post: "Post",
"Please input title and content": "Please input title and content",
"Created": "Created",
Created: "Created",
"Last Updated": "Last Updated",
"Unpin": "Unpin",
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",
"Description": "Description",
Description: "Description",
"No monitors available.": "No monitors available.",
"Add one": "Add one",
"No Monitors": "No Monitors",
"Untitled Group": "Untitled Group",
"Services": "Services",
"Discard": "Discard",
"Cancel": "Cancel",
Services: "Services",
Discard: "Discard",
Cancel: "Cancel",
"Powered by": "Powered by",
shrinkDatabaseDescription: "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
serwersms: "SerwerSMS.pl",
@@ -354,6 +358,9 @@ export default {
serwersmsPhoneNumber: "Phone number",
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
stackfield: "Stackfield",
Customize: "Customize",
"Custom Footer": "Custom Footer",
"Custom CSS": "Custom CSS",
smtpDkimSettings: "DKIM Settings",
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
documentation: "documentation",
@@ -371,4 +378,85 @@ export default {
alertaAlertState: "Alert State",
alertaRecoverState: "Recover State",
deleteStatusPageMsg: "Are you sure want to delete this status page?",
Proxies: "Proxies",
default: "Default",
enabled: "Enabled",
setAsDefault: "Set As Default",
deleteProxyMsg: "Are you sure want to delete this proxy for all monitors?",
proxyDescription: "Proxies must be assigned to a monitor to function.",
enableProxyDescription: "This proxy will not effect on monitor requests until it is activated. You can control temporarily disable the proxy from all monitors by activation status.",
setAsDefaultProxyDescription: "This proxy will be enabled by default for new monitors. You can still disable the proxy separately for each monitor.",
"Certificate Chain": "Certificate Chain",
Valid: "Valid",
Invalid: "Invalid",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
"Sms template must contain parameters: ": "Sms template must contain parameters: ",
"Bark Endpoint": "Bark Endpoint",
WebHookUrl: "WebHookUrl",
SecretKey: "SecretKey",
"For safety, must use secret key": "For safety, must use secret key",
"Device Token": "Device Token",
Platform: "Platform",
iOS: "iOS",
Android: "Android",
Huawei: "Huawei",
High: "High",
Retry: "Retry",
Topic: "Topic",
"WeCom Bot Key": "WeCom Bot Key",
"Setup Proxy": "Setup Proxy",
"Proxy Protocol": "Proxy Protocol",
"Proxy Server": "Proxy Server",
"Proxy server has authentication": "Proxy server has authentication",
User: "User",
Installed: "Installed",
"Not installed": "Not installed",
Running: "Running",
"Not running": "Not running",
"Remove Token": "Remove Token",
Start: "Start",
Stop: "Stop",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "Add New Status Page",
Slug: "Slug",
"Accept characters:": "Accept characters:",
"startOrEndWithOnly": "Start or end with {0} only",
"No consecutive dashes": "No consecutive dashes",
Next: "Next",
"The slug is already taken. Please choose another slug.": "The slug is already taken. Please choose another slug.",
"No Proxy": "No Proxy",
"HTTP Basic Auth": "HTTP Basic Auth",
"New Status Page": "New Status Page",
"Page Not Found": "Page Not Found",
"Reverse Proxy": "Reverse Proxy",
Backup: "Backup",
About: "About",
wayToGetCloudflaredURL: "(Download cloudflared from {0})",
cloudflareWebsite: "Cloudflare Website",
"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:",
"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.",
"Other Software": "Other Software",
"For example: nginx, Apache and Traefik.": "For example: nginx, Apache and Traefik.",
"Please read": "Please read",
"Subject:": "Subject:",
"Valid To:": "Valid To:",
"Days Remaining:": "Days Remaining:",
"Issuer:": "Issuer:",
"Fingerprint:": "Fingerprint:",
"No status pages": "No status pages",
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
"Proxy": "Proxy",
"Date Created": "Date Created",
onebotHttpAddress: "OneBot HTTP Address",
onebotMessageType: "OneBot Message Type",
onebotGroupMessage: "Group",
onebotPrivateMessage: "Private",
onebotUserOrGroupId: "Group/User ID",
onebotSafetyTips: "For safety, must set access token",
"PushDeer Key": "PushDeer Key",
};

View File

@@ -197,7 +197,7 @@ export default {
line: "Line Messenger",
mattermost: "Mattermost",
"Status Page": "Státusz oldal",
"Status Pages": "Státusz oldal",
"Status Pages": "Státusz oldalak",
"Primary Base URL": "Elsődleges URL",
"Push URL": "Meghívandó URL",
needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.",
@@ -370,4 +370,5 @@ export default {
alertaApiKey: "API kulcs",
alertaAlertState: "Figyelmeztetési állapot",
alertaRecoverState: "Visszaállási állapot",
deleteStatusPageMsg: "Biztos, hogy törölni akarja a státusz oldalt?",
};

View File

@@ -180,8 +180,8 @@ export default {
"Add a monitor": "Добавить монитор",
"Edit Status Page": "Редактировать",
"Go to Dashboard": "Панель управления",
"Status Page": "Мониторинг",
"Status Pages": "Панель мониторингов",
"Status Page": "Страница статуса",
"Status Pages": "Страницы статуса",
Discard: "Отмена",
"Create Incident": "Создать инцидент",
"Switch to Dark Theme": "Тёмная тема",
@@ -311,28 +311,28 @@ export default {
"One record": "Одна запись",
steamApiKeyDescription: "Для мониторинга игрового сервера Steam вам необходим Web-API ключ Steam. Зарегистрировать его можно здесь: ",
"Certificate Chain": "Цепочка сертификатов",
"Valid": "Действительный",
Valid: "Действительный",
"Hide Tags": "Скрыть тэги",
Title: "Название инцидента:",
Content: "Содержание инцидента:",
Post: "Опубликовать",
"Cancel": "Отмена",
"Created": "Создано",
"Unpin": "Открепить",
Cancel: "Отмена",
Created: "Создано",
Unpin: "Открепить",
"Show Tags": "Показать тэги",
"recent": "Сейчас",
recent: "Сейчас",
"3h": "3 часа",
"6h": "6 часов",
"24h": "24 часа",
"1w": "1 неделя",
"No monitors available.": "Нет доступных мониторов",
"Add one": "Добавить новый",
"Backup": "Резервная копия",
"Security": "Безопасность",
Backup: "Резервная копия",
Security: "Безопасность",
"Shrink Database": "Сжать Базу Данных",
"Current User": "Текущий пользователь",
"About": "О программе",
"Description": "Описание",
About: "О программе",
Description: "Описание",
"Powered by": "Работает на основе скрипта от",
shrinkDatabaseDescription: "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.",
deleteStatusPageMsg: "Вы действительно хотите удалить эту страницу статуса сервисов?",
@@ -343,14 +343,58 @@ export default {
primary: "ОСНОВНОЙ",
light: "СВЕТЛЫЙ",
dark: "ТЕМНЫЙ",
"New Status Page": "Новый мониторинг",
"New Status Page": "Новая страница статуса",
"Show update if available": "Показывать доступные обновления",
"Also check beta release": "Проверять обновления для бета версий",
"Add New Status Page": "Добавить страницу мониторинга",
"Next": "Далее",
"Add New Status Page": "Добавить страницу статуса",
Next: "Далее",
"Accept characters: a-z 0-9 -": "Разрешены символы: a-z 0-9 -",
"Start or end with a-z 0-9 only": "Начало и окончание имени только на символы: a-z 0-9",
"No consecutive dashes --": "Запрещено использовать тире --",
"HTTP Options": "HTTP Опции",
"Basic Auth": "HTTP Авторизация",
PushByTechulus: "Push by Techulus",
clicksendsms: "ClickSend SMS",
GoogleChat: "Google Chat (только Google Workspace)",
apiCredentials: "API реквизиты",
Done: "Готово",
Info: "Инфо",
"Steam API Key": "Steam API-Ключ",
"Pick a RR-Type...": "Выберите RR-Тип...",
"Pick Accepted Status Codes...": "Выберите принятые коды состояния...",
Default: "По умолчанию",
"Please input title and content": "Пожалуйста, введите название и содержание",
"Last Updated": "Последнее Обновление",
"Untitled Group": "Группа без названия",
Services: "Сервисы",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Пользователь (включая префикс webapi_)",
serwersmsAPIPassword: "API Пароль",
serwersmsPhoneNumber: "Номер телефона",
serwersmsSenderName: "SMS Имя Отправителя (регистрированный через пользовательский портал)",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM Настройки",
smtpDkimDesc: "Пожалуйста ознакомьтесь с {0} Nodemailer DKIM для использования.",
documentation: "документацией",
smtpDkimDomain: "Имя Домена",
smtpDkimKeySelector: "Ключ",
smtpDkimPrivateKey: "Приватный ключ",
smtpDkimHashAlgo: "Алгоритм хэша (опционально)",
smtpDkimheaderFieldNames: "Заголовок ключей для подписи (опционально)",
smtpDkimskipFields: "Заколовок ключей не для подписи (опционально)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "Конечная точка API",
alertaEnvironment: "Среда",
alertaApiKey: "Ключ API",
alertaAlertState: "Состояние алерта",
alertaRecoverState: "Состояние восстановления",
Proxies: "Прокси",
default: "По умолчанию",
enabled: "Включено",
setAsDefault: "Установлено по умолчанию",
deleteProxyMsg: "Вы действительно хотите удалить этот прокси для всех мониторов?",
proxyDescription: "Прокси должны быть привязаны к монитору, чтобы работать.",
enableProxyDescription: "Этот прокси не будет влиять на запросы монитора, пока не будет активирован. Вы можете контролировать временное отключение прокси для всех мониторов через статус активации.",
setAsDefaultProxyDescription: "Этот прокси будет по умолчанию включен для новых мониторов. Вы всё ещё можете отдельно отключать прокси в каждом мониторе.",
};

392
src/languages/uk-UA.js Normal file
View File

@@ -0,0 +1,392 @@
export default {
languageName: "Український",
checkEverySecond: "Перевірка кожні {0} секунд",
retriesDescription: "Максимальна кількість спроб перед позначенням сервісу як недоступного та надсиланням повідомлення",
ignoreTLSError: "Ігнорувати помилку TLS/SSL для сайтів HTTPS",
upsideDownModeDescription: "Реверс статусу сервісу. Якщо сервіс доступний, він позначається як НЕДОСТУПНИЙ.",
maxRedirectDescription: "Максимальна кількість перенаправлень. Поставте 0, щоб вимкнути перенаправлення.",
acceptedStatusCodesDescription: "Виберіть коди статусів для визначення доступності сервісу.",
passwordNotMatchMsg: "Повторення паролю не збігається.",
notificationDescription: "Прив'яжіть повідомлення до моніторів.",
keywordDescription: "Пошук слова в чистому HTML або JSON-відповіді (чутливо до регістру)",
pauseDashboardHome: "Пауза",
deleteMonitorMsg: "Ви дійсно хочете видалити цей монітор?",
deleteNotificationMsg: "Ви дійсно хочете видалити це повідомлення для всіх моніторів?",
resolverserverDescription: "Cloudflare є сервером за замовчуванням. Ви завжди можете змінити цей сервер.",
rrtypeDescription: "Виберіть тип ресурсного запису, який ви хочете відстежувати",
pauseMonitorMsg: "Ви дійсно хочете поставити на паузу?",
Settings: "Налаштування",
Dashboard: "Панель управління",
"New Update": "Оновлення",
Language: "Мова",
Appearance: "Зовнішній вигляд",
Theme: "Тема",
General: "Загальне",
Version: "Версія",
"Check Update On GitHub": "Перевірити оновлення на GitHub",
List: "Список",
Add: "Додати",
"Add New Monitor": "Новий монітор",
"Quick Stats": "Статистика",
Up: "Доступний",
Down: "Недоступний",
Pending: "Очікування",
Unknown: "Невідомо",
Pause: "Пауза",
Name: "Ім'я",
Status: "Статус",
DateTime: "Дата і час",
Message: "Повідомлення",
"No important events": "Важливих подій немає",
Resume: "Відновити",
Edit: "Змінити",
Delete: "Видалити",
Current: "Поточний",
Uptime: "Аптайм",
"Cert Exp.": "Сертифікат спливає",
days: "днів",
day: "день",
"-day": " днів",
hour: "година",
"-hour": " години",
Response: "Відповідь",
Ping: "Пінг",
"Monitor Type": "Тип монітора",
Keyword: "Ключове слово",
"Friendly Name": "Ім'я",
URL: "URL",
Hostname: "Ім'я хоста",
Port: "Порт",
"Heartbeat Interval": "Частота опитування",
Retries: "Спроб",
Advanced: "Додатково",
"Upside Down Mode": "Реверс статусу",
"Max. Redirects": "Макс. кількість перенаправлень",
"Accepted Status Codes": "Припустимі коди статусу",
Save: "Зберегти",
Notifications: "Повідомлення",
"Not available, please setup.": "Доступних сповіщень немає, необхідно створити.",
"Setup Notification": "Створити сповіщення",
Light: "Світла",
Dark: "Темна",
Auto: "Авто",
"Theme - Heartbeat Bar": "Тема - Смуга частоти опитування",
Normal: "Звичайний",
Bottom: "Знизу",
None: "Відсутня",
Timezone: "Часовий пояс",
"Search Engine Visibility": "Індексація пошуковими системами:",
"Allow indexing": "Дозволити індексування",
"Discourage search engines from indexing site": "Заборонити індексування",
"Change Password": "Змінити пароль",
"Current Password": "Поточний пароль",
"New Password": "Новий пароль",
"Repeat New Password": "Повтор нового пароля",
"Update Password": "Оновити пароль",
"Disable Auth": "Вимкнути авторизацію",
"Enable Auth": "Увімкнути авторизацію",
Logout: "Вийти",
Leave: "Відміна",
"I understand, please disable": "Я розумію, все одно відключити",
Confirm: "Підтвердити",
Yes: "Так",
No: "Ні",
Username: "Логін",
Password: "Пароль",
"Remember me": "Запам'ятати мене",
Login: "Вхід до системи",
"No Monitors, please": "Моніторів немає, будь ласка",
"No Monitors": "Монітори відсутні",
"add one": "створіть новий",
"Notification Type": "Тип повідомлення",
Email: "Пошта",
Test: "Перевірка",
"Certificate Info": "Інформація про сертифікат",
"Resolver Server": "DNS сервер",
"Resource Record Type": "Тип ресурсного запису",
"Last Result": "Останній результат",
"Create your admin account": "Створіть обліковий запис адміністратора",
"Repeat Password": "Повторіть пароль",
respTime: "Час відповіді (мс)",
notAvailableShort: "Н/д",
Create: "Створити",
clearEventsMsg: "Ви дійсно хочете видалити всю статистику подій цього монітора?",
clearHeartbeatsMsg: "Ви дійсно хочете видалити всю статистику опитувань цього монітора?",
confirmClearStatisticsMsg: "Ви дійсно хочете видалити ВСЮ статистику?",
"Clear Data": "Видалити статистику",
Events: "Події",
Heartbeats: "Опитування",
"Auto Get": "Авто-отримання",
enableDefaultNotificationDescription: "Для кожного нового монітора це повідомлення буде включено за замовчуванням. Ви все ще можете відключити повідомлення в кожному моніторі окремо.",
"Default enabled": "Використовувати за промовчанням",
"Also apply to existing monitors": "Застосувати до існуючих моніторів",
Export: "Експорт",
Import: "Імпорт",
backupDescription: "Ви можете зберегти резервну копію всіх моніторів та повідомлень у вигляді JSON-файлу",
backupDescription2: "P.S.: Історія та події збережені не будуть",
backupDescription3: "Важливі дані, такі як токени повідомлень, додаються під час експорту, тому зберігайте файли в безпечному місці",
alertNoFile: "Виберіть файл для імпорту.",
alertWrongFileType: "Виберіть JSON-файл.",
twoFAVerifyLabel: "Будь ласка, введіть свій токен, щоб перевірити роботу 2FA",
tokenValidSettingsMsg: "Токен дійсний! Тепер ви можете зберегти налаштування 2FA.",
confirmEnableTwoFAMsg: "Ви дійсно хочете увімкнути 2FA?",
confirmDisableTwoFAMsg: "Ви дійсно хочете вимкнути 2FA?",
"Apply on all existing monitors": "Застосувати до всіх існуючих моніторів",
"Verify Token": "Перевірити токен",
"Setup 2FA": "Налаштування 2FA",
"Enable 2FA": "Увімкнути 2FA",
"Disable 2FA": "Вимкнути 2FA",
"2FA Settings": "Налаштування 2FA",
"Two Factor Authentication": "Двофакторна аутентифікація",
Active: "Активно",
Inactive: "Неактивно",
Token: "Токен",
"Show URI": "Показати URI",
"Clear all statistics": "Очистити статистику",
retryCheckEverySecond: "Повтор кожні {0} секунд",
importHandleDescription: "Виберіть \"Пропустити існуючі\", якщо ви хочете пропустити кожен монітор або повідомлення з таким же ім'ям. \"Перезаписати\" видалить кожен існуючий монітор або повідомлення та додасть заново. Варіант \"Не перевіряти\" примусово відновлює всі монітори і повідомлення, навіть якщо вони вже існують.",
confirmImportMsg: "Ви дійсно хочете відновити резервну копію? Переконайтеся, що ви вибрали відповідний варіант імпорту.",
"Heartbeat Retry Interval": "Інтервал повтору опитування",
"Import Backup": "Імпорт",
"Export Backup": "Експорт",
"Skip existing": "Пропустити існуючі",
Overwrite: "Перезаписати",
Options: "Опції",
"Keep both": "Не перевіряти",
Tags: "Теги",
"Add New below or Select...": "Додати новий або вибрати...",
"Tag with this name already exist.": "Такий тег вже існує.",
"Tag with this value already exist.": "Тег із таким значенням вже існує.",
color: "колір",
"value (optional)": "значення (опціонально)",
Gray: "Сірий",
Red: "Червоний",
Orange: "Помаранчевий",
Green: "Зелений",
Blue: "Синій",
Indigo: "Індиго",
Purple: "Пурпурний",
Pink: "Рожевий",
"Search...": "Пошук...",
"Avg. Ping": "Середнє значення пінгу",
"Avg. Response": "Середній час відповіді",
"Entry Page": "Головна сторінка",
statusPageNothing: "Тут порожньо. Додайте групу або монітор.",
"No Services": "Немає сервісів",
"All Systems Operational": "Всі системи працюють у штатному режимі",
"Partially Degraded Service": "Сервіси працюють частково",
"Degraded Service": "Всі сервіси не працюють",
"Add Group": "Додати групу",
"Add a monitor": "Додати монітор",
"Edit Status Page": "Редагувати",
"Go to Dashboard": "Панель управління",
"Status Page": "Сторінка статусу",
"Status Pages": "Сторінки статусу",
Discard: "Скасування",
"Create Incident": "Створити інцидент",
"Switch to Dark Theme": "Темна тема",
"Switch to Light Theme": "Світла тема",
telegram: "Telegram",
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 (Підтримка 50+ сервісів повідомлень)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
"Primary Base URL": "Основна URL",
"Push URL": "URL пуша",
needPushEvery: "До цієї URL необхідно звертатися кожні {0} секунд",
pushOptionalParams: "Опціональні параметри: {0}",
defaultNotificationName: "Моє повідомлення {notification} ({number})",
here: "тут",
Required: "Потрібно",
"Bot Token": "Токен бота",
wayToGetTelegramToken: "Ви можете взяти токен тут - {0}.",
"Chat ID": "ID чату",
supportTelegramChatID: "Підтримуються ID чатів, груп та каналів",
wayToGetTelegramChatID: "Ви можете взяти ID вашого чату, відправивши повідомлення боту і перейшовши по цьому URL для перегляду chat_id:",
"YOUR BOT TOKEN HERE": "ВАШ ТОКЕН БОТА ТУТ",
chatIDNotFound: "ID чату не знайдено; будь ласка, відправте спочатку повідомлення боту",
"Post URL": "Post URL",
"Content Type": "Тип контенту",
webhookJsonDesc: "{0} підходить для будь-яких сучасних HTTP-серверів, наприклад Express.js",
webhookFormDataDesc: "{multipart} підходить для PHP. JSON-вивід необхідно буде обробити за допомогою {decodeFunction}",
secureOptionNone: "Ні / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "Ігнорувати помилки TLS",
"From Email": "Від кого",
emailCustomSubject: "Своя тема",
"To Email": "Кому",
smtpCC: "Копія",
smtpBCC: "Прихована копія",
"Discord Webhook URL": "Discord Вебхук URL",
wayToGetDiscordURL: "Ви можете створити його в Параметрах сервера -> Інтеграції -> Створити вебхук",
"Bot Display Name": "Ім'я бота, що відображається",
"Prefix Custom Message": "Свій префікс повідомлення",
"Hello @everyone is...": "Привіт {'@'}everyone це...",
"Webhook URL": "URL вебхука",
wayToGetTeamsURL: "Як створити URL вебхука ви можете дізнатися тут - {0}.",
Номер: "Номер",
Recipients: "Одержувачі",
needSignalAPI: "Вам необхідний клієнт Signal із підтримкою REST API.",
wayToCheckSignalURL: "Пройдіть по цьому URL, щоб дізнатися як налаштувати такий клієнт:",
signalImportant: "ВАЖЛИВО: Не можна змішувати в Одержувачах групи та номери!",
"Application Token": "Токен програми",
"Server URL": "URL сервера",
Priority: "Пріоритет",
"Icon Emoji": "Іконка Emoji",
"Channel Name": "Ім'я каналу",
"Uptime Kuma URL": "Uptime Kuma URL",
aboutWebhooks: "Більше інформації про вебхуки: {0}",
aboutChannelName: "Введіть ім'я каналу в поле {0} Ім'я каналу, якщо ви хочете обійти канал вебхука. Наприклад: #other-channel",
aboutKumaURL: "Якщо поле Uptime Kuma URL в налаштуваннях залишиться порожнім, за замовчуванням буде використовуватися посилання на проект на GitHub.",
emojiCheatSheet: "Шпаргалка по Emoji: {0}",
"User Key": "Ключ користувача",
Device: "Пристрій",
"Message Title": "Заголовок повідомлення",
"Notification Sound": "Звук повідомлення",
"More info on:": "Більше інформації: {0}",
pushoverDesc1: "Екстренний пріоритет (2) має таймуут повтору за замовчуванням 30 секунд і закінчується через 1 годину.",
pushoverDesc2: "Якщо ви бажаєте надсилати повідомлення різним пристроям, необхідно заповнити поле Пристрій.",
"SMS Type": "Тип SMS",
octopushTypePremium: "Преміум (Швидкий - рекомендується для алертів)",
octopushTypeLowCost: "Дешевий (Повільний - іноді блокується операторами)",
checkPrice: "Тарифи {0}:",
octopushLegacyHint: "Ви використовуєте стару версію Octopush (2011-2020) або нову?",
"Check octopush prices": "Тарифи Octopush {0}.",
octopushPhoneNumber: "Номер телефону (між. формат, наприклад: +380123456789)",
octopushSMSSender: "Ім'я відправника SMS: 3-11 символів алвафіту, цифр та пробілів (a-zA-Z0-9)",
"LunaSea Device ID": "ID пристрою LunaSea",
"Apprise URL": "Apprise URL",
"Example:": "Приклад: {0}",
"Read more:": "Докладніше: {0}",
"Status:": "Статус: {0}",
"Read more": "Докладніше",
appriseInstalled: "Apprise встановлено.",
appriseNotInstalled: "Apprise не встановлено. {0}",
"Access Token": "Токен доступу",
"Channel access token": "Токен доступу каналу",
"Line Developers Console": "Консоль розробників Line",
lineDevConsoleTo: "Консоль розробників Line - {0}",
"Basic Settings": "Базові налаштування",
"User ID": "ID користувача",
"Messaging API": "API повідомлень",
wayToGetLineChannelToken: "Спочатку зайдіть в {0}, створіть провайдера та канал (API повідомлень), потім ви зможете отримати токен доступу каналу та ID користувача з вищезгаданих пунктів меню.",
"Icon URL": "URL іконки",
aboutIconURL: "Ви можете надати посилання на іконку в полі \"URL іконки\", щоб перевизначити картинку профілю за замовчуванням. Не використовується, якщо задана іконка Emoji.",
aboutMattermostChannelName: "Ви можете перевизначити канал за замовчуванням, в який пише вебхук, ввівши ім'я каналу в полі \"Ім'я каналу\". Це необхідно включити в налаштуваннях вебхука Mattermost. Наприклад: #other-channel",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - дешево та повільно, часто перевантажений. Тільки для одержувачів з Польщі.",
promosmsTypeFlash: "SMS FLASH - повідомлення автоматично з'являться на пристрої одержувача. Тільки для одержувачів з Польщі.",
promosmsTypeFull: "SMS FULL - преміум-рівень SMS, можна використовувати своє ім'я відправника (попередньо зареєструвавши його). Надійно для алертів.",
promosmsTypeSpeed: "SMS SPEED - найвищий пріоритет у системі. Дуже швидко і надійно, але дуже дорого (вдвічі дорожче, ніж SMS FULL).",
promosmsPhoneNumber: "Номер телефону (для одержувачів з Польщі можна пропустити код регіону)",
promosmsSMSSender: "Ім'я відправника SMS: Зареєстроване або одне з імен за замовчуванням: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Feishu WebHookURL": "Feishu WebHookURL",
matrixHomeserverURL: "URL сервера (разом з http(s):// і опціонально порт)",
"Internal Room Id": "Внутрішній ID кімнати",
matrixDesc1: "Внутрішній ID кімнати можна знайти в Подробицях у параметрах каналу вашого Matrix клієнта. Він повинен виглядати приблизно як !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "Рекомендується створити нового користувача і не використовувати токен доступу особистого користувача Matrix, тому що це спричиняє повний доступ до облікового запису та до кімнат, в яких ви є. Замість цього створіть нового користувача і запросіть його тільки в ту кімнату, в якій ви хочете отримувати повідомлення.Токен доступу можна отримати, виконавши команду {0}",
Method: "Метод",
Body: "Тіло",
Headers: "Заголовки",
PushUrl: "URL пуша",
HeadersInvalidFormat: "Заголовки запиту некоректні JSON: ",
BodyInvalidFormat: "Тіло запиту некоректне JSON: ",
"Monitor History": "Статистика",
clearDataOlderThan: "Зберігати статистику за {0} днів.",
PasswordsDoNotMatch: "Паролі не співпадають.",
records: "записів",
"One record": "Один запис",
steamApiKeyDescription: "Для моніторингу ігрового сервера Steam вам потрібен Web-API ключ Steam. Зареєструвати його можна тут: ",
"Certificate Chain": "Ланцюжок сертифікатів",
Valid: "Дійсний",
"Hide Tags": "Приховати теги",
Title: "Назва інциденту:",
Content: "Зміст інциденту:",
Post: "Опублікувати",
Cancel: "Скасувати",
Created: "Створено",
Unpin: "Відкріпити",
"Show Tags": "Показати теги",
recent: "Зараз",
"3h": "3 години",
"6h": "6 годин",
"24h": "24 години",
"1w": "1 тиждень",
"No monitors available.": "Немає доступних моніторів",
"Add one": "Додати новий",
Backup: "Резервна копія",
Security: "Безпека",
"Shrink Database": "Стиснути базу даних",
"Current User": "Поточний користувач",
About: "Про програму",
Description: "Опис",
"Powered by": "Працює на основі скрипту від",
shrinkDatabaseDescription: "Включає VACUUM для бази даних SQLite. Якщо база даних була створена на версії 1.10.0 і більше, AUTO_VACUUM вже включений і ця дія не потрібна.",
Style: "Стиль",
info: "ІНФО",
warning: "УВАГА",
danger: "ПОМИЛКА",
primary: "ОСНОВНИЙ",
light: "СВІТЛИЙ",
dark: "ТЕМНИЙ",
"New Status Page": "Нова сторінка статусу",
"Show update if available": "Показувати доступні оновлення",
"Also check beta release": "Перевіряти оновлення для бета версій",
"Add New Status Page": "Додати сторінку статусу",
Next: "Далі",
"Acz characters: a-z 0-9 -": "Дозволені символи: a-z 0-9 -",
"Start or end with a-z 0-9 only": "Початок та закінчення імені лише на символи: a-z 0-9",
"No consecutive dashes --": "Заборонено використовувати тире --",
"HTTP Options": "HTTP Опції",
"Basic Auth": "HTTP Авторизація",
PushByTechulus: "Push by Techulus",
clicksendsms: "ClickSend SMS",
GoogleChat: "Google Chat (тільки Google Workspace)",
apiCredentials: "API реквізити",
Done: "Готово",
Info: "Інфо",
"Steam API Key": "Steam API-Ключ",
"Pick a RR-Type...": "Виберіть RR-тип...",
"Pick Accepted Status Codes...": "Виберіть прийняті коди стану...",
Default: "За замовчуванням",
"Please input title and content": "Будь ласка, введіть назву та зміст",
"Last Updated": "Останнє Оновлення",
"Untitled Group": "Група без назви",
Services: "Сервіси",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API Користувач (включаючи префікс webapi_)",
serwersmsAPIPassword: "API Пароль",
serwersmsPhoneNumber: "Номер телефону",
serwersmsSenderName: "SMS ім'я відправника (реєстрований через портал користувача)",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM Налаштування",
smtpDkimDesc: "Повернутися до Nodemailer DKIM {0} для використання.",
documentation: "документація",
smtpDkimDomain: "Ім'я домена",
smtpDkimKeySelector: "Ключ",
smtpDkimPrivateKey: "Приватний ключ",
smtpDkimHashAlgo: "Алгоритм хеша (опціонально)",
smtpDkimheaderFieldNames: "Заголовок ключів для підпису (опціонально)",
smtpDkimskipFields: "Заколовок ключів не для підпису (опціонально)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "Кінцева точка API",
alertaEnvironment: "Середовище",
alertaApiKey: "Ключ API",
alertaAlertState: "Стан алерту",
alertaRecoverState: "Стан відновлення",
deleteStatusPageMsg: "Дійсно хочете видалити цю сторінку статусів?",
};

View File

@@ -88,8 +88,8 @@ export default {
Dark: "黑暗",
Auto: "自动",
"Theme - Heartbeat Bar": "主题 - 心跳栏",
Normal: "正常显示",
Bottom: "靠下显示",
Normal: "正常", // 此处还供 Gorush 的通知优先级功能使用,不应翻译为“正常显示”
Bottom: "靠下",
None: "不显示",
Timezone: "时区",
"Search Engine Visibility": "搜索引擎可见性",
@@ -373,4 +373,89 @@ export default {
"For safety, must use secret key": "出于安全考虑,必须使用加签密钥",
WeCom: "企业微信群机器人",
"WeCom Bot Key": "企业微信群机器人 Key",
PushByTechulus: "Push by Techulus",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API 接入点",
alertaEnvironment: "环境参数",
alertaApiKey: "API Key",
alertaAlertState: "报警时的严重性",
alertaRecoverState: "恢复后的严重性",
deleteStatusPageMsg: "您确认要删除此状态页吗?",
Proxies: "代理",
default: "默认",
enabled: "启用",
setAsDefault: "设为默认",
deleteProxyMsg: "您确认要在所有监控项中删除此代理吗?",
proxyDescription: "代理必须配置到至少一个监控项后才会工作。",
enableProxyDescription: "此代理必须启用才能对监控项的网络请求起作用。您可以通过修改激活状态,临时在所有监控项中禁用此代理。",
setAsDefaultProxyDescription: "此代理会对新创建的监控项默认激活,您仍可以在监控项配置中单独禁用此代理。",
"Proxy Protocol": "代理协议",
"Proxy Server": "代理服务器",
"Server Address": "服务器地址",
"Certificate Chain": "证书链",
Valid: "有效",
Invalid: "无效",
AccessKeyId: "AccessKey ID",
SecretAccessKey: "AccessKey Secret",
/* 以下为阿里云短信服务 API Dysms#SendSms 的参数 */
PhoneNumbers: "PhoneNumbers",
TemplateCode: "TemplateCode",
SignName: "SignName",
/* 以上为阿里云短信服务 API Dysms#SendSms 的参数 */
"Bark Endpoint": "Bark 接入点",
"Device Token": "Apple Device Token",
Platform: "平台",
iOS: "iOS",
Android: "Android",
Huawei: "华为",
High: "高",
Retry: "重试次数",
Topic: "Gorush Topic",
"Setup Proxy": "设置代理",
"Proxy server has authentication": "代理服务器启用了身份验证功能",
User: "用户名",
Installed: "已安装",
"Not installed": "未安装",
Running: "运行中",
"Not running": "未运行",
"Message:": "信息:",
wayToGetCloudflaredURL: "(可从 {0} 下载 cloudflared",
cloudflareWebsite: "Cloudflare 网站",
"Don't know how to get the token? Please read the guide:": "不知道如何获取 Token请阅读指南",
"The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.": "如果您正在通过 Cloudflare Tunnel 访问网站,则停止可能会导致当前连接断开。您确定要停止吗?请输入密码以确认。",
"Other Software": "其他软件",
"For example: nginx, Apache and Traefik.": "例如nginx、Apache 和 Traefik。",
"Please read": "请阅读",
"Remove Token": "移除 Token",
Start: "启动",
Stop: "停止",
"Uptime Kuma": "Uptime Kuma",
"Add New Status Page": "添加新的状态页",
Slug: "路径",
"Accept characters:": "可接受的字符:",
"startOrEndWithOnly": "开头和结尾必须为 {0}",
"No consecutive dashes": "不能有连续的破折号",
Next: "下一步",
"The slug is already taken. Please choose another slug.": "该路径已被使用。请选择其他路径。",
"No Proxy": "无代理",
"HTTP Basic Auth": "HTTP 基础身份验证",
"New Status Page": "新的状态页",
"Page Not Found": "状态页未找到",
"Reverse Proxy": "反向代理",
"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: "出于安全原因请务必设置AccessToken",
};

View File

@@ -200,4 +200,182 @@ export default {
line: "Line Messenger",
mattermost: "Mattermost",
deleteStatusPageMsg: "是否確定刪除這個 Status Page",
"Push URL": "推送網址",
needPushEvery: "您應每 {0} 秒呼叫此網址。",
pushOptionalParams: "選填參數:{0}",
defaultNotificationName: "我的 {notification} 通知 ({number})",
here: "此處",
Required: "必填",
"Bot Token": "機器人權杖",
wayToGetTelegramToken: "您可以從 {0} 取得 Token。",
"Chat ID": "聊天 ID",
supportTelegramChatID: "支援 對話/群組/頻道的聊天 ID",
wayToGetTelegramChatID: "傳送訊息給機器人,並前往以下網址以取得您的 chat ID",
"YOUR BOT TOKEN HERE": "在此填入您的機器人權杖",
chatIDNotFound: "找不到 Chat ID請先傳送訊息給機器人",
"Post URL": "Post 網址",
"Content Type": "Content Type",
webhookJsonDesc: "{0} 適合任何現代的 HTTP 伺服器,如 Express.js",
webhookFormDataDesc: "{multipart} 適合 PHP。 JSON 必須先經由 {decodeFunction} 剖析。",
secureOptionNone: "無 / STARTTLS (25, 587)",
secureOptionTLS: "TLS (465)",
"Ignore TLS Error": "忽略 TLS 錯誤",
"From Email": "寄件人",
emailCustomSubject: "自訂主旨",
"To Email": "收件人",
smtpCC: "CC",
smtpBCC: "BCC",
"Discord Webhook URL": "Discord Webhook 網址",
wayToGetDiscordURL: "您可以前往伺服器設定 -> 整合 -> Webhook -> 新 Webhook 以取得",
"Bot Display Name": "機器人顯示名稱",
"Prefix Custom Message": "前綴自訂訊息",
"Webhook URL": "Webhook 網址",
wayToGetTeamsURL: "您可以前往此頁面以了解如何建立 Webhook 網址 {0}。",
Number: "號碼",
Recipients: "收件人",
needSignalAPI: "您需要有 REST API 的 Signal 客戶端。",
wayToCheckSignalURL: "您可以前往下列網址以了解如何設定:",
signalImportant: "注意: 不得混合收件人的群組和號碼!",
"Application Token": "應用程式權杖",
"Server URL": "伺服器網址",
Priority: "優先度",
"Icon Emoji": "Emoji 圖示",
"Channel Name": "頻道名稱",
"Uptime Kuma URL": "Uptime Kuma 網址",
aboutWebhooks: "更多關於 Webhook 的資訊: {0}",
aboutChannelName: "如果您不想使用 Webhook 頻道,請在 {0} 頻道名稱欄位填入您想使用的頻道。例如: #其他頻道",
aboutKumaURL: "如果您未填入 Uptime Kuma 網址。將預設使用專案 Github 頁面。",
emojiCheatSheet: "Emoji 一覽表: {0}",
PushByTechulus: "Push by Techulus",
clicksendsms: "ClickSend SMS",
GoogleChat: "Google Chat (僅限 Google Workspace)",
"User Key": "使用者金鑰",
Device: "裝置",
"Message Title": "訊息標題",
"Notification Sound": "通知音效",
"More info on:": "更多資訊: {0}",
pushoverDesc1: "緊急優先度 (2) 的重試間隔為 30 秒並且會在 1 小時後過期。",
pushoverDesc2: "如果您想要傳送通知到不同裝置,請填寫裝置欄位。",
"SMS Type": "簡訊類型",
octopushTypePremium: "Premium (快速 - 建議用於警報)",
octopushTypeLowCost: "Low Cost (緩慢 - 有時會被營運商阻擋)",
checkPrice: "查看 {0} 價格:",
apiCredentials: "API 認證",
octopushLegacyHint: "您使用的是舊版的 Octopush (2011-2020) 還是新版?",
"Check octopush prices": "查看 octopush 價格 {0}。",
octopushPhoneNumber: "電話號碼 (intl 格式,例如:+33612345678) ",
octopushSMSSender: "簡訊寄件人名稱3-11位英數字元及空白 (a-zA-Z0-9)",
"LunaSea Device ID": "LunaSea 裝置 ID",
"Apprise URL": "Apprise 網址",
"Example:": "範例:{0}",
"Read more:": "深入瞭解:{0}",
"Status:": "狀態:{0}",
"Read more": "深入瞭解",
appriseInstalled: "已安裝 Apprise。",
appriseNotInstalled: "尚未安裝 Apprise。{0}",
"Access Token": "存取權杖",
"Channel access token": "頻道存取權杖",
"Line Developers Console": "Line 開發者控制台",
lineDevConsoleTo: "Line 開發者控制台 - {0}",
"Basic Settings": "基本設定",
"User ID": "使用者 ID",
"Messaging API": "Messaging API",
wayToGetLineChannelToken: "首先,前往 {0},建立 provider 和 channel (Messaging API)。接著您就可以從上面提到的選單項目中取得頻道存取權杖及使用者 ID。",
"Icon URL": "圖示網址",
aboutIconURL: "您可以在 \"圖示網址\" 中提供圖片網址以覆蓋預設個人檔案圖片。若已設定 Emoji 圖示,將忽略此設定。",
aboutMattermostChannelName: "您可以在 \"頻道名稱\" 欄位中填寫頻道名稱以覆蓋 Webhook 的預設頻道。必須在 Mattermost 的 Webhook 設定中啟用。例如:#其他頻道",
matrix: "Matrix",
promosmsTypeEco: "SMS ECO - 便宜,但是很慢且經常過載。僅限位於波蘭的收件人。",
promosmsTypeFlash: "SMS FLASH - 訊息會自動在收件人的裝置上顯示。僅限位於波蘭的收件人。",
promosmsTypeFull: "SMS FULL - 高級版,您可以使用您的寄件人名稱 (必須先註冊名稱。對於警報來說十分可靠。",
promosmsTypeSpeed: "SMS SPEED - 系統中的最高優先度。快速、可靠,但昂貴 (約 SMS FULL 的兩倍價格)。",
promosmsPhoneNumber: "電話號碼 (若收件人位於波蘭則無需輸入區域代碼)",
promosmsSMSSender: "簡訊寄件人名稱預先註冊的名稱或以下的預設名稱InfoSMS、SMS Info、MaxSMS、INFO、SMS",
"Feishu WebHookUrl": "飛書 WebHook 網址",
matrixHomeserverURL: "Homeserver 網址 (開頭為 http(s)://,結尾可能帶連接埠)",
"Internal Room Id": "Internal Room ID",
matrixDesc1: "您可以在 Matrix 客戶端的房間設定中的進階選項找到 internal room ID。應該看起來像 !QMdRCpUIfLwsfjxye6:home.server。",
matrixDesc2: "使用您自己的 Matrix 使用者存取權杖將賦予存取您的帳號和您加入的房間的完整權限。建議建立新使用者,並邀請至您想要接收通知的房間中。您可以執行 {0} 以取得存取權杖",
Method: "方法",
Body: "主體",
Headers: "標頭",
PushUrl: "Push URL",
HeadersInvalidFormat: "要求標頭不是有效的 JSON",
BodyInvalidFormat: "請求主體不是有效的 JSON",
"Monitor History": "監測器歷史紀錄",
clearDataOlderThan: "保留 {0} 天內的監測器歷史紀錄。",
PasswordsDoNotMatch: "密碼不相符。",
records: "記錄",
"One record": "一項記錄",
"Showing {from} to {to} of {count} records": "正在顯示 {count} 項記錄中的 {from} 至 {to} 項",
steamApiKeyDescription: "若要監測 Steam 遊戲伺服器,您將需要 Steam Web-API 金鑰。您可以在此註冊您的 API 金鑰:",
"Current User": "目前使用者",
recent: "最近",
Done: "完成",
Info: "資訊",
Security: "安全性",
"Steam API Key": "Steam API 金鑰",
"Shrink Database": "壓縮資料庫",
"Pick a RR-Type...": "選擇資源記錄類型...",
"Pick Accepted Status Codes...": "選擇可接受的狀態碼...",
Default: "預設",
"HTTP Options": "HTTP 選項",
"Create Incident": "建立事件",
Title: "標題",
Content: "內容",
Style: "樣式",
info: "資訊",
warning: "警告",
danger: "危險",
primary: "主要",
light: "淺色",
dark: "暗色",
Post: "發佈",
"Please input title and content": "請輸入標題及內容",
Created: "建立",
"Last Updated": "最後更新",
Unpin: "取消釘選",
"Switch to Light Theme": "切換至淺色佈景主題",
"Switch to Dark Theme": "切換至深色佈景主題",
"Show Tags": "顯示標籤",
"Hide Tags": "隱藏標籤",
Description: "描述",
"No monitors available.": "沒有可用的監測器。",
"Add one": "新增一個",
"No Monitors": "無監測器",
"Untitled Group": "未命名群組",
Services: "服務",
Discard: "捨棄",
Cancel: "取消",
shrinkDatabaseDescription: "觸發 SQLite 的資料庫清理 (VACUUM)。如果您的資料庫是在 1.10.0 版本後建立AUTO_VACUUM 已自動啟用,則無需此操作。",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API 使用者名稱 (包括 webapi_ 前綴)",
serwersmsAPIPassword: "API 密碼",
serwersmsPhoneNumber: "電話號碼",
serwersmsSenderName: "SMS 寄件人名稱 (由客戶入口網站註冊)",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM 設定",
smtpDkimDesc: "請參考 Nodemailer DKIM {0} 使用方式。",
documentation: "文件",
smtpDkimDomain: "網域名稱",
smtpDkimKeySelector: "DKIM 選取器",
smtpDkimPrivateKey: "私密金鑰",
smtpDkimHashAlgo: "雜湊演算法 (選填)",
smtpDkimheaderFieldNames: "要簽署的郵件標頭 (選填)",
smtpDkimskipFields: "不簽署的郵件標頭 (選填)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "環境",
alertaApiKey: "API 金鑰",
alertaAlertState: "警示狀態",
alertaRecoverState: "恢復狀態",
Proxies: "代理伺服器",
default: "預設",
enabled: "啟用",
setAsDefault: "設為預設",
deleteProxyMsg: "您確定要為所有監測器刪除此代理伺服器嗎?",
proxyDescription: "必須將代理伺服器指派給監測器才能運作。",
enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。",
setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。",
};

View File

@@ -239,11 +239,13 @@ export default {
"rocket.chat": "Rocket.Chat",
pushover: "Pushover",
pushy: "Pushy",
PushByTechulus: "Push by Techulus",
octopush: "Octopush",
promosms: "PromoSMS",
clicksendsms: "ClickSend SMS",
lunasea: "LunaSea",
apprise: "Apprise (支援 50 種以上的通知服務)",
GoogleChat: "Google Chat (僅限 Google Workspace)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
@@ -352,5 +354,30 @@ export default {
serwersmsAPIPassword: "API 密碼",
serwersmsPhoneNumber: "電話號碼",
serwersmsSenderName: "SMS 寄件人名稱 (由客戶入口網站註冊)",
"stackfield": "Stackfield",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM 設定",
smtpDkimDesc: "請參考 Nodemailer DKIM {0} 使用方式。",
documentation: "文件",
smtpDkimDomain: "網域名稱",
smtpDkimKeySelector: "DKIM 選取器",
smtpDkimPrivateKey: "私密金鑰",
smtpDkimHashAlgo: "雜湊演算法 (選填)",
smtpDkimheaderFieldNames: "要簽署的郵件標頭 (選填)",
smtpDkimskipFields: "不簽署的郵件標頭 (選填)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "環境",
alertaApiKey: "API 金鑰",
alertaAlertState: "警示狀態",
alertaRecoverState: "恢復狀態",
deleteStatusPageMsg: "您確定要刪除此狀態頁嗎?",
Proxies: "代理伺服器",
default: "預設",
enabled: "啟用",
setAsDefault: "設為預設",
deleteProxyMsg: "您確定要為所有監測器刪除此代理伺服器嗎?",
proxyDescription: "必須將代理伺服器指派給監測器才能運作。",
enableProxyDescription: "此代理伺服器在啟用前不會在監測器上生效,您可以藉由控制啟用狀態來暫時對所有的監測器停用代理伺服器。",
setAsDefaultProxyDescription: "預設情況下,新監測器將啟用此代理伺服器。您仍可分別停用各監測器的代理伺服器。",
};

View File

@@ -3,5 +3,6 @@
</template>
<script>
export default {}
export default {};
</script>

View File

@@ -203,6 +203,7 @@ main {
color: white;
position: fixed;
width: 100%;
z-index: 99999;
}
.dark {

View File

@@ -1,6 +1,6 @@
import { io } from "socket.io-client";
import { useToast } from "vue-toastification";
import jwt_decode from "jwt-decode";
import jwtDecode from "jwt-decode";
import Favico from "favico.js";
const toast = useToast();
@@ -40,8 +40,17 @@ export default {
notificationList: [],
statusPageListLoaded: false,
statusPageList: [],
proxyList: [],
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
showReverseProxyGuide: true,
cloudflared: {
cloudflareTunnelToken: "",
installed: null,
running: false,
message: "",
errorMessage: "",
currentPassword: "",
}
};
},
@@ -80,7 +89,7 @@ export default {
}
socket = io(wsHost, {
transports: ["websocket"],
transports: [ "websocket" ],
});
socket.on("info", (info) => {
@@ -99,7 +108,7 @@ export default {
socket.on("monitorList", (data) => {
// Add Helper function
Object.entries(data).forEach(([monitorID, monitor]) => {
Object.entries(data).forEach(([ monitorID, monitor ]) => {
monitor.getUrl = () => {
try {
return new URL(monitor.url);
@@ -120,6 +129,16 @@ export default {
this.statusPageList = data;
});
socket.on("proxyList", (data) => {
this.proxyList = data.map(item => {
item.auth = !!item.auth;
item.active = !!item.active;
item.default = !!item.default;
return item;
});
});
socket.on("heartbeat", (data) => {
if (! (data.monitorID in this.heartbeatList)) {
this.heartbeatList[data.monitorID] = [];
@@ -231,6 +250,12 @@ export default {
this.socket.firstConnect = false;
});
// cloudflared
socket.on("cloudflared_installed", (res) => this.cloudflared.installed = res);
socket.on("cloudflared_running", (res) => this.cloudflared.running = res);
socket.on("cloudflared_message", (res) => this.cloudflared.message = res);
socket.on("cloudflared_errorMessage", (res) => this.cloudflared.errorMessage = res);
socket.on("cloudflared_token", (res) => this.cloudflared.cloudflareTunnelToken = res);
},
storage() {
@@ -241,7 +266,7 @@ export default {
const jwtToken = this.$root.storage().token;
if (jwtToken && jwtToken !== "autoLogin") {
return jwt_decode(jwtToken);
return jwtDecode(jwtToken);
}
return undefined;
},

View File

@@ -6,6 +6,7 @@ export default {
userTheme: localStorage.theme,
userHeartbeatBar: localStorage.heartbeatBarTheme,
statusPageTheme: "light",
forceStatusPageTheme: false,
path: "",
};
},
@@ -27,6 +28,10 @@ export default {
computed: {
theme() {
// As entry can be status page now, set forceStatusPageTheme to true to use status page theme
if (this.forceStatusPageTheme) {
return this.statusPageTheme;
}
// Entry no need dark
if (this.path === "") {

View File

@@ -21,7 +21,9 @@
<div class="form-text">
<ul>
<li>{{ $t("Accept characters:") }} <mark>a-z</mark> <mark>0-9</mark> <mark>-</mark></li>
<li>{{ $t("Start or end with") }} <mark>a-z</mark> <mark>0-9</mark> only</li>
<i18n-t tag="li" keypath="startOrEndWithOnly">
<mark>a-z</mark> <mark>0-9</mark>
</i18n-t>
<li>{{ $t("No consecutive dashes") }} <mark>--</mark></li>
</ul>
</div>

View File

@@ -25,9 +25,9 @@ export default {
MonitorList,
},
data() {
return {}
return {};
},
}
};
</script>
<style lang="scss" scoped>

View File

@@ -118,6 +118,7 @@ export default {
return 0;
});
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.heartBeatList = result;
return result;

View File

@@ -32,6 +32,9 @@
<option value="steam">
Steam Game Server
</option>
<option value="mqtt">
MQTT
</option>
</select>
</div>
@@ -67,15 +70,15 @@
</div>
<!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam'" class="my-3">
<!-- TCP Port / Ping / DNS / Steam / MQTT only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt'" class="my-3">
<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>
</div>
<!-- Port -->
<!-- For TCP Port / Steam Type -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam'" class="my-3">
<!-- For TCP Port / Steam / MQTT Type -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'mqtt'" class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
@@ -115,6 +118,36 @@
</div>
</template>
<!-- MQTT -->
<!-- For MQTT Type -->
<template v-if="monitor.type === 'mqtt'">
<div class="my-3">
<label for="mqttUsername" class="form-label">MQTT {{ $t("Username") }}</label>
<input id="mqttUsername" v-model="monitor.mqttUsername" type="text" class="form-control">
</div>
<div class="my-3">
<label for="mqttPassword" class="form-label">MQTT {{ $t("Password") }}</label>
<input id="mqttPassword" v-model="monitor.mqttPassword" type="password" class="form-control">
</div>
<div class="my-3">
<label for="mqttTopic" class="form-label">MQTT {{ $t("Topic") }}</label>
<input id="mqttTopic" v-model="monitor.mqttTopic" type="text" class="form-control" required>
<div class="form-text">
{{ $t("topicExplanation") }}
</div>
</div>
<div class="my-3">
<label for="mqttSuccessMessage" class="form-label">MQTT {{ $t("successMessage") }}</label>
<input id="mqttSuccessMessage" v-model="monitor.mqttSuccessMessage" type="text" class="form-control">
<div class="form-text">
{{ $t("successMessageExplanation") }}
</div>
</div>
</template>
<!-- Interval -->
<div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
@@ -139,6 +172,15 @@
<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">
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
<label class="form-check-label" for="expiry-notification">
{{ $t("Domain Name Expiry Notification") }}
</label>
<div class="form-text">
</div>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls">
@@ -222,6 +264,34 @@
{{ $t("Setup Notification") }}
</button>
<!-- Proxies -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword'">
<h2 class="mt-5 mb-2">{{ $t("Proxy") }}</h2>
<p v-if="$root.proxyList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<div v-if="$root.proxyList.length > 0" class="form-check my-3">
<input id="proxy-disable" v-model="monitor.proxyId" :value="null" name="proxy" class="form-check-input" type="radio">
<label class="form-check-label" for="proxy-disable">{{ $t("No Proxy") }}</label>
</div>
<div v-for="proxy in $root.proxyList" :key="proxy.id" class="form-check my-3">
<input :id="`proxy-${proxy.id}`" v-model="monitor.proxyId" :value="proxy.id" name="proxy" class="form-check-input" type="radio">
<label class="form-check-label" :for="`proxy-${proxy.id}`">
{{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }})
<a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a>
</label>
<span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("default") }}</span>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.proxyDialog.show()">
{{ $t("Setup Proxy") }}
</button>
</div>
<!-- HTTP Options -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
<h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
@@ -285,12 +355,14 @@
</form>
<NotificationDialog ref="notificationDialog" @added="addedNotification" />
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
</div>
</transition>
</template>
<script>
import NotificationDialog from "../components/NotificationDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue";
import CopyableInput from "../components/CopyableInput.vue";
@@ -302,6 +374,7 @@ const toast = useToast();
export default {
components: {
ProxyDialog,
CopyableInput,
NotificationDialog,
TagsManager,
@@ -353,21 +426,32 @@ export default {
},
bodyPlaceholder() {
return this.$t("Example:", [`
return this.$t("Example:", [ `
{
"key": "value"
}`]);
}` ]);
},
headersPlaceholder() {
return this.$t("Example:", [`
return this.$t("Example:", [ `
{
"HeaderName": "HeaderValue"
}`]);
}` ]);
}
},
watch: {
"$root.proxyList"() {
if (this.isAdd) {
if (this.$root.proxyList && !this.monitor.proxyId) {
const proxy = this.$root.proxyList.find(proxy => proxy.default);
if (proxy) {
this.monitor.proxyId = proxy.id;
}
}
}
},
"$route.fullPath"() {
this.init();
@@ -435,12 +519,26 @@ export default {
notificationIDList: {},
ignoreTls: false,
upsideDown: false,
expiryNotification: false,
maxredirects: 10,
accepted_statuscodes: ["200-299"],
accepted_statuscodes: [ "200-299" ],
dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1",
proxyId: null,
mqttUsername: "",
mqttPassword: "",
mqttTopic: "",
mqttSuccessMessage: "",
};
if (this.$root.proxyList && !this.monitor.proxyId) {
const proxy = this.$root.proxyList.find(proxy => proxy.default);
if (proxy) {
this.monitor.proxyId = proxy.id;
}
}
for (let i = 0; i < this.$root.notificationList.length; i++) {
if (this.$root.notificationList[i].isDefault == true) {
this.monitor.notificationIDList[this.$root.notificationList[i].id] = true;
@@ -532,6 +630,12 @@ export default {
addedNotification(id) {
this.monitor.notificationIDList[id] = true;
},
// Added a Proxy Event
// Enable it if the proxy is added in EditMonitor.vue
addedProxy(id) {
this.monitor.proxyId = id;
},
},
};
</script>

View File

@@ -1,19 +1,45 @@
<template>
<div></div>
<div>
<StatusPage v-if="statusPageSlug" :override-slug="statusPageSlug" />
</div>
</template>
<script>
import axios from "axios";
import StatusPage from "./StatusPage.vue";
export default {
components: {
StatusPage,
},
data() {
return {
statusPageSlug: null,
};
},
async mounted() {
let entryPage = (await axios.get("/api/entry-page")).data;
if (entryPage === "statusPage") {
this.$router.push("/status");
// There are only 2 cases that could come in here.
// 1. Matched status Page domain name
// 2. Vue Frontend Dev
let res = (await axios.get("/api/entry-page")).data;
if (res.type === "statusPageMatchedDomain") {
this.statusPageSlug = res.statusPageSlug;
this.$root.forceStatusPageTheme = true;
} else if (res.type === "entryPage") { // Dev only. For production, the logic is in the server side
const entryPage = res.entryPage;
if (entryPage === "statusPage") {
this.$router.push("/status");
} else {
this.$router.push("/dashboard");
}
} else {
this.$router.push("/dashboard");
}
},
};

View File

@@ -11,6 +11,6 @@ export default {
components: {
MonitorList,
},
}
};
</script>

View File

@@ -12,7 +12,7 @@
<div class="shadow-box">
<template v-if="$root.statusPageListLoaded">
<span v-if="Object.keys($root.statusPageList).length === 0" class="d-flex align-items-center justify-content-center my-3">
No status pages
{{ $t("No status pages") }}
</span>
<!-- use <a> instead of <router-link>, because the heartbeat won't load. -->
@@ -92,7 +92,6 @@ export default {
}
.info {
.title {
font-weight: bold;
font-size: 20px;

View File

@@ -75,12 +75,18 @@ export default {
notifications: {
title: this.$t("Notifications"),
},
"reverse-proxy": {
title: this.$t("Reverse Proxy"),
},
"monitor-history": {
title: this.$t("Monitor History"),
},
security: {
title: this.$t("Security"),
},
proxies: {
title: this.$t("Proxies"),
},
backup: {
title: this.$t("Backup"),
},
@@ -115,6 +121,10 @@ export default {
this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data;
if (this.settings.checkUpdate === undefined) {
this.settings.checkUpdate = true;
}
if (this.settings.searchEngineIndex === undefined) {
this.settings.searchEngineIndex = false;
}
@@ -131,10 +141,18 @@ export default {
});
},
saveSettings() {
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
/**
* Save Settings
* @param currentPassword (Optional) Only need for disableAuth to true
*/
saveSettings(callback, currentPassword) {
this.$root.getSocket().emit("setSettings", this.settings, currentPassword, (res) => {
this.$root.toastRes(res);
this.loadSettings();
if (callback) {
callback();
}
});
},
}

View File

@@ -2,49 +2,80 @@
<div v-if="loadedTheme" class="container mt-3">
<!-- Sidebar for edit mode -->
<div v-if="enableEditMode" class="sidebar">
<div class="my-3">
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
<div class="input-group">
<span id="basic-addon3" class="input-group-text">/status/</span>
<input id="slug" v-model="config.slug" type="text" class="form-control">
<div class="sidebar-body">
<div class="my-3">
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
<div class="input-group">
<span id="basic-addon3" class="input-group-text">/status/</span>
<input id="slug" v-model="config.slug" type="text" class="form-control">
</div>
</div>
</div>
<div class="my-3">
<label for="title" class="form-label">{{ $t("Title") }}</label>
<input id="title" v-model="config.title" type="text" class="form-control">
</div>
<div class="my-3">
<label for="title" class="form-label">{{ $t("Title") }}</label>
<input id="title" v-model="config.title" type="text" class="form-control">
</div>
<div class="my-3">
<label for="description" class="form-label">{{ $t("Description") }}</label>
<textarea id="description" v-model="config.description" class="form-control"></textarea>
</div>
<!-- Description -->
<div class="my-3">
<label for="description" class="form-label">{{ $t("Description") }}</label>
<textarea id="description" v-model="config.description" class="form-control"></textarea>
</div>
<div class="my-3 form-check form-switch">
<input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light">
<label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
</div>
<!-- Footer Text -->
<div class="my-3">
<label for="footer-text" class="form-label">{{ $t("Footer Text") }}</label>
<textarea id="footer-text" v-model="config.footerText" class="form-control"></textarea>
</div>
<div class="my-3 form-check form-switch">
<input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox">
<label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
</div>
<div class="my-3 form-check form-switch">
<input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light">
<label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
</div>
<div v-if="false" class="my-3">
<label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label>
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
</div>
<div class="my-3 form-check form-switch">
<input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox">
<label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
</div>
<div v-if="false" class="my-3">
<label for="cname" class="form-label">Domain Names <sup>Coming Soon</sup></label>
<textarea id="cname" v-model="config.domanNames" rows="3" disabled class="form-control" :placeholder="domainNamesPlaceholder"></textarea>
</div>
<!-- Show Powered By -->
<div class="my-3 form-check form-switch">
<input id="show-powered-by" v-model="config.showPoweredBy" class="form-check-input" type="checkbox">
<label class="form-check-label" for="show-powered-by">{{ $t("Show Powered By") }}</label>
</div>
<div class="danger-zone">
<button class="btn btn-danger me-2" @click="deleteDialog">
<font-awesome-icon icon="trash" />
{{ $t("Delete") }}
</button>
<div v-if="false" class="my-3">
<label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label>
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
</div>
<!-- Domain Name List -->
<div class="my-3">
<label class="form-label">
Domain Names
<font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" />
</label>
<ul class="list-group domain-name-list">
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="removeDomain(index)" />
</li>
</ul>
</div>
<!-- Custom CSS -->
<div class="my-3">
<div class="mb-1">{{ $t("Custom CSS") }}</div>
<prism-editor v-model="config.customCSS" class="css-editor" :highlight="highlighter" line-numbers></prism-editor>
</div>
<div class="danger-zone">
<button class="btn btn-danger me-2" @click="deleteDialog">
<font-awesome-icon icon="trash" />
{{ $t("Delete") }}
</button>
</div>
</div>
<!-- Sidebar Footer -->
@@ -55,7 +86,7 @@
</button>
<button class="btn btn-danger me-2" @click="discard">
<font-awesome-icon icon="save" />
<font-awesome-icon icon="undo" />
{{ $t("Discard") }}
</button>
</div>
@@ -67,7 +98,7 @@
<h1 class="mb-4 title-flex">
<!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod">
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<img :src="logoURL" alt class="logo me-2" :class="logoClass" @load="statusPageLogoLoaded" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span>
@@ -120,7 +151,7 @@
<!-- Incident Date -->
<div class="date mt-3">
{{ $t("Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
{{ $t("Date Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
<span v-if="incident.lastUpdatedDate">
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
</span>
@@ -227,13 +258,24 @@
</div>
<footer class="mt-5 mb-4">
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
<div class="custom-footer-text text-start">
<strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong>
</div>
<Editable v-model="config.footerText" tag="div" :contenteditable="enableEditMode" :noNL="false" class="alert-heading p-2" />
<p v-if="config.showPoweredBy">
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
</p>
</footer>
</div>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
{{ $t("deleteStatusPageMsg") }}
</Confirm>
<component is="style" v-if="config.customCSS" type="text/css">
{{ config.customCSS }}
</component>
</div>
</template>
@@ -247,11 +289,20 @@ import dayjs from "dayjs";
import Favico from "favico.js";
import { getResBaseURL } from "../util-frontend";
import Confirm from "../components/Confirm.vue";
// import Prism Editor
import { PrismEditor } from "vue-prism-editor";
import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhere
// import highlighting library (you can use any library you want just return html string)
import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-css";
import "prismjs/themes/prism-tomorrow.css"; // import syntax highlighting styles
const toast = useToast();
const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
// eslint-disable-next-line no-unused-vars
let feedInterval;
const favicon = new Favico({
@@ -259,10 +310,12 @@ const favicon = new Favico({
});
export default {
components: {
PublicGroupList,
ImageCropUpload,
Confirm,
PrismEditor,
},
// Leave Page for vue route change
@@ -278,6 +331,14 @@ export default {
next();
},
props: {
overrideSlug: {
type: String,
required: false,
default: null,
},
},
data() {
return {
slug: null,
@@ -294,7 +355,6 @@ export default {
loadedData: false,
baseURL: "",
clickedEditButton: false,
domainNamesPlaceholder: "domain1.com\ndomain2.com\n..."
};
},
computed: {
@@ -389,6 +449,29 @@ export default {
},
watch: {
/**
* If connected to the socket and logged in, request private data of this statusPage
* @param connected
*/
"$root.loggedIn"(loggedIn) {
if (loggedIn) {
this.$root.getSocket().emit("getStatusPage", this.slug, (res) => {
if (res.ok) {
this.config = res.config;
if (!this.config.customCSS) {
this.config.customCSS = "body {\n" +
" \n" +
"}\n";
}
} else {
toast.error(res.msg);
}
});
}
},
/**
* Selected a monitor and add to the list.
*/
@@ -449,7 +532,7 @@ export default {
this.baseURL = getResBaseURL();
},
async mounted() {
this.slug = this.$route.params.slug;
this.slug = this.overrideSlug || this.$route.params.slug;
if (!this.slug) {
this.slug = "default";
@@ -458,6 +541,10 @@ export default {
axios.get("/api/status-page/" + this.slug).then((res) => {
this.config = res.data.config;
if (!this.config.domainNameList) {
this.config.domainNameList = [];
}
if (this.config.icon) {
this.imgDataUrl = this.config.icon;
}
@@ -480,6 +567,10 @@ export default {
},
methods: {
highlighter(code) {
return highlight(code, languages.css);
},
updateHeartbeatList() {
// If editMode, it will use the data from websocket.
if (! this.editMode) {
@@ -575,6 +666,10 @@ export default {
});
},
addDomainField() {
this.config.domainNameList.push("");
},
discard() {
location.href = "/status/" + this.slug;
},
@@ -592,6 +687,11 @@ export default {
}
},
statusPageLogoLoaded(eventPayload) {
// Remark: may not work in dev, due to cros
favicon.image(eventPayload.target);
},
createIncident() {
this.enableEditIncidentMode = true;
@@ -652,6 +752,10 @@ export default {
return dayjs.utc(date).fromNow();
},
removeDomain(index) {
this.config.domainNameList.splice(index, 1);
},
}
};
</script>
@@ -700,9 +804,7 @@ h1 {
top: 0;
width: 300px;
height: 100vh;
padding: 15px 15px 68px 15px;
overflow-x: hidden;
overflow-y: auto;
border-right: 1px solid #ededed;
.danger-zone {
@@ -710,13 +812,25 @@ h1 {
padding-top: 15px;
}
.sidebar-body {
padding: 0 10px 10px 10px;
overflow-x: hidden;
overflow-y: auto;
height: calc(100% - 70px);
}
.sidebar-footer {
width: 100%;
bottom: 0;
left: 0;
padding: 15px;
position: absolute;
border-top: 1px solid #ededed;
border-right: 1px solid #ededed;
padding: 10px;
width: 300px;
height: 70px;
position: fixed;
left: 0;
bottom: 0;
background-color: white;
display: flex;
align-items: center;
}
}
@@ -773,7 +887,7 @@ footer {
.incident {
.content {
&[contenteditable=true] {
&[contenteditable="true"] {
min-height: 60px;
}
}
@@ -803,9 +917,45 @@ footer {
}
.sidebar-footer {
border-right-color: $dark-border-color;
border-top-color: $dark-border-color;
background-color: $dark-header-bg;
}
}
}
.domain-name-list {
li {
display: flex;
align-items: center;
padding: 10px 0 10px 10px;
.domain-input {
flex-grow: 1;
background-color: transparent;
border: none;
color: $dark-font-color;
outline: none;
&::placeholder {
color: #1d2634;
}
}
}
}
/* required class */
.css-editor {
/* we dont use `language-` classes anymore so thats why we need to add background and text color manually */
border-radius: 1rem;
padding: 10px 5px;
border: 1px solid #ced4da;
.dark & {
background: $dark-bg;
border: 1px solid $dark-border-color;
}
}
</style>

View File

@@ -14,8 +14,10 @@ import Entry from "./pages/Entry.vue";
import Appearance from "./components/settings/Appearance.vue";
import General from "./components/settings/General.vue";
import Notifications from "./components/settings/Notifications.vue";
import ReverseProxy from "./components/settings/ReverseProxy.vue";
import MonitorHistory from "./components/settings/MonitorHistory.vue";
import Security from "./components/settings/Security.vue";
import Proxies from "./components/settings/Proxies.vue";
import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue";
import ManageStatusPage from "./pages/ManageStatusPage.vue";
@@ -83,6 +85,10 @@ const routes = [
path: "notifications",
component: Notifications,
},
{
path: "reverse-proxy",
component: ReverseProxy,
},
{
path: "monitor-history",
component: MonitorHistory,
@@ -91,6 +97,10 @@ const routes = [
path: "security",
component: Security,
},
{
path: "proxies",
component: Proxies,
},
{
path: "backup",
component: Backup,

View File

@@ -7,6 +7,12 @@ import { localeDirection, currentLocale } from "./i18n";
dayjs.extend(utc);
dayjs.extend(timezone);
/**
* Returns the offset from UTC in hours for the current locale.
* @returns {number} The offset from UTC in hours.
*
* Generated by Trelent
*/
function getTimezoneOffset(timeZone) {
const now = new Date();
const tzString = now.toLocaleString("en-US", {
@@ -18,6 +24,13 @@ function getTimezoneOffset(timeZone) {
return -offset;
}
/**
* Returns a list of timezones sorted by their offset from UTC.
* @param {Array} timezones - An array of timezone objects.
* @returns {Array} A list of the given timezones sorted by their offset from UTC.
*
* Generated by Trelent
*/
export function timezoneList() {
let result = [];

View File

@@ -7,7 +7,7 @@
// Backend uses the compiled file util.js
// Frontend uses util.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
const _dayjs = require("dayjs");
const dayjs = _dayjs;
exports.isDev = process.env.NODE_ENV === "development";
@@ -44,12 +44,92 @@ function ucfirst(str) {
return firstLetter.toUpperCase() + str.substr(1);
}
exports.ucfirst = ucfirst;
/**
* @deprecated Use log.debug
* @since https://github.com/louislam/uptime-kuma/pull/910
* @param msg
*/
function debug(msg) {
if (exports.isDev) {
console.log(msg);
}
exports.log.log("", msg, "debug");
}
exports.debug = debug;
class Logger {
constructor() {
/**
* UPTIME_KUMA_HIDE_LOG=debug_monitor,info_monitor
*
* Example:
* [
* "debug_monitor", // Hide all logs that level is debug and the module is monitor
* "info_monitor",
* ]
*/
this.hideLog = {
info: [],
warn: [],
error: [],
debug: [],
};
if (typeof process !== "undefined" && process.env.UPTIME_KUMA_HIDE_LOG) {
let list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map(v => v.toLowerCase());
for (let pair of list) {
// split first "_" only
let values = pair.split(/_(.*)/s);
if (values.length >= 2) {
this.hideLog[values[0]].push(values[1]);
}
}
this.debug("server", "UPTIME_KUMA_HIDE_LOG is set");
this.debug("server", this.hideLog);
}
}
log(module, msg, level) {
if (this.hideLog[level] && this.hideLog[level].includes(module)) {
return;
}
module = module.toUpperCase();
level = level.toUpperCase();
const now = new Date().toISOString();
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
if (level === "INFO") {
console.info(formattedMessage);
}
else if (level === "WARN") {
console.warn(formattedMessage);
}
else if (level === "ERROR") {
console.error(formattedMessage);
}
else if (level === "DEBUG") {
if (exports.isDev) {
console.debug(formattedMessage);
}
}
else {
console.log(formattedMessage);
}
}
info(module, msg) {
this.log(module, msg, "info");
}
warn(module, msg) {
this.log(module, msg, "warn");
}
error(module, msg) {
this.log(module, msg, "error");
}
debug(module, msg) {
this.log(module, msg, "debug");
}
exception(module, exception, msg) {
let finalMessage = exception;
if (msg) {
finalMessage = `${msg}: ${exception}`;
}
this.log(module, finalMessage, "error");
}
}
exports.log = new Logger();
function polyfill() {
/**
* String.prototype.replaceAll() polyfill

View File

@@ -49,12 +49,105 @@ export function ucfirst(str: string) {
return firstLetter.toUpperCase() + str.substr(1);
}
/**
* @deprecated Use log.debug
* @since https://github.com/louislam/uptime-kuma/pull/910
* @param msg
*/
export function debug(msg: any) {
if (isDev) {
console.log(msg);
log.log("", msg, "debug");
}
class Logger {
/**
* UPTIME_KUMA_HIDE_LOG=debug_monitor,info_monitor
*
* Example:
* [
* "debug_monitor", // Hide all logs that level is debug and the module is monitor
* "info_monitor",
* ]
*/
hideLog : any = {
info: [],
warn: [],
error: [],
debug: [],
};
constructor() {
if (typeof process !== "undefined" && process.env.UPTIME_KUMA_HIDE_LOG) {
let list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map(v => v.toLowerCase());
for (let pair of list) {
// split first "_" only
let values = pair.split(/_(.*)/s);
if (values.length >= 2) {
this.hideLog[values[0]].push(values[1]);
}
}
this.debug("server", "UPTIME_KUMA_HIDE_LOG is set");
this.debug("server", this.hideLog);
}
}
log(module: string, msg: any, level: string) {
if (this.hideLog[level] && this.hideLog[level].includes(module)) {
return;
}
module = module.toUpperCase();
level = level.toUpperCase();
const now = new Date().toISOString();
const formattedMessage = (typeof msg === "string") ? `${now} [${module}] ${level}: ${msg}` : msg;
if (level === "INFO") {
console.info(formattedMessage);
} else if (level === "WARN") {
console.warn(formattedMessage);
} else if (level === "ERROR") {
console.error(formattedMessage);
} else if (level === "DEBUG") {
if (isDev) {
console.debug(formattedMessage);
}
} else {
console.log(formattedMessage);
}
}
info(module: string, msg: any) {
this.log(module, msg, "info");
}
warn(module: string, msg: any) {
this.log(module, msg, "warn");
}
error(module: string, msg: any) {
this.log(module, msg, "error");
}
debug(module: string, msg: any) {
this.log(module, msg, "debug");
}
exception(module: string, exception: any, msg: any) {
let finalMessage = exception
if (msg) {
finalMessage = `${msg}: ${exception}`
}
this.log(module, finalMessage , "error");
}
}
export const log = new Logger();
declare global { interface String { replaceAll(str: string, newStr: string): string; } }