Merge branch 'master' into feature/request-with-http-proxy

# Conflicts:
#	package-lock.json
#	package.json
#	server/database.js
#	src/languages/en.js
#	src/mixins/socket.js
This commit is contained in:
Louis Lam
2022-04-01 14:57:35 +08:00
80 changed files with 2659 additions and 798 deletions

View File

@@ -92,6 +92,10 @@ textarea.form-control {
}
}
.btn-dark {
background-color: #161B22;
}
@media (max-width: 550px) {
.table-shadow-box {
padding: 10px !important;
@@ -144,6 +148,10 @@ textarea.form-control {
background-color: #090c10;
color: $dark-font-color;
mark, .mark {
background-color: #b6ad86;
}
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
background: $dark-border-color;
}
@@ -159,15 +167,21 @@ textarea.form-control {
border-color: $dark-border-color;
}
.input-group-text {
background-color: #282f39;
border-color: $dark-border-color;
color: $dark-font-color;
}
.form-check-input:checked {
border-color: $primary; // Re-apply bootstrap border
}
.form-switch .form-check-input {
background-color: #232f3b;
}
a,
a:not(.btn),
.table,
.nav-link {
color: $dark-font-color;
@@ -334,11 +348,8 @@ textarea.form-control {
.monitor-list {
&.scrollbar {
min-height: calc(100vh - 240px);
max-height: calc(100vh - 30px);
overflow-y: auto;
position: sticky;
top: 10px;
height: calc(100% - 65px);
}
.item {
@@ -438,6 +449,10 @@ textarea.form-control {
border-radius: 10px !important;
}
.spinner {
color: $primary;
}
// Localization
@import "localization.scss";

View File

@@ -1,5 +1,5 @@
<template>
<div class="shadow-box mb-3">
<div class="shadow-box mb-3" :style="boxStyle">
<div class="list-header">
<div class="placeholder"></div>
<div class="search-wrapper">
@@ -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 }">
@@ -63,9 +65,16 @@ export default {
data() {
return {
searchText: "",
windowTop: 0,
};
},
computed: {
boxStyle() {
return {
height: `calc(100vh - 160px + ${this.windowTop}px)`,
};
},
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
@@ -108,7 +117,20 @@ export default {
return result;
},
},
mounted() {
window.addEventListener("scroll", this.onScroll);
},
beforeUnmount() {
window.removeEventListener("scroll", this.onScroll);
},
methods: {
onScroll() {
if (window.top.scrollY <= 133) {
this.windowTop = window.top.scrollY;
} else {
this.windowTop = 133;
}
},
monitorURL(id) {
return getMonitorRelativeURL(id);
},
@@ -122,6 +144,12 @@ export default {
<style lang="scss" scoped>
@import "../assets/vars.scss";
.shadow-box {
height: calc(100vh - 150px);
position: sticky;
top: 10px;
}
.small-padding {
padding-left: 5px !important;
padding-right: 5px !important;
@@ -142,6 +170,12 @@ export default {
}
}
.dark {
.footer {
// background-color: $dark-bg;
}
}
@media (max-width: 770px) {
.list-header {
margin: -20px;

View File

@@ -145,12 +145,9 @@ export default {
this.id = null;
this.notification = {
name: "",
type: null,
type: "telegram",
isDefault: false,
};
// Set Default value here
this.notification.type = this.notificationTypes[0];
}
this.modal.show();

View File

@@ -41,7 +41,7 @@
<Uptime :monitor="monitor.element" type="24" :pill="true" />
{{ monitor.element.name }}
</div>
<div class="tags">
<div v-if="showTags" class="tags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
@@ -76,6 +76,9 @@ export default {
type: Boolean,
required: true,
},
showTags: {
type: Boolean,
}
},
data() {
return {

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

@@ -4,14 +4,39 @@
<object class="my-4" width="200" height="200" data="/icon.svg" />
<div class="fs-4 fw-bold">Uptime Kuma</div>
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
<div class="my-1 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
<div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div>
<div class="mt-1">
<div class="form-check">
<label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> Show update if available</label>
</div>
<div class="form-check">
<label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> Also check beta release</label>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
},
watch: {
}
};
</script>

View File

@@ -62,31 +62,31 @@
<div class="form-check">
<input
id="entryPageYes"
id="entryPageDashboard"
v-model="settings.entryPage"
class="form-check-input"
type="radio"
name="statusPage"
name="entryPage"
value="dashboard"
required
/>
<label class="form-check-label" for="entryPageYes">
<label class="form-check-label" for="entryPageDashboard">
{{ $t("Dashboard") }}
</label>
</div>
<div class="form-check">
<div v-for="statusPage in $root.statusPageList" :key="statusPage.id" class="form-check">
<input
id="entryPageNo"
:id="'status-page-' + statusPage.id"
v-model="settings.entryPage"
class="form-check-input"
type="radio"
name="statusPage"
value="statusPage"
name="entryPage"
:value="'statusPage-' + statusPage.slug"
required
/>
<label class="form-check-label" for="entryPageNo">
{{ $t("Status Page") }}
<label class="form-check-label" :for="'status-page-' + statusPage.id">
{{ $t("Status Page") }} - {{ statusPage.title }}
</label>
</div>
</div>

View File

@@ -0,0 +1,139 @@
<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">
Message:
<textarea v-model="errorMessage" class="form-control" readonly></textarea>
</div>
<p v-if="installed === false">(Download cloudflared from <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/">Cloudflare Website</a>)</p>
</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>
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">
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">Other Software</h4>
<div>
For example: nginx, Apache and Traefik. <br />
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() {

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 = {

View File

@@ -34,6 +34,9 @@ import {
faAward,
faLink,
faChevronDown,
faPen,
faExternalLinkSquareAlt,
faSpinner,
} from "@fortawesome/free-solid-svg-icons";
library.add(
@@ -67,6 +70,9 @@ library.add(
faAward,
faLink,
faChevronDown,
faPen,
faExternalLinkSquareAlt,
faSpinner,
);
export { FontAwesomeIcon };

View File

@@ -197,6 +197,7 @@ export default {
line: "Line Messenger",
mattermost: "Mattermost",
"Status Page": "Статус страница",
"Status Pages": "Статус страница",
"Primary Base URL": "Основен базов URL адрес",
"Push URL": "Генериран Push URL адрес",
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
@@ -360,4 +361,14 @@ export default {
smtpDkimHashAlgo: "Хеш алгоритъм (по желание)",
smtpDkimheaderFieldNames: "Хедър ключове за подписване (по желание)",
smtpDkimskipFields: "Хедър ключове, които да не се подписват (по желание)",
PushByTechulus: "Push от Techulus",
GoogleChat: "Google Chat (Само за работното пространство на Google)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "Крайна точка на API",
alertaEnvironment: "Среда",
alertaApiKey: "API Ключ",
alertaAlertState: "Състояние на тревога",
alertaRecoverState: "Състояние на възстановяване",
deleteStatusPageMsg: "Сигурни ли сте, че желаете да изтриете тази статус страница?",
};

View File

@@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Upravit stavovou stránku",
"Go to Dashboard": "Přejít na nástěnku",
"Status Page": "Stavová stránka",
"Status Pages": "Stavová stránka",
defaultNotificationName: "Moje {notification} upozornění ({číslo})",
here: "sem",
Required: "Vyžadováno",

View File

@@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Rediger Statusside",
"Go to Dashboard": "Gå til Betjeningspanel",
"Status Page": "Statusside",
"Status Pages": "Statusside",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

View File

@@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Bearbeite Status-Seite",
"Go to Dashboard": "Gehe zum Dashboard",
"Status Page": "Status-Seite",
"Status Pages": "Status-Seite",
telegram: "Telegram",
webhook: "Webhook",
smtp: "E-Mail (SMTP)",

View File

@@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
defaultNotificationName: "My {notification} Alert ({number})",
here: "here",
Required: "Required",
@@ -330,21 +331,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",
@@ -352,7 +353,7 @@ export default {
serwersmsAPIPassword: "API Password",
serwersmsPhoneNumber: "Phone number",
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
"stackfield": "Stackfield",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM Settings",
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
documentation: "documentation",
@@ -363,12 +364,13 @@ export default {
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
smtpDkimskipFields: "Header Keys not to sign (Optional)",
gorush: "Gorush",
alerta: 'Alerta',
alertaApiEndpoint: 'API Endpoint',
alertaEnvironment: 'Environment',
alertaApiKey: 'API Key',
alertaAlertState: 'Alert State',
alertaRecoverState: 'Recover State',
alerta: "Alerta",
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environment",
alertaApiKey: "API Key",
alertaAlertState: "Alert State",
alertaRecoverState: "Recover State",
deleteStatusPageMsg: "Are you sure want to delete this status page?",
Proxies: "Proxies",
default: "Default",
enabled: "Enabled",

View File

@@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Editar página de estado",
"Go to Dashboard": "Ir al panel de control",
"Status Page": "Página de estado",
"Status Pages": "Página de estado",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

View File

@@ -17,6 +17,7 @@ export default {
pauseMonitorMsg: "Kas soovid peatada seire?",
Settings: "Seaded",
"Status Page": "Ülevaade",
"Status Pages": "Ülevaated",
Dashboard: "Töölaud",
"New Update": "Uuem tarkvara versioon on saadaval.",
Language: "Keel",
@@ -197,4 +198,10 @@ export default {
pushbullet: "Pushbullet",
line: "LINE",
mattermost: "Mattermost",
alerta: "Alerta",
alertaApiEndpoint: "API otsik",
alertaEnvironment: "Keskkond",
alertaApiKey: "API võti",
alertaAlertState: "Häireseisund",
alertaRecoverState: "Taasta algolek",
};

View File

@@ -178,6 +178,7 @@ export default {
"Add a monitor": "اضافه کردن مانیتور",
"Edit Status Page": "ویرایش صفحه وضعیت",
"Status Page": "صفحه وضعیت",
"Status Pages": "صفحه وضعیت",
"Go to Dashboard": "رفتن به پیشخوان",
"Uptime Kuma": "آپتایم کوما",
records: "مورد",

View File

@@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Modifier la page de statut",
"Go to Dashboard": "Accéder au tableau de bord",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
defaultNotificationName: "Ma notification {notification} numéro ({number})",
here: "ici",
Required: "Requis",
@@ -304,9 +305,9 @@ export default {
steamApiKeyDescription: "Pour surveiller un serveur Steam, vous avez besoin d'une clé Steam Web-API. Vous pouvez enregistrer votre clé ici : ",
"Current User": "Utilisateur actuel",
recent: "Récent",
alertaApiEndpoint: 'API Endpoint',
alertaEnvironment: 'Environement',
alertaApiEndpoint: "API Endpoint",
alertaEnvironment: "Environement",
alertaApiKey: "Clé de l'API",
alertaAlertState: "État de l'Alerte",
alertaRecoverState: 'État de récupération',
alertaRecoverState: "État de récupération",
};

View File

@@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Uredi Statusnu stranicu",
"Go to Dashboard": "Na Kontrolnu ploču",
"Status Page": "Statusna stranica",
"Status Pages": "Statusne stranice",
defaultNotificationName: "Moja {number}. {notification} obavijest",
here: "ovdje",
Required: "Potrebno",
@@ -346,4 +347,30 @@ export default {
Cancel: "Otkaži",
"Powered by": "Pokreće",
Saved: "Spremljeno",
PushByTechulus: "Push by Techulus",
GoogleChat: "Google Chat (preko platforme Google Workspace)",
shrinkDatabaseDescription: "Pokreni VACUUM operaciju za SQLite. Ako je baza podataka kreirana nakon inačice 1.10.0, AUTO_VACUUM opcija već je uključena te ova akcija nije nužna.",
serwersms: "SerwerSMS.pl",
serwersmsAPIUser: "API korisničko ime (uključujući webapi_ prefiks)",
serwersmsAPIPassword: "API lozinka",
serwersmsPhoneNumber: "Broj telefona",
serwersmsSenderName: "Ime SMS pošiljatelja (registrirano preko korisničkog portala)",
stackfield: "Stackfield",
smtpDkimSettings: "DKIM postavke",
smtpDkimDesc: "Za više informacija, postoji Nodemailer DKIM {0}.",
documentation: "dokumentacija",
smtpDkimDomain: "Domena",
smtpDkimKeySelector: "Odabir ključa",
smtpDkimPrivateKey: "Privatni ključ",
smtpDkimHashAlgo: "Hash algoritam (neobavezno)",
smtpDkimheaderFieldNames: "Ključevi zaglavlja za potpis (neobavezno)",
smtpDkimskipFields: "Ključevi zaglavlja koji se neće potpisati (neobavezno)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "Krajnja točka API-ja (Endpoint)",
alertaEnvironment: "Okruženje (Environment)",
alertaApiKey: "API ključ",
alertaAlertState: "Stanje upozorenja",
alertaRecoverState: "Stanje oporavka",
deleteStatusPageMsg: "Sigurno želite obrisati ovu statusnu stranicu?",
};

View File

@@ -197,6 +197,7 @@ export default {
line: "Line Messenger",
mattermost: "Mattermost",
"Status Page": "Státusz oldal",
"Status Pages": "Státusz oldal",
"Primary Base URL": "Elsődleges URL",
"Push URL": "Meghívandó URL",
needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.",
@@ -361,4 +362,12 @@ export default {
smtpDkimHashAlgo: "Hash algoritmus (nem kötelező)",
smtpDkimheaderFieldNames: "Fejléc kulcsok a bejelentkezéshez (nem kötelező)",
smtpDkimskipFields: "Fejléc kulcsok egyéb esetben (nem kötelező)",
PushByTechulus: "Techulus push",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "API végpont",
alertaEnvironment: "Környezet",
alertaApiKey: "API kulcs",
alertaAlertState: "Figyelmeztetési állapot",
alertaRecoverState: "Visszaállási állapot",
};

View File

@@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Edit Halaman Status",
"Go to Dashboard": "Pergi ke Dasbor",
"Status Page": "Halaman Status",
"Status Pages": "Halaman Status",
defaultNotificationName: "{notification} saya Peringatan ({number})",
here: "di sini",
Required: "Dibutuhkan",

View File

@@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Modifica pagina di stato",
"Go to Dashboard": "Vai alla dashboard",
"Status Page": "Pagina di stato",
"Status Pages": "Pagina di stato",
defaultNotificationName: "Notifica {notification} ({number})",
here: "qui",
Required: "Obbligatorio",

View File

@@ -180,6 +180,7 @@ export default {
"Edit Status Page": "ステータスページ編集",
"Go to Dashboard": "ダッシュボード",
"Status Page": "ステータスページ",
"Status Pages": "ステータスページ",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

View File

@@ -179,6 +179,7 @@ export default {
"Edit Status Page": "상태 페이지 수정",
"Go to Dashboard": "대시보드로 가기",
"Status Page": "상태 페이지",
"Status Pages": "상태 페이지",
defaultNotificationName: "내 {notification} 알림 ({number})",
here: "여기",
Required: "필수",
@@ -188,7 +189,7 @@ export default {
"Chat ID": "채팅 ID",
supportTelegramChatID: "Direct Chat / Group / Channel's Chat ID를 지원해요.",
wayToGetTelegramChatID: "봇에 메시지를 보내 채팅 ID를 얻고 밑에 URL로 이동해 chat_id를 볼 수 있어요.",
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
"YOUR BOT TOKEN HERE": "여기에 BOT 토큰을 적어주세요.",
chatIDNotFound: "채팅 ID를 찾을 수 없어요. 먼저 봇에게 메시지를 보내주세요.",
webhook: "Webhook",
"Post URL": "Post URL",
@@ -281,15 +282,15 @@ export default {
promosmsSMSSender: "SMS 보내는 사람 이름 : 미리 등록된 이름 혹은 기본값 중 하나예요: InfoSMS, SMS Info, MaxSMS, INFO, SMS",
"Primary Base URL": "기본 URL",
"Push URL": "Push URL",
needPushEvery: "You should call this URL every {0} seconds.",
pushOptionalParams: "Optional parameters: {0}",
emailCustomSubject: "Custom Subject",
needPushEvery: "이 URL을 {0} 초 마다 호출할 수 있어요.",
pushOptionalParams: "선택적 파라미터: {0}",
emailCustomSubject: "커스텀 주제",
clicksendsms: "ClickSend SMS",
checkPrice: "{0} 가격 확인:",
apiCredentials: "API credentials",
apiCredentials: "API 인증정보",
octopushLegacyHint: "Octopush 레거시 버전 (2011-2020) 을 사용하시나요? 아니면 새 버전을 사용하시나요?",
"Feishu WebHookUrl": "Feishu WebHookURL",
matrixHomeserverURL: "Homeserver URL (with http(s):// and optionally port)",
matrixHomeserverURL: "Homeserver URL (http(s):// 와 함께 적어주세요. 그리고 포트 번호는 선택적 입니다.)",
"Internal Room Id": "내부 방 ID",
matrixDesc1: "Matrix 클라이언트 방 설정의 고급 섹션에서 내부 방 ID를 찾을 수 있어요. 내부 방 ID는 이렇게 생겼답니다: !QMdRCpUIfLwsfjxye6:home.server.",
matrixDesc2: "사용자의 모든 방에 대한 엑세스가 허용될 수 있어서 새로운 사용자를 만들고 원하는 방에만 초대한 후 엑세스 토큰을 사용하는 것이 좋아요. {0} 이 명령어를 통해 엑세스 토큰을 얻을 수 있어요.",
@@ -349,6 +350,6 @@ export default {
serwersmsAPIUser: "API Usename (webapi_ 접두사 포함)",
serwersmsAPIPassword: "API 비밀번호",
serwersmsPhoneNumber: "휴대전화 번호",
serwersmsSenderName: "보내는 사람 이름 (registered via customer portal)",
serwersmsSenderName: "보내는 사람 이름 (customer portal를 통해 가입된 정보)",
stackfield: "Stackfield",
};

View File

@@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Rediger statusside",
"Go to Dashboard": "Gå til Dashboard",
"Status Page": "Statusside",
"Status Pages": "Statusside",
defaultNotificationName: "Min {notification} varsling ({number})",
here: "her",
Required: "Obligatorisk",

View File

@@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Wijzig status pagina",
"Go to Dashboard": "Ga naar Dashboard",
"Status Page": "Status Pagina",
"Status Pages": "Status Pagina",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

View File

@@ -179,6 +179,7 @@ export default {
"Edit Status Page": "Edytuj stronę statusu",
"Go to Dashboard": "Idź do panelu",
"Status Page": "Strona statusu",
"Status Pages": "Strona statusu",
defaultNotificationName: "Moje powiadomienie {notification} ({number})",
here: "tutaj",
Required: "Wymagane",

View File

@@ -169,6 +169,7 @@ export default {
"Avg. Ping": "Ping Médio.",
"Avg. Response": "Resposta Média. ",
"Status Page": "Página de Status",
"Status Pages": "Página de Status",
"Entry Page": "Página de entrada",
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
"No Services": "Nenhum Serviço",

View File

@@ -180,7 +180,8 @@ export default {
"Add a monitor": "Добавить монитор",
"Edit Status Page": "Редактировать",
"Go to Dashboard": "Панель управления",
"Status Page": "Мониторинг",
"Status Page": "Страница статуса",
"Status Pages": "Страницы статуса",
Discard: "Отмена",
"Create Incident": "Создать инцидент",
"Switch to Dark Theme": "Тёмная тема",
@@ -310,28 +311,82 @@ export default {
"One record": "Одна запись",
steamApiKeyDescription: "Для мониторинга игрового сервера Steam вам необходим Web-API ключ Steam. Зарегистрировать его можно здесь: ",
"Certificate Chain": "Цепочка сертификатов",
"Valid": "Действительный",
Valid: "Действительный",
"Hide Tags": "Скрыть тэги",
"Title": "Название инцидента:",
"Content": "Содержание инцидента:",
"Post": "Опубликовать",
"Cancel": "Отмена",
"Created": "Создано",
"Unpin": "Открепить",
Title: "Название инцидента:",
Content: "Содержание инцидента:",
Post: "Опубликовать",
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: "Вы действительно хотите удалить эту страницу статуса сервисов?",
Style: "Стиль",
info: "ИНФО",
warning: "ВНИМАНИЕ",
danger: "ОШИБКА",
primary: "ОСНОВНОЙ",
light: "СВЕТЛЫЙ",
dark: "ТЕМНЫЙ",
"New Status Page": "Новая страница статуса",
"Show update if available": "Показывать доступные обновления",
"Also check beta release": "Проверять обновления для бета версий",
"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: "Please refer to the Nodemailer DKIM {0} for usage.",
documentation: "документация",
smtpDkimDomain: "Имя Домена",
smtpDkimKeySelector: "Ключ",
smtpDkimPrivateKey: "Приватный ключ",
smtpDkimHashAlgo: "Алгоритм хэша (опционально)",
smtpDkimheaderFieldNames: "Заголовок ключей для подписи (опционально)",
smtpDkimskipFields: "Заколовок ключей не для подписи (опционально)",
gorush: "Gorush",
alerta: "Alerta",
alertaApiEndpoint: "Конечная точка API",
alertaEnvironment: "Среда",
alertaApiKey: "Ключ API",
alertaAlertState: "Состояние алерта",
alertaRecoverState: "Состояние восстановления",
};

View File

@@ -182,7 +182,8 @@ export default {
"Add a monitor": "Dodaj monitor",
"Edit Status Page": "Uredi statusno stran",
"Go to Dashboard": "Pojdi na nadzorno ploščo",
"Status Page": "Status",
"Status Page": "Página de Status",
"Status Pages": "Página de Status",
defaultNotificationName: "Moje {notification} Obvestilo ({number})",
here: "tukaj",
Required: "Obvezno",

View File

@@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

View File

@@ -180,6 +180,7 @@ export default {
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",

View File

@@ -108,94 +108,4 @@ export default {
"Repeat Password": "Upprepa Lösenord",
respTime: "Svarstid (ms)",
notAvailableShort: "Ej Tillg.",
Create: "Create",
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
"Clear Data": "Clear Data",
Events: "Events",
Heartbeats: "Heartbeats",
"Auto Get": "Auto Get",
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
"Default enabled": "Default enabled",
"Also apply to existing monitors": "Also apply to existing monitors",
Export: "Export",
Import: "Import",
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
backupDescription2: "PS: History and event data is not included.",
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
alertNoFile: "Please select a file to import.",
alertWrongFileType: "Please select a JSON file.",
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
"Apply on all existing monitors": "Apply on all existing monitors",
"Verify Token": "Verify Token",
"Setup 2FA": "Setup 2FA",
"Enable 2FA": "Enable 2FA",
"Disable 2FA": "Disable 2FA",
"2FA Settings": "2FA Settings",
"Two Factor Authentication": "Two Factor Authentication",
Active: "Active",
Inactive: "Inactive",
Token: "Token",
"Show URI": "Show URI",
"Clear all statistics": "Clear all Statistics",
retryCheckEverySecond: "Retry every {0} seconds.",
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Import Backup": "Import Backup",
"Export Backup": "Export Backup",
"Skip existing": "Skip existing",
Overwrite: "Overwrite",
Options: "Options",
"Keep both": "Keep both",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
};

View File

@@ -124,7 +124,7 @@ export default {
tokenValidSettingsMsg: "Token geçerli! Şimdi 2FA ayarlarını kaydedebilirsiniz. ",
confirmEnableTwoFAMsg: "2FA'ı etkinleştirmek istediğinizden emin misiniz?",
confirmDisableTwoFAMsg: "2FA'ı devre dışı bırakmak istediğinize emin misiniz?",
"Heartbeat Retry Interval": "Sağlık Dırımları Tekrar Deneme Sıklığı",
"Heartbeat Retry Interval": "Sağlık Durumları Tekrar Deneme Sıklığı",
"Import Backup": "Yedeği içe aktar",
"Export Backup": "Yedeği dışa aktar",
Export: "Dışa aktar",
@@ -149,52 +149,4 @@ export default {
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
Active: "Aktif",
Inactive: "İnaktif",
Token: "Token",
"Show URI": "Show URI",
Tags: "Tags",
"Add New below or Select...": "Add New below or Select...",
"Tag with this name already exist.": "Tag with this name already exist.",
"Tag with this value already exist.": "Tag with this value already exist.",
color: "color",
"value (optional)": "value (optional)",
Gray: "Gray",
Red: "Red",
Orange: "Orange",
Green: "Green",
Blue: "Blue",
Indigo: "Indigo",
Purple: "Purple",
Pink: "Pink",
"Search...": "Search...",
"Avg. Ping": "Avg. Ping",
"Avg. Response": "Avg. Response",
"Entry Page": "Entry Page",
statusPageNothing: "Nothing here, please add a group or a monitor.",
"No Services": "No Services",
"All Systems Operational": "All Systems Operational",
"Partially Degraded Service": "Partially Degraded Service",
"Degraded Service": "Degraded Service",
"Add Group": "Add Group",
"Add a monitor": "Add a monitor",
"Edit Status Page": "Edit Status Page",
"Go to Dashboard": "Go to Dashboard",
"Status Page": "Status Page",
telegram: "Telegram",
webhook: "Webhook",
smtp: "Email (SMTP)",
discord: "Discord",
teams: "Microsoft Teams",
signal: "Signal",
gotify: "Gotify",
slack: "Slack",
"rocket.chat": "Rocket.chat",
pushover: "Pushover",
pushy: "Pushy",
octopush: "Octopush",
promosms: "PromoSMS",
lunasea: "LunaSea",
apprise: "Apprise (Support 50+ Notification services)",
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
};

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

@@ -183,6 +183,7 @@ export default {
"Edit Status Page": "Sửa trang trạng thái",
"Go to Dashboard": "Đi tới Dashboard",
"Status Page": "Trang trạng thái",
"Status Pages": "Trang trạng thái",
defaultNotificationName: "My {notification} Alerts ({number})",
here: "tại đây",
Required: "Bắt buộc",

View File

@@ -185,6 +185,7 @@ export default {
"Edit Status Page": "编辑状态页面",
"Go to Dashboard": "前往仪表盘",
"Status Page": "状态页面",
"Status Pages": "状态页面",
defaultNotificationName: "{notification} 通知({number}",
here: "这里",
Required: "必填",

View File

@@ -96,7 +96,7 @@ export default {
Test: "測試",
keywordDescription: "搜索 HTML 或 JSON 裡是否有出現關鍵字(注意英文大細階)",
"Certificate Info": "憑證詳細資料",
deleteMonitorMsg: "是否確定刪除這個監測器",
deleteMonitorMsg: "是否確定刪除這個監測器",
deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。",
"Resolver Server": "DNS 伺服器",
"Resource Record Type": "DNS 記錄類型",
@@ -180,6 +180,7 @@ export default {
"Edit Status Page": "編輯 Status Page",
"Go to Dashboard": "前往主控台",
"Status Page": "Status Page",
"Status Pages": "Status Pages",
telegram: "Telegram",
webhook: "Webhook",
smtp: "電郵 (SMTP)",
@@ -198,4 +199,5 @@ export default {
pushbullet: "Pushbullet",
line: "Line Messenger",
mattermost: "Mattermost",
deleteStatusPageMsg: "是否確定刪除這個 Status Page",
};

View File

@@ -183,6 +183,7 @@ export default {
"Edit Status Page": "編輯狀態頁",
"Go to Dashboard": "前往儀表板",
"Status Page": "狀態頁",
"Status Pages": "狀態頁",
defaultNotificationName: "我的 {notification} 通知 ({number})",
here: "此處",
Required: "必填",

View File

@@ -3,6 +3,9 @@
<div v-if="! $root.socket.connected && ! $root.socket.firstConnect" class="lost-connection">
<div class="container-fluid">
{{ $root.connectionErrorMsg }}
<div v-if="$root.showReverseProxyGuide">
Using a Reverse Proxy? <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">Check how to config it for WebSocket</a>
</div>
</div>
</div>
@@ -18,10 +21,10 @@
</a>
<ul class="nav nav-pills">
<li class="nav-item me-2">
<a href="/status" class="nav-link status-page">
<font-awesome-icon icon="stream" /> {{ $t("Status Page") }}
</a>
<li v-if="$root.loggedIn" class="nav-item me-2">
<router-link to="/manage-status-page" class="nav-link">
<font-awesome-icon icon="stream" /> {{ $t("Status Pages") }}
</router-link>
</li>
<li v-if="$root.loggedIn" class="nav-item me-2">
<router-link to="/dashboard" class="nav-link">
@@ -45,7 +48,7 @@
</header>
<main>
<router-view v-if="$root.loggedIn" />
<router-view v-if="$root.loggedIn || forceShowContent" />
<Login v-if="! $root.loggedIn && $root.allowLoginDialog" />
</main>
@@ -184,6 +187,9 @@ main {
padding: 5px;
background-color: crimson;
color: white;
position: fixed;
width: 100%;
z-index: 99999;
}
.dark {

View File

@@ -1,16 +1,21 @@
import { io } from "socket.io-client";
import { useToast } from "vue-toastification";
import jwt_decode from "jwt-decode";
import Favico from "favico.js";
const toast = useToast();
let socket;
const noSocketIOPages = [
"/status-page",
"/status",
"/"
/^\/status-page$/, // /status-page
/^\/status/, // /status**
/^\/$/ // /
];
const favicon = new Favico({
animation: "none"
});
export default {
data() {
@@ -33,8 +38,19 @@ export default {
uptimeList: { },
tlsInfoList: {},
notificationList: [],
statusPageListLoaded: false,
statusPageList: [],
proxyList: [],
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
showReverseProxyGuide: true,
cloudflared: {
cloudflareTunnelToken: "",
installed: null,
running: false,
message: "",
errorMessage: "",
currentPassword: "",
}
};
},
@@ -52,8 +68,12 @@ export default {
}
// No need to connect to the socket.io for status page
if (! bypass && noSocketIOPages.includes(location.pathname)) {
return;
if (! bypass && location.pathname) {
for (let page of noSocketIOPages) {
if (location.pathname.match(page)) {
return;
}
}
}
this.socket.initedSocketIO = true;
@@ -104,6 +124,11 @@ export default {
this.notificationList = data;
});
socket.on("statusPageList", (data) => {
this.statusPageListLoaded = true;
this.statusPageList = data;
});
socket.on("proxyList", (data) => {
this.proxyList = data.map(item => {
item.auth = !!item.auth;
@@ -180,6 +205,7 @@ export default {
socket.on("connect_error", (err) => {
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
this.connectionErrorMsg = `Cannot connect to the socket server. [${err}] Reconnecting...`;
this.showReverseProxyGuide = true;
this.socket.connected = false;
this.socket.firstConnect = false;
});
@@ -194,6 +220,7 @@ export default {
console.log("Connected to the socket server");
this.socket.connectCount++;
this.socket.connected = true;
this.showReverseProxyGuide = false;
// Reset Heartbeat list if it is re-connect
if (this.socket.connectCount >= 2) {
@@ -223,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() {
@@ -250,6 +283,14 @@ export default {
}
},
toastSuccess(msg) {
toast.success(msg);
},
toastError(msg) {
toast.error(msg);
},
login(username, password, token, callback) {
socket.emit("login", {
username,
@@ -403,10 +444,49 @@ export default {
return result;
},
stats() {
let result = {
up: 0,
down: 0,
unknown: 0,
pause: 0,
};
for (let monitorID in this.$root.monitorList) {
let beat = this.$root.lastHeartbeatList[monitorID];
let monitor = this.$root.monitorList[monitorID];
if (monitor && ! monitor.active) {
result.pause++;
} else if (beat) {
if (beat.status === 1) {
result.up++;
} else if (beat.status === 0) {
result.down++;
} else if (beat.status === 2) {
result.up++;
} else {
result.unknown++;
}
} else {
result.unknown++;
}
}
return result;
},
},
watch: {
// Update Badge
"stats.down"(to, from) {
if (to !== from) {
favicon.badge(to);
}
},
// Reload the SPA if the server version is changed.
"info.version"(to, from) {
if (from && from !== to) {
@@ -420,9 +500,15 @@ export default {
// Reconnect the socket io, if status-page to dashboard
"$route.fullPath"(newValue, oldValue) {
if (noSocketIOPages.includes(newValue)) {
return;
if (newValue) {
for (let page of noSocketIOPages) {
if (newValue.match(page)) {
return;
}
}
}
this.initSocketIO();
},

View File

@@ -33,7 +33,7 @@ export default {
return "light";
}
if (this.path === "/status-page" || this.path === "/status") {
if (this.path.startsWith("/status-page") || this.path.startsWith("/status")) {
return this.statusPageTheme;
} else {
if (this.userTheme === "auto") {

View File

@@ -0,0 +1,79 @@
<template>
<transition name="slide-fade" appear>
<div>
<h1 class="mb-3">
{{ $t("Add New Status Page") }}
</h1>
<form @submit.prevent="submit">
<div class="shadow-box">
<div class="mb-3">
<label for="name" class="form-label">{{ $t("Name") }}</label>
<input id="name" v-model="title" type="text" class="form-control" required>
</div>
<div class="mb-4">
<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="slug" type="text" class="form-control" required>
</div>
<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>
<li>{{ $t("No consecutive dashes") }} <mark>--</mark></li>
</ul>
</div>
</div>
<div class="mt-2 mb-1">
<button id="monitor-submit-btn" class="btn btn-primary w-100" type="submit" :disabled="processing">{{ $t("Next") }}</button>
</div>
</div>
</form>
</div>
</transition>
</template>
<script>
export default {
components: {
},
data() {
return {
title: "",
slug: "",
processing: false,
};
},
methods: {
async submit() {
this.processing = true;
this.$root.getSocket().emit("addStatusPage", this.title, this.slug, (res) => {
this.processing = false;
if (res.ok) {
location.href = "/status/" + this.slug + "?edit";
} else {
if (res.msg.includes("UNIQUE constraint")) {
this.$root.toastError(this.$t("The slug is already taken. Please choose another slug."));
} else {
this.$root.toastRes(res);
}
}
});
}
}
};
</script>
<style lang="scss" scoped>
.shadow-box {
padding: 20px;
}
</style>

View File

@@ -9,19 +9,19 @@
<div class="row">
<div class="col">
<h3>{{ $t("Up") }}</h3>
<span class="num">{{ stats.up }}</span>
<span class="num">{{ $root.stats.up }}</span>
</div>
<div class="col">
<h3>{{ $t("Down") }}</h3>
<span class="num text-danger">{{ stats.down }}</span>
<span class="num text-danger">{{ $root.stats.down }}</span>
</div>
<div class="col">
<h3>{{ $t("Unknown") }}</h3>
<span class="num text-secondary">{{ stats.unknown }}</span>
<span class="num text-secondary">{{ $root.stats.unknown }}</span>
</div>
<div class="col">
<h3>{{ $t("pauseDashboardHome") }}</h3>
<span class="num text-secondary">{{ stats.pause }}</span>
<span class="num text-secondary">{{ $root.stats.pause }}</span>
</div>
</div>
</div>
@@ -89,37 +89,6 @@ export default {
};
},
computed: {
stats() {
let result = {
up: 0,
down: 0,
unknown: 0,
pause: 0,
};
for (let monitorID in this.$root.monitorList) {
let beat = this.$root.lastHeartbeatList[monitorID];
let monitor = this.$root.monitorList[monitorID];
if (monitor && ! monitor.active) {
result.pause++;
} else if (beat) {
if (beat.status === 1) {
result.up++;
} else if (beat.status === 0) {
result.down++;
} else if (beat.status === 2) {
result.up++;
} else {
result.unknown++;
}
} else {
result.unknown++;
}
}
return result;
},
importantHeartBeatList() {
let result = [];

View File

@@ -0,0 +1,118 @@
<template>
<transition name="slide-fade" appear>
<div>
<h1 class="mb-3">
{{ $t("Status Pages") }}
</h1>
<div>
<router-link to="/add-status-page" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("New Status Page") }}</router-link>
</div>
<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
</span>
<!-- use <a> instead of <router-link>, because the heartbeat won't load. -->
<a v-for="statusPage in $root.statusPageList" :key="statusPage.slug" :href="'/status/' + statusPage.slug" class="item">
<img :src="icon(statusPage.icon)" alt class="logo me-2" />
<div class="info">
<div class="title">{{ statusPage.title }}</div>
<div class="slug">/status/{{ statusPage.slug }}</div>
</div>
</a>
</template>
<div v-else class="d-flex align-items-center justify-content-center my-3 spinner">
<font-awesome-icon icon="spinner" size="2x" spin />
</div>
</div>
</div>
</transition>
</template>
<script>
import { getResBaseURL } from "../util-frontend";
export default {
components: {
},
data() {
return {
};
},
computed: {
},
mounted() {
},
methods: {
icon(icon) {
if (icon === "/icon.svg") {
return icon;
} else {
return getResBaseURL() + icon;
}
}
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.item {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
border-radius: 10px;
transition: all ease-in-out 0.15s;
padding: 10px;
&:hover {
background-color: $highlight-white;
}
&.active {
background-color: #cdf8f4;
}
$logo-width: 70px;
.logo {
width: $logo-width;
// Better when the image is loading
min-height: 1px;
}
.info {
.title {
font-weight: bold;
font-size: 20px;
}
.slug {
font-size: 14px;
}
}
}
.dark {
.item {
&:hover {
background-color: $dark-bg2;
}
&.active {
background-color: $dark-bg2;
}
}
}
</style>

99
src/pages/NotFound.vue Normal file
View File

@@ -0,0 +1,99 @@
<template>
<div>
<!-- Desktop header -->
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
<router-link to="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
<span class="fs-4 title">Uptime Kuma</span>
</router-link>
</header>
<!-- Mobile header -->
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
<object class="bi" width="40" height="40" data="/icon.svg" />
<span class="fs-4 title ms-2">Uptime Kuma</span>
</router-link>
</header>
<div class="content">
<div>
<strong>🐻 {{ $t("Page Not Found") }}</strong>
</div>
<div class="guide">
Most likely causes:
<ul>
<li>The resource is no longer available.</li>
<li>There might be a typing error in the address.</li>
</ul>
What you can try:<br />
<ul>
<li>Retype the address.</li>
<li><a href="#" class="go-back" @click="goBack()">Go back to the previous page.</a></li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
async mounted() {
},
methods: {
goBack() {
history.back();
}
}
};
</script>
<style scoped lang="scss">
@import "../assets/vars.scss";
.go-back {
text-decoration: none;
color: $primary !important;
}
.content {
display: flex;
justify-content: center;
align-content: center;
align-items: center;
flex-direction: column;
gap: 50px;
padding-top: 30px;
strong {
font-size: 24px;
}
}
.guide {
max-width: 800px;
font-size: 14px;
}
.title {
font-weight: bold;
}
.dark {
header {
background-color: $dark-header-bg;
border-bottom-color: $dark-header-bg !important;
span {
color: #f0f6fc;
}
}
.bottom-nav {
background-color: $dark-bg;
}
}
</style>

View File

@@ -75,6 +75,9 @@ export default {
notifications: {
title: this.$t("Notifications"),
},
"reverse-proxy": {
title: this.$t("Reverse Proxy"),
},
"monitor-history": {
title: this.$t("Monitor History"),
},
@@ -134,10 +137,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();
}
});
},
}
@@ -170,6 +181,8 @@ footer {
margin: 0.5em;
padding: 0.7em 1em;
cursor: pointer;
border-left-width: 0;
transition: all ease-in-out 0.1s;
}
.menu-item:hover {

View File

@@ -1,45 +1,54 @@
<template>
<div v-if="loadedTheme" class="container mt-3">
<!-- Logo & Title -->
<h1 class="mb-4">
<!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod">
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span>
<!-- Uploader -->
<!-- url="/api/status-page/upload-logo" -->
<ImageCropUpload v-model="showImageCropUpload"
field="img"
:width="128"
:height="128"
:langType="$i18n.locale"
img-format="png"
:noCircle="true"
:noSquare="false"
@crop-success="cropSuccess"
/>
<!-- Title -->
<Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
</h1>
<!-- Admin functions -->
<div v-if="hasToken" class="mb-4">
<div v-if="!enableEditMode">
<button class="btn btn-info me-2" @click="edit">
<font-awesome-icon icon="edit" />
{{ $t("Edit Status Page") }}
</button>
<a href="/dashboard" class="btn btn-info">
<font-awesome-icon icon="tachometer-alt" />
{{ $t("Go to Dashboard") }}
</a>
<!-- 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>
</div>
<div v-else>
<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>
<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 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="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 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>
<div class="danger-zone">
<button class="btn btn-danger me-2" @click="deleteDialog">
<font-awesome-icon icon="trash" />
{{ $t("Delete") }}
</button>
</div>
<!-- Sidebar Footer -->
<div class="sidebar-footer">
<button class="btn btn-success me-2" @click="save">
<font-awesome-icon icon="save" />
{{ $t("Save") }}
@@ -49,167 +58,182 @@
<font-awesome-icon icon="save" />
{{ $t("Discard") }}
</button>
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
<font-awesome-icon icon="bullhorn" />
{{ $t("Create Incident") }}
</button>
<!--
<button v-if="isPublished" class="btn btn-light me-2" @click="">
<font-awesome-icon icon="save" />
{{ $t("Unpublish") }}
</button>
<button v-if="!isPublished" class="btn btn-info me-2" @click="">
<font-awesome-icon icon="save" />
{{ $t("Publish") }}
</button>-->
<!-- Set Default Language -->
<!-- Set theme -->
<button v-if="theme == 'dark'" class="btn btn-light me-2" @click="changeTheme('light')">
<font-awesome-icon icon="save" />
{{ $t("Switch to Light Theme") }}
</button>
<button v-if="theme == 'light'" class="btn btn-dark me-2" @click="changeTheme('dark')">
<font-awesome-icon icon="save" />
{{ $t("Switch to Dark Theme") }}
</button>
<button class="btn btn-secondary me-2" @click="changeTagsVisibilty(!tagsVisible)">
<template v-if="tagsVisible">
<font-awesome-icon icon="eye-slash" />
{{ $t("Hide Tags") }}
</template>
<template v-else>
<font-awesome-icon icon="eye" />
{{ $t("Show Tags") }}
</template>
</button>
</div>
</div>
<!-- Incident -->
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
<!-- Incident Date -->
<div class="date mt-3">
{{ $t("Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
<span v-if="incident.lastUpdatedDate">
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
<!-- Main Status Page -->
<div :class="{ edit: enableEditMode}" class="main">
<!-- Logo & Title -->
<h1 class="mb-4 title-flex">
<!-- Logo -->
<span class="logo-wrapper" @click="showImageCropUploadMethod">
<img :src="logoURL" alt class="logo me-2" :class="logoClass" @load="statusPageLogoLoaded" />
<font-awesome-icon v-if="enableEditMode" class="icon-upload" icon="upload" />
</span>
</div>
<div v-if="editMode" class="mt-3">
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
<font-awesome-icon icon="bullhorn" />
{{ $t("Post") }}
</button>
<!-- Uploader -->
<!-- url="/api/status-page/upload-logo" -->
<ImageCropUpload v-model="showImageCropUpload"
field="img"
:width="128"
:height="128"
:langType="$i18n.locale"
img-format="png"
:noCircle="true"
:noSquare="false"
@crop-success="cropSuccess"
/>
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
<font-awesome-icon icon="edit" />
{{ $t("Edit") }}
</button>
<!-- Title -->
<Editable v-model="config.title" tag="span" :contenteditable="editMode" :noNL="true" />
</h1>
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident">
<font-awesome-icon icon="times" />
{{ $t("Cancel") }}
</button>
<div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
<button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ $t("Style") }}: {{ $t(incident.style) }}
<!-- Admin functions -->
<div v-if="hasToken" class="mb-4">
<div v-if="!enableEditMode">
<button class="btn btn-info me-2" @click="edit">
<font-awesome-icon icon="edit" />
{{ $t("Edit Status Page") }}
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li>
</ul>
</div>
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
<font-awesome-icon icon="unlink" />
{{ $t("Unpin") }}
</button>
</div>
</div>
<!-- Overall Status -->
<div class="shadow-box list p-4 overall-status mb-4">
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
<font-awesome-icon icon="question-circle" class="ok" />
{{ $t("No Services") }}
</div>
<template v-else>
<div v-if="allUp">
<font-awesome-icon icon="check-circle" class="ok" />
{{ $t("All Systems Operational") }}
</div>
<div v-else-if="partialDown">
<font-awesome-icon icon="exclamation-circle" class="warning" />
{{ $t("Partially Degraded Service") }}
</div>
<div v-else-if="allDown">
<font-awesome-icon icon="times-circle" class="danger" />
{{ $t("Degraded Service") }}
<a href="/manage-status-page" class="btn btn-info">
<font-awesome-icon icon="tachometer-alt" />
{{ $t("Go to Dashboard") }}
</a>
</div>
<div v-else>
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
</div>
</template>
</div>
<!-- Description -->
<strong v-if="editMode">{{ $t("Description") }}:</strong>
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
<div v-if="editMode" class="mb-4">
<div>
<button class="btn btn-primary btn-add-group me-2" @click="addGroup">
<font-awesome-icon icon="plus" />
{{ $t("Add Group") }}
</button>
</div>
<div class="mt-3">
<div v-if="allMonitorList.length > 0 && loadedData">
<label>{{ $t("Add a monitor") }}:</label>
<select v-model="selectedMonitor" class="form-control">
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
</select>
</div>
<div v-else class="text-center">
{{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link>
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
<font-awesome-icon icon="bullhorn" />
{{ $t("Create Incident") }}
</button>
</div>
</div>
</div>
<div class="mb-4">
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
<!-- 👀 Nothing here, please add a group or a monitor. -->
👀 {{ $t("statusPageNothing") }}
<!-- Incident -->
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
<!-- Incident Date -->
<div class="date mt-3">
{{ $t("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>
</div>
<div v-if="editMode" class="mt-3">
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
<font-awesome-icon icon="bullhorn" />
{{ $t("Post") }}
</button>
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
<font-awesome-icon icon="edit" />
{{ $t("Edit") }}
</button>
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident">
<font-awesome-icon icon="times" />
{{ $t("Cancel") }}
</button>
<div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
<button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ $t("Style") }}: {{ $t(incident.style) }}
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li>
</ul>
</div>
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
<font-awesome-icon icon="unlink" />
{{ $t("Unpin") }}
</button>
</div>
</div>
<PublicGroupList :edit-mode="enableEditMode" />
<!-- Overall Status -->
<div class="shadow-box list p-4 overall-status mb-4">
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
<font-awesome-icon icon="question-circle" class="ok" />
{{ $t("No Services") }}
</div>
<template v-else>
<div v-if="allUp">
<font-awesome-icon icon="check-circle" class="ok" />
{{ $t("All Systems Operational") }}
</div>
<div v-else-if="partialDown">
<font-awesome-icon icon="exclamation-circle" class="warning" />
{{ $t("Partially Degraded Service") }}
</div>
<div v-else-if="allDown">
<font-awesome-icon icon="times-circle" class="danger" />
{{ $t("Degraded Service") }}
</div>
<div v-else>
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
</div>
</template>
</div>
<!-- Description -->
<strong v-if="editMode">{{ $t("Description") }}:</strong>
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
<div v-if="editMode" class="mb-4">
<div>
<button class="btn btn-primary btn-add-group me-2" @click="addGroup">
<font-awesome-icon icon="plus" />
{{ $t("Add Group") }}
</button>
</div>
<div class="mt-3">
<div v-if="allMonitorList.length > 0 && loadedData">
<label>{{ $t("Add a monitor") }}:</label>
<select v-model="selectedMonitor" class="form-control">
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
</select>
</div>
<div v-else class="text-center">
{{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link>
</div>
</div>
</div>
<div class="mb-4">
<div v-if="$root.publicGroupList.length === 0 && loadedData" class="text-center">
<!-- 👀 Nothing here, please add a group or a monitor. -->
👀 {{ $t("statusPageNothing") }}
</div>
<PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" />
</div>
<footer class="mt-5 mb-4">
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
</footer>
</div>
<footer class="mt-5 mb-4">
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
</footer>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
{{ $t("deleteStatusPageMsg") }}
</Confirm>
</div>
</template>
@@ -220,16 +244,25 @@ import ImageCropUpload from "vue-image-crop-upload";
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
import { useToast } from "vue-toastification";
import dayjs from "dayjs";
import Favico from "favico.js";
import { getResBaseURL } from "../util-frontend";
import Confirm from "../components/Confirm.vue";
const toast = useToast();
const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
let feedInterval;
const favicon = new Favico({
animation: "none"
});
export default {
components: {
PublicGroupList,
ImageCropUpload
ImageCropUpload,
Confirm,
},
// Leave Page for vue route change
@@ -247,6 +280,7 @@ export default {
data() {
return {
slug: null,
enableEditMode: false,
enableEditIncidentMode: false,
hasToken: false,
@@ -259,6 +293,8 @@ export default {
loadedTheme: false,
loadedData: false,
baseURL: "",
clickedEditButton: false,
domainNamesPlaceholder: "domain1.com\ndomain2.com\n..."
};
},
computed: {
@@ -296,15 +332,7 @@ export default {
},
isPublished() {
return this.config.statusPagePublished;
},
theme() {
return this.config.statusPageTheme;
},
tagsVisible() {
return this.config.statusPageTags
return this.config.published;
},
logoClass() {
@@ -378,13 +406,28 @@ export default {
},
// Set Theme
"config.statusPageTheme"() {
this.$root.statusPageTheme = this.config.statusPageTheme;
"config.theme"() {
this.$root.statusPageTheme = this.config.theme;
this.loadedTheme = true;
},
"config.title"(title) {
document.title = title;
},
"$root.monitorList"() {
let count = Object.keys(this.$root.monitorList).length;
// Since publicGroupList is getting from public rest api, monitors' tags may not present if showTags = false
if (count > 0) {
for (let group of this.$root.publicGroupList) {
for (let monitor of group.monitorList) {
if (monitor.tags === undefined && this.$root.monitorList[monitor.id]) {
monitor.tags = this.$root.monitorList[monitor.id].tags;
}
}
}
}
}
},
@@ -403,28 +446,24 @@ export default {
});
// Special handle for dev
const env = process.env.NODE_ENV;
if (env === "development" || localStorage.dev === "dev") {
this.baseURL = location.protocol + "//" + location.hostname + ":3001";
}
this.baseURL = getResBaseURL();
},
async mounted() {
axios.get("/api/status-page/config").then((res) => {
this.config = res.data;
this.slug = this.$route.params.slug;
if (this.config.logo) {
this.imgDataUrl = this.config.logo;
if (!this.slug) {
this.slug = "default";
}
axios.get("/api/status-page/" + this.slug).then((res) => {
this.config = res.data.config;
if (this.config.icon) {
this.imgDataUrl = this.config.icon;
}
});
axios.get("/api/status-page/incident").then((res) => {
if (res.data.ok) {
this.incident = res.data.incident;
}
});
axios.get("/api/status-page/monitor-list").then((res) => {
this.$root.publicGroupList = res.data;
this.incident = res.data.incident;
this.$root.publicGroupList = res.data.publicGroupList;
});
// 5mins a loop
@@ -432,31 +471,87 @@ export default {
feedInterval = setInterval(() => {
this.updateHeartbeatList();
}, (300 + 10) * 1000);
// Go to edit page if ?edit present
// null means ?edit present, but no value
if (this.$route.query.edit || this.$route.query.edit === null) {
this.edit();
}
},
methods: {
updateHeartbeatList() {
// If editMode, it will use the data from websocket.
if (! this.editMode) {
axios.get("/api/status-page/heartbeat").then((res) => {
this.$root.heartbeatList = res.data.heartbeatList;
this.$root.uptimeList = res.data.uptimeList;
axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
const { heartbeatList, uptimeList } = res.data;
this.$root.heartbeatList = heartbeatList;
this.$root.uptimeList = uptimeList;
const heartbeatIds = Object.keys(heartbeatList);
const downMonitors = heartbeatIds.reduce((downMonitorsAmount, currentId) => {
const monitorHeartbeats = heartbeatList[currentId];
const lastHeartbeat = monitorHeartbeats.at(-1);
if (lastHeartbeat) {
return lastHeartbeat.status === 0 ? downMonitorsAmount + 1 : downMonitorsAmount;
} else {
return downMonitorsAmount;
}
}, 0);
favicon.badge(downMonitors);
this.loadedData = true;
});
}
},
edit() {
this.$root.initSocketIO(true);
this.enableEditMode = true;
if (this.hasToken) {
this.$root.initSocketIO(true);
this.enableEditMode = true;
this.clickedEditButton = true;
}
},
save() {
this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
let startTime = new Date();
this.config.slug = this.config.slug.trim().toLowerCase();
this.$root.getSocket().emit("saveStatusPage", this.slug, this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
if (res.ok) {
this.enableEditMode = false;
this.$root.publicGroupList = res.publicGroupList;
location.reload();
// Add some delay, so that the side menu animation would be better
let endTime = new Date();
let time = 100 - (endTime - startTime) / 1000;
if (time < 0) {
time = 0;
}
setTimeout(() => {
location.href = "/status/" + this.config.slug;
}, time);
} else {
toast.error(res.msg);
}
});
},
deleteDialog() {
this.$refs.confirmDelete.show();
},
deleteStatusPage() {
this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
if (res.ok) {
this.enableEditMode = false;
location.href = "/manage-status-page";
} else {
toast.error(res.msg);
}
@@ -481,30 +576,7 @@ export default {
},
discard() {
location.reload();
},
changeTheme(name) {
this.config.statusPageTheme = name;
},
changeTagsVisibilty(newState) {
this.config.statusPageTags = newState;
// On load, the status page will not include tags if it's not enabled for security reasons
// Which means if we enable tags, it won't show in the UI until saved
// So we have this to enhance UX and load in the tags from the authenticated source instantly
this.$root.publicGroupList = this.$root.publicGroupList.map((group) => {
return {
...group,
monitorList: group.monitorList.map((monitor) => {
// We only include the tags if visible so we can reuse the logic to hide the tags on disable
return {
...monitor,
tags: newState ? this.$root.monitorList[monitor.id].tags : []
}
})
}
});
location.href = "/status/" + this.slug;
},
/**
@@ -520,6 +592,11 @@ export default {
}
},
statusPageLogoLoaded(eventPayload) {
// Remark: may not work in dev, due to cros
favicon.image(eventPayload.target);
},
createIncident() {
this.enableEditIncidentMode = true;
@@ -540,7 +617,7 @@ export default {
return;
}
this.$root.getSocket().emit("postIncident", this.incident, (res) => {
this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => {
if (res.ok) {
this.enableEditIncidentMode = false;
@@ -571,7 +648,7 @@ export default {
},
unpinIncident() {
this.$root.getSocket().emit("unpinIncident", () => {
this.$root.getSocket().emit("unpinIncident", this.slug, () => {
this.incident = null;
});
},
@@ -614,6 +691,40 @@ h1 {
}
}
.main {
transition: all ease-in-out 0.1s;
&.edit {
margin-left: 300px;
}
}
.sidebar {
position: fixed;
left: 0;
top: 0;
width: 300px;
height: 100vh;
padding: 15px 15px 68px 15px;
overflow-x: hidden;
overflow-y: auto;
border-right: 1px solid #ededed;
.danger-zone {
border-top: 1px solid #ededed;
padding-top: 15px;
}
.sidebar-footer {
width: 100%;
bottom: 0;
left: 0;
padding: 15px;
position: absolute;
border-top: 1px solid #ededed;
}
}
footer {
text-align: center;
font-size: 14px;
@@ -623,6 +734,12 @@ footer {
min-width: 50px;
}
.title-flex {
display: flex;
align-items: center;
gap: 10px;
}
.logo-wrapper {
display: inline-block;
position: relative;
@@ -681,4 +798,19 @@ footer {
}
}
.dark {
.sidebar {
background-color: $dark-header-bg;
border-right-color: $dark-border-color;
.danger-zone {
border-top-color: $dark-border-color;
}
.sidebar-footer {
border-top-color: $dark-border-color;
}
}
}
</style>

View File

@@ -14,11 +14,15 @@ 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";
import AddStatusPage from "./pages/AddStatusPage.vue";
import NotFound from "./pages/NotFound.vue";
const routes = [
{
@@ -81,6 +85,10 @@ const routes = [
path: "notifications",
component: Notifications,
},
{
path: "reverse-proxy",
component: ReverseProxy,
},
{
path: "monitor-history",
component: MonitorHistory,
@@ -103,6 +111,14 @@ const routes = [
},
]
},
{
path: "/manage-status-page",
component: ManageStatusPage,
},
{
path: "/add-status-page",
component: AddStatusPage,
},
],
},
],
@@ -119,6 +135,14 @@ const routes = [
path: "/status",
component: StatusPage,
},
{
path: "/status/:slug",
component: StatusPage,
},
{
path: "/:pathMatch(.*)*",
component: NotFound,
},
];
export const router = createRouter({

View File

@@ -51,7 +51,19 @@ export function timezoneList() {
}
export function setPageLocale() {
const html = document.documentElement
html.setAttribute('lang', currentLocale() )
html.setAttribute('dir', localeDirection() )
const html = document.documentElement;
html.setAttribute("lang", currentLocale() );
html.setAttribute("dir", localeDirection() );
}
/**
* Mainly used for dev, because the backend and the frontend are in different ports.
*/
export function getResBaseURL() {
const env = process.env.NODE_ENV;
if (env === "development" || localStorage.dev === "dev") {
return location.protocol + "//" + location.hostname + ":3001";
} else {
return "";
}
}