Merge branch 'louislam:master' into master

This commit is contained in:
Mohit Nagaraj 2025-03-16 22:06:30 +05:30 committed by GitHub
commit e2d011d38f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
129 changed files with 8608 additions and 5843 deletions

View file

@ -32,7 +32,6 @@ tsconfig.json
/extra/healthcheck.exe
/extra/healthcheck
/extra/exe-builder
/extra/push-examples
/extra/uptime-kuma-push
# Comment the following line if you want to rebuild the healthcheck binary

2
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,2 @@
---
blank_issues_enabled: false

View file

@ -1,17 +0,0 @@
---
name: "Security Issue"
about: "Just for alerting @louislam, do not provide any details here"
title: "Security Issue"
ref: "main"
labels:
- security
---
DO NOT PROVIDE ANY DETAILS HERE. Please privately report to https://github.com/louislam/uptime-kuma/security/advisories/new.
Why need this issue? It is because GitHub Advisory do not send a notification to @louislam, it is a workaround to do so.
Your GitHub Advisory URL:

View file

@ -0,0 +1,45 @@
---
name: "🛡️ Security Issue"
description: |
Notify Louis Lam about a security concern. Please do NOT include any sensitive details in this issue.
# title: "Security Issue"
labels: [security]
assignees: [louislam]
body:
- type: "markdown"
attributes:
value: |
## **⚠️ Report a Security Vulnerability**
### **IMPORTANT: DO NOT SHARE VULNERABILITY DETAILS HERE**
If you have discovered a security vulnerability, please report it securely using the GitHub Security Advisory.
**Note**: This issue is only for notifying the maintainers of the repository, as the GitHub Security Advisory does not automatically send notifications.
- **Confidentiality**: The information you provide in the GitHub Security Advisory will initially remain confidential. However, once the vulnerability is addressed, the advisory will be publicly disclosed on GitHub.
- **Access and Visibility**: Until the advisory is published, it will only be visible to the maintainers of the repository and invited collaborators.
- **Credit**: You will be automatically credited as a contributor for identifying and reporting the vulnerability. Your contribution will be reflected in the MITRE Credit System.
- **Important Reminder**: **Do not include any sensitive or detailed vulnerability information in this issue.** This issue is only for sharing the advisory URL to notify the maintainers of the repository, not for discussing the vulnerability itself.
**Thank you for helping us keep Uptime Kuma secure!**
## **Step 1: Submit a GitHub Security Advisory**
Right-click the link below and select `Open link in new tab` to access the page. This will keep the security issue open, allowing you to easily return and paste the Advisory URL here later.
➡️ [Create a New Security Advisory](https://github.com/louislam/uptime-kuma/security/advisories/new)
## **Step 2: Share the Advisory URL**
Once you've created your advisory, please share the URL below. This will notify Louis Lam and enable them to take the appropriate action.
- type: "textarea"
id: github-advisory-url
validations:
required: true
attributes:
label: "GitHub Advisory URL for @louislam"
placeholder: |
Please paste the GitHub Advisory URL here. Only the URL is required.
Example: https://github.com/louislam/uptime-kuma/security/advisories/GHSA-8h5r-7t6l-q3kz

View file

@ -78,7 +78,7 @@ jobs:
e2e-test:
needs: [ ]
runs-on: ARM64
runs-on: ubuntu-24.04-arm
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v4

View file

@ -1,4 +1,4 @@
name: json-yaml-validate
name: validate
on:
push:
branches:
@ -25,3 +25,19 @@ jobs:
with:
comment: "true" # enable comment mode
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
# General validations
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
- name: Validate language JSON files
run: node ./extra/check-lang-json.js
- name: Validate knex migrations filename
run: node ./extra/check-knex-filenames.mjs

View file

@ -62,6 +62,7 @@ Requirements:
- Platform
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
- ❌ FreeBSD / OpenBSD / NetBSD
- ❌ Replit / Heroku
- [Node.js](https://nodejs.org/en/download/) 18 / 20.4
- [npm](https://docs.npmjs.com/cli/) 9

View file

@ -0,0 +1,7 @@
exports.up = function (knex) {
return knex("monitor").whereNull("json_path_operator").update("json_path_operator", "==");
};
exports.down = function (knex) {
// changing the json_path_operator back to null for all "==" is not possible anymore
// we have lost the context which fields have been set explicitely in >= v2.0 and which would need to be reverted
};

View file

@ -0,0 +1,13 @@
// Update info_json column to LONGTEXT mainly for MariaDB
exports.up = function (knex) {
return knex.schema
.alterTable("monitor_tls_info", function (table) {
table.text("info_json", "longtext").alter();
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor_tls_info", function (table) {
table.text("info_json", "text").alter();
});
};

View file

@ -1,3 +1,13 @@
# Download Apprise deb package
FROM node:20-bookworm-slim AS download-apprise
WORKDIR /app
COPY ./extra/download-apprise.mjs ./download-apprise.mjs
RUN apt update && \
apt --yes --no-install-recommends install curl && \
npm install cheerio semver && \
node ./download-apprise.mjs
# Base Image (Slim)
# If the image changed, the second stage image should be changed too
FROM node:20-bookworm-slim AS base2-slim
ARG TARGETPLATFORM
@ -27,8 +37,9 @@ RUN apt update && \
# apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867)
# Switching to testing repo is no longer working, as the testing repo is not bookworm anymore.
# python3-paho-mqtt (#4859)
RUN curl http://ftp.debian.org/debian/pool/main/a/apprise/apprise_1.8.0-2_all.deb --output apprise.deb && \
apt update && \
# TODO: no idea how to delete the deb file after installation as it becomes a layer already
COPY --from=download-apprise /app/apprise.deb ./apprise.deb
RUN apt update && \
apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \
rm -rf /var/lib/apt/lists/* && \
rm -f apprise.deb && \

View file

@ -27,7 +27,6 @@ RUN mkdir ./data
# ⭐ Main Image
############################################
FROM $BASE_IMAGE AS release
USER node
WORKDIR /app
LABEL org.opencontainers.image.source="https://github.com/louislam/uptime-kuma"
@ -46,6 +45,7 @@ CMD ["node", "server/server.js"]
# Rootless Image
############################################
FROM release AS rootless
USER node
############################################
# Mark as Nightly

View file

@ -5,7 +5,7 @@ const util = require("../../src/util");
util.polyfill();
const version = process.env.VERSION;
const version = process.env.RELEASE_BETA_VERSION;
console.log("Beta Version: " + version);

View file

@ -0,0 +1,72 @@
import fs from "fs";
const dir = "./db/knex_migrations";
// Get the file list (ending with .js) from the directory
const files = fs.readdirSync(dir).filter((file) => file !== "README.md");
// They are wrong, but they had been merged, so allowed.
const exceptionList = [
"2024-08-24-000-add-cache-bust.js",
"2024-10-1315-rabbitmq-monitor.js",
];
// Correct format: YYYY-MM-DD-HHmm-description.js
for (const file of files) {
if (exceptionList.includes(file)) {
continue;
}
// Check ending with .js
if (!file.endsWith(".js")) {
console.error(`It should end with .js: ${file}`);
process.exit(1);
}
const parts = file.split("-");
// Should be at least 5 parts
if (parts.length < 5) {
console.error(`Invalid format: ${file}`);
process.exit(1);
}
// First part should be a year >= 2024
const year = parseInt(parts[0], 10);
if (isNaN(year) || year < 2023) {
console.error(`Invalid year: ${file}`);
process.exit(1);
}
// Second part should be a month
const month = parseInt(parts[1], 10);
if (isNaN(month) || month < 1 || month > 12) {
console.error(`Invalid month: ${file}`);
process.exit(1);
}
// Third part should be a day
const day = parseInt(parts[2], 10);
if (isNaN(day) || day < 1 || day > 31) {
console.error(`Invalid day: ${file}`);
process.exit(1);
}
// Fourth part should be HHmm
const time = parts[3];
// Check length is 4
if (time.length !== 4) {
console.error(`Invalid time: ${file}`);
process.exit(1);
}
const hour = parseInt(time.substring(0, 2), 10);
const minute = parseInt(time.substring(2), 10);
if (isNaN(hour) || hour < 0 || hour > 23 || isNaN(minute) || minute < 0 || minute > 59) {
console.error(`Invalid time: ${file}`);
process.exit(1);
}
}
console.log("All knex filenames are correct.");

27
extra/check-lang-json.js Normal file
View file

@ -0,0 +1,27 @@
// For #5231
const fs = require("fs");
let path = "./src/lang";
// list directories in the lang directory
let jsonFileList = fs.readdirSync(path);
for (let jsonFile of jsonFileList) {
if (!jsonFile.endsWith(".json")) {
continue;
}
let jsonPath = path + "/" + jsonFile;
let originalContent = fs.readFileSync(jsonPath, "utf8");
let langData = JSON.parse(originalContent);
let formattedContent = JSON.stringify(langData, null, 4) + "\n";
if (originalContent !== formattedContent) {
console.error(`File ${jsonFile} is not formatted correctly.`);
process.exit(1);
}
}
console.log("All lang json files are formatted correctly.");

View file

@ -0,0 +1,57 @@
// Go to http://ftp.debian.org/debian/pool/main/a/apprise/ using fetch api, where it is a apache directory listing page
// Use cheerio to parse the html and get the latest version of Apprise
// call curl to download the latest version of Apprise
// Target file: the latest version of Apprise, which the format is apprise_{VERSION}_all.deb
import * as cheerio from "cheerio";
import semver from "semver";
import * as childProcess from "child_process";
const baseURL = "http://ftp.debian.org/debian/pool/main/a/apprise/";
const response = await fetch(baseURL);
if (!response.ok) {
throw new Error("Failed to fetch page of Apprise Debian repository.");
}
const html = await response.text();
const $ = cheerio.load(html);
// Get all the links in the page
const linkElements = $("a");
// Filter the links which match apprise_{VERSION}_all.deb
const links = [];
const pattern = /apprise_(.*?)_all.deb/;
for (let i = 0; i < linkElements.length; i++) {
const link = linkElements[i];
if (link.attribs.href.match(pattern) && !link.attribs.href.includes("~")) {
links.push({
filename: link.attribs.href,
version: link.attribs.href.match(pattern)[1],
});
}
}
console.log(links);
// semver compare and download
let latestLink = {
filename: "",
version: "0.0.0",
};
for (const link of links) {
if (semver.gt(link.version, latestLink.version)) {
latestLink = link;
}
}
const downloadURL = baseURL + latestLink.filename;
console.log(`Downloading ${downloadURL}...`);
let result = childProcess.spawnSync("curl", [ downloadURL, "--output", "apprise.deb" ]);
console.log(result.stdout?.toString());
console.error(result.stderr?.toString());
process.exit(result.status !== null ? result.status : 1);

View file

@ -1,19 +0,0 @@
#!/usr/bin/env node
const childProcess = require("child_process");
let env = process.env;
let cmd = process.argv[2];
let args = process.argv.slice(3);
let replacedArgs = [];
for (let arg of args) {
for (let key in env) {
arg = arg.replaceAll(`$${key}`, env[key]);
}
replacedArgs.push(arg);
}
let child = childProcess.spawn(cmd, replacedArgs);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

View file

@ -1 +0,0 @@
packages/

View file

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reflection" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.1" newVersion="4.1.1.1" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime.InteropServices" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View file

@ -1,84 +0,0 @@
using System.ComponentModel;
namespace UptimeKuma {
partial class DownloadForm {
/// <summary>
/// Required designer variable.
/// </summary>
private IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DownloadForm));
this.progressBar = new System.Windows.Forms.ProgressBar();
this.label = new System.Windows.Forms.Label();
this.labelData = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// progressBar
//
this.progressBar.Location = new System.Drawing.Point(12, 12);
this.progressBar.Name = "progressBar";
this.progressBar.Size = new System.Drawing.Size(472, 41);
this.progressBar.TabIndex = 0;
//
// label
//
this.label.Location = new System.Drawing.Point(12, 59);
this.label.Name = "label";
this.label.Size = new System.Drawing.Size(472, 23);
this.label.TabIndex = 1;
this.label.Text = "Preparing...";
//
// labelData
//
this.labelData.Location = new System.Drawing.Point(12, 82);
this.labelData.Name = "labelData";
this.labelData.Size = new System.Drawing.Size(472, 23);
this.labelData.TabIndex = 2;
//
// DownloadForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(496, 117);
this.Controls.Add(this.labelData);
this.Controls.Add(this.label);
this.Controls.Add(this.progressBar);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.Name = "DownloadForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Uptime Kuma";
this.Load += new System.EventHandler(this.DownloadForm_Load);
this.ResumeLayout(false);
}
private System.Windows.Forms.Label labelData;
private System.Windows.Forms.Label label;
private System.Windows.Forms.ProgressBar progressBar;
#endregion
}
}

View file

@ -1,204 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
namespace UptimeKuma {
public partial class DownloadForm : Form {
private readonly Queue<DownloadItem> downloadQueue = new();
private readonly WebClient webClient = new();
private DownloadItem currentDownloadItem;
public DownloadForm() {
InitializeComponent();
}
private void DownloadForm_Load(object sender, EventArgs e) {
webClient.DownloadProgressChanged += DownloadProgressChanged;
webClient.DownloadFileCompleted += DownloadFileCompleted;
label.Text = "Reading latest version...";
// Read json from https://uptime.kuma.pet/version
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
var versionObj = JsonConvert.DeserializeObject<Version>(versionJson);
var nodeVersion = versionObj.nodejs;
var uptimeKumaVersion = versionObj.latest;
var hasUpdateFile = File.Exists("update");
if (!Directory.Exists("node")) {
downloadQueue.Enqueue(new DownloadItem {
URL = $"https://nodejs.org/dist/v{nodeVersion}/node-v{nodeVersion}-win-x64.zip",
Filename = "node.zip",
TargetFolder = "node"
});
}
if (!Directory.Exists("core") || hasUpdateFile) {
// It is update, rename the core folder to core.old
if (Directory.Exists("core")) {
// Remove the old core.old folder
if (Directory.Exists("core.old")) {
Directory.Delete("core.old", true);
}
Directory.Move("core", "core.old");
}
downloadQueue.Enqueue(new DownloadItem {
URL = $"https://github.com/louislam/uptime-kuma/archive/refs/tags/{uptimeKumaVersion}.zip",
Filename = "core.zip",
TargetFolder = "core"
});
File.WriteAllText("version.json", versionJson);
// Delete the update file
if (hasUpdateFile) {
File.Delete("update");
}
}
DownloadNextFile();
}
void DownloadNextFile() {
if (downloadQueue.Count > 0) {
var item = downloadQueue.Dequeue();
currentDownloadItem = item;
// Download if the zip file is not existing
if (!File.Exists(item.Filename)) {
label.Text = item.URL;
webClient.DownloadFileAsync(new Uri(item.URL), item.Filename);
} else {
progressBar.Value = 100;
label.Text = "Use local " + item.Filename;
DownloadFileCompleted(null, null);
}
} else {
npmSetup();
}
}
void npmSetup() {
labelData.Text = "";
var npm = "..\\node\\npm.cmd";
var cmd = $"{npm} ci --production & {npm} run download-dist & exit";
var startInfo = new ProcessStartInfo {
FileName = "cmd.exe",
Arguments = $"/k \"{cmd}\"",
RedirectStandardOutput = false,
RedirectStandardError = false,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = false,
WorkingDirectory = "core"
};
var process = new Process();
process.StartInfo = startInfo;
process.EnableRaisingEvents = true;
process.Exited += (_, e) => {
progressBar.Value = 100;
if (process.ExitCode == 0) {
Task.Delay(2000).ContinueWith(_ => {
Application.Restart();
});
label.Text = "Done";
} else {
label.Text = "Failed, exit code: " + process.ExitCode;
}
};
process.Start();
label.Text = "Installing dependencies and download dist files";
progressBar.Value = 50;
process.WaitForExit();
}
void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) {
progressBar.Value = e.ProgressPercentage;
var total = e.TotalBytesToReceive / 1024;
var current = e.BytesReceived / 1024;
if (total > 0) {
labelData.Text = $"{current}KB/{total}KB";
}
}
void DownloadFileCompleted(object sender, AsyncCompletedEventArgs e) {
Extract(currentDownloadItem);
DownloadNextFile();
}
void Extract(DownloadItem item) {
if (Directory.Exists(item.TargetFolder)) {
var dir = new DirectoryInfo(item.TargetFolder);
dir.Delete(true);
}
if (Directory.Exists("temp")) {
var dir = new DirectoryInfo("temp");
dir.Delete(true);
}
labelData.Text = $"Extracting {item.Filename}...";
ZipFile.ExtractToDirectory(item.Filename, "temp");
string[] dirList;
// Move to the correct level
dirList = Directory.GetDirectories("temp");
if (dirList.Length > 0) {
var dir = dirList[0];
// As sometime ExtractToDirectory is still locking the directory, loop until ok
while (true) {
try {
Directory.Move(dir, item.TargetFolder);
break;
} catch (Exception exception) {
Thread.Sleep(1000);
}
}
} else {
MessageBox.Show("Unexcepted Error: Cannot move extracted files, folder not found.");
}
labelData.Text = $"Extracted";
if (Directory.Exists("temp")) {
var dir = new DirectoryInfo("temp");
dir.Delete(true);
}
File.Delete(item.Filename);
}
}
public class DownloadItem {
public string URL { get; set; }
public string Filename { get; set; }
public string TargetFolder { get; set; }
}
}

View file

@ -1,377 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAA
AABgAAAAAQAgAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA////BPT09Bfu7u4e8fHxJPPz8yv19fUy9fX1M/Pz8yvx8fEk9vb2HPPz8xXMzMwFAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//
/wHv7+8f7u7uPPPz81Tx8fFs8fHxgPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGB8fHxcfHx8V3x8fFI9PT0MOvr6w0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AADy8vIU8fHxS/Dw8Hbx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fFr9PT0R/Dw8CIAAAABAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA8vLyFPHx8Vnx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fFs9fX1Mb+/vwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAICAgALy8vI88fHxfvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy8nby8vI8gICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAzMzMBfHx8Vrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyYf///wwAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8vLyYPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8W/z8/MWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+9R8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLw8PB26urqDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLy8ijx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu7w7Ifj79ud2u7PtNLrw83P677dzeu85c3r
u+rM67rwzOu68c7rverQ68Dj0uvD3NbuyM3b7c+64u7apujv5ZPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxXgAAAAEAAAAAAAAAAAAAAAAAAAAA4+PjCfDw
8Hfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLd7tSmzeu92MbqsvvG6bH/xumy/8fq
s//H6rP/yOq0/8jqtf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//Q7MDx1u7Kz9/t
163s8OuJ8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu/v7y8AAAAAAAAAAAAA
AAAAAAAA7u7uPfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC5PDdl8jqtuTE6a7/xOmv/8Xp
sP/G6bH/xumx/8bpsv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zr
u//N67v/zey8/87svf/P67742e3Mx+jv5ZLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw
8HWAgIACAAAAAAAAAACqqqoD8vLyc/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLf7degxOiu+cPo
rf/D6a7/xOmu/8Xpr//F6bD/xumx/8bpsf/G6bL/x+qz/8fqs//I6rT/yOq1/8nqtv/J6rb/yuu3/8rr
uP/L67j/y+u5/8zruv/M67v/zeu7/83svP/O7L3/zuy9/87svfzc7tK28fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fEkAAAAAAAAAADz8/Mq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgunv
5o3D6a/0wuis/8Lorf/D6K3/xOmu/8Tprv/F6a//xemw/8bpsf/G6bH/xumy/8fqs//H6rP/yOq0/8jq
tf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/87svf/O7L3/3e/TtPHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAADy8vJM8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgszqutDB6Kv/weir/8LorP/D6K3/w+it/8Tprv/E6a7/xemv/8XpsP/G6bH/xumx/8bp
sv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zru//N67v/zey8/87s
vf/O7L3/zuy++u3w6Yzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJ1AAAAAAAAAADx8fFr8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC6O/kjsDoqvzA6Kr/weir/8Loq//C6Kz/w+it/8Porf/E6a7/xOmu/8Xp
r//F6bD/xumx/8bpsf/G6bL/x+qz/8fqtP/I6rT/yOq1/8nqtv/J6rb/yuu3/8rruP/L67n/y+u5/8zr
uv/M67v/zeu7/83svP/O7L3/zuy9/93u07Xx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC////Bv//
/wfx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1ezJsr/nqf/A56n/weiq/8Hoq//C6Kv/wuis/8Po
rf/D6K3/xOmu/8Pprv+856T/uOed/7bmmv+05Zf/teWZ/7jnnf+86KP/wOio/8fqs//J6rb/yeq2/8rr
t//K67j/y+u5/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/9buyNLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8vLyE/Ly8hPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCy+q6zr/nqP/A56n/wOep/8Ho
qv/B6Kv/wuir/8LorP+u5Y//neF2/5bgav+V4Gr/luBr/5fhbP+Y4W7/meFv/5rhcf+b4nL/nOJ0/53i
dv+j5H//reaM/7nnnf/E6q//y+y4/8vruf/L67n/zOu6/8zru//N67v/zey8/9Lsxd/x8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC7+/vIPb29hzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/n
qP+/56j/wOep/8Dnqf/B6Kr/weir/7nmn/+R32T/kt9l/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nh
b/+a4XH/m+Jy/5zidP+d4nX/nuN3/5/jeP+f4nn/weqq/8rruP/L67n/y+u5/8zruv/M67v/zeu7/9Ls
w+Lx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwI/Hx8SXx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGCxeix5L/nqP+/56j/v+eo/8Dnqf/A56n/weiq/7Pllv+Q3mP/kd9k/5LfZf+T32f/lOBo/5Xg
av+W4Gv/l+Ft/5jhbv+Z4W//muFx/5vicv+c4nT/neJ1/57jd/+f43j/xOmu/8rrt//K67j/y+u5/8vr
uf/M67r/zOu7/9Tsxtfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC9PT0GO/v7yDx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGCx+m037/nqP+/56j/v+eo/7/nqP/A56n/wOip/7TmmP+P3mH/kN5j/5Hf
ZP+S32b/k99n/5TgaP+V4Gr/luBr/5fhbf+Y4W7/meFw/5rhcf+b4nL/nOJ0/53idf+h5Hz/yuu2/8nq
t//K67f/yuu4/8vruf/L67n/zOu6/9ftysrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7e3tDvT0
9Bfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCyOq117/nqP+/56j/v+eo/7/nqP+/56j/wOep/7vn
of+O3mD/j95h/5DeY/+R32T/kt9m/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nhcP+a4nH/m+Jy/5zi
dP+r5Yr/yOq1/8nqtv/J6rf/yuu3/8rruP/L67n/y+u5/9zu1LHx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLz8/OA////A+7u7g/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCz+q+xb/nqP+/56j/v+eo/7/n
qP+/56j/v+eo/8Dnqf+S4Gb/jt5g/4/eYf+Q3mP/kd9k/5LfZv+T32f/lOBo/5Xgav+W4Gv/l+Ft/5jh
bv+Z4XD/muJx/5vic/+4553/yOq0/8jqtf/J6rb/yeq3/8rrt//K67j/y+u5/+bw4Zfx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fFrAAAAAP///wHz8/N88fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1+zMrr/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+f4Xn/jd5f/47eYP+P3mH/kN5j/5HfZP+S32b/k99n/5Tg
af+V4Gr/luBr/5fhbf+Y4W7/meFw/5vic//F6rD/x+q0/8jqtP/I6rX/yeq2/8nqt//K67f/zOu88u/x
74Px8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLv7+9QAAAAAAAAAADw8PBm8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC5e7gk7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//jN1d/43eX/+O3mD/j95h/5De
Y/+R32T/kt9m/5PfZ/+U4Gn/leBq/5bga/+X4W3/mOFu/6rliP/G6rL/x+qz/8fqtP/I6rT/yOq1/8nq
tv/J6rf/1OzGy/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YL19fUzAAAAAAAAAADy8vJO8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgsPoru2/56j/v+eo/7/nqP+/56j/v+eo/7/nqP++6Kf/j95i/4zd
Xf+N3l//jt5g/4/eYv+Q3mP/kd9k/5LfZv+T32f/lOBp/5Xgav+W4Gz/l+Ft/7voov/G6bL/xuqy/8fq
s//H6rT/yOq1/8jqtf/J6rb/4e/Zo/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PARAAAAAAAA
AADu7u4u8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgszpvMm/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/q+SL/4vdXP+M3V3/jd5f/47eYP+P3mL/kN9j/5HfZP+S32b/k99n/5Tgaf+V4Gr/qOOH/8Xp
sP/G6bH/xumy/8bqsv/H6rP/x+q0/8jqtf/K67jy8PHwhPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8WoAAAAAAAAAAAAAAADo6OgL8fHxgfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguDv2J2/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/6Xjgv+L3Vz/jN1d/43eX/+O3mD/j95i/5DfY/+R32T/kt9m/5Pf
Z/+k44D/xOmu/8XpsP/F6bD/xumx/8bpsv/G6rL/x+qz/8fqtP/W7cnB8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvPz80AAAAAAAAAAAAAAAAAAAAAA8PDwZ/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLD6K/rv+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//kt5n/4zdXf+N3l//jt5g/4/e
Yv+Q32P/luFs/67kj//D6K3/xOmu/8Tpr//F6bD/xemw/8bpsf/G6bL/xuqy/8fqtP7o7+WR8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xYAAAAAAAAAAAAAAAAAAAAA8vLyPPHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLV7ci0v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOio/7Xl
mv+u5I7/rOSM/67kj/+35pz/wumr/8Lorf/D6K3/w+it/8Tprv/E6a//xemw/8XpsP/G6bH/xumy/9Ds
wNPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyZQAAAAAAAAAAAAAAAAAAAAAAAAAA////DPHx
8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/wOep/8Doqv/B6Kr/weir/8LorP/C6K3/w+it/8Porv/E6a7/xOmv/8Xp
sP/F6bD/yOq18uvw6Yvx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7+/vMQAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAPHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6O/ij8LorPG/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weiq/8Hoq//C6Kz/wuit/8Po
rf/D6K7/xOmu/8Tpr//F6bH74u/anvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PB6////BQAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPPz8yrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguHu
2pnB56v2v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/wOiq/8Ho
q//B6Kv/wuis/8Lorf/D6K3/w+mu/8Tprv3b7dKq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fFJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHy8vJf8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLi7tyXwumt8L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/wOep/8Doqv/B6Kv/weir/8LorP/C6K3/xOiv+d7u1aTx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvLy8nb///8KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+8Q8/Pze/Hx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6/Dpiszqu82/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weir/8Hoq//H6bTj5e7elfHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8yoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA9fX1MvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLe7tShx+mz3r/n
qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/xumy5drtz6rv8e+D8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyTgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPHx8Unx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgubv45DU68e2y+q6z8XoseTD6a7uweir9MPpru7F6bHly+q50tLsxLrl796U8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJh////AwAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wHx8fFZ8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8Wzf398IAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8D8/PzVfHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwZujo
6AsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA////AfHx8Ujx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fFa////BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/Mp8vLydvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8/PzfPHx8TcAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CvLy8lDz8/N/8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvPz84Hx8fFa8PDwEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AADw8PAR8vLyTvHx8X3x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fF/8/PzVvT09BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wXz8/Mq8/PzU/Hx8XDx8fGB8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLy8vJz8fHxWO/v7y////8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8G7e3tHfLy
8ifu7u4u8PDwNPT09C/y8vIo7+/vH+Pj4wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAP///////wAA////////AAD///////8AAP//gAf//wAA//gAAD//AAD/wAAAB/8AAP+A
AAAB/wAA/gAAAAB/AAD8AAAAAD8AAPgAAAAAHwAA8AAAAAAPAADwAAAAAAcAAOAAAAAABwAA4AAAAAAD
AADAAAAAAAMAAMAAAAAAAwAAwAAAAAABAACAAAAAAAEAAIAAAAAAAQAAgAAAAAABAACAAAAAAAEAAIAA
AAAAAQAAgAAAAAABAACAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAAABwAAwAAAAAAH
AADgAAAAAAcAAOAAAAAADwAA4AAAAAAPAADwAAAAAB8AAPAAAAAAHwAA+AAAAAA/AAD8AAAAAD8AAPwA
AAAAfwAA/gAAAAD/AAD/AAAAAf8AAP+AAAAD/wAA/8AAAAf/AAD/8AAAH/8AAP/8AAA//wAA//8AAf//
AAD//+AP//8AAP///////wAA////////AAD///////8AACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAgICAAu/v7xD09PQX7u7uHvDw8CP29vYb8vLyFOrq6gwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICA
gALy8vIm7+/vT/Pz82fz8/N98fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw8Hrw8PBm7+/vUPT0
9C3o6OgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOPj
4wnz8/NC8vLydPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YHy8vJj8/PzKoCAgAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AADx8fEl8vLydfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxcfHx8SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA9PT0LfHx8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8/PzgPLy8j0AAAABAAAAAAAA
AAAAAAAAAAAAAO3t7Rzx8fGA8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLr8OmM5O7emeTv
3Z7h79mj5fDem+nv45Tu8u6H8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy
8joAAAAAAAAAAAAAAAD///8E8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC7vDshtns0K7N67zayeq288fq
s//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/P7L7w0+zF29vv0Lrn8OKX8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8/PzfvPz8xUAAAAAAAAAAPX19TLx8fGC8fHxgvHx8YLx8fGC8fHxgt3u1KXF6rHzxOmv/8Xp
sP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/M67v/zey8/87svf/S7MPj4u7Zp/Hx
8YLx8fGC8fHxgvHx8YLx8fGC8/PzVQAAAAAAAAAA8fHxavHx8YLx8fGC8fHxgvHx8YLf7defwuis/cPo
rf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruv/M67v/zey8/87s
vf/N67z/3e7SufHx8YLx8fGC8fHxgvHx8YLz8/N8////Bf///w3x8fGC8fHxgvHx8YLx8fGC8fHxgsXp
sOnB6Kv/wuis/8Porf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vr
uv/M67v/zey8/87svf/O67z96/Hoj/Hx8YLx8fGC8fHxgvHx8YLy8vIm8/PzK/Hx8YLx8fGC8fHxgvHx
8YLg79icwOep/8Hoqv/B6Kv/wuis/8Porf/E6a7/wuit/73opP+76KL/u+eh/77opv/D6a3/yeu1/8nq
tv/K67f/y+u5/8zruv/M67v/zey8/87svf/d7tSz8fHxgvHx8YLx8fGC8fHxgvHx8Tby8vI68fHxgvHx
8YLx8fGC8fHxgtTrxre/56j/wOep/8Hoqv/B6Kv/uOad/53idv+V4Gn/leBq/5fhbP+Y4W//muFx/5vi
c/+e4Xb/puWD/7PmlP/D6a3/y+u5/8zruv/M67v/zey8/9rtzsHx8fGC8fHxgvHx8YLx8fGC8/PzQfPz
80Lx8fGC8fHxgvHx8YLx8fGC0OvAwr/nqP+/56j/wOep/8Hoqv+o44b/kd9k/5LfZv+U4Gj/leBq/5fh
bf+Y4W//muFx/5vic/+d4nX/n+N3/7fnm//K67j/y+u5/8zruv/M67v/2u3QvPHx8YLx8fGC8fHxgvHx
8YLy8vI98/PzP/Hx8YLx8fGC8fHxgvHx8YLQ6sK/v+eo/7/nqP+/56j/wOep/6jjhv+P3mL/kd9k/5Lf
Zv+U4Gj/leBr/5fhbf+Y4W//muFx/5zic/+d4nX/v+mm/8nqt//K67j/y+u5/8zruv/f79au8fHxgvHx
8YLx8fGC8fHxgvX19TLx8fE38fHxgvHx8YLx8fGC8fHxgtTrybO/56j/v+eo/7/nqP+/56j/sOSS/47e
YP+P3mL/kd9k/5LfZv+U4Gj/leBr/5fhbf+Z4W//muJx/5/jd//H6bP/yeq2/8nqt//K67j/y+u5/+nv
45Tx8fGC8fHxgvHx8YLx8fGC7+/vIPHx8SXx8fGC8fHxgvHx8YLx8fGC4e/Zm7/nqP+/56j/v+eo/7/n
qP+956X/jt5h/47eYP+P3mL/kd9k/5LfZv+U4Gn/luBr/5fhbf+Z4W//q+aK/8fqs//I6rT/yeq2/8nq
t//N7Lvw8fHxgvHx8YLx8fGC8fHxgvPz84D///8G6+vrDfHx8YLx8fGC8fHxgvHx8YLv8e+Dweis87/n
qP+/56j/v+eo/7/nqP+d4XX/jN1e/47eYP+P3mL/kd9k/5PfZ/+U4Gn/luBr/5fhbf+86KP/xuqy/8fq
s//I6rX/yeq2/9Tsx8nx8fGC8fHxgvHx8YLx8fGC8PDwaAAAAAAAAAAA8fHxbPHx8YLx8fGC8fHxgvHx
8YLM6rrMv+eo/7/nqP+/56j/v+eo/7blmv+N3V//jN1e/47eYP+Q3mL/kd9k/5PfZ/+U4Gn/qeSH/8Xp
sP/G6bH/xuqy/8fqs//I6rX/5fDem/Hx8YLx8fGC8fHxgvHx8YLz8/M/AAAAAAAAAADz8/NB8fHxgvHx
8YLx8fGC8fHxgt3s06O/56j/v+eo/7/nqP+/56j/v+eo/7Xmmf+U32n/jN1e/47eYP+Q3mL/k99o/6zk
i//D6a7/xemv/8XpsP/G6bH/xuqy/8vqu+jx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xUAAAAAAAAAAPT0
9Bfx8fGC8fHxgvHx8YLx8fGC8fHvg8Tpsee/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+35pz/suWV/7Xm
mf/A6Kj/wuit/8Porf/E6a7/xemv/8XpsP/G6bH/3e3UqvHx8YLx8fGC8fHxgvHx8YLw8PBmAAAAAAAA
AAAAAAAAAAAAAPHx8W7x8fGC8fHxgvHx8YLx8fGC4u7cmMHnqvm/56j/v+eo/7/nqP+/56j/v+eo/7/n
qP+/56j/wOep/8Hoqv/C6Kz/wuit/8Porf/E6a7/xemv/9Hrwszx8fGC8fHxgvHx8YLx8fGC8fHxgvX1
9TEAAAAAAAAAAAAAAAAAAAAA7u7uO/Hx8YLx8fGC8fHxgvHx8YLx8fGC3e7SpMHoqfq/56j/v+eo/7/n
qP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz/wuit/8Porf/O67zV8PHwhPHx8YLx8fGC8fHxgvHx
8YLy8vJ2////BQAAAAAAAAAAAAAAAAAAAACqqqoD8PDwafHx8YLx8fGC8fHxgvHx8YLx8fGC4O/YnMTo
ruy/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz90uvEwe/x74Px8fGC8fHxgvHx
8YLx8fGC8fHxgvPz8ykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/MW8fHxfPHx8YLx8fGC8fHxgvHx
8YLx8fGC8PLuhdXtyLXF6bHlv+eo/7/nqP+/56j/v+eo/7/nqP/B6Kv0zeq8zOXv4JTx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADy8vIm8fHxgPHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLs8OmJ4e/Zm93u06Pf7def5+/hkvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxXf///wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AADy8vIo8/PzffHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8VnMzMwFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAD29vYb8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz83/v7+9BgICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8/PzQPLy8nnx8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz84Hx8fFc9PT0GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////B/X19TLx8fFc8PDwevHx
8YLx8fGC8fHxgvHx8YLx8fGC8fHxgPHx8Wv09PRE9PT0FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAA7+/vEPb29hvw8PAj7+/vH/T09Be/v78EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////8B///wAA//wAAD/wAAAP4AAAB+AA
AAfAAAADwAAAA4AAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAADwAAAA8AAAAPAAAAH4AAAB+AA
AA/wAAAP+AAAH/gAAD/+AAB//wAB///AA///+B////////////8oAAAAEAAAACAAAAABACAAAAAAAAAE
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CfDw8BH///8GAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAAu7u7i7x8fFe8PDwevHx8YLx8fGC8fHxgvDw
8Hvx8fFs7+/vT/Dw8CMAAAABAAAAAAAAAAAAAAAA5ubmCvLy8l/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx
8YLx8fGC8fHxgvHx8YLx8fGC8/PzZu7u7g8AAAAAAAAAAPHx8V3x8fGC8fHxgunv5o7Z7c200+vFytTs
xc7W7cnH2+7QueLu2qbu8OyH8fHxgvHx8YLx8fFu////BfHx8STx8fGC8fHxgtrtzq3D6a/8xemw/8bp
sv/I6rT/yeq2/8vruP/M67v/z+u++Nzu0bjx8fGC8fHxgu/v7zDx8fFI8fHxguzw6ojC56z3wuis/8Tp
rv/E6q3/weiq/8fqsv/J6rb/y+u5/8zru//N67z/6/HpjfHx8YLy8vJN8fHxXPHx8YLg79icv+eo/8Ho
qv+k4n//lOBo/5fhbf+a4XH/n+J5/7Pmlv/L67n/zOu7/+Xw353x8fGC8fHxXvHx8Vrx8fGC4O3Zm7/n
qP+/56j/nuF3/5HfZP+U4Gj/l+Ft/5ricf+x5pL/yeq3/8vruf/r8emN8fHxgu/v70/x8fFK8fHxguzw
6ojA6Kn8v+eo/6njiP+O3mD/kd9k/5Tgaf+X4W3/vuim/8jqtP/N67zr8fHxgvHx8YLy8vI68/PzK/Hx
8YLx8fGCx+m03L/nqP++6Kb/meBw/47eYP+S32X/q+SL/8XpsP/G6rL/1+zLvvHx8YLz8/OB8PDwEdXV
1Qbx8fF98fHxgt/t1Z/A56j9v+eo/7/nqP+656H/vuim/8Lorf/E6a7/yOq18Ovw6Yvx8fGC8vLyYwAA
AAAAAAAA8fHxR/Hx8YLx8fGC2O3NrMDnqfq/56j/v+eo/7/nqP/B6Kv/xumy7OTu3Zfx8fGC8/PzgfLy
8icAAAAAAAAAAP///wPz8/Nm8fHxgvHx8YLo7+SO0+zFuczquszM6bzJ1+zMru7w7Ibx8fGC8fHxgvHx
8UcAAAAAAAAAAAAAAAAAAAAA4+PjCfHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgfPz
80D///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8/PzK/Ly8mDz8/N+8fHxgvHx8YLy8vJ68vLyUezs
7BsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAevr6w3j4+MJAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD8fwAA4AcAAMADAACAAQAAgAEAAIABAACAAQAAgAEAAIAB
AADAAwAAwAMAAOAHAADwDwAA/n8AAP//AAA=
</value>
</data>
</root>

View file

@ -1,3 +0,0 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura DisableCompression='true' IncludeDebugSymbols='false' />
</Weavers>

View file

@ -1,141 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCompression" type="xs:boolean">
<xs:annotation>
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCleanup" type="xs:boolean">
<xs:annotation>
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View file

@ -1,262 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32;
using Newtonsoft.Json;
using UptimeKuma.Properties;
namespace UptimeKuma {
static class Program {
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args) {
var cwd = Path.GetDirectoryName(Application.ExecutablePath);
if (cwd != null) {
Environment.CurrentDirectory = cwd;
}
bool isIntranet = args.Contains("--intranet");
if (isIntranet) {
Console.WriteLine("The --intranet argument was provided, so we will not try to access the internet. The first time this application runs you'll need to run it without the --intranet param or copy the result from another machine to the intranet server.");
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new UptimeKumaApplicationContext(isIntranet));
}
}
public class UptimeKumaApplicationContext : ApplicationContext
{
private static Mutex mutex = null;
const string appName = "Uptime Kuma";
private NotifyIcon trayIcon;
private Process process;
private MenuItem statusMenuItem;
private MenuItem runWhenStarts;
private MenuItem openMenuItem;
private RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
private readonly bool intranetOnly;
public UptimeKumaApplicationContext(bool intranetOnly) {
// Single instance only
bool createdNew;
mutex = new Mutex(true, appName, out createdNew);
if (!createdNew) {
return;
}
this.intranetOnly = intranetOnly;
var startingText = "Starting server...";
trayIcon = new NotifyIcon();
trayIcon.Text = startingText;
runWhenStarts = new MenuItem("Run when system starts", RunWhenStarts);
runWhenStarts.Checked = registryKey.GetValue(appName) != null;
statusMenuItem = new MenuItem(startingText);
statusMenuItem.Enabled = false;
openMenuItem = new MenuItem("Open", Open);
openMenuItem.Enabled = false;
trayIcon.Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location);
trayIcon.ContextMenu = new ContextMenu(new MenuItem[] {
statusMenuItem,
openMenuItem,
//new("Debug Console", DebugConsole),
runWhenStarts,
new("Check for Update...", CheckForUpdate),
new("Visit GitHub...", VisitGitHub),
new("About", About),
new("Exit", Exit),
});
trayIcon.MouseDoubleClick += new MouseEventHandler(Open);
trayIcon.Visible = true;
var hasUpdateFile = File.Exists("update");
if (!hasUpdateFile && Directory.Exists("core") && Directory.Exists("node") && Directory.Exists("core/node_modules") && Directory.Exists("core/dist")) {
// Go go go
StartProcess();
} else {
DownloadFiles();
}
}
void DownloadFiles() {
if (intranetOnly) {
return;
}
var form = new DownloadForm();
form.Closed += Exit;
form.Show();
}
private void RunWhenStarts(object sender, EventArgs e) {
if (registryKey == null) {
MessageBox.Show("Error: Unable to set startup registry key.");
return;
}
if (runWhenStarts.Checked) {
registryKey.DeleteValue(appName, false);
runWhenStarts.Checked = false;
} else {
registryKey.SetValue(appName, Application.ExecutablePath);
runWhenStarts.Checked = true;
}
}
void StartProcess() {
var startInfo = new ProcessStartInfo {
FileName = "node/node.exe",
Arguments = "server/server.js --data-dir=\"../data/\"",
RedirectStandardOutput = false,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = "core"
};
process = new Process();
process.StartInfo = startInfo;
process.EnableRaisingEvents = true;
process.Exited += ProcessExited;
try {
process.Start();
//Open(null, null);
// Async task to check if the server is ready
Task.Run(() => {
var runningText = "Server is running";
using TcpClient tcpClient = new TcpClient();
while (true) {
try {
tcpClient.Connect("127.0.0.1", 3001);
statusMenuItem.Text = runningText;
openMenuItem.Enabled = true;
trayIcon.Text = runningText;
break;
} catch (Exception) {
System.Threading.Thread.Sleep(2000);
}
}
});
} catch (Exception e) {
MessageBox.Show("Startup failed: " + e.Message, "Uptime Kuma Error");
}
}
void StopProcess() {
process?.Kill();
}
void Open(object sender, EventArgs e) {
Process.Start("http://localhost:3001");
}
void DebugConsole(object sender, EventArgs e) {
}
void CheckForUpdate(object sender, EventArgs e) {
if (intranetOnly) {
return;
}
// Check version.json exists
if (File.Exists("version.json")) {
// Load version.json and compare with the latest version from GitHub
var currentVersionObj = JsonConvert.DeserializeObject<Version>(File.ReadAllText("version.json"));
var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version");
var latestVersionObj = JsonConvert.DeserializeObject<Version>(versionJson);
// Compare version, if the latest version is newer, then update
if (new System.Version(latestVersionObj.latest).CompareTo(new System.Version(currentVersionObj.latest)) > 0) {
var result = MessageBox.Show("A new version is available. Do you want to update?", "Update", MessageBoxButtons.YesNo);
if (result == DialogResult.Yes) {
// Create a empty file `update`, so the app will download the core files again at startup
File.Create("update").Close();
trayIcon.Visible = false;
process?.Kill();
// Restart the app, it will download the core files again at startup
Application.Restart();
}
} else {
MessageBox.Show("You are using the latest version.");
}
}
}
void VisitGitHub(object sender, EventArgs e) {
if (intranetOnly) {
MessageBox.Show("You have parsed in --intranet so we will not try to access the internet or visit github.com, please go to https://github.com/louislam/uptime-kuma if you want to visit github.");
return;
}
Process.Start("https://github.com/louislam/uptime-kuma");
}
void About(object sender, EventArgs e)
{
MessageBox.Show("Uptime Kuma Windows Runtime v1.0.0" + Environment.NewLine + "© 2023 Louis Lam", "Info");
}
void Exit(object sender, EventArgs e)
{
// Hide tray icon, otherwise it will remain shown until user mouses over it
trayIcon.Visible = false;
process?.Kill();
Application.Exit();
}
void ProcessExited(object sender, EventArgs e) {
if (process.ExitCode != 0) {
var line = "";
while (!process.StandardOutput.EndOfStream)
{
line += process.StandardOutput.ReadLine();
}
MessageBox.Show("Uptime Kuma exited unexpectedly. Exit code: " + process.ExitCode + " " + line);
}
trayIcon.Visible = false;
Application.Exit();
}
}
}

View file

@ -1,36 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Uptime Kuma")]
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Uptime Kuma")]
[assembly: AssemblyProduct("Uptime Kuma")]
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.2.0")]
[assembly: AssemblyFileVersion("1.0.2.0")]

View file

@ -1,62 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace UptimeKuma.Properties {
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder",
"4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance",
"CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if ((resourceMan == null)) {
global::System.Resources.ResourceManager temp =
new global::System.Resources.ResourceManager("UptimeKuma.Properties.Resources",
typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get { return resourceCulture; }
set { resourceCulture = value; }
}
}
}

View file

@ -1,117 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -1,23 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace UptimeKuma.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(
"Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance =
((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get { return defaultInstance; }
}
}
}

View file

@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View file

@ -1,203 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>UptimeKuma</RootNamespace>
<AssemblyName>uptime-kuma</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<ApplicationIcon>..\..\public\favicon.ico</ApplicationIcon>
<LangVersion>9</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup>
<PostBuildEvent>COPY "$(SolutionDir)bin\Debug\uptime-kuma.exe" "%UserProfile%\Desktop\uptime-kuma-win64\"</PostBuildEvent>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Win32.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll</HintPath>
</Reference>
<Reference Include="mscorlib" />
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.AppContext, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.AppContext.4.3.0\lib\net463\System.AppContext.dll</HintPath>
</Reference>
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Console, Version=4.0.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Console.4.3.1\lib\net46\System.Console.dll</HintPath>
</Reference>
<Reference Include="System.Core" />
<Reference Include="System.Diagnostics.DiagnosticSource, Version=7.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>packages\System.Diagnostics.DiagnosticSource.7.0.1\lib\net462\System.Diagnostics.DiagnosticSource.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Tracing, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll</HintPath>
</Reference>
<Reference Include="System.Globalization.Calendars, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll</HintPath>
</Reference>
<Reference Include="System.IO, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.IO.4.3.0\lib\net462\System.IO.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression, Version=4.1.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.IO.Compression.ZipFile, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem.Primitives, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Linq, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Linq.4.3.0\lib\net463\System.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Linq.Expressions, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Linq.Expressions.4.3.0\lib\net463\System.Linq.Expressions.dll</HintPath>
</Reference>
<Reference Include="System.Memory, Version=4.0.1.2, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http, Version=4.1.1.3, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll</HintPath>
</Reference>
<Reference Include="System.Net.Sockets, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll</HintPath>
</Reference>
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Reflection, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll</HintPath>
</Reference>
<Reference Include="System.Runtime, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Extensions, Version=4.1.1.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.Extensions.4.3.1\lib\net462\System.Runtime.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.InteropServices.4.3.0\lib\net463\System.Runtime.InteropServices.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Algorithms, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Encoding, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Primitives, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.X509Certificates, Version=4.1.1.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll</HintPath>
</Reference>
<Reference Include="System.Text.RegularExpressions, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Text.RegularExpressions.4.3.1\lib\net463\System.Text.RegularExpressions.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.ReaderWriter, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="DownloadForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="DownloadForm.Designer.cs">
<DependentUpon>DownloadForm.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Version.cs" />
<EmbeddedResource Include="DownloadForm.resx">
<DependentUpon>DownloadForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="..\..\public\favicon.ico">
<Link>favicon.ico</Link>
</None>
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<Content Include=".gitignore" />
<Content Include="app.manifest" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets'))" />
</Target>
<Import Project="packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets" Condition="Exists('packages\NETStandard.Library.2.0.3\build\netstandard2.0\NETStandard.Library.targets')" />
</Project>

View file

@ -1,16 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UptimeKuma", "UptimeKuma.csproj", "{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -1,3 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=UptimeKuma_002FProperties_002FResources/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -1,9 +0,0 @@
namespace UptimeKuma {
public class Version {
public string latest { get; set; }
public string slow { get; set; }
public string beta { get; set; }
public string nodejs { get; set; }
public string exe { get; set; }
}
}

View file

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC Manifest Options
If you want to change the Windows User Account Control level replace the
requestedExecutionLevel node with one of the following.
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
Specifying requestedExecutionLevel element will disable file and registry virtualization.
Remove this element if your application requires this virtualization for backwards
compatibility.
-->
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View file

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.NETCore.Platforms" version="7.0.0" targetFramework="net472" />
<package id="Microsoft.Win32.Primitives" version="4.3.0" targetFramework="net472" />
<package id="NETStandard.Library" version="2.0.3" targetFramework="net472" />
<package id="Newtonsoft.Json" version="13.0.2" targetFramework="net472" />
<package id="System.AppContext" version="4.3.0" targetFramework="net472" />
<package id="System.Console" version="4.3.1" targetFramework="net472" />
<package id="System.Diagnostics.DiagnosticSource" version="7.0.1" targetFramework="net472" />
<package id="System.Net.Http" version="4.3.4" targetFramework="net472" />
<package id="System.Runtime.Extensions" version="4.3.1" targetFramework="net472" />
<package id="System.Security.Cryptography.Algorithms" version="4.3.1" targetFramework="net472" />
<package id="System.Security.Cryptography.X509Certificates" version="4.3.2" targetFramework="net472" />
<package id="System.Text.RegularExpressions" version="4.3.1" targetFramework="net472" />
<package id="System.Xml.ReaderWriter" version="4.3.1" targetFramework="net472" />
<package id="System.Memory" version="4.5.5" targetFramework="net472" />
<package id="System.Net.Primitives" version="4.3.1" targetFramework="net472" />
<package id="System.Runtime" version="4.3.1" targetFramework="net472" />
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
<package id="System.Collections" version="4.3.0" targetFramework="net472" />
<package id="System.Collections.Concurrent" version="4.3.0" targetFramework="net472" />
<package id="System.Diagnostics.Debug" version="4.3.0" targetFramework="net472" />
<package id="System.Diagnostics.Tools" version="4.3.0" targetFramework="net472" />
<package id="System.Diagnostics.Tracing" version="4.3.0" targetFramework="net472" />
<package id="System.Globalization" version="4.3.0" targetFramework="net472" />
<package id="System.Globalization.Calendars" version="4.3.0" targetFramework="net472" />
<package id="System.IO" version="4.3.0" targetFramework="net472" />
<package id="System.IO.Compression" version="4.3.0" targetFramework="net472" />
<package id="System.IO.Compression.ZipFile" version="4.3.0" targetFramework="net472" />
<package id="System.IO.FileSystem" version="4.3.0" targetFramework="net472" />
<package id="System.IO.FileSystem.Primitives" version="4.3.0" targetFramework="net472" />
<package id="System.Linq" version="4.3.0" targetFramework="net472" />
<package id="System.Linq.Expressions" version="4.3.0" targetFramework="net472" />
<package id="System.Net.Sockets" version="4.3.0" targetFramework="net472" />
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
<package id="System.ObjectModel" version="4.3.0" targetFramework="net472" />
<package id="System.Reflection" version="4.3.0" targetFramework="net472" />
<package id="System.Reflection.Extensions" version="4.3.0" targetFramework="net472" />
<package id="System.Reflection.Primitives" version="4.3.0" targetFramework="net472" />
<package id="System.Resources.ResourceManager" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net472" />
<package id="System.Runtime.Handles" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.InteropServices" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net472" />
<package id="System.Runtime.Numerics" version="4.3.0" targetFramework="net472" />
<package id="System.Security.Cryptography.Encoding" version="4.3.0" targetFramework="net472" />
<package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net472" />
<package id="System.Text.Encoding" version="4.3.0" targetFramework="net472" />
<package id="System.Text.Encoding.Extensions" version="4.3.0" targetFramework="net472" />
<package id="System.Threading" version="4.3.0" targetFramework="net472" />
<package id="System.Threading.Tasks" version="4.3.0" targetFramework="net472" />
<package id="System.Threading.Timer" version="4.3.0" targetFramework="net472" />
<package id="System.Xml.XDocument" version="4.3.0" targetFramework="net472" />
</packages>

View file

@ -15,7 +15,6 @@ if (newVersion) {
// Process package.json
pkg.version = newVersion;
pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion);
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
// Process README.md

View file

@ -1,6 +0,0 @@
console.log("Git Push and Publish the release note on github, then press any key to continue");
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on("data", process.exit.bind(process, 0));

64
extra/release/beta.mjs Normal file
View file

@ -0,0 +1,64 @@
import "dotenv/config";
import {
ver,
buildDist,
buildImage,
checkDocker,
checkTagExists,
checkVersionFormat,
getRepoNames,
pressAnyKey,
execSync, uploadArtifacts,
} from "./lib.mjs";
import semver from "semver";
const repoNames = getRepoNames();
const version = process.env.RELEASE_BETA_VERSION;
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
console.log("RELEASE_BETA_VERSION:", version);
if (!githubToken) {
console.error("GITHUB_TOKEN is required");
process.exit(1);
}
// Check if the version is a valid semver
checkVersionFormat(version);
// Check if the semver identifier is "beta"
const semverIdentifier = semver.prerelease(version);
console.log("Semver identifier:", semverIdentifier);
if (semverIdentifier[0] !== "beta") {
console.error("VERSION should have a semver identifier of 'beta'");
process.exit(1);
}
// Check if docker is running
checkDocker();
// Check if the tag exists
await checkTagExists(repoNames, version);
// node extra/beta/update-version.js
execSync("node ./extra/beta/update-version.js");
// Build frontend dist
buildDist();
// Build slim image (rootless)
buildImage(repoNames, [ "beta-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
// Build full image (rootless)
buildImage(repoNames, [ "beta-rootless", ver(version, "rootless") ], "rootless");
// Build slim image
buildImage(repoNames, [ "beta-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
// Build full image
buildImage(repoNames, [ "beta", version ], "release");
await pressAnyKey();
// npm run upload-artifacts
uploadArtifacts(version, githubToken);

57
extra/release/final.mjs Normal file
View file

@ -0,0 +1,57 @@
import "dotenv/config";
import {
ver,
buildDist,
buildImage,
checkDocker,
checkTagExists,
checkVersionFormat,
getRepoNames,
pressAnyKey, execSync, uploadArtifacts
} from "./lib.mjs";
const repoNames = getRepoNames();
const version = process.env.RELEASE_VERSION;
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
console.log("RELEASE_VERSION:", version);
if (!githubToken) {
console.error("GITHUB_TOKEN is required");
process.exit(1);
}
// Check if the version is a valid semver
checkVersionFormat(version);
// Check if docker is running
checkDocker();
// Check if the tag exists
await checkTagExists(repoNames, version);
// node extra/beta/update-version.js
execSync("node extra/update-version.js");
// Build frontend dist
buildDist();
// Build slim image (rootless)
buildImage(repoNames, [ "2-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
// Build full image (rootless)
buildImage(repoNames, [ "2-rootless", ver(version, "rootless") ], "rootless");
// Build slim image
buildImage(repoNames, [ "next-slim", "2-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
// Build full image
buildImage(repoNames, [ "next", "2", version ], "release");
await pressAnyKey();
// npm run upload-artifacts
uploadArtifacts(version, githubToken);
// node extra/update-wiki-version.js
execSync("node extra/update-wiki-version.js");

251
extra/release/lib.mjs Normal file
View file

@ -0,0 +1,251 @@
import "dotenv/config";
import * as childProcess from "child_process";
import semver from "semver";
export const dryRun = process.env.RELEASE_DRY_RUN === "1";
if (dryRun) {
console.info("Dry run enabled.");
}
/**
* Check if docker is running
* @returns {void}
*/
export function checkDocker() {
try {
childProcess.execSync("docker ps");
} catch (error) {
console.error("Docker is not running. Please start docker and try again.");
process.exit(1);
}
}
/**
* Get Docker Hub repository name
*/
export function getRepoNames() {
if (process.env.RELEASE_REPO_NAMES) {
// Split by comma
return process.env.RELEASE_REPO_NAMES.split(",").map((name) => name.trim());
}
return [
"louislam/uptime-kuma",
"ghcr.io/louislam/uptime-kuma",
];
}
/**
* Build frontend dist
* @returns {void}
*/
export function buildDist() {
if (!dryRun) {
childProcess.execSync("npm run build", { stdio: "inherit" });
} else {
console.info("[DRY RUN] npm run build");
}
}
/**
* Build docker image and push to Docker Hub
* @param {string[]} repoNames Docker Hub repository names
* @param {string[]} tags Docker image tags
* @param {string} target Dockerfile's target name
* @param {string} buildArgs Docker build args
* @param {string} dockerfile Path to Dockerfile
* @param {string} platform Build platform
* @returns {void}
*/
export function buildImage(repoNames, tags, target, buildArgs = "", dockerfile = "docker/dockerfile", platform = "linux/amd64,linux/arm64,linux/arm/v7") {
let args = [
"buildx",
"build",
"-f",
dockerfile,
"--platform",
platform,
];
for (let repoName of repoNames) {
// Add tags
for (let tag of tags) {
args.push("-t", `${repoName}:${tag}`);
}
}
args = [
...args,
"--target",
target,
];
// Add build args
if (buildArgs) {
args.push("--build-arg", buildArgs);
}
args = [
...args,
".",
"--push",
];
if (!dryRun) {
childProcess.spawnSync("docker", args, { stdio: "inherit" });
} else {
console.log(`[DRY RUN] docker ${args.join(" ")}`);
}
}
/**
* Check if the version already exists on Docker Hub
* TODO: use semver to compare versions if it is greater than the previous?
* @param {string[]} repoNames repository name (Only check the name with single slash)
* @param {string} version Version to check
* @returns {void}
*/
export async function checkTagExists(repoNames, version) {
// Skip if the tag is not on Docker Hub
// louislam/uptime-kuma
let dockerHubRepoNames = repoNames.filter((name) => {
return name.split("/").length === 2;
});
for (let repoName of dockerHubRepoNames) {
await checkTagExistsSingle(repoName, version);
}
}
/**
* Check if the version already exists on Docker Hub
* @param {string} repoName repository name
* @param {string} version Version to check
* @returns {Promise<void>}
*/
export async function checkTagExistsSingle(repoName, version) {
console.log(`Checking if version ${version} exists on Docker Hub:`, repoName);
// Get a list of tags from the Docker Hub repository
let tags = [];
// It is mainly to check my careless mistake that I forgot to update the release version in .env, so `page_size` is set to 100 is enough, I think.
const response = await fetch(`https://hub.docker.com/v2/repositories/${repoName}/tags/?page_size=100`);
if (response.ok) {
const data = await response.json();
tags = data.results.map((tag) => tag.name);
} else {
console.error("Failed to get tags from Docker Hub");
process.exit(1);
}
// Check if the version already exists
if (tags.includes(version)) {
console.error(`Version ${version} already exists`);
process.exit(1);
}
}
/**
* Check the version format
* @param {string} version Version to check
* @returns {void}
*/
export function checkVersionFormat(version) {
if (!version) {
console.error("VERSION is required");
process.exit(1);
}
// Check the version format, it should be a semver and must be like this: "2.0.0-beta.0"
if (!semver.valid(version)) {
console.error("VERSION is not a valid semver version");
process.exit(1);
}
}
/**
* Press any key to continue
* @returns {Promise<void>}
*/
export function pressAnyKey() {
console.log("Git Push and Publish the release note on github, then press any key to continue");
process.stdin.setRawMode(true);
process.stdin.resume();
return new Promise(resolve => process.stdin.once("data", data => {
process.stdin.setRawMode(false);
process.stdin.pause();
resolve();
}));
}
/**
* Append version identifier
* @param {string} version Version
* @param {string} identifier Identifier
* @returns {string} Version with identifier
*/
export function ver(version, identifier) {
const obj = semver.parse(version);
if (obj.prerelease.length === 0) {
obj.prerelease = [ identifier ];
} else {
obj.prerelease[0] = [ obj.prerelease[0], identifier ].join("-");
}
return obj.format();
}
/**
* Upload artifacts to GitHub
* docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain
* @param {string} version Version
* @param {string} githubToken GitHub token
* @returns {void}
*/
export function uploadArtifacts(version, githubToken) {
let args = [
"buildx",
"build",
"-f",
"docker/dockerfile",
"--platform",
"linux/amd64",
"-t",
"louislam/uptime-kuma:upload-artifact",
"--build-arg",
`VERSION=${version}`,
"--build-arg",
"GITHUB_TOKEN",
"--target",
"upload-artifact",
".",
"--progress",
"plain",
];
if (!dryRun) {
childProcess.spawnSync("docker", args, {
stdio: "inherit",
env: {
...process.env,
GITHUB_TOKEN: githubToken,
},
});
} else {
console.log(`[DRY RUN] docker ${args.join(" ")}`);
}
}
/**
* Execute a command
* @param {string} cmd Command to execute
* @returns {void}
*/
export function execSync(cmd) {
if (!dryRun) {
childProcess.execSync(cmd, { stdio: "inherit" });
} else {
console.info(`[DRY RUN] ${cmd}`);
}
}

16
extra/release/nightly.mjs Normal file
View file

@ -0,0 +1,16 @@
import { buildDist, buildImage, checkDocker, getRepoNames } from "./lib.mjs";
// Docker Hub repository name
const repoNames = getRepoNames();
// Check if docker is running
checkDocker();
// Build frontend dist (it will build on the host machine, TODO: build on a container?)
buildDist();
// Build full image (rootless)
buildImage(repoNames, [ "nightly2-rootless" ], "nightly-rootless");
// Build full image
buildImage(repoNames, [ "nightly2" ], "nightly");

View file

@ -0,0 +1,6 @@
import { uploadArtifacts } from "./lib.mjs";
const version = process.env.RELEASE_BETA_VERSION;
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
uploadArtifacts(version, githubToken);

View file

@ -0,0 +1,6 @@
import { uploadArtifacts } from "./lib.mjs";
const version = process.env.RELEASE_VERSION;
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
uploadArtifacts(version, githubToken);

View file

@ -0,0 +1,25 @@
// For #5231
const fs = require("fs");
let path = "../src/lang";
// list directories in the lang directory
let jsonFileList = fs.readdirSync(path);
for (let jsonFile of jsonFileList) {
if (!jsonFile.endsWith(".json")) {
continue;
}
let jsonPath = path + "/" + jsonFile;
let langData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
for (let key in langData) {
if (langData[key] === "") {
delete langData[key];
}
}
fs.writeFileSync(jsonPath, JSON.stringify(langData, null, 4) + "\n");
}

View file

@ -0,0 +1,24 @@
const { R } = require("redbean-node");
const Database = require("../server/database");
const args = require("args-parser")(process.argv);
const { Settings } = require("../server/settings");
const main = async () => {
console.log("Connecting the database");
Database.initDataDir(args);
await Database.connect(false, false, true);
console.log("Deleting all data from aggregate tables");
await R.exec("DELETE FROM stat_minutely");
await R.exec("DELETE FROM stat_hourly");
await R.exec("DELETE FROM stat_daily");
console.log("Resetting the aggregate table state");
await Settings.set("migrateAggregateTableState", "");
await Database.close();
console.log("Done");
};
main();

View file

@ -1,9 +0,0 @@
// Check if docker is running
const { exec } = require("child_process");
exec("docker ps", (err, stdout, stderr) => {
if (err) {
console.error("Docker is not running. Please start docker and try again.");
process.exit(1);
}
});

View file

@ -5,7 +5,7 @@ const util = require("../src/util");
util.polyfill();
const newVersion = process.env.VERSION;
const newVersion = process.env.RELEASE_VERSION;
console.log("New Version: " + newVersion);

View file

@ -1,7 +1,7 @@
const childProcess = require("child_process");
const fs = require("fs");
const newVersion = process.env.VERSION;
const newVersion = process.env.RELEASE_VERSION;
if (!newVersion) {
console.log("Missing version");

7014
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "2.0.0-dev",
"version": "2.0.0-beta.1",
"license": "MIT",
"repository": {
"type": "git",
@ -34,20 +34,14 @@
"playwright-show-report": "playwright show-report ./private/playwright-report",
"tsc": "tsc",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker": "npm run build && npm run build-docker-full && npm run build-docker-slim",
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
"build-docker-slim": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim -t louislam/uptime-kuma:$VERSION-slim --target release --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push",
"build-docker-full": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2 -t louislam/uptime-kuma:$VERSION --target release . --push",
"build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2 --target nightly . --push",
"build-docker-slim-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim-rootless -t louislam/uptime-kuma:$VERSION-slim-rootless --target rootless --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push",
"build-docker-full-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-rootless -t louislam/uptime-kuma:$VERSION-rootless --target rootless . --push",
"build-docker-nightly-rootless": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2-rootless --target nightly-rootless . --push",
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.23.14 && npm ci --production && npm run download-dist",
"upload-artifacts": "node extra/release/upload-artifacts.mjs",
"upload-artifacts-beta": "node extra/release/upload-artifacts-beta.mjs",
"setup": "git checkout 1.23.16 && npm ci --omit dev && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@ -58,8 +52,9 @@
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
"release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
"release-final": "node ./extra/release/final.mjs",
"release-beta": "node ./extra/release/beta.mjs",
"release-nightly": "node ./extra/release/nightly.mjs",
"git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev",
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
@ -68,7 +63,8 @@
"sort-contributors": "node extra/sort-contributors.js",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X"
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js"
},
"dependencies": {
"@grpc/grpc-js": "~1.8.22",
@ -99,8 +95,8 @@
"html-escaper": "^3.0.3",
"http-cookie-agent": "~5.0.4",
"http-graceful-shutdown": "~3.1.7",
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
"http-proxy-agent": "~7.0.2",
"https-proxy-agent": "~7.0.6",
"iconv-lite": "~0.6.3",
"isomorphic-ws": "^5.0.0",
"jsesc": "~3.0.2",
@ -108,7 +104,7 @@
"jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2",
"kafkajs": "^2.2.4",
"knex": "^2.4.2",
"knex": "~3.1.0",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"marked": "^14.0.0",
@ -116,13 +112,13 @@
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~11.0.0",
"mysql2": "~3.9.6",
"mysql2": "~3.11.3",
"nanoid": "~3.3.4",
"net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.9.13",
"nostr-tools": "^1.13.1",
"nostr-tools": "^2.10.4",
"notp": "~2.0.3",
"openid-client": "^5.4.2",
"password-hash": "~1.2.2",
@ -139,7 +135,7 @@
"semver": "~7.5.4",
"socket.io": "~4.8.0",
"socket.io-client": "~4.8.0",
"socks-proxy-agent": "6.1.1",
"socks-proxy-agent": "~8.0.5",
"tar": "~6.2.1",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",
@ -147,7 +143,7 @@
"ws": "^8.13.0"
},
"devDependencies": {
"@actions/github": "~5.1.1",
"@actions/github": "~6.0.0",
"@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
@ -173,7 +169,7 @@
"cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"dompurify": "~3.1.7",
"dompurify": "~3.2.4",
"eslint": "~8.14.0",
"eslint-plugin-jsdoc": "~46.4.6",
"eslint-plugin-vue": "~8.7.1",
@ -194,7 +190,7 @@
"testcontainers": "^10.13.1",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~5.2.8",
"vite": "~5.4.14",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "^7.0.15",
"vue": "~3.4.2",

View file

@ -6,6 +6,11 @@ const knex = require("knex");
const path = require("path");
const { EmbeddedMariaDB } = require("./embedded-mariadb");
const mysql = require("mysql2/promise");
const { Settings } = require("./settings");
const { UptimeCalculator } = require("./uptime-calculator");
const dayjs = require("dayjs");
const { SimpleMigrationServer } = require("./utils/simple-migration-server");
const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysql2-columncompiler");
/**
* Database & App Data Folder
@ -194,6 +199,14 @@ class Database {
* @returns {Promise<void>}
*/
static async connect(testMode = false, autoloadModels = true, noLog = false) {
// Patch "mysql2" knex client
// Workaround: Tried extending the ColumnCompiler class, but it didn't work for unknown reasons, so I override the function via prototype
const { getDialectByNameOrAlias } = require("knex/lib/dialects");
const mysql2 = getDialectByNameOrAlias("mysql2");
mysql2.prototype.columnCompiler = function () {
return new KumaColumnCompiler(this, ...arguments);
};
const acquireConnectionTimeout = 120 * 1000;
let dbConfig;
try {
@ -283,7 +296,7 @@ class Database {
client: "mysql2",
connection: {
socketPath: embeddedMariaDB.socketPath,
user: "node",
user: embeddedMariaDB.username,
database: "kuma",
timezone: "Z",
typeCast: function (field, next) {
@ -379,9 +392,11 @@ class Database {
/**
* Patch the database
* @param {number} port Start the migration server for aggregate tables on this port if provided
* @param {string} hostname Start the migration server for aggregate tables on this hostname if provided
* @returns {Promise<void>}
*/
static async patch() {
static async patch(port = undefined, hostname = undefined) {
// Still need to keep this for old versions of Uptime Kuma
if (Database.dbConfig.type === "sqlite") {
await this.patchSqlite();
@ -391,9 +406,23 @@ class Database {
// https://knexjs.org/guide/migrations.html
// https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261
try {
// Disable foreign key check for SQLite
// Known issue of knex: https://github.com/drizzle-team/drizzle-orm/issues/1813
if (Database.dbConfig.type === "sqlite") {
await R.exec("PRAGMA foreign_keys = OFF");
}
await R.knex.migrate.latest({
directory: Database.knexMigrationsPath,
});
// Enable foreign key check for SQLite
if (Database.dbConfig.type === "sqlite") {
await R.exec("PRAGMA foreign_keys = ON");
}
await this.migrateAggregateTable(port, hostname);
} catch (e) {
// Allow missing patch files for downgrade or testing pr.
if (e.message.includes("the following files are missing:")) {
@ -711,6 +740,175 @@ class Database {
}
}
/**
* Migrate the old data in the heartbeat table to the new format (stat_daily, stat_hourly, stat_minutely)
* It should be run once while upgrading V1 to V2
*
* Normally, it should be in transaction, but UptimeCalculator wasn't designed to be in transaction before that.
* I don't want to heavily modify the UptimeCalculator, so it is not in transaction.
* Run `npm run reset-migrate-aggregate-table-state` to reset, in case the migration is interrupted.
* @param {number} port Start the migration server on this port if provided
* @param {string} hostname Start the migration server on this hostname if provided
* @returns {Promise<void>}
*/
static async migrateAggregateTable(port, hostname = undefined) {
log.debug("db", "Enter Migrate Aggregate Table function");
// Add a setting for 2.0.0-dev users to skip this migration
if (process.env.SET_MIGRATE_AGGREGATE_TABLE_TO_TRUE === "1") {
log.warn("db", "SET_MIGRATE_AGGREGATE_TABLE_TO_TRUE is set to 1, skipping aggregate table migration forever (for 2.0.0-dev users)");
await Settings.set("migrateAggregateTableState", "migrated");
}
let migrateState = await Settings.get("migrateAggregateTableState");
// Skip if already migrated
// If it is migrating, it possibly means the migration was interrupted, or the migration is in progress
if (migrateState === "migrated") {
log.debug("db", "Migrated aggregate table already, skip");
return;
} else if (migrateState === "migrating") {
log.warn("db", "Aggregate table migration is already in progress, or it was interrupted");
throw new Error("Aggregate table migration is already in progress");
}
/**
* Start migration server for displaying the migration status
* @type {SimpleMigrationServer}
*/
let migrationServer;
let msg;
if (port) {
migrationServer = new SimpleMigrationServer();
await migrationServer.start(port, hostname);
}
log.info("db", "Migrating Aggregate Table");
log.info("db", "Getting list of unique monitors");
// Get a list of unique monitors from the heartbeat table, using raw sql
let monitors = await R.getAll(`
SELECT DISTINCT monitor_id
FROM heartbeat
ORDER BY monitor_id ASC
`);
// Stop if stat_* tables are not empty
for (let table of [ "stat_minutely", "stat_hourly", "stat_daily" ]) {
let countResult = await R.getRow(`SELECT COUNT(*) AS count FROM ${table}`);
let count = countResult.count;
if (count > 0) {
log.warn("db", `Aggregate table ${table} is not empty, migration will not be started (Maybe you were using 2.0.0-dev?)`);
await migrationServer?.stop();
return;
}
}
await Settings.set("migrateAggregateTableState", "migrating");
let progressPercent = 0;
let part = 100 / monitors.length;
let i = 1;
for (let monitor of monitors) {
// Get a list of unique dates from the heartbeat table, using raw sql
let dates = await R.getAll(`
SELECT DISTINCT DATE(time) AS date
FROM heartbeat
WHERE monitor_id = ?
ORDER BY date ASC
`, [
monitor.monitor_id
]);
for (let date of dates) {
// New Uptime Calculator
let calculator = new UptimeCalculator();
calculator.monitorID = monitor.monitor_id;
calculator.setMigrationMode(true);
// Get all the heartbeats for this monitor and date
let heartbeats = await R.getAll(`
SELECT status, ping, time
FROM heartbeat
WHERE monitor_id = ?
AND DATE(time) = ?
ORDER BY time ASC
`, [ monitor.monitor_id, date.date ]);
if (heartbeats.length > 0) {
msg = `[DON'T STOP] Migrating monitor data ${monitor.monitor_id} - ${date.date} [${progressPercent.toFixed(2)}%][${i}/${monitors.length}]`;
log.info("db", msg);
migrationServer?.update(msg);
}
for (let heartbeat of heartbeats) {
await calculator.update(heartbeat.status, parseFloat(heartbeat.ping), dayjs(heartbeat.time));
}
progressPercent += (Math.round(part / dates.length * 100) / 100);
// Lazy to fix the floating point issue, it is acceptable since it is just a progress bar
if (progressPercent > 100) {
progressPercent = 100;
}
}
i++;
}
msg = "Clearing non-important heartbeats";
log.info("db", msg);
migrationServer?.update(msg);
await Database.clearHeartbeatData(true);
await Settings.set("migrateAggregateTableState", "migrated");
await migrationServer?.stop();
if (monitors.length > 0) {
log.info("db", "Aggregate Table Migration Completed");
} else {
log.info("db", "No data to migrate");
}
}
/**
* Remove all non-important heartbeats from heartbeat table, keep last 24-hour or {KEEP_LAST_ROWS} rows for each monitor
* @param {boolean} detailedLog Log detailed information
* @returns {Promise<void>}
*/
static async clearHeartbeatData(detailedLog = false) {
let monitors = await R.getAll("SELECT id FROM monitor");
const sqlHourOffset = Database.sqlHourOffset();
for (let monitor of monitors) {
if (detailedLog) {
log.info("db", "Deleting non-important heartbeats for monitor " + monitor.id);
}
await R.exec(`
DELETE FROM heartbeat
WHERE monitor_id = ?
AND important = 0
AND time < ${sqlHourOffset}
AND id NOT IN (
SELECT id FROM ( -- written this way for Maria's support
SELECT id
FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT ?
) AS limited_ids
)
`, [
monitor.id,
-24,
monitor.id,
100,
]);
}
}
}
module.exports = Database;

View file

@ -14,9 +14,15 @@ class EmbeddedMariaDB {
mariadbDataDir = "/app/data/mariadb";
runDir = "/app/data/run/mariadb";
runDir = "/app/data/run";
socketPath = this.runDir + "/mysqld.sock";
socketPath = this.runDir + "/mariadb.sock";
/**
* The username to connect to the MariaDB
* @type {string}
*/
username = null;
/**
* @type {ChildProcessWithoutNullStreams}
@ -46,16 +52,42 @@ class EmbeddedMariaDB {
/**
* Start the embedded MariaDB
* @throws {Error} If the current user is not "node" or "root"
* @returns {Promise<void>|void} A promise that resolves when the MariaDB is started or void if it is already started
*/
start() {
// Check if the current user is "node" or "root"
this.username = require("os").userInfo().username;
if (this.username !== "node" && this.username !== "root") {
throw new Error("Embedded Mariadb supports only 'node' or 'root' user, but the current user is: " + this.username);
}
this.initDB();
this.startChildProcess();
return new Promise((resolve) => {
let interval = setInterval(() => {
if (this.started) {
clearInterval(interval);
resolve();
} else {
log.info("mariadb", "Waiting for Embedded MariaDB to start...");
}
}, 1000);
});
}
/**
* Start the child process
* @returns {void}
*/
startChildProcess() {
if (this.childProcess) {
log.info("mariadb", "Already started");
return;
}
this.initDB();
this.running = true;
log.info("mariadb", "Starting Embedded MariaDB");
this.childProcess = childProcess.spawn(this.exec, [
@ -63,6 +95,8 @@ class EmbeddedMariaDB {
"--datadir=" + this.mariadbDataDir,
`--socket=${this.socketPath}`,
`--pid-file=${this.runDir}/mysqld.pid`,
// Don't add the following option, the mariadb will not report message to the console, which affects initDBAfterStarted()
// "--log-error=" + `${this.mariadbDataDir}/mariadb-error.log`,
]);
this.childProcess.on("close", (code) => {
@ -72,8 +106,8 @@ class EmbeddedMariaDB {
log.info("mariadb", "Stopped Embedded MariaDB: " + code);
if (code !== 0) {
log.info("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user");
this.start();
log.error("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user");
this.startChildProcess();
}
});
@ -86,7 +120,7 @@ class EmbeddedMariaDB {
});
let handler = (data) => {
log.debug("mariadb", data.toString("utf-8"));
log.info("mariadb", data.toString("utf-8"));
if (data.toString("utf-8").includes("ready for connections")) {
this.initDBAfterStarted();
}
@ -94,17 +128,6 @@ class EmbeddedMariaDB {
this.childProcess.stdout.on("data", handler);
this.childProcess.stderr.on("data", handler);
return new Promise((resolve) => {
let interval = setInterval(() => {
if (this.started) {
clearInterval(interval);
resolve();
} else {
log.info("mariadb", "Waiting for Embedded MariaDB to start...");
}
}, 1000);
});
}
/**
@ -129,9 +152,11 @@ class EmbeddedMariaDB {
recursive: true,
});
let result = childProcess.spawnSync("mysql_install_db", [
let result = childProcess.spawnSync("mariadb-install-db", [
"--user=node",
"--ldata=" + this.mariadbDataDir,
"--auth-root-socket-user=node",
"--datadir=" + this.mariadbDataDir,
"--auth-root-authentication-method=socket",
]);
if (result.status !== 0) {
@ -143,6 +168,17 @@ class EmbeddedMariaDB {
}
}
// Check the owner of the mariadb directory, and change it if necessary
let stat = fs.statSync(this.mariadbDataDir);
if (stat.uid !== 1000 || stat.gid !== 1000) {
fs.chownSync(this.mariadbDataDir, 1000, 1000);
}
// Check the permission of the mariadb directory, and change it if it is not 755
if (stat.mode !== 0o755) {
fs.chmodSync(this.mariadbDataDir, 0o755);
}
if (!fs.existsSync(this.runDir)) {
log.info("mariadb", `Embedded MariaDB: ${this.runDir} is not found, create one now.`);
fs.mkdirSync(this.runDir, {
@ -150,6 +186,13 @@ class EmbeddedMariaDB {
});
}
stat = fs.statSync(this.runDir);
if (stat.uid !== 1000 || stat.gid !== 1000) {
fs.chownSync(this.runDir, 1000, 1000);
}
if (stat.mode !== 0o755) {
fs.chmodSync(this.runDir, 0o755);
}
}
/**
@ -159,7 +202,7 @@ class EmbeddedMariaDB {
async initDBAfterStarted() {
const connection = mysql.createConnection({
socketPath: this.socketPath,
user: "node",
user: this.username,
});
let result = await connection.execute("CREATE DATABASE IF NOT EXISTS `kuma`");

View file

@ -1,21 +1,22 @@
const { R } = require("redbean-node");
const { log } = require("../../src/util");
const { setSetting, setting } = require("../util-server");
const Database = require("../database");
const { Settings } = require("../settings");
const dayjs = require("dayjs");
const DEFAULT_KEEP_PERIOD = 180;
const DEFAULT_KEEP_PERIOD = 365;
/**
* Clears old data from the heartbeat table of the database.
* Clears old data from the heartbeat table and the stat_daily of the database.
* @returns {Promise<void>} A promise that resolves when the data has been cleared.
*/
const clearOldData = async () => {
let period = await setting("keepDataPeriodDays");
await Database.clearHeartbeatData();
let period = await Settings.get("keepDataPeriodDays");
// Set Default Period
if (period == null) {
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
period = DEFAULT_KEEP_PERIOD;
}
@ -25,23 +26,28 @@ const clearOldData = async () => {
parsedPeriod = parseInt(period);
} catch (_) {
log.warn("clearOldData", "Failed to parse setting, resetting to default..");
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
parsedPeriod = DEFAULT_KEEP_PERIOD;
}
if (parsedPeriod < 1) {
log.info("clearOldData", `Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
} else {
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
const sqlHourOffset = Database.sqlHourOffset();
try {
await R.exec(
"DELETE FROM heartbeat WHERE time < " + sqlHourOffset,
[ parsedPeriod * -24 ]
);
// Heartbeat
await R.exec("DELETE FROM heartbeat WHERE time < " + sqlHourOffset, [
parsedPeriod * -24,
]);
let timestamp = dayjs().subtract(parsedPeriod, "day").utc().startOf("day").unix();
// stat_daily
await R.exec("DELETE FROM stat_daily WHERE timestamp < ? ", [
timestamp,
]);
if (Database.dbConfig.type === "sqlite") {
await R.exec("PRAGMA optimize;");
@ -50,6 +56,8 @@ const clearOldData = async () => {
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
}
}
log.debug("clearOldData", "Data cleared.");
};
module.exports = {

View file

@ -1434,7 +1434,7 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
log.debug("monitor", "Sending to " + notification.name);
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will be expired in ${daysRemaining} days`);
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will expire in ${daysRemaining} days`);
sent = true;
} catch (e) {
log.error("monitor", "Cannot send cert notification to " + notification.name);
@ -1522,7 +1522,7 @@ class Monitor extends BeanModel {
*/
static async getMonitorTag(monitorIDs) {
return await R.getAll(`
SELECT monitor_tag.monitor_id, monitor_tag.tag_id, tag.name, tag.color
SELECT monitor_tag.monitor_id, monitor_tag.tag_id, monitor_tag.value, tag.name, tag.color
FROM monitor_tag
JOIN tag ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id IN (${monitorIDs.map((_) => "?").join(",")})
@ -1567,6 +1567,8 @@ class Monitor extends BeanModel {
}
tagsMap.get(row.monitor_id).push({
tag_id: row.tag_id,
monitor_id: row.monitor_id,
value: row.value,
name: row.name,
color: row.color
});

View file

@ -485,7 +485,7 @@ function ApiCache() {
}
if (typeof duration === "string") {
let split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
let split = duration.match(/^([\d\.,]+)\s?([a-zA-Z]+)$/);
if (split.length === 3) {
let len = parseFloat(split[1]);

View file

@ -240,6 +240,14 @@ class RealBrowserMonitorType extends MonitorType {
const context = await browser.newContext();
const page = await context.newPage();
// Prevent Local File Inclusion
// Accept only http:// and https://
// https://github.com/louislam/uptime-kuma/security/advisories/GHSA-2qgm-m29m-cj2h
let url = new URL(monitor.url);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("Invalid url protocol, only http and https are allowed.");
}
const res = await page.goto(monitor.url, {
waitUntil: "networkidle",
timeout: monitor.interval * 1000 * 0.8,

View file

@ -17,7 +17,7 @@ class AliyunSMS extends NotificationProvider {
if (heartbeatJSON != null) {
let msgBody = JSON.stringify({
name: monitorJSON["name"],
time: heartbeatJSON["time"],
time: heartbeatJSON["localDateTime"],
status: this.statusToString(heartbeatJSON["status"]),
msg: heartbeatJSON["msg"],
});

View file

@ -72,6 +72,7 @@ class GoogleChat extends NotificationProvider {
// construct json data
let data = {
fallbackText: chatHeader["title"],
cardsV2: [
{
card: {

View file

@ -1,11 +1,10 @@
const NotificationProvider = require("./notification-provider");
const {
relayInit,
getPublicKey,
getEventHash,
getSignature,
finalizeEvent,
Relay,
kinds,
nip04,
nip19
nip19,
} = require("nostr-tools");
// polyfills for node versions
@ -31,7 +30,6 @@ class Nostr extends NotificationProvider {
const createdAt = Math.floor(Date.now() / 1000);
const senderPrivateKey = await this.getPrivateKey(notification.sender);
const senderPublicKey = getPublicKey(senderPrivateKey);
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
// Create NIP-04 encrypted direct message event for each recipient
@ -39,34 +37,41 @@ class Nostr extends NotificationProvider {
for (const recipientPublicKey of recipientsPublicKeys) {
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
let event = {
kind: 4,
pubkey: senderPublicKey,
kind: kinds.EncryptedDirectMessage,
created_at: createdAt,
tags: [[ "p", recipientPublicKey ]],
content: ciphertext,
};
event.id = getEventHash(event);
event.sig = getSignature(event, senderPrivateKey);
events.push(event);
const signedEvent = finalizeEvent(event, senderPrivateKey);
events.push(signedEvent);
}
// Publish events to each relay
const relays = notification.relays.split("\n");
let successfulRelays = 0;
// Connect to each relay
for (const relayUrl of relays) {
const relay = relayInit(relayUrl);
try {
await relay.connect();
successfulRelays++;
const relay = await Relay.connect(relayUrl);
let eventIndex = 0;
// Publish events
for (const event of events) {
relay.publish(event);
}
// Authenticate to the relay, if required
try {
await relay.publish(events[0]);
eventIndex = 1;
} catch (error) {
continue;
if (relay.challenge) {
await relay.auth(async (evt) => {
return finalizeEvent(evt, senderPrivateKey);
});
}
}
try {
for (let i = eventIndex; i < events.length; i++) {
await relay.publish(events[i]);
}
successfulRelays++;
} catch (error) {
console.error(`Failed to publish event to ${relayUrl}:`, error);
} finally {
relay.close();
}
@ -90,7 +95,7 @@ class Nostr extends NotificationProvider {
const { data } = senderDecodeResult;
return data;
} catch (error) {
throw new Error(`Failed to get private key: ${error.message}`);
throw new Error(`Failed to decode private key for sender ${sender}: ${error.message}`);
}
}
@ -109,10 +114,10 @@ class Nostr extends NotificationProvider {
if (type === "npub") {
publicKeys.push(data);
} else {
throw new Error("not an npub");
throw new Error(`Recipient ${recipient} is not an npub`);
}
} catch (error) {
throw new Error(`Error decoding recipient: ${error}`);
throw new Error(`Error decoding recipient ${recipient}: ${error}`);
}
}
return publicKeys;

View file

@ -1,3 +1,6 @@
const { Liquid } = require("liquidjs");
const { DOWN } = require("../../src/util");
class NotificationProvider {
/**
@ -49,6 +52,50 @@ class NotificationProvider {
}
}
/**
* Renders a message template with notification context
* @param {string} template the template
* @param {string} msg the message that will be included in the context
* @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only)
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {Promise<string>} rendered template
*/
async renderTemplate(template, msg, monitorJSON, heartbeatJSON) {
const engine = new Liquid();
const parsedTpl = engine.parse(template);
// Let's start with dummy values to simplify code
let monitorName = "Monitor Name not available";
let monitorHostnameOrURL = "testing.hostname";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
monitorHostnameOrURL = this.extractAddress(monitorJSON);
}
let serviceStatus = "⚠️ Test";
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
const context = {
// for v1 compatibility, to be removed in v3
"STATUS": serviceStatus,
"NAME": monitorName,
"HOSTNAME_OR_URL": monitorHostnameOrURL,
// variables which are officially supported
"status": serviceStatus,
"name": monitorName,
"hostnameOrURL": monitorHostnameOrURL,
monitorJSON,
heartbeatJSON,
msg,
};
return engine.render(parsedTpl, context);
}
/**
* Throws an error
* @param {any} error The error to throw

View file

@ -11,7 +11,8 @@ class PushDeer extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
const url = `${serverUrl.trim().replace(/\/*$/, "")}/message/push`;
// capture group below is nessesary to prevent an ReDOS-attack
const url = `${serverUrl.trim().replace(/([^/])\/+$/, "$1")}/message/push`;
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;

View file

@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setSettings, setting } = require("../util-server");
const { getMonitorRelativeURL, UP } = require("../../src/util");
const { getMonitorRelativeURL, UP, log } = require("../../src/util");
class Slack extends NotificationProvider {
name = "slack";
@ -50,15 +50,20 @@ class Slack extends NotificationProvider {
const address = this.extractAddress(monitorJSON);
if (address) {
actions.push({
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit site",
},
"value": "Site",
"url": address,
});
try {
actions.push({
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit site",
},
"value": "Site",
"url": new URL(address),
});
} catch (e) {
log.debug("slack", `Failed to parse address ${address} as URL`);
}
}
return actions;

View file

@ -1,7 +1,5 @@
const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider");
const { DOWN } = require("../../src/util");
const { Liquid } = require("liquidjs");
class SMTP extends NotificationProvider {
name = "smtp";
@ -53,15 +51,11 @@ class SMTP extends NotificationProvider {
const customSubject = notification.customSubject?.trim() || "";
const customBody = notification.customBody?.trim() || "";
const context = this.generateContext(msg, monitorJSON, heartbeatJSON);
const engine = new Liquid();
if (customSubject !== "") {
const tpl = engine.parse(customSubject);
subject = await engine.render(tpl, context);
subject = await this.renderTemplate(customSubject, msg, monitorJSON, heartbeatJSON);
}
if (customBody !== "") {
const tpl = engine.parse(customBody);
body = await engine.render(tpl, context);
body = await this.renderTemplate(customBody, msg, monitorJSON, heartbeatJSON);
}
}
@ -78,43 +72,6 @@ class SMTP extends NotificationProvider {
return okMsg;
}
/**
* Generate context for LiquidJS
* @param {string} msg the message that will be included in the context
* @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only)
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {{STATUS: string, status: string, HOSTNAME_OR_URL: string, hostnameOrUrl: string, NAME: string, name: string, monitorJSON: ?object, heartbeatJSON: ?object, msg: string}} the context
*/
generateContext(msg, monitorJSON, heartbeatJSON) {
// Let's start with dummy values to simplify code
let monitorName = "Monitor Name not available";
let monitorHostnameOrURL = "testing.hostname";
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
monitorHostnameOrURL = this.extractAddress(monitorJSON);
}
let serviceStatus = "⚠️ Test";
if (heartbeatJSON !== null) {
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
}
return {
// for v1 compatibility, to be removed in v3
"STATUS": serviceStatus,
"NAME": monitorName,
"HOSTNAME_OR_URL": monitorHostnameOrURL,
// variables which are officially supported
"status": serviceStatus,
"name": monitorName,
"hostnameOrURL": monitorHostnameOrURL,
monitorJSON,
heartbeatJSON,
msg,
};
}
}
module.exports = SMTP;

View file

@ -22,6 +22,14 @@ class Telegram extends NotificationProvider {
params.message_thread_id = notification.telegramMessageThreadID;
}
if (notification.telegramUseTemplate) {
params.text = await this.renderTemplate(notification.telegramTemplate, msg, monitorJSON, heartbeatJSON);
if (notification.telegramTemplateParseMode !== "plain") {
params.parse_mode = notification.telegramTemplateParseMode;
}
}
await axios.get(`${url}/bot${notification.telegramBotToken}/sendMessage`, {
params: params,
});

View file

@ -1,7 +1,6 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const FormData = require("form-data");
const { Liquid } = require("liquidjs");
class Webhook extends NotificationProvider {
name = "webhook";
@ -28,17 +27,7 @@ class Webhook extends NotificationProvider {
config.headers = formData.getHeaders();
data = formData;
} else if (notification.webhookContentType === "custom") {
// Initialize LiquidJS and parse the custom Body Template
const engine = new Liquid();
const tpl = engine.parse(notification.webhookCustomBody);
// Insert templated values into Body
data = await engine.render(tpl,
{
msg,
heartbeatJSON,
monitorJSON
});
data = await this.renderTemplate(notification.webhookCustomBody, msg, monitorJSON, heartbeatJSON);
}
if (notification.webhookAdditionalHeaders) {

View file

@ -24,7 +24,7 @@ class Whapi extends NotificationProvider {
"body": msg,
};
let url = (notification.whapiApiUrl || "https://gate.whapi.cloud/").replace(/\/+$/, "") + "/messages/text";
let url = (notification.whapiApiUrl || "https://gate.whapi.cloud/").replace(/([^/])\/+$/, "$1") + "/messages/text";
await axios.post(url, data, config);

View file

@ -0,0 +1,57 @@
const NotificationProvider = require("./notification-provider");
const { DOWN, UP } = require("../../src/util");
const { default: axios } = require("axios");
class YZJ extends NotificationProvider {
name = "YZJ";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
if (heartbeatJSON !== null) {
msg = `${this.statusToString(heartbeatJSON["status"])} ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
const config = {
headers: {
"Content-Type": "application/json",
},
};
const params = {
content: msg
};
// yzjtype=0 => general robot
const url = `${notification.yzjWebHookUrl}?yzjtype=0&yzjtoken=${notification.yzjToken}`;
const result = await axios.post(url, params, config);
if (!result.data?.success) {
throw new Error(result.data?.errmsg ?? "yzj's server did not respond with the expected result");
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Convert status constant to string
* @param {string} status The status constant
* @returns {string} status
*/
statusToString(status) {
switch (status) {
case DOWN:
return "❌";
case UP:
return "✅";
default:
return status;
}
}
}
module.exports = YZJ;

View file

@ -69,6 +69,7 @@ const Cellsynt = require("./notification-providers/cellsynt");
const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid");
const YZJ = require("./notification-providers/yzj");
class Notification {
@ -154,7 +155,8 @@ class Notification {
new GtxMessaging(),
new Cellsynt(),
new Wpush(),
new SendGrid()
new SendGrid(),
new YZJ()
];
for (let item of list) {
if (! item.name) {

View file

@ -1,7 +1,7 @@
const { R } = require("redbean-node");
const HttpProxyAgent = require("http-proxy-agent");
const HttpsProxyAgent = require("https-proxy-agent");
const SocksProxyAgent = require("socks-proxy-agent");
const { HttpProxyAgent } = require("http-proxy-agent");
const { HttpsProxyAgent } = require("https-proxy-agent");
const { SocksProxyAgent } = require("socks-proxy-agent");
const { debug } = require("../src/util");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const { CookieJar } = require("tough-cookie");
@ -100,17 +100,17 @@ class Proxy {
let jar = new CookieJar();
const proxyOptions = {
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
cookies: { jar },
};
const proxyUrl = new URL(`${proxy.protocol}://${proxy.host}:${proxy.port}`);
if (proxy.auth) {
proxyOptions.auth = `${proxy.username}:${proxy.password}`;
proxyUrl.username = proxy.username;
proxyUrl.password = proxy.password;
}
debug(`Proxy Options: ${JSON.stringify(proxyOptions)}`);
debug(`Proxy URL: ${proxyUrl.toString()}`);
debug(`HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
debug(`HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
@ -122,15 +122,15 @@ class Proxy {
// eslint-disable-next-line no-case-declarations
const HttpsCookieProxyAgent = createCookieAgent(HttpsProxyAgent);
httpAgent = new HttpCookieProxyAgent({
...httpAgentOptions || {},
httpAgent = new HttpCookieProxyAgent(proxyUrl.toString(), {
...(httpAgentOptions || {}),
...proxyOptions,
});
httpsAgent = new HttpsCookieProxyAgent(proxyUrl.toString(), {
...(httpsAgentOptions || {}),
...proxyOptions,
});
httpsAgent = new HttpsCookieProxyAgent({
...httpsAgentOptions || {},
...proxyOptions,
});
break;
case "socks":
case "socks5":
@ -138,10 +138,9 @@ class Proxy {
case "socks4":
// eslint-disable-next-line no-case-declarations
const SocksCookieProxyAgent = createCookieAgent(SocksProxyAgent);
agent = new SocksCookieProxyAgent({
agent = new SocksCookieProxyAgent(proxyUrl.toString(), {
...httpAgentOptions,
...httpsAgentOptions,
...proxyOptions,
tls: {
rejectUnauthorized: httpsAgentOptions.rejectUnauthorized,
},

View file

@ -15,11 +15,13 @@ const server = UptimeKumaServer.getInstance();
router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
slug = slug.toLowerCase();
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
slug = slug.toLowerCase();
await StatusPage.handleStatusPageRSSResponse(response, slug);
});
@ -37,6 +39,7 @@ router.get("/status-page", cache("5 minutes"), async (request, response) => {
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
slug = slug.toLowerCase();
try {
// Get Status Page
@ -69,6 +72,7 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
let uptimeList = {};
let slug = request.params.slug;
slug = slug.toLowerCase();
let statusPageID = await StatusPage.slugToID(slug);
let monitorIDList = await R.getCol(`
@ -111,6 +115,7 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async (request, response) => {
allowDevAllOrigin(response);
let slug = request.params.slug;
slug = slug.toLowerCase();
try {
// Get Status Page
@ -145,7 +150,8 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
// overall status-page status badge
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
const slug = request.params.slug;
let slug = request.params.slug;
slug = slug.toLowerCase();
const statusPageID = await StatusPage.slugToID(slug);
const {
label,

View file

@ -1604,18 +1604,20 @@ let needSetup = false;
await server.start();
server.httpServer.listen(port, hostname, () => {
server.httpServer.listen(port, hostname, async () => {
if (hostname) {
log.info("server", `Listening on ${hostname}:${port}`);
} else {
log.info("server", `Listening on ${port}`);
}
startMonitors();
await startMonitors();
// Put this here. Start background jobs after the db and server is ready to prevent clear up during db migration.
await initBackgroundJobs();
checkVersion.startInterval();
});
await initBackgroundJobs();
// Start cloudflared at the end if configured
await cloudflaredAutoStart(cloudflaredToken);
@ -1714,7 +1716,7 @@ async function initDatabase(testMode = false) {
log.info("server", "Connected to the database");
// Patch the database
await Database.patch();
await Database.patch(port, hostname);
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret",
@ -1809,7 +1811,11 @@ async function startMonitors() {
}
for (let monitor of list) {
await monitor.start(io);
try {
await monitor.start(io);
} catch (e) {
log.error("monitor", e);
}
// Give some delays, so all monitors won't make request at the same moment when just start the server.
await sleep(getRandomInt(300, 1000));
}

View file

@ -220,13 +220,17 @@ module.exports.statusPageSocketHandler = (socket) => {
// Delete groups that are not in the list
log.debug("socket", "Delete groups that are not in the list");
const slots = groupIDList.map(() => "?").join(",");
if (groupIDList.length === 0) {
await R.exec("DELETE FROM `group` WHERE status_page_id = ?", [ statusPage.id ]);
} else {
const slots = groupIDList.map(() => "?").join(",");
const data = [
...groupIDList,
statusPage.id
];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
const data = [
...groupIDList,
statusPage.id
];
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
}
const server = UptimeKumaServer.getInstance();
@ -288,6 +292,7 @@ module.exports.statusPageSocketHandler = (socket) => {
ok: true,
msg: "successAdded",
msgi18n: true,
slug: slug
});
} catch (error) {

View file

@ -12,7 +12,6 @@ class UptimeCalculator {
* @private
* @type {{string:UptimeCalculator}}
*/
static list = {};
/**
@ -55,6 +54,15 @@ class UptimeCalculator {
lastHourlyStatBean = null;
lastMinutelyStatBean = null;
/**
* For migration purposes.
* @type {boolean}
*/
migrationMode = false;
statMinutelyKeepHour = 24;
statHourlyKeepDay = 30;
/**
* Get the uptime calculator for a monitor
* Initializes and returns the monitor if it does not exist
@ -189,16 +197,19 @@ class UptimeCalculator {
/**
* @param {number} status status
* @param {number} ping Ping
* @param {dayjs.Dayjs} date Date (Only for migration)
* @returns {dayjs.Dayjs} date
* @throws {Error} Invalid status
*/
async update(status, ping = 0) {
let date = this.getCurrentDate();
async update(status, ping = 0, date) {
if (!date) {
date = this.getCurrentDate();
}
let flatStatus = this.flatStatus(status);
if (flatStatus === DOWN && ping > 0) {
log.warn("uptime-calc", "The ping is not effective when the status is DOWN");
log.debug("uptime-calc", "The ping is not effective when the status is DOWN");
}
let divisionKey = this.getMinutelyKey(date);
@ -297,47 +308,61 @@ class UptimeCalculator {
}
await R.store(dailyStatBean);
let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
hourlyStatBean.up = hourlyData.up;
hourlyStatBean.down = hourlyData.down;
hourlyStatBean.ping = hourlyData.avgPing;
hourlyStatBean.pingMin = hourlyData.minPing;
hourlyStatBean.pingMax = hourlyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
if (Object.keys(extras).length > 0) {
hourlyStatBean.extras = JSON.stringify(extras);
let currentDate = this.getCurrentDate();
// For migration mode, we don't need to store old hourly and minutely data, but we need 30-day's hourly data
// Run anyway for non-migration mode
if (!this.migrationMode || date.isAfter(currentDate.subtract(this.statHourlyKeepDay, "day"))) {
let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
hourlyStatBean.up = hourlyData.up;
hourlyStatBean.down = hourlyData.down;
hourlyStatBean.ping = hourlyData.avgPing;
hourlyStatBean.pingMin = hourlyData.minPing;
hourlyStatBean.pingMax = hourlyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
if (Object.keys(extras).length > 0) {
hourlyStatBean.extras = JSON.stringify(extras);
}
}
await R.store(hourlyStatBean);
}
await R.store(hourlyStatBean);
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
minutelyStatBean.up = minutelyData.up;
minutelyStatBean.down = minutelyData.down;
minutelyStatBean.ping = minutelyData.avgPing;
minutelyStatBean.pingMin = minutelyData.minPing;
minutelyStatBean.pingMax = minutelyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
if (Object.keys(extras).length > 0) {
minutelyStatBean.extras = JSON.stringify(extras);
// For migration mode, we don't need to store old hourly and minutely data, but we need 24-hour's minutely data
// Run anyway for non-migration mode
if (!this.migrationMode || date.isAfter(currentDate.subtract(this.statMinutelyKeepHour, "hour"))) {
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
minutelyStatBean.up = minutelyData.up;
minutelyStatBean.down = minutelyData.down;
minutelyStatBean.ping = minutelyData.avgPing;
minutelyStatBean.pingMin = minutelyData.minPing;
minutelyStatBean.pingMax = minutelyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
if (Object.keys(extras).length > 0) {
minutelyStatBean.extras = JSON.stringify(extras);
}
}
await R.store(minutelyStatBean);
}
await R.store(minutelyStatBean);
// Remove the old data
log.debug("uptime-calc", "Remove old data");
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
this.monitorID,
this.getMinutelyKey(date.subtract(24, "hour")),
]);
// No need to remove old data in migration mode
if (!this.migrationMode) {
// Remove the old data
// TODO: Improvement: Convert it to a job?
log.debug("uptime-calc", "Remove old data");
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
this.monitorID,
this.getMinutelyKey(currentDate.subtract(this.statMinutelyKeepHour, "hour")),
]);
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
this.monitorID,
this.getHourlyKey(date.subtract(30, "day")),
]);
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
this.monitorID,
this.getHourlyKey(currentDate.subtract(this.statHourlyKeepDay, "day")),
]);
}
return date;
}
@ -812,6 +837,14 @@ class UptimeCalculator {
return dayjs.utc();
}
/**
* For migration purposes.
* @param {boolean} value Migration mode on/off
* @returns {void}
*/
setMigrationMode(value) {
this.migrationMode = value;
}
}
class UptimeDataResult {

View file

@ -0,0 +1,22 @@
const ColumnCompilerMySQL = require("knex/lib/dialects/mysql/schema/mysql-columncompiler");
const { formatDefault } = require("knex/lib/formatter/formatterUtils");
const { log } = require("../../../../../../../src/util");
class KumaColumnCompiler extends ColumnCompilerMySQL {
/**
* Override defaultTo method to handle default value for TEXT fields
* @param {any} value Value
* @returns {string|void} Default value (Don't understand why it can return void or string, but it's the original code, lol)
*/
defaultTo(value) {
if (this.type === "text" && typeof value === "string") {
log.debug("defaultTo", `${this.args[0]}: ${this.type} ${value} ${typeof value}`);
// MySQL 8.0 is required and only if the value is written as an expression: https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html
// MariaDB 10.2 is required: https://mariadb.com/kb/en/text/
return `default (${formatDefault(value, this.type, this.client)})`;
}
return super.defaultTo.apply(this, arguments);
}
}
module.exports = KumaColumnCompiler;

View file

@ -0,0 +1,84 @@
const express = require("express");
const http = require("node:http");
const { log } = require("../../src/util");
/**
* SimpleMigrationServer
* For displaying the migration status of the server
* Also, it is used to let Docker healthcheck know the status of the server, as the main server is not started yet, healthcheck will think the server is down incorrectly.
*/
class SimpleMigrationServer {
/**
* Express app instance
* @type {?Express}
*/
app;
/**
* Server instance
* @type {?Server}
*/
server;
/**
* Response object
* @type {?Response}
*/
response;
/**
* Start the server
* @param {number} port Port
* @param {string} hostname Hostname
* @returns {Promise<void>}
*/
start(port, hostname) {
this.app = express();
this.server = http.createServer(this.app);
this.app.get("/", (req, res) => {
res.set("Content-Type", "text/plain");
res.write("Migration is in progress, listening message...\n");
if (this.response) {
this.response.write("Disconnected\n");
this.response.end();
}
this.response = res;
// never ending response
});
return new Promise((resolve) => {
this.server.listen(port, hostname, () => {
if (hostname) {
log.info("migration", `Migration server is running on http://${hostname}:${port}`);
} else {
log.info("migration", `Migration server is running on http://localhost:${port}`);
}
resolve();
});
});
}
/**
* Update the message
* @param {string} msg Message to update
* @returns {void}
*/
update(msg) {
this.response?.write(msg + "\n");
}
/**
* Stop the server
* @returns {Promise<void>}
*/
async stop() {
this.response?.write("Finished, please refresh this page.\n");
this.response?.end();
await this.server?.close();
}
}
module.exports = {
SimpleMigrationServer,
};

View file

@ -619,7 +619,7 @@ $shadow-box-padding: 20px;
bottom: 0;
margin-left: -$shadow-box-padding;
margin-right: -$shadow-box-padding;
z-index: 100;
z-index: 10;
background-color: rgba(white, 0.2);
backdrop-filter: blur(2px);
border-radius: 0 0 10px 10px;

View file

@ -73,12 +73,6 @@ export default {
}
},
created() {
if (this.model.length === 0) {
this.addCondition();
}
},
methods: {
getNewGroup() {
return {

View file

@ -183,6 +183,7 @@ export default {
"ServerChan": "ServerChan (Server酱)",
"smsc": "SMSC",
"WPush": "WPush(wpush.cn)",
"YZJ": "YZJ (云之家自定义机器人)"
};
// Sort by notification name

View file

@ -197,7 +197,7 @@ export default {
return tagOptions;
},
selectedTags() {
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.id === tag.id));
return this.preSelectedTags.concat(this.newTags).filter(tag => !this.deleteTags.find(monitorTag => monitorTag.tag_id === tag.tag_id));
},
colorOptions() {
return colorOptions(this);

View file

@ -0,0 +1,75 @@
<template>
<div class="form-text mb-2">
<i18n-t tag="div" keypath="liquidIntroduction">
<a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a>
</i18n-t>
<code v-pre>{{ msg }}</code>: {{ $t("templateMsg") }}<br />
<code v-pre>{{ name }}</code>: {{ $t("templateServiceName") }}<br />
<code v-pre>{{ status }}</code>: {{ $t("templateStatus") }}<br />
<code v-pre>{{ hostnameOrURL }}</code>: {{ $t("templateHostnameOrURL") }}<br />
<code v-pre>{{ heartbeatJSON }}</code>: {{ $t("templateHeartbeatJSON") }} <b>({{ $t("templateLimitedToUpDownNotifications") }})</b><br />
<code v-pre>{{ monitorJSON }}</code>: {{ $t("templateMonitorJSON") }} <b>({{ $t("templateLimitedToUpDownCertNotifications") }})</b><br />
</div>
<input
:id="id"
ref="templatedInput"
v-model="model"
type="text"
class="form-control"
:placeholder="placeholder"
:required="required"
autocomplete="false"
>
</template>
<script>
export default {
props: {
/**
* The value of the templated input.
*/
modelValue: {
type: String,
default: ""
},
/**
* id for the templated input.
*/
id: {
type: String,
required: true,
},
/**
* Whether the templated input is required.
* @example true
*/
required: {
type: Boolean,
required: true,
},
/**
* Placeholder text for the templated input.
*/
placeholder: {
type: String,
default: ""
},
},
emits: [ "update:modelValue" ],
computed: {
/**
* Send value update to parent on change.
*/
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
};
</script>

View file

@ -0,0 +1,80 @@
<template>
<div class="form-text mb-2">
<i18n-t tag="div" keypath="liquidIntroduction">
<a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a>
</i18n-t>
<code v-pre>{{ msg }}</code>: {{ $t("templateMsg") }}<br />
<code v-pre>{{ name }}</code>: {{ $t("templateServiceName") }}<br />
<code v-pre>{{ status }}</code>: {{ $t("templateStatus") }}<br />
<code v-pre>{{ hostnameOrURL }}</code>: {{ $t("templateHostnameOrURL") }}<br />
<code v-pre>{{ heartbeatJSON }}</code>: {{ $t("templateHeartbeatJSON") }} <b>({{ $t("templateLimitedToUpDownNotifications") }})</b><br />
<code v-pre>{{ monitorJSON }}</code>: {{ $t("templateMonitorJSON") }} <b>({{ $t("templateLimitedToUpDownCertNotifications") }})</b><br />
</div>
<textarea
:id="id"
ref="templatedTextarea"
v-model="model"
class="form-control"
:placeholder="placeholder"
:required="required"
autocomplete="false"
></textarea>
</template>
<script>
export default {
props: {
/**
* The value of the templated textarea.
*/
modelValue: {
type: String,
default: ""
},
/**
* id for the templated textarea.
*/
id: {
type: String,
required: true,
},
/**
* Whether the templated textarea is required.
* @example true
*/
required: {
type: Boolean,
required: true,
},
/**
* Placeholder text for the templated textarea.
*/
placeholder: {
type: String,
default: ""
},
},
emits: [ "update:modelValue" ],
computed: {
/**
* Send value update to parent on change.
*/
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
};
</script>
<style lang="scss" scoped>
textarea {
min-height: 150px;
}
</style>

View file

@ -67,25 +67,15 @@
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
</div>
<p class="form-text">
<i18n-t tag="div" keypath="smtpLiquidIntroduction" class="form-text mb-3">
<a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a>
</i18n-t>
<code v-pre>{{name}}</code>: {{ $t("emailTemplateServiceName") }}<br />
<code v-pre>{{msg}}</code>: {{ $t("emailTemplateMsg") }}<br />
<code v-pre>{{status}}</code>: {{ $t("emailTemplateStatus") }}<br />
<code v-pre>{{heartbeatJSON}}</code>: {{ $t("emailTemplateHeartbeatJSON") }}<b>{{ $t("emailTemplateLimitedToUpDownNotification") }}</b><br />
<code v-pre>{{monitorJSON}}</code>: {{ $t("emailTemplateMonitorJSON") }} <b>{{ $t("emailTemplateLimitedToUpDownNotification") }}</b><br />
<code v-pre>{{hostnameOrURL}}</code>: {{ $t("emailTemplateHostnameOrURL") }}<br />
</p>
<div class="mb-3">
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
<TemplatedInput id="subject-email" v-model="$parent.notification.customSubject" :required="false" placeholder=""></TemplatedInput>
<div class="form-text">{{ $t("leave blank for default subject") }}</div>
</div>
<div class="mb-3">
<label for="body-email" class="form-label">{{ $t("emailCustomBody") }}</label>
<textarea id="body-email" v-model="$parent.notification.customBody" type="text" class="form-control" autocomplete="false" placeholder=""></textarea>
<TemplatedTextarea id="body-email" v-model="$parent.notification.customBody" :required="false" placeholder=""></TemplatedTextarea>
<div class="form-text">{{ $t("leave blank for default body") }}</div>
</div>
@ -124,11 +114,15 @@
<script>
import HiddenInput from "../HiddenInput.vue";
import TemplatedInput from "../TemplatedInput.vue";
import TemplatedTextarea from "../TemplatedTextarea.vue";
import ToggleSection from "../ToggleSection.vue";
export default {
components: {
HiddenInput,
TemplatedInput,
TemplatedTextarea,
ToggleSection,
},
computed: {

View file

@ -5,20 +5,20 @@
</div>
<div class="mb-3">
<label for="sendgrid-from-email" class="form-label">{{ $t("From Email") }}</label>
<input id="sendgrid-from-email" v-model="$parent.notification.sendgridFromEmail" type="email" class="form-control" required>
<input id="sendgrid-from-email" v-model="$parent.notification.sendgridFromEmail" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="sendgrid-to-email" class="form-label">{{ $t("To Email") }}</label>
<input id="sendgrid-to-email" v-model="$parent.notification.sendgridToEmail" type="email" class="form-control" required>
<input id="sendgrid-to-email" v-model="$parent.notification.sendgridToEmail" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="sendgrid-cc-email" class="form-label">{{ $t("smtpCC") }}</label>
<input id="sendgrid-cc-email" v-model="$parent.notification.sendgridCcEmail" type="email" class="form-control">
<input id="sendgrid-cc-email" v-model="$parent.notification.sendgridCcEmail" type="text" class="form-control">
<div class="form-text">{{ $t("Separate multiple email addresses with commas") }}</div>
</div>
<div class="mb-3">
<label for="sendgrid-bcc-email" class="form-label">{{ $t("smtpBCC") }}</label>
<input id="sendgrid-bcc-email" v-model="$parent.notification.sendgridBccEmail" type="email" class="form-control">
<input id="sendgrid-bcc-email" v-model="$parent.notification.sendgridBccEmail" type="text" class="form-control">
<small class="form-text text-muted">{{ $t("Separate multiple email addresses with commas") }}</small>
</div>
<div class="mb-3">

View file

@ -4,6 +4,9 @@
<input id="slack-webhook-url" v-model="$parent.notification.slackwebhookURL" type="text" class="form-control" required>
<label for="slack-username" class="form-label">{{ $t("Username") }}</label>
<input id="slack-username" v-model="$parent.notification.slackusername" type="text" class="form-control">
<div class="form-text">
{{ $t("aboutSlackUsername") }}
</div>
<label for="slack-iconemo" class="form-label">{{ $t("Icon Emoji") }}</label>
<input id="slack-iconemo" v-model="$parent.notification.slackiconemo" type="text" class="form-control">
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>

View file

@ -32,7 +32,42 @@
<label for="message_thread_id" class="form-label">{{ $t("telegramMessageThreadID") }}</label>
<input id="message_thread_id" v-model="$parent.notification.telegramMessageThreadID" type="text" class="form-control">
<p class="form-text">{{ $t("telegramMessageThreadIDDescription") }}</p>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input v-model="$parent.notification.telegramUseTemplate" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("telegramUseTemplate") }}</label>
</div>
<div class="form-text">
{{ $t("telegramUseTemplateDescription") }}
</div>
</div>
<template v-if="$parent.notification.telegramUseTemplate">
<div class="mb-3">
<label class="form-label" for="message_parse_mode">{{ $t("Message Format") }}</label>
<select
id="message_parse_mode"
v-model="$parent.notification.telegramTemplateParseMode"
class="form-select"
required
>
<option value="plain">{{ $t("Plain Text") }}</option>
<option value="HTML">HTML</option>
<option value="MarkdownV2">MarkdownV2</option>
</select>
<i18n-t tag="p" keypath="telegramTemplateFormatDescription" class="form-text">
<a href="https://core.telegram.org/bots/api#formatting-options" target="_blank">{{ $t("documentation") }}</a>
</i18n-t>
<label class="form-label" for="message_template">{{ $t('Message Template') }}</label>
<TemplatedTextarea id="message_template" v-model="$parent.notification.telegramTemplate" :required="true" :placeholder="telegramTemplatedTextareaPlaceholder"></TemplatedTextarea>
</div>
</template>
<div class="mb-3">
<div class="form-check form-switch">
<input v-model="$parent.notification.telegramSendSilently" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t("telegramSendSilently") }}</label>
@ -57,11 +92,24 @@
<script>
import HiddenInput from "../HiddenInput.vue";
import TemplatedTextarea from "../TemplatedTextarea.vue";
import axios from "axios";
export default {
components: {
HiddenInput,
TemplatedTextarea,
},
computed: {
telegramTemplatedTextareaPlaceholder() {
return this.$t("Example:", [
`
Uptime Kuma Alert{% if monitorJSON %} - {{ monitorJSON['name'] }}{% endif %}
{{ msg }}
`,
]);
}
},
methods: {
/**
@ -115,3 +163,9 @@ export default {
}
};
</script>
<style lang="scss" scoped>
textarea {
min-height: 150px;
}
</style>

View file

@ -32,20 +32,7 @@
</template>
</i18n-t>
<template v-else-if="$parent.notification.webhookContentType == 'custom'">
<i18n-t tag="div" keypath="liquidIntroduction" class="form-text">
<a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a>
</i18n-t>
<code v-pre>{{msg}}</code>: {{ $t("templateMsg") }}<br />
<code v-pre>{{heartbeatJSON}}</code>: {{ $t("templateHeartbeatJSON") }} <b>({{ $t("templateLimitedToUpDownNotifications") }})</b><br />
<code v-pre>{{monitorJSON}}</code>: {{ $t("templateMonitorJSON") }} <b>({{ $t("templateLimitedToUpDownCertNotifications") }})</b><br />
<textarea
id="customBody"
v-model="$parent.notification.webhookCustomBody"
class="form-control"
:placeholder="customBodyPlaceholder"
required
></textarea>
<TemplatedTextarea id="customBody" v-model="$parent.notification.webhookCustomBody" :required="true" :placeholder="customBodyPlaceholder"></TemplatedTextarea>
</template>
</div>
@ -67,7 +54,12 @@
</template>
<script>
import TemplatedTextarea from "../TemplatedTextarea.vue";
export default {
components: {
TemplatedTextarea,
},
data() {
return {
showAdditionalHeadersField: this.$parent.notification.webhookAdditionalHeaders != null,

View file

@ -0,0 +1,19 @@
<template>
<div class="mb-3">
<label for="yzjWebHookUrl" class="form-label">{{ $t("YZJ Webhook URL") }}<span style="color: red;"><sup>*</sup></span></label>
<input id="yzjWebHookUrl" v-model="$parent.notification.yzjWebHookUrl" type="url" class="form-control" required />
<i18n-t class="form-text" keypath="wayToGetTeamsURL">
<a href="https://www.yunzhijia.com/opendocs/docs.html#/tutorial/index/robot" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="yzjToken" class="form-label">{{ $t("YZJ Robot Token") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="yzjToken" v-model="$parent.notification.yzjToken" :required="true" autocomplete="new-password"></HiddenInput>
<i18n-t class="form-text" keypath="wayToGetLineNotifyToken">
<a href="https://www.yunzhijia.com/opendocs/docs.html#/server-api/im/index?id=%e6%8e%a5%e5%8f%a3%e5%9c%b0%e5%9d%80%e5%92%8c%e6%8e%88%e6%9d%83%e7%a0%81" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
</template>
<script setup lang="ts">
import HiddenInput from "../HiddenInput.vue";
</script>

View file

@ -67,6 +67,7 @@ import Cellsynt from "./Cellsynt.vue";
import WPush from "./WPush.vue";
import SIGNL4 from "./SIGNL4.vue";
import SendGrid from "./SendGrid.vue";
import YZJ from "./YZJ.vue";
/**
* Manage all notification form.
@ -142,6 +143,7 @@ const NotificationFormList = {
"Cellsynt": Cellsynt,
"WPush": WPush,
"SendGrid": SendGrid,
"YZJ": YZJ,
};
export default NotificationFormList;

View file

@ -733,5 +733,12 @@
"Remove the expiry notification": "إزالة تنبيه يوم تاريخ الإنتهاء",
"tailscalePingWarning": "من أجل استخدام مراقبة Tailscale Ping، تحتاج إلى تثبيت Uptime Kuma بدون Docker وكذلك تثبيت عميل Tailscale على الخادم الخاص بك.",
"Clone Monitor": "نسخ المراقبة",
"telegramMessageThreadIDDescription": "معرف فريد اختياري لسلسلة الرسائل المستهدفة (الموضوع) في المنتدى؛ لمجموعات المنتدى الكبرى فقط"
"telegramMessageThreadIDDescription": "معرف فريد اختياري لسلسلة الرسائل المستهدفة (الموضوع) في المنتدى؛ لمجموعات المنتدى الكبرى فقط",
"emailCustomBody": "نص مخصص",
"emailTemplateStatus": "الحالة",
"leave blank for default subject": "اترك فارغاً ليتم تعيين الموضوع تلقائياً",
"leave blank for default body": "اترك فارغاً ليتم تعيين النص تلقائياً",
"emailTemplateServiceName": "اسم الخدمة",
"emailTemplateHostnameOrURL": "اسم المضيف أو عنوان URL",
"smspartnerPhoneNumber": "رقم الهاتف"
}

View file

@ -1050,5 +1050,53 @@
"less than": "по-малко от",
"greater than": "по-голямо от",
"greater than or equal to": "по-голямо или равно на",
"record": "запис"
"record": "запис",
"CurlDebugInfo": "За да отстраните грешки в монитора, можете или да поставите това в терминала на Вашата машина, или в терминала на машината, на която работи \"Uptime Kuma\" и да видите заявката.{newiline}Моля, вземете под внимание мрежовите разлики като {firewalls}, {dns_resolvers} или {docker_networks}.",
"shrinkDatabaseDescriptionSqlite": "Инициира {vacuum} за база данни тип SQLite. Функцията {auto_vacuum} вече е активирана, но това не дефрагментира базата данни, нито препакетира отделните страници на базата данни по начина, по който го прави командата {vacuum}.",
"ignoredTLSError": "TLS/SSL грешките са игнорирани",
"Debug": "Отстраняване на грешки",
"Copy": "Копирай",
"CopyToClipboardError": "Неуспешно копиране в клипборда: {error}",
"CopyToClipboardSuccess": "Копирано!",
"firewalls": "защитни стени",
"dns resolvers": "DNS преобразуватели",
"docker networks": "docker мрежи",
"CurlDebugInfoOAuth2CCUnsupported": "Пълният oauth клиентски идентификационен поток не се поддържа в {curl}.{newline}Моля, вземете токен на носител и го предайте чрез опцията {oauth2_bearer}.",
"CurlDebugInfoProxiesUnsupported": "Поддръжката на прокси в горната команда {curl} в момента не е внедрена.",
"Message format": "Формат на съобщението",
"Send rich messages": "Изпращай съобщения в \"rich\" формат",
"Sound": "Звук",
"Notification Channel": "Канал за известяване",
"Alphanumerical string and hyphens only": "Само буквено-цифров низ и тирета",
"Arcade": "Arcade",
"Correct": "Правилно",
"Fail": "Грешка",
"Harp": "Арфа",
"Reveal": "Разкрий",
"Bubble": "Балон",
"Doorbell": "Звънец на врата",
"Flute": "Флейта",
"Money": "Пари",
"Scifi": "Nаучна фантастика",
"Clear": "Премахни",
"Elevator": "Асансьор",
"Guitar": "Китара",
"Pop": "Поп",
"Custom sound to override default notification sound": "Персонализиран звук, заменящ звука за известяване по подразбиране",
"Time Sensitive (iOS Only)": "Зависещи от часа (само за iOS)",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "Известията от типа \"Зависещи от часа\" ще бъдат доставени незабавно, дори ако устройството е в режим „Не безпокойте“.",
"From": "От",
"Can be found on:": "Можте да се откриете на: {0}",
"The phone number of the recipient in E.164 format.": "Телефонният номер на получателя във формат E.164.",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "Идентификационен номер на подателя на текста или телефонен номер във формат E.164, в случай, че желаете да получавате отговори.",
"rabbitmqNodesRequired": "Моля, задайте възлите за този монитор.",
"rabbitmqNodesInvalid": "Моля, използвайте пълния URL адрес (започващ с 'http') за RabbitMQ възли.",
"RabbitMQ Password": "RabbitMQ парола",
"RabbitMQ Username": "RabbitMQ потребител",
"SendGrid API Key": "SendGrid API ключ",
"Separate multiple email addresses with commas": "Разделяйте отделните имейл адреси със запетаи",
"RabbitMQ Nodes": "Възли за управление на RabbitMQ",
"rabbitmqNodesDescription": "Въведете URL адреса на възлите за управление на RabbitMQ, включително протокол и порт. Пример: {0}",
"rabbitmqHelpText": "За да използвате монитора, ще трябва да активирате добавката за управление във вашата настройка на RabbitMQ. За повече информация моля, вижте {rabitmq_documentation}.",
"aboutSlackUsername": "Променя показваното име на подателя на съобщението. Ако желаете да споменете някого, вместо това го включете в приятелското име."
}

View file

@ -1,10 +1,10 @@
{
"setupDatabaseChooseDatabase": "আপনি কোন ডাটাবেজটি ব্যবহার করতে চান?",
"setupDatabaseEmbeddedMariaDB": "আপনাকে কিছু নিযুক্ত করতে হবে না। এই ডকার ইমেজটি (Docker image) স্বয়ংক্রিয়ভাবে আপনার জন্য মারিয়া ডিবি (MariaDB) বসিয়েছে এবং প্রস্তুত করেছে।Uptime Kuma ইউনিক্স সকেটের (Unix Socket) মাধ্যমে এই ডাটাবেসের সাথে সংযুক্ত হবে।",
"setupDatabaseMariaDB": "একটি বহিরাগত মারিয়া ডিবি (MariaDB) ডাটাবেসের সাথে সংযোগ করুন। আপনাকে ডাটাবেস সংযোগ তথ্য নিযুক্ত করতে হবে।",
"setupDatabaseEmbeddedMariaDB": "আপনাকে কিছু সেটআপ করার প্রয়োজন নেই। এই ডকার ইমেজে স্বয়ংক্রিয়ভাবে MariaDB এম্বেড এবং কনফিগার করা হয়েছে। Uptime Kuma এই ডাটাবেজের সাথে ইউনিক্স সকেটের মাধ্যমে সংযুক্ত হবে।",
"setupDatabaseMariaDB": "বহিরাগত MariaDB ডাটাবেজের সাথে সংযোগ করতে হবে। আপনাকে ডাটাবেজের সংযোগ তথ্য সেট করতে হবে।",
"Add": "সংযোগ করুন",
"dbName": "ডাটাবেজের নাম",
"languageName": "ইংরেজি",
"languageName": "বাংলা",
"Settings": "সেটিংস",
"Dashboard": "ড্যাশবোর্ড",
"Help": "সাহায্য",
@ -14,5 +14,216 @@
"Check Update On GitHub": "GitHub-এ আপডেট চেক করুন",
"List": "তালিকা",
"General": "সাধারণ",
"Game": "খেলা"
"Game": "খেলা",
"disable authentication": "প্রমাণীকরণ বন্ধ করুন",
"pauseDashboardHome": "থামুন",
"disableauth.message2": "এটি Uptime Kuma এর সামনে {intendThirdPartyAuth} এর মতো পরিস্থিতির জন্য ডিজাইন করা হয়েছে, যেমন Cloudflare Access, Authelia অথবা অন্যান্য প্রমাণীকরণ ব্যবস্থা।",
"I understand, please disable": "আমি বুঝতে পারছি, দয়া করে অক্ষম করুন",
"Certificate Info": "সার্টিফিকেট তথ্য",
"Create your admin account": "আপনার অ্যাডমিন অ্যাকাউন্ট তৈরি করুন",
"Import": "ইমপোর্ট",
"Default enabled": "ডিফল্ট সক্রিয়",
"Heartbeats": "হার্টবিট",
"Affected Monitors": "প্রভাবিত মনিটরগুলো",
"All Status Pages": "সমস্ত স্ট্যাটাস পেজ",
"alertNoFile": "অনুগ্রহ করে আমদানির জন্য একটি ফাইল নির্বাচন করুন।",
"alertWrongFileType": "অনুগ্রহ করে একটি JSON ফাইল নির্বাচন করুন।",
"Clear all statistics": "সব পরিসংখ্যান মুছে ফেলুন",
"Setup 2FA": "2FA সেট আপ করুন",
"Two Factor Authentication": "দুই ফ্যাক্টর প্রমাণীকরণ",
"Add New Tag": "নতুন ট্যাগ যোগ করুন",
"Tag with this name already exist.": "এই নামের ট্যাগ আগে থেকেই আছে।",
"Pink": "গোলাপী",
"Search monitored sites": "পর্যবেক্ষণ করা সাইট অনুসন্ধান",
"statusPageNothing": "এখানে কিছুই নেই, অনুগ্রহ করে একটি গ্রুপ বা মনিটর যোগ করুন।",
"Degraded Service": "অধঃপতন সেবা",
"Go to Dashboard": "ড্যাশবোর্ডে যান",
"defaultNotificationName": "আমার {বিজ্ঞপ্তি} সতর্কতা ({number})",
"webhookJsonDesc": "{0} যেকোনো আধুনিক HTTP সার্ভার যেমন Express.js-এর জন্য ভালো",
"liquidIntroduction": "টেম্পলেটবিলিটি লিকুইড টেমপ্লেটিং ভাষার মাধ্যমে অর্জন করা হয়। ব্যবহারের নির্দেশাবলীর জন্য অনুগ্রহ করে {0} দেখুন। এই উপলব্ধ ভেরিয়েবল হল:",
"Notifications": "নোটিফিকেশন",
"Setup Notification": "নোটিফিকেশন সেটআপ করুন",
"Light": "আলো",
"Auto": "স্বয়ংক্রিয়",
"Theme - Heartbeat Bar": "থিম - হার্টবিট বার",
"styleElapsedTime": "হার্টবিট বারের নিচে ব্যবহৃত সময়",
"styleElapsedTimeShowNoLine": "শোর করুন (কোনো লাইন নেই)",
"styleElapsedTimeShowWithLine": "শো করুন (লাইন সহ)",
"Normal": "স্বাভাবিক",
"Bottom": "নীচে",
"Timezone": "টাইমজোন",
"Search Engine Visibility": "সার্চ ইঞ্জিন দৃশ্যমানতা",
"Allow indexing": "ইন্ডেক্সিং অনুমোদন করুন",
"Discourage search engines from indexing site": "সার্চ ইঞ্জিনগুলোকে সাইটের ইন্ডেক্সিং থেকে বিরত রাখুন",
"Change Password": "পাসওয়ার্ড পরিবর্তন করুন",
"Current Password": "বর্তমান পাসওয়ার্ড",
"New Password": "নতুন পাসওয়ার্ড",
"Repeat New Password": "নতুন পাসওয়ার্ডটি আবার দিন",
"Update Password": "পাসওয়ার্ড আপডেট করুন",
"Enable Auth": "প্রমাণীকরণ চালু করুন",
"disableauth.message1": "আপনি কি সত্যিই {disableAuth} করতে চান?",
"where you intend to implement third-party authentication": "যেখানে আপনি তৃতীয় পক্ষের প্রমাণীকরণ বাস্তবায়ন করতে চান",
"Please use this option carefully!": "দয়া করে এই অপশনটি সাবধানে ব্যবহার করুন!",
"Logout": "লগ আউট",
"Leave": "লিভ",
"Confirm": "নিশ্চিত",
"Yes": "হ্যাঁ",
"No": "না",
"Username": "ইউজার নেম",
"Password": "পাসওয়ার্ড",
"Remember me": "আমাকে মনে রাখুন",
"Login": "লগ ইন",
"add one": "একটি যোগ করুন",
"Notification Type": "নোটিফিকেশনের ধরন",
"Email": "ইমেইল",
"Test": "পরীক্ষা",
"Resolver Server": "সমাধানকারী সার্ভার",
"Resource Record Type": "রিসোর্স রেকর্ড টাইপ",
"Last Result": "শেষ ফলাফল",
"Repeat Password": "পাসওয়ার্ড পুনরায় দিন",
"Import Backup": "ইমপোর্ট ব্যাকআপ",
"Export Backup": "এক্সপোর্ট ব্যাকআপ",
"Export": "এক্সপোর্ট",
"respTime": "প্রতিক্রিয়া সময় (ms)",
"notAvailableShort": "প্রযোজ্য নয়",
"Apply on all existing monitors": "সমস্ত বিদ্যমান মনিটরে প্রয়োগ করুন",
"Create": "তৈরি",
"Clear Data": "ডেটা সাফ",
"Events": "ইভেন্ট সমূহ",
"Auto Get": "অটো গ্রহণ",
"Schedule maintenance": "রক্ষণাবেক্ষণ সূচী করুন",
"Pick Affected Monitors...": "প্রভাবিত মনিটর নির্বাচন করুন…",
"Start of maintenance": "রক্ষণাবেক্ষণের শুরু",
"Select status pages...": "স্ট্যাটাস পেজগুলো নির্বাচন করুন…",
"Skip existing": "বিদ্যমান এড়িয়ে যান",
"Overwrite": "ওভাররাইট",
"Options": "অপশন",
"Keep both": "দুটোই রাখুন",
"Verify Token": "টোকেন যাচাই করুন",
"Enable 2FA": "2FA সক্ষম করুন",
"Disable 2FA": "2FA নিষ্ক্রিয় করুন",
"2FA Settings": "2FA সেটিংস",
"filterActive": "সক্রিয়",
"filterActivePaused": "বিরতি",
"Active": "সক্রিয়",
"Inactive": "নিষ্ক্রিয়",
"Token": "টোকেন",
"Show URI": "URI দেখান",
"Tags": "ট্যাগ",
"Add New below or Select...": "নীচে নতুন যোগ করুন বা নির্বাচন করুন…",
"Tag with this value already exist.": "এই মান সহ ট্যাগ ইতিমধ্যেই বিদ্যমান।",
"color": "রং",
"value (optional)": "মান (ঐচ্ছিক)",
"Red": "লাল",
"Orange": "কমলা",
"Green": "সবুজ",
"Blue": "নীল",
"Indigo": "নীল",
"Purple": "বেগুনি",
"Custom": "কাস্টম",
"Search...": "অনুসন্ধান…",
"Avg. Ping": "গড় পিং",
"Avg. Response": "গড় প্রতিক্রিয়া",
"Entry Page": "প্রবেশ পৃষ্ঠা",
"statusPageRefreshIn": "রিফ্রেশ হবে: {0}",
"No Services": "কোনো পরিষেবা নেই",
"All Systems Operational": "সমস্ত সিস্টেম অপারেশনাল",
"Partially Degraded Service": "আংশিকভাবে অবনমিত পরিষেবা",
"Add Group": "গ্রুপ যোগ করুন",
"Add a monitor": "একটি মনিটর যোগ করুন",
"Edit Status Page": "এডিট স্টেটাস পেজ",
"Status Page": "স্ট্যাটাস পেজ",
"Status Pages": "স্ট্যাটাস পেজ সমূহ",
"here": "এখানে",
"Required": "প্রয়োজন",
"Post URL": "পোস্ট URL",
"Content Type": "বিষয়বস্তুর প্রকার",
"webhookFormDataDesc": "{multipart} PHP এর জন্য ভালো। JSON-কে {decodeFunction} দিয়ে পার্স করতে হবে",
"templateMsg": "বিজ্ঞপ্তির বার্তা",
"templateHeartbeatJSON": "হৃদস্পন্দন বর্ণনাকারী বস্তু",
"templateMonitorJSON": "মনিটরের বর্ণনাকারী বস্তু",
"templateLimitedToUpDownNotifications": "শুধুমাত্র UP/DOWN বিজ্ঞপ্তির জন্য উপলব্ধ",
"Gray": "ধূসর",
"Add New Monitor": "নতুন আপটাইম মনিটর যোগ করুন",
"statusMaintenance": "রক্ষণাবেক্ষণ",
"Friendly Name": "বন্ধুত্বপূর্ণ নাম",
"URL": "ইউআরএল",
"settingUpDatabaseMSG": "ডাটাবেস সেটআপ করা হচ্ছে। এটি কিছুটা সময় নিতে পারে, অনুগ্রহ করে ধৈর্য ধরুন।",
"Response": "রেসপন্স",
"Ping": "পিং",
"Keyword": "কীওয়ার্ড",
"Invert Keyword": "ইনভার্ট কীওয়ার্ড",
"setupDatabaseSQLite": "একটি সাধারণ ডাটাবেস ফাইল, যা ছোট পরিসরের ডিপ্লয়মেন্টের জন্য সুপারিশ করা হয়। v2.0.0 এর পূর্বে, Uptime Kuma ডিফল্ট ডাটাবেস হিসেবে SQLite ব্যবহার করত।",
"Unknown": "অজানা",
"Cannot connect to the socket server": "সকেট সার্ভারের সাথে সংযোগ স্থাপন করা যাচ্ছে না",
"Reconnecting...": "পুনরায় সংযোগ স্থাপন করা হচ্ছে...",
"Passive Monitor Type": "নিষ্ক্রিয় মনিটরের ধরন",
"markdownSupported": "মার্কডাউন সিনট্যাক্স সাপোর্ট",
"Pause": "থামুন",
"-day": "-দিন",
"hour": "ঘন্টা",
"Host URL": "হোস্ট ইউআরএল",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "আপনি যে সার্ভারে সংযোগ করতে চান তার হোস্টনেম প্রবেশ করুন অথবা যদি আপনি একটি {local_mta} ব্যবহার করার পরিকল্পনা করেন, তাহলে {localhost} লিখুন",
"ignoreTLSErrorGeneral": "সংযোগের জন্য TLS/SSL ত্রুটি উপেক্ষা করুন",
"Upside Down Mode": "আপসাইড ডাউন মোড",
"Pending": "প্রক্রিয়াধীন",
"Push URL": "পুশ URL",
"needPushEvery": "আপনাকে এই URL টি প্রতি {0} সেকেন্ডে কল করতে হবে।",
"Dark": "অন্ধকার",
"Up": "আপ",
"webhookAdditionalHeadersTitle": "অতিরিক্ত শিরোনাম",
"pushOthers": "অন্যান্য",
"programmingLanguages": "প্রোগ্রামিং ভাষাসমূহ",
"Not available, please setup.": "উপলব্ধ নয়, অনুগ্রহ করে সেটআপ করুন।",
"Down": "ডাউন",
"Monitor Type": "মনিটরের ধরন",
"Expected Value": "প্রত্যাশিত মান",
"Home": "হোম",
"Maintenance": "রক্ষণাবেক্ষণ",
"General Monitor Type": "সাধারণ মনিটরের ধরন",
"Specific Monitor Type": "নির্দিষ্ট মনিটরের ধরন",
"Monitor": "মনিটর | মনিটরগুলো",
"day": "দিন | দিনগুলো",
"-hour": "-ঘন্টা",
"Hostname": "হোস্টনেম",
"locally configured mail transfer agent": "লোকালি কনফিগারড মেইল ট্রান্সফার এজেন্ট",
"Port": "পোর্ট",
"Heartbeat Interval": "হার্টবিট ইন্টারভাল",
"Max. Redirects": "সর্বোচ্চ রিডাইরেক্ট",
"Primary Base URL": "প্রাথমিক বেস URL",
"Message": "বার্তা",
"Resume": "পুনরায় শুরু",
"Delete": "মুছে ফেলুন",
"now": "এখন",
"time ago": "{} বয়স",
"-year": "-বছর",
"Json Query Expression": "JSON কোয়েরি এক্সপ্রেশন",
"timeoutAfter": "{0} সেকেন্ড পর টাইমআউট",
"Retries": "পুনরায় চেষ্টা করে",
"checkEverySecond": "প্রতিটি {0} সেকেন্ডে চেক করুন",
"retryCheckEverySecond": "প্রতিটি {0} সেকেন্ডে পুনরায় চেষ্টা করুন",
"resendEveryXTimes": "প্রতিটি {0} বার পুনরায় পাঠান",
"pushOptionalParams": "ঐচ্ছিক প্যারামিটারসমূহ: {0}",
"Save": "সংরক্ষণ",
"ignoredTLSError": "TLS/SSL ত্রুটিগুলি উপেক্ষা করা হয়েছে",
"Accepted Status Codes": "গ্রহণযোগ্য স্ট্যাটাস কোডসমূহ",
"Disable Auth": "প্রমাণীকরণ বন্ধ করুন",
"Theme": "থিম",
"Name": "নাম",
"Status": "অবস্থা",
"DateTime": "তারিখ ও সময়",
"No important events": "কোনো গুরুত্বপূর্ণ ইভেন্ট নেই",
"Edit": "সম্পাদনা",
"Current": "বর্তমান",
"Uptime": "আপটাইম",
"Request Timeout": "রিকোয়েস্ট টাইমআউট",
"Heartbeat Retry Interval": "হার্টবিট রিট্রাই ইন্টারভাল",
"Advanced": "অ্যাডভান্স",
"retriesDescription": "সার্ভিসটি ডাউন হিসেবে চিহ্নিত হওয়ার আগে এবং একটি নোটিফিকেশন পাঠানোর জন্য সর্বোচ্চ পুনরায় চেষ্টা করার সংখ্যা",
"upsideDownModeDescription": "স্ট্যাটাসটি উল্টো করে দিন। যদি সার্ভিসটি পৌঁছানো যায়, তবে এটি DOWN হবে।",
"maxRedirectDescription": "অনুসরণ করার জন্য সর্বোচ্চ রিডাইরেক্ট সংখ্যা। রিডাইরেক্ট নিষ্ক্রিয় করতে 0 সেট করুন।",
"ignoreTLSError": "HTTPS ওয়েবসাইটগুলির জন্য TLS/SSL ত্রুটিগুলি উপেক্ষা করুন",
"pushViewCode": "পুশ মনিটর কীভাবে ব্যবহার করবেন? (কোড দেখুন)",
"Appearance": "দেখানোর ধরন",
"Quick Stats": "তাৎক্ষণিক পরিসংখ্যান"
}

View file

@ -65,7 +65,7 @@
"URL": "URL",
"Hostname": "Nom del servidor",
"Port": "Port",
"timeoutAfter": "¡Timeout' després de {0",
"timeoutAfter": "¡Timeout' després de {0} segons",
"locally configured mail transfer agent": "Agent de transferència de correu configurat localment",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Introduïu el nom del servidor al qual voleu connectar-vos o {localhost} si voleu utilitzar un {local_mta}",
"Host URL": "URL del servidor",
@ -213,5 +213,18 @@
"Email": "Correu",
"Last Result": "Darrer Resultat",
"Add New Tag": "Afegir nova etiqueta",
"Tag with this value already exist.": "Ja existeix una etiqueta amb aquest valor."
"Tag with this value already exist.": "Ja existeix una etiqueta amb aquest valor.",
"defaultNotificationName": "La meva {notification} Alerta ({number})",
"Required": "Obligatori",
"Post URL": "Posar URL",
"Content Type": "Content Type",
"Json Query Expression": "Json Query Expression",
"now": "ara",
"-year": "-any",
"Status Pages": "Pàgines d'estat",
"here": "aquí",
"time ago": "fa {0}",
"ignoredTLSError": "Errors TLS/SSL ignorats",
"webhookFormDataDesc": "{multipart} es bo per PHP. El JSON haurà d'analitzar-se amb {decodeFunction}",
"webhookJsonDesc": "{0} es bo per qualsevol servidor HTTP modern com Express.js"
}

View file

@ -802,8 +802,8 @@
"Json Query": "Json dotaz",
"Badge Duration (in hours)": "Zobrazení odznaku (v hodinách)",
"Badge Preview": "Náhled odznaku",
"Notify Channel": "Kanál nofitikací",
"aboutNotifyChannel": "Upozornění kanálu spustí upozornění na počítači nebo v mobilu pro všechny členy kanálu, ať už jsou dostupní nebo ne.",
"Notify Channel": "Upozornit kanál",
"aboutNotifyChannel": "Upozornění na kanál spustí upozornění na počítači nebo v mobilním telefonu pro všechny členy kanálu, ať už je jejich dostupnost nastavena na aktivní nebo na nepřítomnost.",
"filterActive": "Aktivní",
"filterActivePaused": "Pozastaveno",
"Enter the list of brokers": "Vytvořte seznam zprostředkovatelů",
@ -1051,5 +1051,45 @@
"Client ID": "ID klienta",
"Client Secret": "Tajemství klienta",
"OAuth Scope": "OAuth rozsah",
"Optional: Space separated list of scopes": "Volitelné: seznam rozsahů oddělte mezerami"
"Optional: Space separated list of scopes": "Volitelné: seznam rozsahů oddělte mezerami",
"Debug": "Ladění",
"CopyToClipboardError": "Nelze zkopírovat do schránky: {error}",
"ignoredTLSError": "Chyby TLS/SSL byly ignorovány",
"Copy": "Kopírovat",
"aboutSlackUsername": "Mění zobrazované jméno odesílatele zprávy. Pokud někoho chcete zmínit, použijte raději pole obecný název.",
"Message format": "Formát zprávy",
"Notification Channel": "Kanál notifikací",
"Alphanumerical string and hyphens only": "Pouze alfanumerické řetězce a pomlčky",
"Sound": "Zvuk",
"Custom sound to override default notification sound": "Použít vlastní zvuk místo výchozího zvuku upozornění",
"Correct": "Správně",
"Fail": "Chyba",
"Reveal": "Odhalení",
"Doorbell": "Zvonek",
"Flute": "Flétna",
"Money": "Peníze",
"Elevator": "Výtah",
"Guitar": "Kytara",
"Time Sensitive (iOS Only)": "Časově kritické (pouze pro iOS)",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "Časově kritická upozornění budou doručena okamžitě, i když je zařízení v režimu nerušit.",
"From": "Od",
"Can be found on:": "Lze nalézt na: {0}",
"The phone number of the recipient in E.164 format.": "Telefonní číslo příjemce ve formátu E.164 (+420...).",
"RabbitMQ Nodes": "RabbitMQ uzly pro správu",
"rabbitmqNodesDescription": "URL RabbitMQ uzlů pro správu zadávejte včetně protokolu a portu. Příklad: {0}",
"rabbitmqNodesRequired": "Prosím, definujte uzly pro tento dohled.",
"rabbitmqNodesInvalid": "Prosím, použijte plně kvalifikované URL (začínající na 'http)' RabbitMQ uzlů.",
"RabbitMQ Username": "RabbitMQ uživatelské jméno",
"Send rich messages": "Poslat rozsáhlé zprávy",
"Arcade": "Arkáda",
"Harp": "Harfa",
"Bubble": "Bublinky",
"Scifi": "Scifi",
"Pop": "Prasknutí",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "Pro zajištění příjmu odpovědí zadejte ID odesílatele textu nebo telefonní číslo ve formátu E.164.",
"RabbitMQ Password": "RabbitMQ heslo",
"rabbitmqHelpText": "Abyste mohli používat tento monitor, musíte v nastavení RabbitMQ povolit modul pro správu. Další informace naleznete na adrese {rabitmq_documentation}.",
"SendGrid API Key": "SendGrid API klíč",
"Separate multiple email addresses with commas": "Více e-mailových adres oddělte čárkami",
"Clear": "Odstranění"
}

View file

@ -45,7 +45,7 @@
"Port": "Port",
"Heartbeat Interval": "Hjerteslag interval",
"Retries": "Gentagelser",
"retriesDescription": "Maksimalt antal gentagelser, før tjenesten markeres som inaktiv og sender en meddelelse.",
"retriesDescription": "Maksimalt antal gentagelser, før tjenesten markeres som inaktiv og sender en meddelelse",
"Advanced": "Avanceret",
"ignoreTLSError": "Ignorér TLS/SSL fejl for HTTPS websteder",
"Upside Down Mode": "Omvendt tilstand",
@ -99,7 +99,7 @@
"deleteNotificationMsg": "Er du sikker på, at du vil slette denne underretning for alle overvågere?",
"resolverserverDescription": "Cloudflare er standardserveren, den kan til enhver tid ændres.",
"Resolver Server": "Navne-server",
"rrtypeDescription": "Vælg den type RR, du vil overvåge.",
"rrtypeDescription": "Vælg den type RR, du vil overvåge",
"Last Result": "Seneste resultat",
"pauseMonitorMsg": "Er du sikker på at du vil standse overvågningen?",
"Create your admin account": "Opret din administratorkonto",
@ -125,7 +125,7 @@
"backupDescription3": "Følsom data, f.eks. underretnings-tokener, er inkluderet i eksportfilen. Gem den sikkert.",
"alertNoFile": "Vælg en fil der skal importeres.",
"alertWrongFileType": "Vælg venligst en JSON-fil.",
"twoFAVerifyLabel": "Indtast venligst dit token for at bekræfte, at 2FA fungerer",
"twoFAVerifyLabel": "Indtast venligst dit token for at bekræfte, at 2FA fungerer:",
"tokenValidSettingsMsg": "Token er gyldigt! Du kan nu gemme 2FA -indstillingerne.",
"confirmEnableTwoFAMsg": "Er du sikker på at du vil aktivere 2FA?",
"confirmDisableTwoFAMsg": "Er du sikker på at du vil deaktivere 2FA?",
@ -141,7 +141,7 @@
"Token": "Token",
"Show URI": "Vis URI",
"Clear all statistics": "Ryd alle Statistikker",
"retryCheckEverySecond": "Prøv igen hvert {0} sekund.",
"retryCheckEverySecond": "Prøv igen hvert {0} sekund",
"importHandleDescription": "Vælg 'Spring over eksisterende', hvis du vil springe over hver overvåger eller underretning med samme navn. 'Overskriv' sletter alle eksisterende overvågere og underretninger.",
"confirmImportMsg": "Er du sikker på at importere sikkerhedskopien? Sørg for, at du har valgt den rigtige importindstilling.",
"Heartbeat Retry Interval": "Hjerteslag gentagelsesinterval",
@ -277,7 +277,7 @@
"lineDevConsoleTo": "Line Udviklerkonsol - {0}",
"Basic Settings": "Basisindstillinger",
"User ID": "Bruger-ID",
"Messaging API": "Messaging API",
"Messaging API": "Besked API",
"wayToGetLineChannelToken": "Tilgå først {0}, opret en udbyder og kanal (Messaging API), så kan du få kanaladgangstoken'et og bruger-ID'et fra de ovennævnte menupunkter.",
"Icon URL": "Ikon URL",
"aboutIconURL": "Du kan angive et link til et billede i \"Ikon URL\" for at tilsidesætte standardprofilbilledet. Vil ikke blive brugt, hvis Ikon Emoji er angivet.",
@ -415,7 +415,7 @@
"signedInDisp": "Logget ind som {0}",
"Certificate Expiry Notification": "Meddelelse om udløbsdato for certifikatet",
"API Username": "API Brugernavn",
"API Key": "API Key",
"API Key": "API Nøgle",
"Steam Game Server": "Steam Game Server",
"What you can try:": "Hvad du kan prøve:",
"Go back to the previous page.": "Gå tilbage til forrige side.",
@ -621,11 +621,11 @@
"confirmDeleteTagMsg": "Er du sikker på at du vil slette denne etiket? Overvågninger med denne etiket vil ikke blive slettet.",
"resendEveryXTimes": "Gensend hver {0} gang",
"resendDisabled": "Gensendelse deaktiveret",
"Reconnecting...": "Genforbinder...",
"Reconnecting...": "Genopretter forbindelse...",
"successPaused": "Standset med succes.",
"Reset Token": "Nulstil nøgle",
"Show Clickable Link": "Vis klikbart link",
"atLeastOneMonitor": "Vælg mindst én berørt overvågning.",
"atLeastOneMonitor": "Vælg mindst én berørt overvågning",
"authInvalidToken": "Ugyldig nøgle.",
"authIncorrectCreds": "Brugernavn eller adgangskode er ikke korrekt.",
"2faAlreadyEnabled": "2FA er allerede aktiveret.",
@ -656,5 +656,147 @@
"telegramSendSilentlyDescription": "Send beskeden lydløst. Brugerne vil modtage en notifikation uden lyd.",
"pushoverMessageTtl": "Message TTL (Sekunder)",
"setupDatabaseEmbeddedMariaDB": "Du skal ikke opsætte noget. Docker billedet har integreret og konfigureret MariaDB automatisk. Uptime Kuma vil forbinde til databasen vha. en Unix socket.",
"setupDatabaseMariaDB": "Oprette forbindelse til en ekstern MariaDB database. Du skal indstille databasens forbindelsesinformation."
"setupDatabaseMariaDB": "Oprette forbindelse til en ekstern MariaDB database. Du skal indstille databasens forbindelsesinformation.",
"styleElapsedTimeShowWithLine": "Vis (med linjer)",
"styleElapsedTime": "Forløbet tid under heartbeat-bjælken",
"templateMsg": "besked fra notifikationen",
"templateHeartbeatJSON": "objekt, der beskriver heartbeat",
"templateMonitorJSON": "objekt, der beskriver monitoren",
"templateLimitedToUpDownNotifications": "kun tilgængelig for UP/DOWN notifikationer",
"webhookAdditionalHeadersTitle": "Yderligere Headers",
"webhookAdditionalHeadersDesc": "Angiver yderligere headers, der sendes med webhooken. Hver header skal defineres som en JSON-nøgle/værdi.",
"webhookBodyCustomOption": "Brugerdefineret Body",
"successKeyword": "Succesnøgleord",
"startOrEndWithOnly": "Kun start eller slut med {0}",
"setAsDefaultProxyDescription": "Denne proxy vil som standard være aktiveret for nye monitorer. Du kan stadig deaktivere proxyen individuelt for hver monitor.",
"No consecutive dashes": "Ingen på hinanden følgende bindestreger",
"statusPageSpecialSlugDesc": "Speciel slug {0}: Denne side vises, når der ikke angives en slug",
"Query": "Kø",
"Add a new expiry notification day": "Tilføj en ny udløbsnotifikationsdag",
"Remove the expiry notification": "Fjern udløbsnotifikationsdagen",
"Refresh Interval": "Opdateringsinterval",
"Refresh Interval Description": "Statussiden vil udføre en fuld opdatering af sitet hvert {0} sekunder",
"RadiusSecret": "Radius-hemmelighed",
"RadiusCalledStationId": "Called Station ID",
"RadiusCalledStationIdDescription": "Identifikator for den kaldte enhed",
"RadiusCallingStationId": "Calling Station Id",
"RadiusCallingStationIdDescription": "Identifikator for den kaldende enhed",
"Check how to config it for WebSocket": "Tjek, hvordan det konfigureres til WebSocket",
"Connection String": "Forbindelsesstreng",
"Workstation": "Arbejdsstation",
"telegramMessageThreadID": "(Valgfrit) Beskedtråd-ID",
"telegramMessageThreadIDDescription": "Valgfri unik identifikator for målbeskedtråden (emnet) i forummet; kun for forum-supergrupper",
"telegramProtectContent": "Beskyt videresendelse/gemning",
"telegramProtectContentDescription": "Hvis aktiveret, vil bot-beskeder i Telegram være beskyttet mod videresendelse og gemning.",
"disableCloudflaredNoAuthMsg": "Du er i No Auth-tilstand, en adgangskode er ikke påkrævet.",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "En liste over notifikationstjenester kan findes i Home Assistant under “Developer Tools > Services”. Søg efter “notification” for at finde navnet på din enhed/telefon.",
"Then choose an action, for example switch the scene to where an RGB light is red.": "Vælg derefter en handling, for eksempel at skifte scenen til en, hvor et RGB-lys er rødt.",
"backupRecommend": "Sikkerhedskopier venligst volumen eller data-mappen (./data/) direkte i stedet.",
"and": "og",
"startDateTime": "Startdato/-tidspunkt",
"endDateTime": "Slutdato/-tidspunkt",
"cronExpression": "Cron-udtryk",
"cronSchedule": "Tidsplan: ",
"warningTimezone": "Den bruger serverens tidszone",
"enableNSCD": "Aktivér NSCD (Name Service Cache Daemon) for at cache alle DNS-forespørgsler",
"chromeExecutable": "Chrome/Chromium-eksekverbar fil",
"Single Maintenance Window": "Enkelt vedligeholdelsesvindue",
"Edit Maintenance": "Rediger vedligeholdelse",
"emailCustomisableContent": "Brugerdefinerbart indhold",
"leave blank for default subject": "lad stå tomt for standardemne",
"emailCustomBody": "Brugerdefineret body",
"leave blank for default body": "lad stå tomt for standard-body",
"emailTemplateServiceName": "Servicenavn",
"emailTemplateHostnameOrURL": "Værtsnavn eller URL",
"emailTemplateStatus": "Status",
"emailTemplateMonitorJSON": "objekt, der beskriver monitoren",
"emailTemplateHeartbeatJSON": "objekt, der beskriver heartbeat",
"emailTemplateMsg": "besked fra notifikationen",
"emailTemplateLimitedToUpDownNotification": "kun tilgængelig for UP/DOWN heartbeats, ellers null",
"Select message type": "Vælg beskedtype",
"Send to channel": "Send til kanal",
"Create new forum post": "Opret nyt forumpost",
"postToExistingThread": "Send til eksisterende tråd / forumpost",
"forumPostName": "Forumpost-navn",
"threadForumPostID": "Tråd- / Forumpost-ID",
"e.g. {discordThreadID}": "f.eks. {discordThreadID}",
"wayToGetDiscordThreadId": "At hente en tråd- / forumpost-ID ligner processen for at hente en kanal-ID. Læs mere om, hvordan du får IDs {0}",
"Channel access token (Long-lived)": "Kanaladgangstoken (langvarig)",
"Your User ID": "Din bruger-ID",
"dataRetentionTimeError": "Opbevaringsperioden skal være 0 eller højere",
"infiniteRetention": "Sæt til 0 for uendelig opbevaring.",
"enableGRPCTls": "Tillad afsendelse af gRPC-forespørgsel med TLS-forbindelse",
"affectedStatusPages": "Vis denne vedligeholdelsesbesked på udvalgte statussider",
"invertKeywordDescription": "Se efter, at nøgleordet mangler i stedet for at være til stede.",
"octopushAPIKey": "“API-nøgle” fra HTTP API-legitimationsoplysninger i kontrolpanelet",
"octopushLogin": "“Login” fra HTTP API-legitimationsoplysninger i kontrolpanelet",
"pushoversounds pushover": "Pushover (standard)",
"pushoversounds bike": "Cykel",
"pushoversounds bugle": "Signalhorn",
"pushoversounds cashregister": "Kasseapparat",
"pushoversounds cosmic": "Kosmisk",
"pushoversounds falling": "Faldende",
"pushoversounds gamelan": "Gamelan",
"pushoversounds incoming": "Indkommende",
"pushoversounds intermission": "Pause",
"pushoversounds spacealarm": "Rumalarm",
"pushoversounds tugboat": "Slæbebåd",
"pushoversounds climb": "Klatring (lang)",
"pushoversounds persistent": "Vedvarende (lang)",
"pushoversounds echo": "Pushover Echo (lang)",
"pushoversounds updown": "Op Ned (lang)",
"GoogleChat": "Google Chat (kun Google Workspace)",
"styleElapsedTimeShowNoLine": "Vis (ingen linjer)",
"Slug": "Slug",
"The slug is already taken. Please choose another slug.": "Denne slug er allerede i brug. Vælg venligst en anden.",
"There might be a typing error in the address.": "Der er muligvis en stavfejl i adressen.",
"Long-Lived Access Token": "Long-Lived Access Token",
"Search monitored sites": "Søg overvågede sites",
"liquidIntroduction": "Templatability opnås via Liquid-templeringssproget. Se venligst {0} for brugsanvisninger. Her er de tilgængelige variabler:",
"templateLimitedToUpDownCertNotifications": "kun tilgængelig for UP/DOWN/Certifikatudløb notifikationer",
"shrinkDatabaseDescriptionSqlite": "Udløs database-{vacuum} for SQLite. {auto_vacuum} er allerede aktiveret, men dette defragmenterer ikke databasen eller ompakker de enkelte database-sider på samme måde som {vacuum}-kommandoen gør.",
"Check/Uncheck": "Markér/Afjern markering",
"enableProxyDescription": "Denne proxy vil ikke påvirke monitor-forespørgsler, før den er aktiveret. Du kan midlertidigt deaktivere proxyen for alle monitorer via aktiveringsstatus.",
"RadiusSecretDescription": "Delt hemmelighed mellem klient og server",
"noDockerHostMsg": "Ikke tilgængelig. Opsæt en Docker-host først.",
"DockerHostRequired": "Angiv venligst Docker-hosten for denne monitor.",
"tailscalePingWarning": "For at bruge Tailscale Ping-monitoren skal du installere Uptime Kuma uden Docker og også installere Tailscale-klienten på din server.",
"trustProxyDescription": "Tillid til X-Forwarded-* headers. Hvis du vil hente den korrekte klient-IP, og din Uptime Kuma er bag en proxy såsom Nginx eller Apache, bør du aktivere dette.",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "En langvarig adgangstoken kan oprettes ved at klikke på dit profilnavn (nederst til venstre), rulle ned til bunden og derefter klikke på “Opret token”. ",
"backupOutdatedWarning": "Forældet: Da mange funktioner er blevet tilføjet, og denne backup-funktion er lidt vedligeholdelsesfri, kan den ikke generere eller gendanne en komplet backup.",
"invalidCronExpression": "Ugyldigt Cron-udtryk: {0}",
"chromeExecutableDescription": "For Docker-brugere: Hvis Chromium endnu ikke er installeret, kan det tage et par minutter at installere og vise testresultatet. Det kræver 1 GB diskplads.",
"Effective Date Range": "Gyldig datointerval (Valgfrit)",
"DateTime Range": "Dato-/tidsinterval",
"notificationRegional": "Regional",
"smtpLiquidIntroduction": "De følgende to felter kan tilpasses via Liquid-templateringssproget. Se venligst {0} for brugsanvisninger. Her er de tilgængelige variabler:",
"whatHappensAtForumPost": "Opret en ny forumpost. Dette sender IKKE beskeder i en eksisterende post. For at sende i en eksisterende post, brug ”{option}”",
"grpcMethodDescription": "Metodenavnet konverteres til camelCase-format, såsom sayHello, check osv.",
"affectedMonitorsDescription": "Vælg de monitorer, der er påvirket af den aktuelle vedligeholdelse",
"jsonQueryDescription": "Parse og udtræk specifikke data fra serverens JSON-svar ved hjælp af en JSON-forespørgsel, eller brug ”$” for det rå svar, hvis JSON ikke forventes. Resultatet sammenlignes derefter med den forventede værdi som strenge. Se {0} for dokumentation, og brug {1} til at eksperimentere med forespørgsler.",
"pushoversounds classical": "Klassisk",
"pushoversounds pianobar": "Piano Bar",
"wayToGetKookBotToken": "Opret en applikation og få din bot-token på {0}",
"wayToGetKookGuildID": "Tænd for Developer Mode i Kook-indstillingerne, og højreklik på guilden for at få dens ID",
"successKeywordExplanation": "MQTT-nøgleord, der vil blive betragtet som succes",
"endpoint": "endpoint",
"topicExplanation": "MQTT-emne til overvågning",
"settingUpDatabaseMSG": "Opsætter databasen. Det kan tage et stykke tid, så vær tålmodig.",
"now": "nu",
"Json Query Expression": "Json Query Expression",
"locally configured mail transfer agent": "lokalt konfigureret mail overførsels agent",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Indtast enten værtsnavnet på serveren du vil forbinde til eller {localhost} hvis du planlægger at bruge en {local_mta}",
"timeoutAfter": "Timeout efter {0} sekunder",
"ignoredTLSError": "TLS/SSL-fejl er blevet ignoreret",
"Invert Keyword": "Inverter Nøgleord",
"Resend Notification if Down X times consecutively": "Send meddelelse igen, hvis Ned X gange i træk",
"Expected Value": "Forventede Værdi",
"setupDatabaseSQLite": "En simpel database-fil, anbefalet til mindre implementeringer. Før v2.0.0, brugte Uptime Kuma SQLite som standard database.",
"ignoreTLSErrorGeneral": "Ignorér TLS/SSL-fejl for forbindelsen",
"time ago": "{0} siden",
"-year": "-år",
"Host URL": "Host URL",
"Request Timeout": "Anmod Timeout",
"Cannot connect to the socket server": "Kan ikke oprette forbindelse til socket serveren",
"pushViewCode": "Hvordan bruger man Push Monitor? (Se kode)"
}

View file

@ -1048,5 +1048,52 @@
"less than or equal to": "kleiner als oder gleich",
"greater than or equal to": "grösser als oder gleich",
"record": "Eintrag",
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut."
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut.",
"ignoredTLSError": "TLS/SSL-Fehler wurden ignoriert",
"Debug": "Debug",
"Copy": "Kopieren",
"CurlDebugInfoOAuth2CCUnsupported": "Der vollständige OAuth-Client-Credential-Flow wird in {curl} nicht unterstützt.{newline}Bitte besorge dir ein Bearer-Token und übergebe ihn über die {oauth2_bearer}-Option.",
"Money": "Geld",
"Custom sound to override default notification sound": "Benutzerdefinierter Ton, um den standardmässigen Benachrichtigungston zu ersetzen",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "Entweder eine Text-Absender-ID oder eine Telefonnummer im E.164-Format, wenn du Antworten erhalten möchtest.",
"CopyToClipboardError": "Konnte nicht in die Zwischenablage kopiert werden: {error}",
"CopyToClipboardSuccess": "Kopiert!",
"CurlDebugInfo": "Um den Monitor zu debuggen, kannst du dies entweder in das Terminal deines eigenen Rechners oder in das Terminal der Maschine, auf der Uptime Kuma läuft, einfügen und überprüfen, was du anforderst.{newiline}Bitte beachte Netzwerkunterschiede wie {firewalls}, {dns_resolvers} oder {docker_networks}.",
"firewalls": "Firewalls",
"dns resolvers": "DNS-Resolver",
"docker networks": "Docker-Netzwerke",
"CurlDebugInfoProxiesUnsupported": "Die Unterstützung von Proxys im oben genannten {curl}-Befehl ist derzeit nicht implementiert.",
"Message format": "Nachrichtenformat",
"Send rich messages": "Sende Rich-Text-Nachrichten",
"Notification Channel": "Benachrichtigungskanal",
"Sound": "Benachrichtigungston",
"Alphanumerical string and hyphens only": "Nur alphanumerische Zeichen und Bindestriche",
"Correct": "Korrekt",
"Fail": "Fehlgeschlagen",
"Harp": "Harfe",
"Reveal": "Enthüllen",
"Bubble": "Blase",
"Doorbell": "Türklingel",
"Flute": "Flöte",
"Scifi": "Science Fiction",
"Clear": "Klar",
"Elevator": "Lift",
"Guitar": "Gitarre",
"Pop": "Pop",
"Time Sensitive (iOS Only)": "Zeitkritisch (nur iOS)",
"The phone number of the recipient in E.164 format.": "Die Telefonnummer des Empfängers im E.164-Format.",
"Can be found on:": "Ist zu finden auf: {0}",
"From": "Von",
"Arcade": "Spielhalle",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "Zeitkritische Benachrichtigungen werden sofort zugestellt, auch wenn sich das Gerät im Nicht stören-Modus befindet.",
"RabbitMQ Nodes": "RabbitMQ-Verwaltungsknoten",
"rabbitmqNodesDescription": "Gib die URL für die RabbitMQ-Verwaltungsknoten einschliesslich Protokoll und Port ein. Beispiel: {0}",
"rabbitmqNodesRequired": "Setze die Knoten für diesen Monitor.",
"RabbitMQ Username": "RabbitMQ Benutzername",
"RabbitMQ Password": "RabbitMQ Passwort",
"SendGrid API Key": "SendGrid-API-Schlüssel",
"Separate multiple email addresses with commas": "Mehrere E-Mail-Adressen mit Kommas trennen",
"rabbitmqNodesInvalid": "Benutze eine vollständig qualifizierte URL (beginnend mit 'http') für RabbitMQ-Knoten.",
"rabbitmqHelpText": "Um den Monitor zu benutzen, musst du das Management Plugin in deinem RabbitMQ-Setup aktivieren. Weitere Informationen siehe {rabitmq_documentation}.",
"aboutSlackUsername": "Ändert den Anzeigenamen des Absenders. Wenn du jemanden erwähnen möchtest, füge ihn stattdessen in den Namen ein."
}

View file

@ -1051,5 +1051,52 @@
"less than or equal to": "kleiner als oder gleich",
"greater than or equal to": "größer als oder gleich",
"record": "Eintrag",
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut."
"shrinkDatabaseDescriptionSqlite": "Datenbank {vacuum} für SQLite auslösen. {auto_vacuum} ist bereits aktiviert, aber dies defragmentiert die Datenbank nicht und packt auch nicht einzelne Datenbankseiten neu, wie es der Befehl {vacuum} tut.",
"ignoredTLSError": "TLS/SSL-Fehler wurden ignoriert",
"Message format": "Nachrichtenformat",
"Notification Channel": "Benachrichtigungskanal",
"Custom sound to override default notification sound": "Benutzerdefinierter Ton, um den standardmäßigen Benachrichtigungston zu ersetzen",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "Zeitkritische Benachrichtigungen werden sofort zugestellt, auch wenn sich das Gerät im Nicht stören-Modus befindet.",
"Debug": "Debug",
"Copy": "Kopieren",
"CopyToClipboardError": "Konnte nicht in die Zwischenablage kopiert werden: {error}",
"CopyToClipboardSuccess": "Kopiert!",
"CurlDebugInfo": "Um den Monitor zu debuggen, kannst du dies entweder in das Terminal deines eigenen Rechners oder in das Terminal der Maschine, auf der Uptime Kuma läuft, einfügen und überprüfen, was du anforderst.{newiline}Bitte beachte Netzwerkunterschiede wie {firewalls}, {dns_resolvers} oder {docker_networks}.",
"firewalls": "Firewalls",
"dns resolvers": "DNS-Resolver",
"docker networks": "Docker-Netzwerke",
"CurlDebugInfoOAuth2CCUnsupported": "Der vollständige OAuth-Client-Credential-Flow wird in {curl} nicht unterstützt.{newline}Bitte besorge dir ein Bearer-Token und übergebe ihn über die {oauth2_bearer}-Option.",
"CurlDebugInfoProxiesUnsupported": "Die Unterstützung von Proxys im oben genannten {curl}-Befehl ist derzeit nicht implementiert.",
"Alphanumerical string and hyphens only": "Nur alphanumerische Zeichen und Bindestriche",
"Correct": "Korrekt",
"Harp": "Harfe",
"Doorbell": "Türklingel",
"Flute": "Flöte",
"Money": "Geld",
"Scifi": "Science Fiction",
"Elevator": "Aufzug",
"Guitar": "Gitarre",
"Sound": "Benachrichtigungston",
"Time Sensitive (iOS Only)": "Zeitkritisch (nur iOS)",
"From": "Von",
"Can be found on:": "Ist zu finden auf: {0}",
"The phone number of the recipient in E.164 format.": "Die Telefonnummer des Empfängers im E.164-Format.",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "Entweder eine Text-Absender-ID oder eine Telefonnummer im E.164-Format, wenn du Antworten erhalten möchtest.",
"Send rich messages": "Sende Rich-Text-Nachrichten",
"Fail": "Fehlgeschlagen",
"Reveal": "Enthüllen",
"Bubble": "Blase",
"Clear": "Klar",
"Pop": "Pop",
"Arcade": "Spielhalle",
"rabbitmqNodesRequired": "Setze die Knoten für diesen Monitor.",
"RabbitMQ Username": "RabbitMQ Benutzername",
"RabbitMQ Password": "RabbitMQ Passwort",
"SendGrid API Key": "SendGrid-API-Schlüssel",
"Separate multiple email addresses with commas": "Mehrere E-Mail-Adressen mit Kommas trennen",
"RabbitMQ Nodes": "RabbitMQ-Verwaltungsknoten",
"rabbitmqNodesDescription": "Gib die URL für die RabbitMQ-Verwaltungsknoten einschließlich Protokoll und Port ein. Beispiel: {0}",
"rabbitmqNodesInvalid": "Benutze eine vollständig qualifizierte URL (beginnend mit 'http') für RabbitMQ-Knoten.",
"rabbitmqHelpText": "Um den Monitor zu benutzen, musst du das Management Plugin in deinem RabbitMQ-Setup aktivieren. Weitere Informationen siehe {rabitmq_documentation}.",
"aboutSlackUsername": "Ändert den Anzeigenamen des Absenders. Wenn du jemanden erwähnen möchtest, füge ihn stattdessen in den Namen ein."
}

View file

@ -97,8 +97,6 @@
"pushOthers": "Others",
"programmingLanguages": "Programming Languages",
"Save": "Save",
"Debug": "Debug",
"Copy": "Copy",
"Notifications": "Notifications",
"Not available, please setup.": "Not available, please set up.",
"Setup Notification": "Set Up Notification",
@ -233,6 +231,9 @@
"templateMonitorJSON": "object describing the monitor",
"templateLimitedToUpDownCertNotifications": "only available for UP/DOWN/Certificate expiry notifications",
"templateLimitedToUpDownNotifications": "only available for UP/DOWN notifications",
"templateServiceName": "service name",
"templateHostnameOrURL": "hostname or URL",
"templateStatus": "status",
"webhookAdditionalHeadersTitle": "Additional Headers",
"webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook. Each header should be defined as a JSON key/value.",
"webhookBodyPresetOption": "Preset - {0}",
@ -251,14 +252,6 @@
"PushUrl": "Push URL",
"HeadersInvalidFormat": "The request headers are not valid JSON: ",
"BodyInvalidFormat": "The request body is not valid JSON: ",
"CopyToClipboardError": "Couldn't copy to clipbard: {error}",
"CopyToClipboardSuccess": "Copied!",
"CurlDebugInfo": "To debug the monitor, you can either paste this into your own machines terminal or into the machines terminal which uptime kuma is running on and see what you are requesting.{newiline}Please be aware of networking differences like {firewalls}, {dns_resolvers} or {docker_networks}.",
"firewalls": "firewalls",
"dns resolvers": "dns resolvers",
"docker networks": "docker networks",
"CurlDebugInfoOAuth2CCUnsupported": "Full oauth client credential flow is not supported in {curl}.{newline}Please get a bearer token and pass it via the {oauth2_bearer} option.",
"CurlDebugInfoProxiesUnsupported": "Proxy support in the above {curl} command is currently not implemented.",
"Monitor History": "Monitor History",
"clearDataOlderThan": "Keep monitor history data for {0} days.",
"PasswordsDoNotMatch": "Passwords do not match.",
@ -431,6 +424,9 @@
"telegramSendSilentlyDescription": "Sends the message silently. Users will receive a notification with no sound.",
"telegramProtectContent": "Protect Forwarding/Saving",
"telegramProtectContentDescription": "If enabled, the bot messages in Telegram will be protected from forwarding and saving.",
"telegramUseTemplate": "Use custom message template",
"telegramUseTemplateDescription": "If enabled, the message will be sent using a custom template.",
"telegramTemplateFormatDescription": "Telegram allows using different markup languages for messages, see Telegram {0} for specifc details.",
"supportTelegramChatID": "Support Direct Chat / Group / Channel's Chat ID",
"wayToGetTelegramChatID": "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id:",
"YOUR BOT TOKEN HERE": "YOUR BOT TOKEN HERE",
@ -529,9 +525,6 @@
"leave blank for default subject": "leave blank for default subject",
"emailCustomBody": "Custom Body",
"leave blank for default body": "leave blank for default body",
"emailTemplateServiceName": "Service Name",
"emailTemplateHostnameOrURL": "Hostname or URL",
"emailTemplateStatus": "Status",
"emailTemplateMonitorJSON": "object describing the monitor",
"emailTemplateHeartbeatJSON": "object describing the heartbeat",
"emailTemplateMsg": "message of the notification",
@ -731,6 +724,7 @@
"Icon Emoji": "Icon Emoji",
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
"aboutWebhooks": "More info about Webhooks on: {0}",
"aboutSlackUsername": "Changes the display name of the message sender. If you want to mention someone, include it in the friendly name instead.",
"aboutChannelName": "Enter the channel name on {0} Channel Name field if you want to bypass the Webhook channel. Ex: #other-channel",
"aboutKumaURL": "If you leave the Uptime Kuma URL field blank, it will default to the Project GitHub page.",
"smtpDkimSettings": "DKIM Settings",
@ -1033,25 +1027,25 @@
"Alphanumerical string and hyphens only": "Alphanumerical string and hyphens only",
"Arcade": "Arcade",
"Correct": "Correct",
"Fail":"Fail",
"Harp":"Harp",
"Reveal":"Reveal",
"Bubble":"Bubble",
"Doorbell":"Doorbell",
"Flute":"Flute",
"Money":"Money",
"Scifi":"Scifi",
"Clear":"Clear",
"Elevator":"Elevator",
"Guitar":"Guitar",
"Pop":"Pop",
"Fail": "Fail",
"Harp": "Harp",
"Reveal": "Reveal",
"Bubble": "Bubble",
"Doorbell": "Doorbell",
"Flute": "Flute",
"Money": "Money",
"Scifi": "Scifi",
"Clear": "Clear",
"Elevator": "Elevator",
"Guitar": "Guitar",
"Pop": "Pop",
"Custom sound to override default notification sound": "Custom sound to override default notification sound",
"Time Sensitive (iOS Only)": "Time Sensitive (iOS Only)",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.",
"From":"From",
"From": "From",
"Can be found on:": "Can be found on: {0}",
"The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.",
"RabbitMQ Nodes": "RabbitMQ Management Nodes",
"rabbitmqNodesDescription": "Enter the URL for the RabbitMQ management nodes including protocol and port. Example: {0}",
"rabbitmqNodesRequired": "Please set the nodes for this monitor.",
@ -1060,5 +1054,10 @@
"RabbitMQ Password": "RabbitMQ Password",
"rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.",
"SendGrid API Key": "SendGrid API Key",
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas"
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas",
"YZJ Webhook URL": "YZJ Webhook URL",
"YZJ Robot Token": "YZJ Robot token",
"Plain Text": "Plain Text",
"Message Template": "Message Template",
"Template Format": "Template Format"
}

View file

@ -9,7 +9,7 @@
"passwordNotMatchMsg": "La contraseña repetida no coincide.",
"notificationDescription": "Por favor asigna una notificación a el/los monitor(es) para hacerlos funcional(es).",
"keywordDescription": "Buscar palabra clave en HTML plano o respuesta JSON. La búsqueda es sensible a mayúsculas.",
"pauseDashboardHome": "Pausado",
"pauseDashboardHome": "Pausar",
"deleteMonitorMsg": "¿Seguro que quieres eliminar este monitor?",
"deleteNotificationMsg": "¿Seguro que quieres eliminar esta notificación para todos los monitores?",
"resolverserverDescription": "Cloudflare es el servidor por defecto, puedes cambiar el servidor de resolución en cualquier momento.",
@ -654,7 +654,7 @@
"gorush": "Gorush",
"squadcast": "Squadcast",
"Maintenance Time Window of a Day": "Ventana de tiempo de mantenimiento de un día",
"Effective Date Range": "Rango de Fechas Efectivas (Opcional)",
"Effective Date Range": "Rango de Fecha Efectivo(Opcional)",
"Free Mobile User Identifier": "Identificador de Usuario de Free Mobile",
"Gateway Type": "Tipo de puerta de enlace",
"SMSManager": "SMSManager",
@ -677,7 +677,7 @@
"alertaRecoverState": "Estado de Recuperación",
"serwersms": "SerwerSMS.pl",
"serwersmsAPIUser": "Nombre de usuario de API (inc. webapi_ prefix)",
"smseagleGroup": "Nombre/s del grupo de la guía telefónica",
"smseagleGroup": "Nombre(s) de grupo(s) de Guía Telefónica",
"Unpin": "Dejar de Fijar",
"Prefix Custom Message": "Prefijo personalizado",
"markdownSupported": "Sintaxis de Markdown soportada",
@ -818,7 +818,7 @@
"showCertificateExpiry": "Mostrar caducidad del certificado",
"noOrBadCertificate": "Certificado Nulo/Incorrecto",
"aboutNotifyChannel": "Notificar canal activará una notificación de escritorio o móvil para todos los miembros del canal, ya sea que su disponibilidad esté activa o ausente.",
"Server URL should not contain the nfty topic": "La URL del servidor no debe contener el tema nfty",
"Server URL should not contain the nfty topic": "La URL del servidor no puede incluír el tópico de ntfy",
"PushDeer Server": "Servidor PushDeer",
"pushDeerServerDescription": "Dejar en blanco para usar el servidor oficial",
"Badge Duration (in hours)": "Duración de la insignia (en horas)",
@ -855,26 +855,26 @@
"2faEnabled": "2FA habilitado.",
"2faDisabled": "2FA deshabilitado.",
"liquidIntroduction": "La plantilla se logra a través del lenguaje de plantillas Liquid. Consulte {0} para obtener instrucciones de uso. Estas son las variables disponibles:",
"templateLimitedToUpDownCertNotifications": "disponible solo para notificaciones de arriba/abajo/caducidad del certificado",
"templateLimitedToUpDownCertNotifications": "solo disponible para notificaciones FUNCIONAL/CAÍDO/Caducidad de certificado",
"emailTemplateMsg": "mensaje de la notificación",
"emailTemplateLimitedToUpDownNotification": "disponible solo para latidos de arriba/abajo, de lo contrario, nulo",
"emailTemplateLimitedToUpDownNotification": "sólo disponible para latidos FUNCIONAL/CAÍDO, de lo contrario nulo",
"setup a new monitor group": "configurar un nuevo grupo de monitores",
"authUserInactiveOrDeleted": "El usuario está inactivo o eliminado.",
"2faAlreadyEnabled": "2FA ya está activado.",
"2faAlreadyEnabled": "2FA ya está habilitado.",
"remoteBrowsersDescription": "Los navegadores remotos son una alternativa a ejecutar Chromium localmente. Configúralos con un servicio como browserless.io o conéctalos a tu propio servidor",
"successKeyword": "Palabra clave de éxito",
"successKeywordExplanation": "MQTT Palabra clave que se considerará como éxito",
"Remove the expiry notification": "Eliminar la notificación de vencimiento",
"Browser Screenshot": "Captura de pantalla del navegador",
"emailCustomisableContent": "Contenido personalizable",
"smtpLiquidIntroduction": "Los siguientes dos campos son personalizables a través del lenguaje de plantillas Liquid. Por favor, consulta {0} para las instrucciones de uso. Estas son las variables disponibles:",
"leave blank for default subject": "dejar en blanco para asunto predeterminado",
"smtpLiquidIntroduction": "Los dos campos siguientes pueden crear plantillas mediante el lenguaje de plantillas Liquid. Consulte {0} para obtener instrucciones de uso. Estas son las variables disponibles:",
"leave blank for default subject": "dejar en blanco para el asunto predeterminado",
"emailCustomBody": "Cuerpo personalizado",
"successAuthChangePassword": "La contraseña se ha actualizado correctamente.",
"successDeleted": "Eliminado con éxito.",
"successDeleted": "Borrado exitosamente.",
"successEdited": "Editado con éxito.",
"successDisabled": "Desactivado con éxito.",
"GrafanaOncallUrl": "Grafana URL de llamada",
"successDisabled": "Deshabilitado con éxito.",
"GrafanaOncallUrl": "URL de llamada de Grafana",
"Reset Token": "Restablecer Token",
"Remote Browsers": "Navegadores remotos",
"Remote Browser": "Navegador remoto",
@ -885,17 +885,17 @@
"successResumed": "Reanudado con éxito.",
"successPaused": "Pausado con éxito.",
"successBackupRestored": "Copia de seguridad restaurada correctamente.",
"successEnabled": "Activado con éxito.",
"successEnabled": "Habilitado exitosamente.",
"tagNotFound": "Etiqueta no encontrada.",
"foundChromiumVersion": "Encontrado Chromium/Chrome. Versión: {0}",
"foundChromiumVersion": "Se encontró Chromium/Chrome. Versión: {0}",
"pushViewCode": "¿Cómo utilizar el monitor Push? (Ver código)",
"pushOthers": "Otros",
"programmingLanguages": "Lenguajes de Programación",
"templateMsg": "mensaje de la notificación",
"templateMonitorJSON": "objeto que describe el monitor",
"templateLimitedToUpDownNotifications": "disponible solo para notificaciones de arriba/abajo",
"templateLimitedToUpDownNotifications": "sólo disponible para notificaciones FUNCIONAL/CAIDO",
"Add a new expiry notification day": "Añadir una nueva notificación de vencimiento",
"leave blank for default body": "dejar en blanco para el cuerpo por defecto",
"leave blank for default body": "dejar en blanco para el cuerpo predeterminado",
"emailTemplateServiceName": "Nombre del Servicio",
"emailTemplateHostnameOrURL": "Nombre del Host o URL",
"emailTemplateStatus": "Estado",
@ -916,7 +916,7 @@
"ntfyPriorityHelptextAllExceptDown": "Todos los eventos son enviados con esta prioridad, excepto los eventos {0}, que tienen una prioridad de {1}",
"templateHeartbeatJSON": "objeto que describe el latido",
"What is a Remote Browser?": "¿Qué es un Navegador Remoto?",
"Your User ID": "Tu ID de usuario",
"Your User ID": "Su ID de usuario",
"Alphanumeric (recommended)": "Alfanumérico (recomendado)",
"Destination": "Destino",
"wayToGetWhapiUrlAndToken": "Puedes obtener la URL de la API y el token accediendo al canal que desee desde {0}",
@ -927,7 +927,7 @@
"Refresh Interval": "Intervalo de refresco",
"ignoreTLSErrorGeneral": "Ignorar errores SSL/TLS durante la conexión",
"documentationOf": "Documentación de {0}",
"wayToGetHeiiOnCallDetails": "Cómo obtener el ID del Disparador y las Claves API se explica en la {documentación}",
"wayToGetHeiiOnCallDetails": "Cómo obtener el Trigger ID y las API Keys se explica en la {documentación}",
"Command": "Comando",
"wayToGetThreemaGateway": "Puedes registrarte para Threema Gateway {0}.",
"threemaRecipient": "Destinatario",
@ -941,7 +941,7 @@
"Host URL": "URL del anfitrión",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Ingresa el nombre del host del servidor al que deseas conectarte o {localhost} si deseas usar un {local_mta}",
"smspartnerPhoneNumberHelptext": "El número debe estar en el formato internacional {0}, {1}. Múltiples números deben estar separados por {2}",
"smspartnerSenderName": "Nombre del remitente de SMS",
"smspartnerSenderName": "Nombre del emisor del SMS",
"smspartnerApiurl": "Puedes encontrar tu clave API en tu panel de control en {0}",
"smspartnerPhoneNumber": "Número(s) de teléfono",
"max 11 alphanumeric characters": "máximo 11 caracteres alfanuméricos",
@ -951,7 +951,7 @@
"Originator type": "Tipo de originador",
"Telephone number": "Número de teléfono",
"Mentioning": "Mencionando",
"Don't mention people": "No mencionar a las personas",
"Don't mention people": "No mencionar personas",
"Mention group": "Mencionar a {group}",
"Bitrix24 Webhook URL": "URL del Webhook de Bitrix24",
"wayToGetBitrix24Webhook": "Puedes crear un webhook siguiendo los pasos en {0}",
@ -961,11 +961,11 @@
"wayToWriteWhapiRecipient": "El número de teléfono con el prefijo internacional, pero sin el signo más al inicio ({0}), el ID de Contacto ({1}) o el ID de Grupo ({2}).",
"From Phone Number / Transmission Path Originating Address (TPOA)": "Número de Teléfono del Remitente / Dirección de Origen de la Ruta de Transmisión (TPOA)",
"To Phone Number": "Al Número de Teléfono",
"Select message type": "Seleccionar tipo de mensaje",
"Select message type": "Seleccionar el tipo de mensaje",
"Send to channel": "Enviar al canal",
"max 15 digits": "máximo 15 dígitos",
"mongodbCommandDescription": "Ejecuta un comando de MongoDB contra la base de datos. Para obtener información sobre los comandos disponibles, consulta la {documentación}",
"whapiRecipient": "Número de Teléfono / ID de Contacto / ID de Grupo",
"whapiRecipient": "Número de teléfono / ID del contacto / ID del grupo",
"cellsyntSplitLongMessages": "Divide mensajes largos en hasta 6 partes. 153 x 6 = 918 caracteres.",
"receiverSevenIO": "Número receptor",
"apiKeySevenIO": "Clave API de SevenIO",
@ -974,7 +974,7 @@
"gtxMessagingToHint": "Formato internacional, con el signo \"+\" al inicio ({e164}, {e212} o {e214})",
"locally configured mail transfer agent": "agente de transferencia de correo configurado localmente",
"wayToGetDiscordThreadId": "Obtener un ID de hilo / publicación en el foro es similar a obtener un ID de canal. Lee más sobre cómo obtener IDs {0}",
"smspartnerSenderNameInfo": "Debe tener entre 3..=11 caracteres normales",
"smspartnerSenderNameInfo": "Debe ser entre 3..=11 carácteres regulares",
"receiverInfoSevenIO": "Si el número receptor no está ubicado en Alemania, debes agregar el código de país delante del número (por ejemplo, para el código de país 1 de EE. UU. usa 117612121212 en lugar de 017612121212)",
"callMeBotGet": "Aquí puedes generar un endpoint para {0}, {1} y {2}. Ten en cuenta que podrías recibir limitaciones de tasa. Las limitaciones de tasa parecen ser: {3}",
"cellsyntOriginator": "Visible en el teléfono móvil del destinatario como originador del mensaje. Los valores permitidos y la función dependen del parámetro originatortype.",
@ -982,11 +982,110 @@
"threemaApiAuthenticationSecret": "Clave Secreta del Gateway-ID",
"threemaBasicModeInfo": "Nota: Esta integración utiliza Threema Gateway en modo básico (encriptación basada en servidor). Puedes encontrar más detalles en {0}.",
"apiKeysDisabledMsg": "Las claves API están desactivadas porque la autenticación está desactivada.",
"Channel access token (Long-lived)": "Token de acceso al canal (de larga duración)",
"Create new forum post": "Crear nueva publicación en el foro",
"Channel access token (Long-lived)": "Token de acceso al canal (Larga duración)",
"Create new forum post": "Crear un nuevo post en el foro",
"postToExistingThread": "Publicar en hilo / publicación existente",
"forumPostName": "Nombre de la publicación en el foro",
"threadForumPostID": "ID del hilo / publicación en el foro",
"e.g. {discordThreadID}": "por ejemplo, {discordThreadID}",
"whatHappensAtForumPost": "Crear una nueva publicación en el foro. Esto NO publica mensajes en una publicación existente. Para publicar en una publicación existente usa \"{option}\""
"whatHappensAtForumPost": "Crear una nueva publicación en el foro. Esto NO publica mensajes en una publicación existente. Para publicar en una publicación existente usa \"{option}\"",
"jsonQueryDescription": "Analice y extraiga datos específicos de la respuesta JSON del servidor mediante una consulta JSON o utilice \"$\" para la respuesta sin formato, si no espera JSON. Luego, el resultado se compara con el valor esperado, como cadenas. Consulte {0} para obtener documentación y use {1} para experimentar con consultas.",
"aboutSlackUsername": "Cambia el nombre que se muestra del remitente del mensaje. Si quieres mencionar a alguien, inclúyelo en el nombre descriptivo.",
"cacheBusterParam": "Añade el parámetro {0}",
"cacheBusterParamDescription": "Parámetro generado aleatoriamente para omitir cachés.",
"Community String": "Cadena comunitaria",
"snmpCommunityStringHelptext": "Esta cadena funciona como contraseña para autenticar y controlar el acceso a dispositivos habilitados para SNMP. Compárela con la configuración de su dispositivo SNMP.",
"privateOnesenderDesc": "Asegúrese de que el número de teléfono sea válido. Para enviar un mensaje a un número de teléfono privado, por ejemplo: 628123456789",
"wayToGetOnesenderUrlandToken": "Puedes obtener la URL y el token en el sitio web de Onesender. Más información {0}",
"Optional: Space separated list of scopes": "Opcional: Lista de ámbitos separados por espacios",
"No tags found.": "No se han encontrado etiquetas.",
"signl4Docs": "Puede encontrar más información sobre cómo configurar SIGNL4 y cómo obtener la URL del webhook de SIGNL4 en {0}.",
"shrinkDatabaseDescriptionSqlite": "Desencadena la base de datos {vacuum} para SQLite. {auto_vacuum} ya está habilitado, pero esto no desfragmenta la base de datos ni reempaqueta páginas de base de datos individuales como lo hace el comando {vacuum}.",
"and": "y",
"Message format": "Formato del mensaje",
"Send rich messages": "Enviar mensajes enriquecidos",
"OID (Object Identifier)": "(Identificador de Objeto) OID",
"snmpOIDHelptext": "Ingrese el OID del sensor o el estado que desea monitorear. Use herramientas de administración de red como navegadores MIB o software SNMP si no está seguro acerca del OID.",
"Condition": "Condición",
"SNMP Version": "Versión SNMP",
"Please enter a valid OID.": "Por favor escribe un OID válido.",
"Host Onesender": "Host de Onesender",
"Token Onesender": "Token de Onesender",
"Recipient Type": "Tipo de Receptor",
"Private Number": "Número Privado",
"groupOnesenderDesc": "Asegúrese de que el ID del grupo sea válido. Para enviar un mensaje al grupo, por ejemplo: 628123456789-342345",
"Group ID": "ID del grupo",
"Add Remote Browser": "Agregar navegador remoto",
"New Group": "Nuevo grupo",
"Group Name": "Nombre del grupo",
"OAuth2: Client Credentials": "OAuth2: Credenciales del Cliente",
"Authentication Method": "Método de autentificación",
"Authorization Header": "Cabecera de Autorización",
"Form Data Body": "Cuerpo de datos del formulario",
"OAuth Token URL": "OAuth Token URL",
"Client ID": "ID del Cliente",
"Client Secret": "Secreto de Cliente",
"OAuth Scope": "Alcance OAuth",
"Go back to home page.": "Volver a la página de inicio.",
"Lost connection to the socket server.": "Se perdió la conexión con el servidor de socket.",
"Cannot connect to the socket server.": "No se puede conectar al servidor de socket.",
"SIGNL4": "SIGNL4",
"SIGNL4 Webhook URL": "URL de SIGNL4 WebhooK",
"Conditions": "Condiciones",
"conditionAdd": "Añadir condición",
"now": "ahora",
"time ago": "hace {0}",
"-year": "-año",
"Json Query Expression": "Expresión de consulta Json",
"ignoredTLSError": "Se han ignorado errores TLS/SSL",
"conditionDelete": "Borrar condición",
"conditionAddGroup": "Añadir grupo",
"conditionDeleteGroup": "Borrar grupo",
"conditionValuePlaceholder": "Valor",
"equals": "igual",
"not equals": "no es igual",
"contains": "contiene",
"not contains": "no contiene",
"starts with": "empieza por",
"not starts with": "no empieza por",
"ends with": "termina con",
"not ends with": "No termina en",
"less than": "menor que",
"greater than": "mayor que",
"less than or equal to": "Menor o igual a",
"greater than or equal to": "Mayor o igual a",
"Notification Channel": "Canal de notificación",
"Sound": "Sonido",
"record": "Registro",
"RabbitMQ Username": "Usuario RabbitMQ",
"RabbitMQ Password": "Contraseña RabbitMQ",
"RabbitMQ Nodes": "Nodos de Gestión RabbitMQ",
"rabbitmqNodesRequired": "Por favor, configure los nodos para este monitor.",
"rabbitmqNodesInvalid": "Por favor, utilice una URL totalmente cualificada (comenzando por 'http') para los nodos RabbitMQ.",
"Separate multiple email addresses with commas": "Separa las múltiples direcciones de correo con comas",
"rabbitmqHelpText": "Para utilizar el monitor, necesitará habilitar el Plugin de Gestión en su configuración RabbitMQ. Para más información, por favor consulte {rabitmq_documentation}.",
"SendGrid API Key": "Clave de la API de SendGrid",
"rabbitmqNodesDescription": "Introduzca la URL para los nodos de gestión de RabbitMQ incluyendo el protocolo y el puerto. Ejemplo: {0}",
"Money": "Dinero",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "O bien un texto de ID de origen o un número de teléfono en formato E.164 si quiere ser capaz de recibir respuestas.",
"Harp": "Arpa",
"Scifi": "Ciencia Ficción",
"Alphanumerical string and hyphens only": "Sólo cadenas alfanuméricas y guiones",
"Time Sensitive (iOS Only)": "Sensible a la Hora (Solo iOS)",
"Doorbell": "Timbre",
"Flute": "Flauta",
"Guitar": "Guitarra",
"Correct": "Correcto",
"Bubble": "Burbuja",
"Elevator": "Ascensor",
"Fail": "Fallo",
"Reveal": "Mostrar",
"Pop": "Pop",
"From": "De",
"Clear": "Limpiar",
"Can be found on:": "Se puede encontrar en: {0}",
"Custom sound to override default notification sound": "Sonidos personalizados prevalecen sobre los sonidos por defecto de las notificaciones",
"The phone number of the recipient in E.164 format.": "El número de teléfono del receptor en formato E.164.",
"Time sensitive notifications will be delivered immediately, even if the device is in do not disturb mode.": "Las notificaciones sensibles en el tiempo se enviarán inmediatamente, incluso si el dispositivo está en modo no molestar.",
"Arcade": "Arcade"
}

View file

@ -242,7 +242,6 @@
"Running": "Töötab",
"resendEveryXTimes": "Saada uuesti {0} korda",
"statusMaintenance": "Hooldus",
"Webhook URL": "",
"Server URL": "Serveri URL",
"Priority": "Tähtsus",
"emojiCheatSheet": "Emotikoni spikker: {0}",
@ -254,7 +253,6 @@
"PushUrl": "Lükka URL",
"Monitor History": "Monitori ajalugu",
"PasswordsDoNotMatch": "Paroolid ei ühti.",
"records": "",
"Current User": "Praegune kasutaja",
"topic": "Teema",
"successMessage": "Edukas sõnum",
@ -308,7 +306,6 @@
"General Monitor Type": "Üldine monitori tüüp",
"webhookAdditionalHeadersDesc": "Lisab täiendavad päised saadetud webhookiga.",
"Read more": "Loe rohkem",
"HeadersInvalidFormat": "",
"clearDataOlderThan": "Hoia monitori ajalugu alles {0} päeva.",
"steamApiKeyDescription": "Steam Game Serveri monitoorimiseks on vaja sul Steam Web-API võtit. Sa saad registreerida enda API võtme siin: ",
"Done": "Tehtud",

View file

@ -216,19 +216,19 @@
"smtpCC": "CC",
"smtpBCC": "BCC",
"discord": "Discord",
"Discord Webhook URL": "Discord Webhook URL",
"wayToGetDiscordURL": "You can get this by going to Server Settings -> Integrations -> Create Webhook",
"Bot Display Name": "Bot Display Name",
"Discord Webhook URL": "Discord Webhook URLa",
"wayToGetDiscordURL": "Hona joanda lortu dezakezu: Server Settings -> Integrations -> View Webhooks -> New Webhook",
"Bot Display Name": "Botaren erakusteko izena",
"Prefix Custom Message": "Prefix Custom Message",
"Hello @everyone is...": "Kaixo {'@'}edonor da…",
"teams": "Microsoft Teams",
"Webhook URL": "Webhook URL",
"Webhook URL": "Webhook URLa",
"wayToGetTeamsURL": "You can learn how to create a webhook URL {0}.",
"wayToGetZohoCliqURL": "You can learn how to create a webhook URL {0}.",
"signal": "Signal",
"Number": "Zenbakia",
"Recipients": "Recipients",
"needSignalAPI": "You need to have a signal client with REST API.",
"Recipients": "Hartzaileak",
"needSignalAPI": "REST APIarekin signal bezeroa eduki behar duzu.",
"wayToCheckSignalURL": "You can check this URL to view how to set one up:",
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
"gotify": "Gotify",
@ -280,8 +280,8 @@
"Read more": "Irakurri gehiago",
"appriseInstalled": "Apprise instalatuta.",
"appriseNotInstalled": "Apprise ez dago instalatuta. {0}",
"Access Token": "Access Token",
"Channel access token": "Channel access token",
"Access Token": "Access Tokena",
"Channel access token": "Kanalaren access tokena",
"Line Developers Console": "Line Developers Console",
"lineDevConsoleTo": "Line Developers Console - {0}",
"Basic Settings": "Oinarrizko ezarpenak",
@ -316,7 +316,7 @@
"One record": "One record",
"steamApiKeyDescription": "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ",
"Current User": "Uneko erabiltzailea",
"topic": "Topic",
"topic": "Gaia",
"topicExplanation": "MQTT topic to monitor",
"successMessage": "Arrakasta mezua",
"successMessageExplanation": "MQTT message that will be considered as success",
@ -401,7 +401,7 @@
"proxyDescription": "Proxies must be assigned to a monitor to function.",
"enableProxyDescription": "This proxy will not effect on monitor requests until it is activated. You can control temporarily disable the proxy from all monitors by activation status.",
"setAsDefaultProxyDescription": "This proxy will be enabled by default for new monitors. You can still disable the proxy separately for each monitor.",
"Certificate Chain": "Certificate Chain",
"Certificate Chain": "Zertifikatu katea",
"Valid": "Baliozkoa",
"Invalid": "Baliogabea",
"AccessKeyId": "AccessKey ID",
@ -476,7 +476,7 @@
"Footer Text": "Oineko testua",
"Show Powered By": "Erakutsi Honekin egina:",
"Domain Names": "Domeinu izenak",
"signedInDisp": "Signed in as {0}",
"signedInDisp": "{0} bezala logeatua",
"signedInDispDisabled": "Auth desgaituta.",
"Certificate Expiry Notification": "Zertifikatu iraungitze jakinarazpena",
"API Username": "API Erabiltzailea",
@ -587,5 +587,113 @@
"statusPageRefreshIn": "{0} barru freskatuko da.",
"now": "orain",
"time ago": "duela {0}",
"-year": "-urte"
"-year": "-urte",
"styleElapsedTimeShowNoLine": "Erakutsi (Lerrorik ez)",
"styleElapsedTimeShowWithLine": "Erakutsi (Lerroarekin)",
"Select": "Hautatu",
"Docker Container": "Docker edukiontzia",
"Container Name / ID": "Edukiontzi izena / ID",
"Packet Size": "Pakete tamaina",
"telegramSendSilently": "Bidali isilik",
"Examples": "Adibideak",
"default: notify all devices": "Defektuz: jakinarazi gailu guztiak",
"Event type:": "Ekintza mota:",
"Event data:": "Ekintza mota:",
"Optional": "Hautazkoa",
"and": "eta",
"sameAsServerTimezone": "Zerbitzariaren ordu-zona bera",
"endDateTime": "Bukaera data/ordua",
"cronExpression": "Cron espresioa",
"cronSchedule": "Programatu: ",
"invalidCronExpression": "Cron espresio baliogabea: {0}",
"recurringInterval": "Tartea",
"No Maintenance": "Mantenurik ez",
"maintenanceStatus-under-maintenance": "Mantenuan",
"maintenanceStatus-inactive": "Aktibatu gabe",
"maintenanceStatus-scheduled": "Programatuta",
"Display Timezone": "Erakutsi ordu zona",
"statusPageMaintenanceEndDate": "Bukatu",
"IconUrl": "Ikono URLa",
"chromeExecutableAutoDetect": "Auto detektatu",
"Schedule Maintenance": "Programatu mantenua",
"Edit Maintenance": "Editatu mantenua",
"Date and Time": "Data eta ordua",
"plugin": "Plugin | Pluginak",
"installing": "Instalatzen",
"uninstalling": "Desinstalatzen",
"confirmUninstallPlugin": "Ziur zaude plugin hau desinstalatu nahi duzula?",
"Clone": "Klonatu",
"cloneOf": "{0}-(a)ren klona",
"emailTemplateStatus": "Egoera",
"emailTemplateMsg": "jakinarazpenaren mezua",
"Select message type": "Hautatu mezu mota",
"Send to channel": "Bidali kanalera",
"Enable TLS": "Gaitu TLS",
"webhookAdditionalHeadersTitle": "Goiburu gehigarriak",
"Reset Token": "Berrezarri tokena",
"selectedMonitorCount": "Hautatuta: {0}",
"HTTP Headers": "HTTP goiburuak",
"Refresh Interval": "Eguneratze tartea",
"tcp": "TCP / HTTP",
"Notification Service": "Jakinarazpen zerbitzua",
"or": "edo",
"startDateTime": "Hasiera data/ordua",
"pauseMaintenanceMsg": "Ziur zaude gelditu nahi duzula?",
"install": "Instalatu",
"uninstall": "Desinstalatu",
"emailTemplateServiceName": "Zerbitzu izena",
"Your User ID": "Zure erabiltzaile IDa",
"Connection Type": "Konexio mota",
"pushOthers": "Besteak",
"programmingLanguages": "Programatze lengoaiak",
"dbName": "Datubase izena",
"resendEveryXTimes": "Berbidali {0} aldiz",
"Reconnecting...": "Birkonektatzen...",
"setupDatabaseChooseDatabase": "Zein datubase erabili nahi duzu?",
"Recurring": "Errepikakorra",
"Mentioning": "Aipatzen",
"Condition": "Baldintza",
"RabbitMQ Username": "RabbitMQ erabiltzailea",
"Expiry": "Iraungitzea",
"pagertreeSilent": "Isildu",
"Group": "Taldea",
"Saved.": "Gordeta.",
"Conditions": "Baldintzak",
"smseagleTo": "Telefono zenbakia(k)",
"Enable Kafka SSL": "Gaitu Kafka SSL",
"Continue": "Jarraitu",
"apiKey-active": "Aktibo",
"apiKey-expired": "Iraungita",
"apiKey-inactive": "Ez-aktibo",
"Expires": "Iraungitzen da",
"Generate": "Sortu",
"pagertreeUrgency": "Larritasuna",
"pagertreeLow": "Baxua",
"pagertreeHigh": "Altua",
"pagertreeCritical": "Larria",
"Destination": "Helburua",
"conditionValuePlaceholder": "Balioa",
"RabbitMQ Password": "RabbitMQ pasahitza",
"Correct": "Zuzena",
"Fail": "Akatsa",
"Elevator": "Igogailua",
"Guitar": "Gitarra",
"pagertreeMedium": "Ertaina",
"Host URL": "Host URLa",
"Affected Monitors": "Kaltetutako monitoreak",
"Pick Affected Monitors...": "Hautatu kaltetutako monitoreak…",
"deleteMaintenanceMsg": "Ziur zaude mantentze lan hau ezabatu nahi duzula?",
"smseagleEncoding": "Unicode gisa bidali",
"Add Another": "Gehitu beste bat",
"Google Analytics ID": "Google Analytics IDa",
"Edit Tag": "Editatu etiketa",
"PushDeer Server": "PushDeer zerbitzaria",
"Show Clickable Link": "Erakutsi lotura klikagarria",
"twilioToNumber": "zenbakira",
"twilioFromNumber": "zenbakitik",
"ntfyUsernameAndPassword": "Erabiltzailea eta pasahitza",
"lunaseaUserID": "Erabiltzaile IDa",
"pagertreeDoNothing": "Ez egin ezer",
"Separate multiple email addresses with commas": "Banatu email helbideak koma ikurrekin",
"settingUpDatabaseMSG": "Datubasea ezartzen. Denbora pixka bat iraun dezake, pazientzia eduki."
}

Some files were not shown because too many files have changed in this diff Show more