summaryrefslogtreecommitdiffstats
path: root/extra
diff options
context:
space:
mode:
Diffstat (limited to 'extra')
-rw-r--r--extra/beta/update-version.js84
-rw-r--r--extra/build-healthcheck.js27
-rw-r--r--extra/check-knex-filenames.mjs72
-rw-r--r--extra/check-lang-json.js27
-rw-r--r--extra/checkout-pr.js33
-rw-r--r--extra/close-incorrect-issue.js57
-rw-r--r--extra/compile-install-script.ps12
-rw-r--r--extra/deploy-demo-server.js60
-rw-r--r--extra/download-apprise.mjs57
-rw-r--r--extra/download-dist.js69
-rw-r--r--extra/healthcheck.go90
-rw-r--r--extra/healthcheck.js55
-rw-r--r--extra/mark-as-nightly.js24
-rw-r--r--extra/push-examples/.gitignore3
-rw-r--r--extra/push-examples/bash-curl/index.sh10
-rw-r--r--extra/push-examples/csharp/index.cs24
-rw-r--r--extra/push-examples/docker/index.sh1
-rw-r--r--extra/push-examples/go/index.go20
-rw-r--r--extra/push-examples/java/index.java32
-rw-r--r--extra/push-examples/javascript-fetch/index.js11
-rw-r--r--extra/push-examples/javascript-fetch/package.json5
-rw-r--r--extra/push-examples/php/index.php13
-rw-r--r--extra/push-examples/powershell/index.ps19
-rw-r--r--extra/push-examples/python/index.py10
-rw-r--r--extra/push-examples/typescript-fetch/README.md19
-rw-r--r--extra/push-examples/typescript-fetch/index.ts11
-rw-r--r--extra/push-examples/typescript-fetch/package.json13
-rw-r--r--extra/rebase-pr.js40
-rw-r--r--extra/reformat-changelog.js44
-rw-r--r--extra/release/beta.mjs65
-rw-r--r--extra/release/final.mjs57
-rw-r--r--extra/release/lib.mjs191
-rw-r--r--extra/release/nightly.mjs16
-rw-r--r--extra/remove-2fa.js65
-rw-r--r--extra/remove-empty-lang-keys.js25
-rw-r--r--extra/remove-playwright-test-data.js6
-rw-r--r--extra/reset-migrate-aggregate-table-state.js24
-rw-r--r--extra/reset-password.js141
-rw-r--r--extra/simple-dns-server.js149
-rw-r--r--extra/simple-mqtt-server.js57
-rw-r--r--extra/sort-contributors.js22
-rw-r--r--extra/update-language-files/.gitignore3
-rw-r--r--extra/update-language-files/index.js103
-rw-r--r--extra/update-language-files/package.json12
-rw-r--r--extra/update-version.js81
-rw-r--r--extra/update-wiki-version.js58
-rw-r--r--extra/upload-github-release-asset.sh64
-rw-r--r--extra/uptime-kuma-push/.gitignore1
-rw-r--r--extra/uptime-kuma-push/Dockerfile18
-rw-r--r--extra/uptime-kuma-push/build.js48
-rw-r--r--extra/uptime-kuma-push/package.json13
-rw-r--r--extra/uptime-kuma-push/uptime-kuma-push.go44
52 files changed, 2185 insertions, 0 deletions
diff --git a/extra/beta/update-version.js b/extra/beta/update-version.js
new file mode 100644
index 0000000..9ab0015
--- /dev/null
+++ b/extra/beta/update-version.js
@@ -0,0 +1,84 @@
+const pkg = require("../../package.json");
+const fs = require("fs");
+const childProcess = require("child_process");
+const util = require("../../src/util");
+
+util.polyfill();
+
+const version = process.env.RELEASE_BETA_VERSION;
+
+console.log("Beta Version: " + version);
+
+if (!version || !version.includes("-beta.")) {
+ console.error("invalid version, beta version only");
+ process.exit(1);
+}
+
+const exists = tagExists(version);
+
+if (! exists) {
+ // Process package.json
+ pkg.version = version;
+ fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
+
+ // Also update package-lock.json
+ const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
+ childProcess.spawnSync(npm, [ "install" ]);
+
+ commit(version);
+ tag(version);
+
+} else {
+ console.log("version tag exists, please delete the tag or use another tag");
+ process.exit(1);
+}
+
+/**
+ * Commit updated files
+ * @param {string} version Version to update to
+ * @returns {void}
+ * @throws Error committing files
+ */
+function commit(version) {
+ let msg = "Update to " + version;
+
+ let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
+ let stdout = res.stdout.toString().trim();
+ console.log(stdout);
+
+ if (stdout.includes("no changes added to commit")) {
+ throw new Error("commit error");
+ }
+
+ res = childProcess.spawnSync("git", [ "push", "origin", "master" ]);
+ console.log(res.stdout.toString().trim());
+}
+
+/**
+ * Create a tag with the specified version
+ * @param {string} version Tag to create
+ * @returns {void}
+ */
+function tag(version) {
+ let res = childProcess.spawnSync("git", [ "tag", version ]);
+ console.log(res.stdout.toString().trim());
+
+ res = childProcess.spawnSync("git", [ "push", "origin", version ]);
+ console.log(res.stdout.toString().trim());
+}
+
+/**
+ * Check if a tag exists for the specified version
+ * @param {string} version Version to check
+ * @returns {boolean} Does the tag already exist
+ * @throws Version is not valid
+ */
+function tagExists(version) {
+ if (! version) {
+ throw new Error("invalid version");
+ }
+
+ let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
+
+ return res.stdout.toString().trim() === version;
+}
diff --git a/extra/build-healthcheck.js b/extra/build-healthcheck.js
new file mode 100644
index 0000000..e4c8026
--- /dev/null
+++ b/extra/build-healthcheck.js
@@ -0,0 +1,27 @@
+const childProcess = require("child_process");
+const fs = require("fs");
+const platform = process.argv[2];
+
+if (!platform) {
+ console.error("No platform??");
+ process.exit(1);
+}
+
+if (platform === "linux/arm/v7") {
+ console.log("Arch: armv7");
+ if (fs.existsSync("./extra/healthcheck-armv7")) {
+ fs.renameSync("./extra/healthcheck-armv7", "./extra/healthcheck");
+ console.log("Already built in the host, skip.");
+ process.exit(0);
+ } else {
+ console.log("prebuilt not found, it will be slow! You should execute `npm run build-healthcheck-armv7` before build.");
+ }
+} else {
+ if (fs.existsSync("./extra/healthcheck-armv7")) {
+ fs.rmSync("./extra/healthcheck-armv7");
+ }
+}
+
+const output = childProcess.execSync("go build -x -o ./extra/healthcheck ./extra/healthcheck.go").toString("utf8");
+console.log(output);
+
diff --git a/extra/check-knex-filenames.mjs b/extra/check-knex-filenames.mjs
new file mode 100644
index 0000000..4911fc5
--- /dev/null
+++ b/extra/check-knex-filenames.mjs
@@ -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.");
diff --git a/extra/check-lang-json.js b/extra/check-lang-json.js
new file mode 100644
index 0000000..dfda348
--- /dev/null
+++ b/extra/check-lang-json.js
@@ -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.");
diff --git a/extra/checkout-pr.js b/extra/checkout-pr.js
new file mode 100644
index 0000000..0328770
--- /dev/null
+++ b/extra/checkout-pr.js
@@ -0,0 +1,33 @@
+const childProcess = require("child_process");
+
+if (!process.env.UPTIME_KUMA_GH_REPO) {
+ console.error("Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
+ process.exit(1);
+}
+
+let inputArray = process.env.UPTIME_KUMA_GH_REPO.split(":");
+
+if (inputArray.length !== 2) {
+ console.error("Invalid format. Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
+}
+
+let name = inputArray[0];
+let branch = inputArray[1];
+
+console.log("Checkout pr");
+
+// Checkout the pr
+let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ]);
+
+console.log(result.stdout.toString());
+console.error(result.stderr.toString());
+
+result = childProcess.spawnSync("git", [ "fetch", name, branch ]);
+
+console.log(result.stdout.toString());
+console.error(result.stderr.toString());
+
+result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]);
+
+console.log(result.stdout.toString());
+console.error(result.stderr.toString());
diff --git a/extra/close-incorrect-issue.js b/extra/close-incorrect-issue.js
new file mode 100644
index 0000000..9bb01b1
--- /dev/null
+++ b/extra/close-incorrect-issue.js
@@ -0,0 +1,57 @@
+const github = require("@actions/github");
+
+(async () => {
+ try {
+ const token = process.argv[2];
+ const issueNumber = process.argv[3];
+ const username = process.argv[4];
+
+ const client = github.getOctokit(token).rest;
+
+ const issue = {
+ owner: "louislam",
+ repo: "uptime-kuma",
+ number: issueNumber,
+ };
+
+ const labels = (
+ await client.issues.listLabelsOnIssue({
+ owner: issue.owner,
+ repo: issue.repo,
+ issue_number: issue.number
+ })
+ ).data.map(({ name }) => name);
+
+ if (labels.length === 0) {
+ console.log("Bad format here");
+
+ await client.issues.addLabels({
+ owner: issue.owner,
+ repo: issue.repo,
+ issue_number: issue.number,
+ labels: [ "invalid-format" ]
+ });
+
+ // Add the issue closing comment
+ await client.issues.createComment({
+ owner: issue.owner,
+ repo: issue.repo,
+ issue_number: issue.number,
+ body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please **DO NOT open blank issues and use our [issue-templates](https://github.com/louislam/uptime-kuma/issues/new/choose) instead**.\nBlank Issues do not contain the context nessesary for a good discussions.`
+ });
+
+ // Close the issue
+ await client.issues.update({
+ owner: issue.owner,
+ repo: issue.repo,
+ issue_number: issue.number,
+ state: "closed"
+ });
+ } else {
+ console.log("Pass!");
+ }
+ } catch (e) {
+ console.log(e);
+ }
+
+})();
diff --git a/extra/compile-install-script.ps1 b/extra/compile-install-script.ps1
new file mode 100644
index 0000000..dd44798
--- /dev/null
+++ b/extra/compile-install-script.ps1
@@ -0,0 +1,2 @@
+# Must enable File Sharing in Docker Desktop
+docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh
diff --git a/extra/deploy-demo-server.js b/extra/deploy-demo-server.js
new file mode 100644
index 0000000..2cc196b
--- /dev/null
+++ b/extra/deploy-demo-server.js
@@ -0,0 +1,60 @@
+require("dotenv").config();
+const { NodeSSH } = require("node-ssh");
+const readline = require("readline");
+const rl = readline.createInterface({ input: process.stdin,
+ output: process.stdout });
+const prompt = (query) => new Promise((resolve) => rl.question(query, resolve));
+
+(async () => {
+ try {
+ console.log("SSH to demo server");
+ const ssh = new NodeSSH();
+ await ssh.connect({
+ host: process.env.UPTIME_KUMA_DEMO_HOST,
+ port: process.env.UPTIME_KUMA_DEMO_PORT,
+ username: process.env.UPTIME_KUMA_DEMO_USERNAME,
+ privateKeyPath: process.env.UPTIME_KUMA_DEMO_PRIVATE_KEY_PATH
+ });
+
+ let cwd = process.env.UPTIME_KUMA_DEMO_CWD;
+ let result;
+
+ const version = await prompt("Enter Version: ");
+
+ result = await ssh.execCommand("git fetch --all", {
+ cwd,
+ });
+ console.log(result.stdout + result.stderr);
+
+ await prompt("Press any key to continue...");
+
+ result = await ssh.execCommand(`git checkout ${version} --force`, {
+ cwd,
+ });
+ console.log(result.stdout + result.stderr);
+
+ result = await ssh.execCommand("npm run download-dist", {
+ cwd,
+ });
+ console.log(result.stdout + result.stderr);
+
+ result = await ssh.execCommand("npm install --production", {
+ cwd,
+ });
+ console.log(result.stdout + result.stderr);
+
+ /*
+ result = await ssh.execCommand("pm2 restart 1", {
+ cwd,
+ });
+ console.log(result.stdout + result.stderr);*/
+
+ } catch (e) {
+ console.log(e);
+ } finally {
+ rl.close();
+ }
+})();
+
+// When done reading prompt, exit program
+rl.on("close", () => process.exit(0));
diff --git a/extra/download-apprise.mjs b/extra/download-apprise.mjs
new file mode 100644
index 0000000..3d31f7c
--- /dev/null
+++ b/extra/download-apprise.mjs
@@ -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);
diff --git a/extra/download-dist.js b/extra/download-dist.js
new file mode 100644
index 0000000..b339ac9
--- /dev/null
+++ b/extra/download-dist.js
@@ -0,0 +1,69 @@
+console.log("Downloading dist");
+const https = require("https");
+const tar = require("tar");
+
+const packageJSON = require("../package.json");
+const fs = require("fs");
+const version = packageJSON.version;
+
+const filename = "dist.tar.gz";
+
+const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`;
+download(url);
+
+/**
+ * Downloads the latest version of the dist from a GitHub release.
+ * @param {string} url The URL to download from.
+ * @returns {void}
+ *
+ * Generated by Trelent
+ */
+function download(url) {
+ console.log(url);
+
+ https.get(url, (response) => {
+ if (response.statusCode === 200) {
+ console.log("Extracting dist...");
+
+ if (fs.existsSync("./dist")) {
+
+ if (fs.existsSync("./dist-backup")) {
+ fs.rmSync("./dist-backup", {
+ recursive: true,
+ force: true,
+ });
+ }
+
+ fs.renameSync("./dist", "./dist-backup");
+ }
+
+ const tarStream = tar.x({
+ cwd: "./",
+ });
+
+ tarStream.on("close", () => {
+ if (fs.existsSync("./dist-backup")) {
+ fs.rmSync("./dist-backup", {
+ recursive: true,
+ force: true,
+ });
+ }
+ console.log("Done");
+ process.exit(0);
+ });
+
+ tarStream.on("error", () => {
+ if (fs.existsSync("./dist-backup")) {
+ fs.renameSync("./dist-backup", "./dist");
+ }
+ console.error("Error from tarStream");
+ });
+
+ response.pipe(tarStream);
+ } else if (response.statusCode === 302) {
+ download(response.headers.location);
+ } else {
+ console.log("dist not found");
+ }
+ });
+}
diff --git a/extra/healthcheck.go b/extra/healthcheck.go
new file mode 100644
index 0000000..f79b3e6
--- /dev/null
+++ b/extra/healthcheck.go
@@ -0,0 +1,90 @@
+/*
+ * If changed, have to run `npm run build-docker-builder-go`.
+ * This script should be run after a period of time (180s), because the server may need some time to prepare.
+ */
+package main
+
+import (
+ "crypto/tls"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "runtime"
+ "strings"
+ "time"
+)
+
+func main() {
+ isFreeBSD := runtime.GOOS == "freebsd"
+
+ // Is K8S + uptime-kuma as the container name
+ // See #2083
+ isK8s := strings.HasPrefix(os.Getenv("UPTIME_KUMA_PORT"), "tcp://")
+
+ // process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
+ http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
+ InsecureSkipVerify: true,
+ }
+
+ client := http.Client{
+ Timeout: 28 * time.Second,
+ }
+
+ sslKey := os.Getenv("UPTIME_KUMA_SSL_KEY")
+ if len(sslKey) == 0 {
+ sslKey = os.Getenv("SSL_KEY")
+ }
+
+ sslCert := os.Getenv("UPTIME_KUMA_SSL_CERT")
+ if len(sslCert) == 0 {
+ sslCert = os.Getenv("SSL_CERT")
+ }
+
+ hostname := os.Getenv("UPTIME_KUMA_HOST")
+ if len(hostname) == 0 && !isFreeBSD {
+ hostname = os.Getenv("HOST")
+ }
+ if len(hostname) == 0 {
+ hostname = "127.0.0.1"
+ }
+
+ port := ""
+ // UPTIME_KUMA_PORT is override by K8S unexpectedly,
+ if !isK8s {
+ port = os.Getenv("UPTIME_KUMA_PORT")
+ }
+ if len(port) == 0 {
+ port = os.Getenv("PORT")
+ }
+ if len(port) == 0 {
+ port = "3001"
+ }
+
+ protocol := ""
+ if len(sslKey) != 0 && len(sslCert) != 0 {
+ protocol = "https"
+ } else {
+ protocol = "http"
+ }
+
+ url := protocol + "://" + hostname + ":" + port
+
+ log.Println("Checking " + url)
+ resp, err := client.Get(url)
+
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ defer resp.Body.Close()
+
+ _, err = ioutil.ReadAll(resp.Body)
+
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ log.Printf("Health Check OK [Res Code: %d]\n", resp.StatusCode)
+
+}
diff --git a/extra/healthcheck.js b/extra/healthcheck.js
new file mode 100644
index 0000000..c9391c4
--- /dev/null
+++ b/extra/healthcheck.js
@@ -0,0 +1,55 @@
+/*
+ * ⚠️ ⚠️ ⚠️ ⚠️ Due to the weird issue in Portainer that the healthcheck script is still pointing to this script for unknown reason.
+ * IT CANNOT BE DROPPED, even though it looks like it is not used.
+ * See more: https://github.com/louislam/uptime-kuma/issues/2774#issuecomment-1429092359
+ *
+ * ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
+ * This script should be run after a period of time (180s), because the server may need some time to prepare.
+ */
+const FBSD = /^freebsd/.test(process.platform);
+
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
+
+let client;
+
+const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
+const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
+
+if (sslKey && sslCert) {
+ client = require("https");
+} else {
+ client = require("http");
+}
+
+// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
+// Dual-stack support for (::)
+let hostname = process.env.UPTIME_KUMA_HOST;
+
+// Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD
+if (!hostname && !FBSD) {
+ hostname = process.env.HOST;
+}
+
+const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
+
+let options = {
+ host: hostname || "127.0.0.1",
+ port: port,
+ timeout: 28 * 1000,
+};
+
+let request = client.request(options, (res) => {
+ console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
+ if (res.statusCode === 302) {
+ process.exit(0);
+ } else {
+ process.exit(1);
+ }
+});
+
+request.on("error", function (err) {
+ console.error("Health Check ERROR");
+ process.exit(1);
+});
+
+request.end();
diff --git a/extra/mark-as-nightly.js b/extra/mark-as-nightly.js
new file mode 100644
index 0000000..d2cb8cb
--- /dev/null
+++ b/extra/mark-as-nightly.js
@@ -0,0 +1,24 @@
+const pkg = require("../package.json");
+const fs = require("fs");
+const util = require("../src/util");
+const dayjs = require("dayjs");
+
+util.polyfill();
+
+const oldVersion = pkg.version;
+const newVersion = oldVersion + "-nightly-" + dayjs().format("YYYYMMDDHHmmss");
+
+console.log("Old Version: " + oldVersion);
+console.log("New Version: " + newVersion);
+
+if (newVersion) {
+ // Process package.json
+ pkg.version = newVersion;
+ pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion);
+ fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
+
+ // Process README.md
+ if (fs.existsSync("README.md")) {
+ fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion));
+ }
+}
diff --git a/extra/push-examples/.gitignore b/extra/push-examples/.gitignore
new file mode 100644
index 0000000..717394d
--- /dev/null
+++ b/extra/push-examples/.gitignore
@@ -0,0 +1,3 @@
+java/Index.class
+csharp/index.exe
+typescript-fetch/index.js
diff --git a/extra/push-examples/bash-curl/index.sh b/extra/push-examples/bash-curl/index.sh
new file mode 100644
index 0000000..3031255
--- /dev/null
+++ b/extra/push-examples/bash-curl/index.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+# Filename: index.sh
+PUSH_URL="https://example.com/api/push/key?status=up&msg=OK&ping="
+INTERVAL=60
+
+while true; do
+ curl -s -o /dev/null $PUSH_URL
+ echo "Pushed!"
+ sleep $INTERVAL
+done
diff --git a/extra/push-examples/csharp/index.cs b/extra/push-examples/csharp/index.cs
new file mode 100644
index 0000000..94eecfb
--- /dev/null
+++ b/extra/push-examples/csharp/index.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Net;
+using System.Threading;
+
+/**
+ * Compile: C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe index.cs
+ * Run: index.exe
+ */
+class Index
+{
+ const string PushURL = "https://example.com/api/push/key?status=up&msg=OK&ping=";
+ const int Interval = 60;
+
+ static void Main(string[] args)
+ {
+ while (true)
+ {
+ WebClient client = new WebClient();
+ client.DownloadString(PushURL);
+ Console.WriteLine("Pushed!");
+ Thread.Sleep(Interval * 1000);
+ }
+ }
+}
diff --git a/extra/push-examples/docker/index.sh b/extra/push-examples/docker/index.sh
new file mode 100644
index 0000000..1eb43d6
--- /dev/null
+++ b/extra/push-examples/docker/index.sh
@@ -0,0 +1 @@
+docker run -d --restart=always --name uptime-kuma-push louislam/uptime-kuma:push "https://example.com/api/push/key?status=up&msg=OK&ping=" 60
diff --git a/extra/push-examples/go/index.go b/extra/push-examples/go/index.go
new file mode 100644
index 0000000..2e518e7
--- /dev/null
+++ b/extra/push-examples/go/index.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+)
+
+func main() {
+ const PushURL = "https://example.com/api/push/key?status=up&msg=OK&ping="
+ const Interval = 60
+
+ for {
+ _, err := http.Get(PushURL)
+ if err == nil {
+ fmt.Println("Pushed!")
+ }
+ time.Sleep(Interval * time.Second)
+ }
+}
diff --git a/extra/push-examples/java/index.java b/extra/push-examples/java/index.java
new file mode 100644
index 0000000..5a77342
--- /dev/null
+++ b/extra/push-examples/java/index.java
@@ -0,0 +1,32 @@
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * Compile: javac index.java
+ * Run: java Index
+ */
+class Index {
+
+ public static final String PUSH_URL = "https://example.com/api/push/key?status=up&msg=OK&ping=";
+ public static final int INTERVAL = 60;
+
+ public static void main(String[] args) {
+ while (true) {
+ try {
+ URL url = new URL(PUSH_URL);
+ HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ con.setRequestMethod("GET");
+ con.getResponseCode();
+ con.disconnect();
+ System.out.println("Pushed!");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ try {
+ Thread.sleep(INTERVAL * 1000);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/extra/push-examples/javascript-fetch/index.js b/extra/push-examples/javascript-fetch/index.js
new file mode 100644
index 0000000..9d21d0d
--- /dev/null
+++ b/extra/push-examples/javascript-fetch/index.js
@@ -0,0 +1,11 @@
+// Supports: Node.js >= 18, Deno, Bun
+const pushURL = "https://example.com/api/push/key?status=up&msg=OK&ping=";
+const interval = 60;
+
+const push = async () => {
+ await fetch(pushURL);
+ console.log("Pushed!");
+};
+
+push();
+setInterval(push, interval * 1000);
diff --git a/extra/push-examples/javascript-fetch/package.json b/extra/push-examples/javascript-fetch/package.json
new file mode 100644
index 0000000..78aefc2
--- /dev/null
+++ b/extra/push-examples/javascript-fetch/package.json
@@ -0,0 +1,5 @@
+{
+ "scripts": {
+ "start": "node index.js"
+ }
+}
diff --git a/extra/push-examples/php/index.php b/extra/push-examples/php/index.php
new file mode 100644
index 0000000..d08b451
--- /dev/null
+++ b/extra/push-examples/php/index.php
@@ -0,0 +1,13 @@
+<?php
+const PUSH_URL = "https://example.com/api/push/key?status=up&msg=OK&ping=";
+const interval = 60;
+
+while (true) {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, PUSH_URL);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_exec($ch);
+ curl_close($ch);
+ echo "Pushed!\n";
+ sleep(interval);
+}
diff --git a/extra/push-examples/powershell/index.ps1 b/extra/push-examples/powershell/index.ps1
new file mode 100644
index 0000000..2894e8d
--- /dev/null
+++ b/extra/push-examples/powershell/index.ps1
@@ -0,0 +1,9 @@
+# Filename: index.ps1
+$pushURL = "https://example.com/api/push/key?status=up&msg=OK&ping="
+$interval = 60
+
+while ($true) {
+ $res = Invoke-WebRequest -Uri $pushURL
+ Write-Host "Pushed!"
+ Start-Sleep -Seconds $interval
+}
diff --git a/extra/push-examples/python/index.py b/extra/push-examples/python/index.py
new file mode 100644
index 0000000..c735b29
--- /dev/null
+++ b/extra/push-examples/python/index.py
@@ -0,0 +1,10 @@
+import urllib.request
+import time
+
+push_url = "https://example.com/api/push/key?status=up&msg=OK&ping="
+interval = 60
+
+while True:
+ urllib.request.urlopen(push_url)
+ print("Pushed!\n")
+ time.sleep(interval)
diff --git a/extra/push-examples/typescript-fetch/README.md b/extra/push-examples/typescript-fetch/README.md
new file mode 100644
index 0000000..c2c96ce
--- /dev/null
+++ b/extra/push-examples/typescript-fetch/README.md
@@ -0,0 +1,19 @@
+# How to run
+
+Node.js (ts-node)
+
+```bash
+ts-node index.ts
+```
+
+Deno
+
+```bash
+deno run --allow-net index.ts
+```
+
+Bun.js
+
+```bash
+bun index.ts
+```
diff --git a/extra/push-examples/typescript-fetch/index.ts b/extra/push-examples/typescript-fetch/index.ts
new file mode 100644
index 0000000..bb14088
--- /dev/null
+++ b/extra/push-examples/typescript-fetch/index.ts
@@ -0,0 +1,11 @@
+// Supports: Deno, Bun, Node.js >= 18 (ts-node)
+const pushURL : string = "https://example.com/api/push/key?status=up&msg=OK&ping=";
+const interval : number = 60;
+
+const push = async () => {
+ await fetch(pushURL);
+ console.log("Pushed!");
+};
+
+push();
+setInterval(push, interval * 1000);
diff --git a/extra/push-examples/typescript-fetch/package.json b/extra/push-examples/typescript-fetch/package.json
new file mode 100644
index 0000000..9d7e974
--- /dev/null
+++ b/extra/push-examples/typescript-fetch/package.json
@@ -0,0 +1,13 @@
+{
+ "scripts": {
+ "ts-node": "ts-node index.ts",
+ "deno": "deno run --allow-net index.ts",
+ "bun": "bun index.ts"
+ },
+ "devDependencies": {
+ "@types/node": "^20.6.0",
+ "ts-node": "^10.9.1",
+ "tslib": "^2.6.2",
+ "typescript": "^5.2.2"
+ }
+}
diff --git a/extra/rebase-pr.js b/extra/rebase-pr.js
new file mode 100644
index 0000000..4921d2e
--- /dev/null
+++ b/extra/rebase-pr.js
@@ -0,0 +1,40 @@
+const { execSync } = require("child_process");
+
+/**
+ * Rebase a PR onto such as 1.23.X or master
+ * @returns {Promise<void>}
+ */
+async function main() {
+ const branch = process.argv[2];
+
+ // Use gh to get current branch's pr id
+ let currentBranchPRID = execSync("gh pr view --json number --jq \".number\"").toString().trim();
+ console.log("Pr ID: ", currentBranchPRID);
+
+ // Use gh commend to get pr commits
+ const prCommits = JSON.parse(execSync(`gh pr view ${currentBranchPRID} --json commits`).toString().trim()).commits;
+
+ console.log("Found commits: ", prCommits.length);
+
+ // Sort the commits by authoredDate
+ prCommits.sort((a, b) => {
+ return new Date(a.authoredDate) - new Date(b.authoredDate);
+ });
+
+ // Get the oldest commit id
+ const oldestCommitID = prCommits[0].oid;
+ console.log("Oldest commit id of this pr:", oldestCommitID);
+
+ // Get the latest commit id of the target branch
+ const latestCommitID = execSync(`git rev-parse origin/${branch}`).toString().trim();
+ console.log("Latest commit id of " + branch + ":", latestCommitID);
+
+ // Get the original parent commit id of the oldest commit
+ const originalParentCommitID = execSync(`git log --pretty=%P -n 1 "${oldestCommitID}"`).toString().trim();
+ console.log("Original parent commit id of the oldest commit:", originalParentCommitID);
+
+ // Rebase the pr onto the target branch
+ execSync(`git rebase --onto ${latestCommitID} ${originalParentCommitID}`);
+}
+
+main();
diff --git a/extra/reformat-changelog.js b/extra/reformat-changelog.js
new file mode 100644
index 0000000..80a1b72
--- /dev/null
+++ b/extra/reformat-changelog.js
@@ -0,0 +1,44 @@
+// Generate on GitHub
+const input = `
+* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
+`;
+
+const template = `
+### 🆕 New Features
+
+### 💇‍♀️ Improvements
+
+### 🐞 Bug Fixes
+
+### ⬆️ Security Fixes
+
+### 🦎 Translation Contributions
+
+### Others
+- Other small changes, code refactoring and comment/doc updates in this repo:
+`;
+
+const lines = input.split("\n").filter((line) => line.trim() !== "");
+
+for (const line of lines) {
+ // Split the last " by "
+ const usernamePullRequesURL = line.split(" by ").pop();
+
+ if (!usernamePullRequesURL) {
+ console.log("Unable to parse", line);
+ continue;
+ }
+
+ const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
+ const pullRequestID = "#" + pullRequestURL.split("/").pop();
+ let message = line.split(" by ").shift();
+
+ if (!message) {
+ console.log("Unable to parse", line);
+ continue;
+ }
+
+ message = message.split("* ").pop();
+ console.log("-", pullRequestID, message, `(Thanks ${username})`);
+}
+console.log(template);
diff --git a/extra/release/beta.mjs b/extra/release/beta.mjs
new file mode 100644
index 0000000..a2c9809
--- /dev/null
+++ b/extra/release/beta.mjs
@@ -0,0 +1,65 @@
+import "dotenv/config";
+import {
+ ver,
+ buildDist,
+ buildImage,
+ checkDocker,
+ checkTagExists,
+ checkVersionFormat,
+ dryRun,
+ getRepoName,
+ pressAnyKey,
+ execSync, uploadArtifacts,
+} from "./lib.mjs";
+import semver from "semver";
+
+const repoName = getRepoName();
+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(repoName, version);
+
+// node extra/beta/update-version.js
+execSync("node ./extra/beta/update-version.js");
+
+// Build frontend dist
+buildDist();
+
+// Build slim image (rootless)
+buildImage(repoName, [ "beta-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
+
+// Build full image (rootless)
+buildImage(repoName, [ "beta-rootless", ver(version, "rootless") ], "rootless");
+
+// Build slim image
+buildImage(repoName, [ "beta-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
+
+// Build full image
+buildImage(repoName, [ "beta", version ], "release");
+
+await pressAnyKey();
+
+// npm run upload-artifacts
+uploadArtifacts();
diff --git a/extra/release/final.mjs b/extra/release/final.mjs
new file mode 100644
index 0000000..d190ac0
--- /dev/null
+++ b/extra/release/final.mjs
@@ -0,0 +1,57 @@
+import "dotenv/config";
+import {
+ ver,
+ buildDist,
+ buildImage,
+ checkDocker,
+ checkTagExists,
+ checkVersionFormat,
+ getRepoName,
+ pressAnyKey, execSync, uploadArtifacts
+} from "./lib.mjs";
+
+const repoName = getRepoName();
+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(repoName, version);
+
+// node extra/beta/update-version.js
+execSync("node extra/update-version.js");
+
+// Build frontend dist
+buildDist();
+
+// Build slim image (rootless)
+buildImage(repoName, [ "2-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
+
+// Build full image (rootless)
+buildImage(repoName, [ "2-rootless", ver(version, "rootless") ], "rootless");
+
+// Build slim image
+buildImage(repoName, [ "next-slim", "2-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
+
+// Build full image
+buildImage(repoName, [ "next", "2", version ], "release");
+
+await pressAnyKey();
+
+// npm run upload-artifacts
+uploadArtifacts();
+
+// node extra/update-wiki-version.js
+execSync("node extra/update-wiki-version.js");
diff --git a/extra/release/lib.mjs b/extra/release/lib.mjs
new file mode 100644
index 0000000..d6c294c
--- /dev/null
+++ b/extra/release/lib.mjs
@@ -0,0 +1,191 @@
+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 getRepoName() {
+ return process.env.RELEASE_REPO_NAME || "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} repoName Docker Hub repository name
+ * @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(repoName, tags, target, buildArgs = "", dockerfile = "docker/dockerfile", platform = "linux/amd64,linux/arm64,linux/arm/v7") {
+ let args = [
+ "buildx",
+ "build",
+ "-f",
+ dockerfile,
+ "--platform",
+ platform,
+ ];
+
+ // 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} repoName Docker Hub repository name
+ * @param {string} version Version to check
+ * @returns {void}
+ */
+export async function checkTagExists(repoName, version) {
+ console.log(`Checking if version ${version} exists on Docker Hub`);
+
+ // 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
+ * @returns {void}
+ */
+export function uploadArtifacts() {
+ execSync("npm run upload-artifacts");
+}
+
+/**
+ * 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}`);
+ }
+}
diff --git a/extra/release/nightly.mjs b/extra/release/nightly.mjs
new file mode 100644
index 0000000..c6641ba
--- /dev/null
+++ b/extra/release/nightly.mjs
@@ -0,0 +1,16 @@
+import { buildDist, buildImage, checkDocker, getRepoName } from "./lib.mjs";
+
+// Docker Hub repository name
+const repoName = getRepoName();
+
+// 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(repoName, [ "nightly2-rootless" ], "nightly-rootless");
+
+// Build full image
+buildImage(repoName, [ "nightly2" ], "nightly");
diff --git a/extra/remove-2fa.js b/extra/remove-2fa.js
new file mode 100644
index 0000000..e6a8b97
--- /dev/null
+++ b/extra/remove-2fa.js
@@ -0,0 +1,65 @@
+console.log("== Uptime Kuma Remove 2FA Tool ==");
+console.log("Loading the database");
+
+const Database = require("../server/database");
+const { R } = require("redbean-node");
+const readline = require("readline");
+const TwoFA = require("../server/2fa");
+const args = require("args-parser")(process.argv);
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+
+const main = async () => {
+ Database.initDataDir(args);
+ await Database.connect();
+
+ try {
+ // No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
+ if (!process.env.TEST_BACKEND) {
+ const user = await R.findOne("user");
+ if (! user) {
+ throw new Error("user not found, have you installed?");
+ }
+
+ console.log("Found user: " + user.username);
+
+ let ans = await question("Are you sure want to remove 2FA? [y/N]");
+
+ if (ans.toLowerCase() === "y") {
+ await TwoFA.disable2FA(user.id);
+ console.log("2FA has been removed successfully.");
+ }
+
+ }
+ } catch (e) {
+ console.error("Error: " + e.message);
+ }
+
+ await Database.close();
+ rl.close();
+
+ console.log("Finished.");
+};
+
+/**
+ * Ask question of user
+ * @param {string} question Question to ask
+ * @returns {Promise<string>} Users response
+ */
+function question(question) {
+ return new Promise((resolve) => {
+ rl.question(question, (answer) => {
+ resolve(answer);
+ });
+ });
+}
+
+if (!process.env.TEST_BACKEND) {
+ main();
+}
+
+module.exports = {
+ main,
+};
diff --git a/extra/remove-empty-lang-keys.js b/extra/remove-empty-lang-keys.js
new file mode 100644
index 0000000..6b34fa0
--- /dev/null
+++ b/extra/remove-empty-lang-keys.js
@@ -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");
+}
diff --git a/extra/remove-playwright-test-data.js b/extra/remove-playwright-test-data.js
new file mode 100644
index 0000000..0628807
--- /dev/null
+++ b/extra/remove-playwright-test-data.js
@@ -0,0 +1,6 @@
+const fs = require("fs");
+
+fs.rmSync("./data/playwright-test", {
+ recursive: true,
+ force: true,
+});
diff --git a/extra/reset-migrate-aggregate-table-state.js b/extra/reset-migrate-aggregate-table-state.js
new file mode 100644
index 0000000..e6c51fb
--- /dev/null
+++ b/extra/reset-migrate-aggregate-table-state.js
@@ -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();
+
diff --git a/extra/reset-password.js b/extra/reset-password.js
new file mode 100644
index 0000000..b87d90f
--- /dev/null
+++ b/extra/reset-password.js
@@ -0,0 +1,141 @@
+console.log("== Uptime Kuma Reset Password Tool ==");
+
+const Database = require("../server/database");
+const { R } = require("redbean-node");
+const readline = require("readline");
+const { initJWTSecret } = require("../server/util-server");
+const User = require("../server/model/user");
+const { io } = require("socket.io-client");
+const { localWebSocketURL } = require("../server/config");
+const args = require("args-parser")(process.argv);
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+
+const main = async () => {
+ if ("dry-run" in args) {
+ console.log("Dry run mode, no changes will be made.");
+ }
+
+ console.log("Connecting the database");
+
+ try {
+ Database.initDataDir(args);
+ await Database.connect(false, false, true);
+ // No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
+ if (!process.env.TEST_BACKEND) {
+ const user = await R.findOne("user");
+ if (! user) {
+ throw new Error("user not found, have you installed?");
+ }
+
+ console.log("Found user: " + user.username);
+
+ while (true) {
+ let password;
+ let confirmPassword;
+
+ // When called with "--new-password" argument for unattended modification (e.g. npm run reset-password -- --new_password=secret)
+ if ("new-password" in args) {
+ console.log("Using password from argument");
+ console.warn("\x1b[31m%s\x1b[0m", "Warning: the password might be stored, in plain text, in your shell's history");
+ password = confirmPassword = args["new-password"] + "";
+ } else {
+ password = await question("New Password: ");
+ confirmPassword = await question("Confirm New Password: ");
+ }
+
+ if (password === confirmPassword) {
+ if (!("dry-run" in args)) {
+ await User.resetPassword(user.id, password);
+
+ // Reset all sessions by reset jwt secret
+ await initJWTSecret();
+
+ // Disconnect all other socket clients of the user
+ await disconnectAllSocketClients(user.username, password);
+ }
+ break;
+ } else {
+ console.log("Passwords do not match, please try again.");
+ }
+ }
+ console.log("Password reset successfully.");
+
+ }
+ } catch (e) {
+ console.error("Error: " + e.message);
+ }
+
+ await Database.close();
+ rl.close();
+
+ console.log("Finished.");
+};
+
+/**
+ * Ask question of user
+ * @param {string} question Question to ask
+ * @returns {Promise<string>} Users response
+ */
+function question(question) {
+ return new Promise((resolve) => {
+ rl.question(question, (answer) => {
+ resolve(answer);
+ });
+ });
+}
+
+/**
+ * Disconnect all socket clients of the user
+ * @param {string} username Username
+ * @param {string} password Password
+ * @returns {Promise<void>} Promise
+ */
+function disconnectAllSocketClients(username, password) {
+ return new Promise((resolve) => {
+ console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients");
+
+ // Disconnect all socket connections
+ const socket = io(localWebSocketURL, {
+ reconnection: false,
+ timeout: 5000,
+ });
+ socket.on("connect", () => {
+ socket.emit("login", {
+ username,
+ password,
+ }, (res) => {
+ if (res.ok) {
+ console.log("Logged in.");
+ socket.emit("disconnectOtherSocketClients");
+ } else {
+ console.warn("Login failed.");
+ console.warn("Please restart the server to disconnect all sessions.");
+ }
+ socket.close();
+ });
+ });
+
+ socket.on("connect_error", function () {
+ // The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
+ // Ask the user to restart the server manually
+ console.warn("Failed to connect to " + localWebSocketURL);
+ console.warn("Please restart the server to disconnect all sessions manually.");
+ resolve();
+ });
+ socket.on("disconnect", () => {
+ resolve();
+ });
+ });
+}
+
+if (!process.env.TEST_BACKEND) {
+ main();
+}
+
+module.exports = {
+ main,
+};
diff --git a/extra/simple-dns-server.js b/extra/simple-dns-server.js
new file mode 100644
index 0000000..38bf74f
--- /dev/null
+++ b/extra/simple-dns-server.js
@@ -0,0 +1,149 @@
+/*
+ * Simple DNS Server
+ * For testing DNS monitoring type, dev only
+ */
+const dns2 = require("dns2");
+
+const { Packet } = dns2;
+
+const server = dns2.createServer({
+ udp: true
+});
+
+server.on("request", (request, send, rinfo) => {
+ for (let question of request.questions) {
+ console.log(question.name, type(question.type), question.class);
+
+ const response = Packet.createResponseFromRequest(request);
+
+ if (question.name === "existing.com") {
+
+ if (question.type === Packet.TYPE.A) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ address: "1.2.3.4"
+ });
+ } else if (question.type === Packet.TYPE.AAAA) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ address: "fe80::::1234:5678:abcd:ef00",
+ });
+ } else if (question.type === Packet.TYPE.CNAME) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ domain: "cname1.existing.com",
+ });
+ } else if (question.type === Packet.TYPE.MX) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ exchange: "mx1.existing.com",
+ priority: 5
+ });
+ } else if (question.type === Packet.TYPE.NS) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ ns: "ns1.existing.com",
+ });
+ } else if (question.type === Packet.TYPE.SOA) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ primary: "existing.com",
+ admin: "admin@existing.com",
+ serial: 2021082701,
+ refresh: 300,
+ retry: 3,
+ expiration: 10,
+ minimum: 10,
+ });
+ } else if (question.type === Packet.TYPE.SRV) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ priority: 5,
+ weight: 5,
+ port: 8080,
+ target: "srv1.existing.com",
+ });
+ } else if (question.type === Packet.TYPE.TXT) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ data: "#v=spf1 include:_spf.existing.com ~all",
+ });
+ } else if (question.type === Packet.TYPE.CAA) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ flags: 0,
+ tag: "issue",
+ value: "ca.existing.com",
+ });
+ }
+
+ }
+
+ if (question.name === "4.3.2.1.in-addr.arpa") {
+ if (question.type === Packet.TYPE.PTR) {
+ response.answers.push({
+ name: question.name,
+ type: question.type,
+ class: question.class,
+ ttl: 300,
+ domain: "ptr1.existing.com",
+ });
+ }
+ }
+
+ send(response);
+ }
+});
+
+server.on("listening", () => {
+ console.log("Listening");
+ console.log(server.addresses());
+});
+
+server.on("close", () => {
+ console.log("server closed");
+});
+
+server.listen({
+ udp: 5300
+});
+
+/**
+ * Get human readable request type from request code
+ * @param {number} code Request code to translate
+ * @returns {string|void} Human readable request type
+ */
+function type(code) {
+ for (let name in Packet.TYPE) {
+ if (Packet.TYPE[name] === code) {
+ return name;
+ }
+ }
+}
diff --git a/extra/simple-mqtt-server.js b/extra/simple-mqtt-server.js
new file mode 100644
index 0000000..66ebfe4
--- /dev/null
+++ b/extra/simple-mqtt-server.js
@@ -0,0 +1,57 @@
+const { log } = require("../src/util");
+
+const mqttUsername = "louis1";
+const mqttPassword = "!@#$LLam";
+
+class SimpleMqttServer {
+ aedes = require("aedes")();
+ server = require("net").createServer(this.aedes.handle);
+
+ /**
+ * @param {number} port Port to listen on
+ */
+ constructor(port) {
+ this.port = port;
+ }
+
+ /**
+ * Start the MQTT server
+ * @returns {void}
+ */
+ start() {
+ this.server.listen(this.port, () => {
+ console.log("server started and listening on port ", this.port);
+ });
+ }
+}
+
+let server1 = new SimpleMqttServer(10000);
+
+server1.aedes.authenticate = function (client, username, password, callback) {
+ if (username && password) {
+ console.log(password.toString("utf-8"));
+ callback(null, username === mqttUsername && password.toString("utf-8") === mqttPassword);
+ } else {
+ callback(null, false);
+ }
+};
+
+server1.aedes.on("subscribe", (subscriptions, client) => {
+ console.log(subscriptions);
+
+ for (let s of subscriptions) {
+ if (s.topic === "test") {
+ server1.aedes.publish({
+ topic: "test",
+ payload: Buffer.from("ok"),
+ }, (error) => {
+ if (error) {
+ log.error("mqtt_server", error);
+ }
+ });
+ }
+ }
+
+});
+
+server1.start();
diff --git a/extra/sort-contributors.js b/extra/sort-contributors.js
new file mode 100644
index 0000000..b60d191
--- /dev/null
+++ b/extra/sort-contributors.js
@@ -0,0 +1,22 @@
+const fs = require("fs");
+
+// Read the file from private/sort-contributors.txt
+const file = fs.readFileSync("private/sort-contributors.txt", "utf8");
+
+// Convert to an array of lines
+let lines = file.split("\n");
+
+// Remove empty lines
+lines = lines.filter((line) => line !== "");
+
+// Remove duplicates
+lines = [ ...new Set(lines) ];
+
+// Remove @weblate and @UptimeKumaBot
+lines = lines.filter((line) => line !== "@weblate" && line !== "@UptimeKumaBot" && line !== "@louislam");
+
+// Sort the lines
+lines = lines.sort();
+
+// Output the lines, concat with " "
+console.log(lines.join(" "));
diff --git a/extra/update-language-files/.gitignore b/extra/update-language-files/.gitignore
new file mode 100644
index 0000000..410c913
--- /dev/null
+++ b/extra/update-language-files/.gitignore
@@ -0,0 +1,3 @@
+package-lock.json
+test.js
+languages/
diff --git a/extra/update-language-files/index.js b/extra/update-language-files/index.js
new file mode 100644
index 0000000..acb6bd4
--- /dev/null
+++ b/extra/update-language-files/index.js
@@ -0,0 +1,103 @@
+// Need to use ES6 to read language files
+
+import fs from "fs";
+import util from "util";
+
+/**
+ * Copy across the required language files
+ * Creates a local directory (./languages) and copies the required files
+ * into it.
+ * @param {string} langCode Code of language to update. A file will be
+ * created with this code if one does not already exist
+ * @param {string} baseLang The second base language file to copy. This
+ * will be ignored if set to "en" as en.js is copied by default
+ * @returns {void}
+ */
+function copyFiles(langCode, baseLang) {
+ if (fs.existsSync("./languages")) {
+ fs.rmSync("./languages", {
+ recursive: true,
+ force: true,
+ });
+ }
+ fs.mkdirSync("./languages");
+
+ if (!fs.existsSync(`../../src/languages/${langCode}.js`)) {
+ fs.closeSync(fs.openSync(`./languages/${langCode}.js`, "a"));
+ } else {
+ fs.copyFileSync(`../../src/languages/${langCode}.js`, `./languages/${langCode}.js`);
+ }
+ fs.copyFileSync("../../src/languages/en.js", "./languages/en.js");
+ if (baseLang !== "en") {
+ fs.copyFileSync(`../../src/languages/${baseLang}.js`, `./languages/${baseLang}.js`);
+ }
+}
+
+/**
+ * Update the specified language file
+ * @param {string} langCode Language code to update
+ * @param {string} baseLangCode Second language to copy keys from
+ * @returns {void}
+ */
+async function updateLanguage(langCode, baseLangCode) {
+ const en = (await import("./languages/en.js")).default;
+ const baseLang = (await import(`./languages/${baseLangCode}.js`)).default;
+
+ let file = langCode + ".js";
+ console.log("Processing " + file);
+ const lang = await import("./languages/" + file);
+
+ let obj;
+
+ if (lang.default) {
+ obj = lang.default;
+ } else {
+ console.log("Empty file");
+ obj = {
+ languageName: "<Your Language name in your language (not in English)>"
+ };
+ }
+
+ // En first
+ for (const key in en) {
+ if (! obj[key]) {
+ obj[key] = en[key];
+ }
+ }
+
+ if (baseLang !== en) {
+ // Base second
+ for (const key in baseLang) {
+ if (! obj[key]) {
+ obj[key] = key;
+ }
+ }
+ }
+
+ const code = "export default " + util.inspect(obj, {
+ depth: null,
+ });
+
+ fs.writeFileSync(`../../src/languages/${file}`, code);
+}
+
+// Get command line arguments
+const baseLangCode = process.env.npm_config_baselang || "en";
+const langCode = process.env.npm_config_language;
+
+// We need the file to edit
+if (langCode == null) {
+ throw new Error("Argument --language=<code> must be provided");
+}
+
+console.log("Base Lang: " + baseLangCode);
+console.log("Updating: " + langCode);
+
+copyFiles(langCode, baseLangCode);
+await updateLanguage(langCode, baseLangCode);
+fs.rmSync("./languages", {
+ recursive: true,
+ force: true,
+});
+
+console.log("Done. Fixing formatting by ESLint...");
diff --git a/extra/update-language-files/package.json b/extra/update-language-files/package.json
new file mode 100644
index 0000000..c729517
--- /dev/null
+++ b/extra/update-language-files/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "update-language-files",
+ "type": "module",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC"
+}
diff --git a/extra/update-version.js b/extra/update-version.js
new file mode 100644
index 0000000..f9aead0
--- /dev/null
+++ b/extra/update-version.js
@@ -0,0 +1,81 @@
+const pkg = require("../package.json");
+const fs = require("fs");
+const childProcess = require("child_process");
+const util = require("../src/util");
+
+util.polyfill();
+
+const newVersion = process.env.RELEASE_VERSION;
+
+console.log("New Version: " + newVersion);
+
+if (! newVersion) {
+ console.error("invalid version");
+ process.exit(1);
+}
+
+const exists = tagExists(newVersion);
+
+if (! exists) {
+
+ // Process package.json
+ pkg.version = newVersion;
+
+ // Replace the version: https://regex101.com/r/hmj2Bc/1
+ pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
+ fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
+
+ // Also update package-lock.json
+ const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
+ childProcess.spawnSync(npm, [ "install" ]);
+
+ commit(newVersion);
+ tag(newVersion);
+
+} else {
+ console.log("version exists");
+}
+
+/**
+ * Commit updated files
+ * @param {string} version Version to update to
+ * @returns {void}
+ * @throws Error when committing files
+ */
+function commit(version) {
+ let msg = "Update to " + version;
+
+ let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
+ let stdout = res.stdout.toString().trim();
+ console.log(stdout);
+
+ if (stdout.includes("no changes added to commit")) {
+ throw new Error("commit error");
+ }
+}
+
+/**
+ * Create a tag with the specified version
+ * @param {string} version Tag to create
+ * @returns {void}
+ */
+function tag(version) {
+ let res = childProcess.spawnSync("git", [ "tag", version ]);
+ console.log(res.stdout.toString().trim());
+}
+
+/**
+ * Check if a tag exists for the specified version
+ * @param {string} version Version to check
+ * @returns {boolean} Does the tag already exist
+ * @throws Version is not valid
+ */
+function tagExists(version) {
+ if (! version) {
+ throw new Error("invalid version");
+ }
+
+ let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
+
+ return res.stdout.toString().trim() === version;
+}
diff --git a/extra/update-wiki-version.js b/extra/update-wiki-version.js
new file mode 100644
index 0000000..f6c9612
--- /dev/null
+++ b/extra/update-wiki-version.js
@@ -0,0 +1,58 @@
+const childProcess = require("child_process");
+const fs = require("fs");
+
+const newVersion = process.env.RELEASE_VERSION;
+
+if (!newVersion) {
+ console.log("Missing version");
+ process.exit(1);
+}
+
+updateWiki(newVersion);
+
+/**
+ * Update the wiki with new version number
+ * @param {string} newVersion Version to update to
+ * @returns {void}
+ */
+function updateWiki(newVersion) {
+ const wikiDir = "./tmp/wiki";
+ const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md";
+
+ safeDelete(wikiDir);
+
+ childProcess.spawnSync("git", [ "clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir ]);
+ let content = fs.readFileSync(howToUpdateFilename).toString();
+
+ // Replace the version: https://regex101.com/r/hmj2Bc/1
+ content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`);
+ fs.writeFileSync(howToUpdateFilename, content);
+
+ childProcess.spawnSync("git", [ "add", "-A" ], {
+ cwd: wikiDir,
+ });
+
+ childProcess.spawnSync("git", [ "commit", "-m", `Update to ${newVersion}` ], {
+ cwd: wikiDir,
+ });
+
+ console.log("Pushing to Github");
+ childProcess.spawnSync("git", [ "push" ], {
+ cwd: wikiDir,
+ });
+
+ safeDelete(wikiDir);
+}
+
+/**
+ * Check if a directory exists and then delete it
+ * @param {string} dir Directory to delete
+ * @returns {void}
+ */
+function safeDelete(dir) {
+ if (fs.existsSync(dir)) {
+ fs.rm(dir, {
+ recursive: true,
+ });
+ }
+}
diff --git a/extra/upload-github-release-asset.sh b/extra/upload-github-release-asset.sh
new file mode 100644
index 0000000..206e3cd
--- /dev/null
+++ b/extra/upload-github-release-asset.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+#
+# Author: Stefan Buck
+# License: MIT
+# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447
+#
+#
+# This script accepts the following parameters:
+#
+# * owner
+# * repo
+# * tag
+# * filename
+# * github_api_token
+#
+# Script to upload a release asset using the GitHub API v3.
+#
+# Example:
+#
+# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip
+#
+
+# Check dependencies.
+set -e
+xargs=$(which gxargs || which xargs)
+
+# Validate settings.
+[ "$TRACE" ] && set -x
+
+CONFIG=$@
+
+for line in $CONFIG; do
+ eval "$line"
+done
+
+# Define variables.
+GH_API="https://api.github.com"
+GH_REPO="$GH_API/repos/$owner/$repo"
+GH_TAGS="$GH_REPO/releases/tags/$tag"
+AUTH="Authorization: token $github_api_token"
+WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie"
+CURL_ARGS="-LJO#"
+
+if [[ "$tag" == 'LATEST' ]]; then
+ GH_TAGS="$GH_REPO/releases/latest"
+fi
+
+# Validate token.
+curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; }
+
+# Read asset tags.
+response=$(curl -sH "$AUTH" $GH_TAGS)
+
+# Get ID of the asset based on given filename.
+eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=')
+[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; }
+
+# Upload asset
+echo "Uploading asset... "
+
+# Construct url
+GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)"
+
+curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET
diff --git a/extra/uptime-kuma-push/.gitignore b/extra/uptime-kuma-push/.gitignore
new file mode 100644
index 0000000..a007fea
--- /dev/null
+++ b/extra/uptime-kuma-push/.gitignore
@@ -0,0 +1 @@
+build/*
diff --git a/extra/uptime-kuma-push/Dockerfile b/extra/uptime-kuma-push/Dockerfile
new file mode 100644
index 0000000..9d619d6
--- /dev/null
+++ b/extra/uptime-kuma-push/Dockerfile
@@ -0,0 +1,18 @@
+FROM node AS build
+RUN useradd --create-home kuma
+USER kuma
+WORKDIR /home/kuma
+ARG TARGETPLATFORM
+COPY --chown=kuma:kuma ./build/ ./build/
+COPY --chown=kuma:kuma build.js build.js
+RUN node build.js $TARGETPLATFORM
+
+FROM debian:bookworm-slim AS release
+RUN useradd --create-home kuma
+USER kuma
+WORKDIR /home/kuma
+COPY --from=build /home/kuma/uptime-kuma-push ./uptime-kuma-push
+
+ENTRYPOINT ["/home/kuma/uptime-kuma-push"]
+
+
diff --git a/extra/uptime-kuma-push/build.js b/extra/uptime-kuma-push/build.js
new file mode 100644
index 0000000..3dc8bf4
--- /dev/null
+++ b/extra/uptime-kuma-push/build.js
@@ -0,0 +1,48 @@
+const fs = require("fs");
+const platform = process.argv[2];
+
+if (!platform) {
+ console.error("No platform??");
+ process.exit(1);
+}
+
+const supportedPlatforms = [
+ {
+ name: "linux/amd64",
+ bin: "./build/uptime-kuma-push-amd64"
+ },
+ {
+ name: "linux/arm64",
+ bin: "./build/uptime-kuma-push-arm64"
+ },
+ {
+ name: "linux/arm/v7",
+ bin: "./build/uptime-kuma-push-armv7"
+ }
+];
+
+let platformObj = null;
+
+// Check if the platform is supported
+for (let i = 0; i < supportedPlatforms.length; i++) {
+ if (supportedPlatforms[i].name === platform) {
+ platformObj = supportedPlatforms[i];
+ break;
+ }
+}
+
+if (platformObj) {
+ let filename = platformObj.bin;
+
+ if (!fs.existsSync(filename)) {
+ console.error(`prebuilt: ${filename} is not found, please build it first`);
+ process.exit(1);
+ }
+
+ fs.renameSync(filename, "./uptime-kuma-push");
+ process.exit(0);
+} else {
+ console.error("Unsupported platform: " + platform);
+ process.exit(1);
+}
+
diff --git a/extra/uptime-kuma-push/package.json b/extra/uptime-kuma-push/package.json
new file mode 100644
index 0000000..f215436
--- /dev/null
+++ b/extra/uptime-kuma-push/package.json
@@ -0,0 +1,13 @@
+{
+ "scripts": {
+ "build-docker": "npm run build-all && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:push . --push --target release",
+ "build-all": "npm run build-win && npm run build-linux-amd64 && npm run build-linux-arm64 && npm run build-linux-armv7 && npm run build-linux-armv6 && npm run build-linux-armv5 && npm run build-linux-riscv64",
+ "build-win": "cross-env GOOS=windows GOARCH=amd64 go build -x -o ./build/uptime-kuma-push.exe uptime-kuma-push.go",
+ "build-linux-amd64": "cross-env GOOS=linux GOARCH=amd64 go build -x -o ./build/uptime-kuma-push-amd64 uptime-kuma-push.go",
+ "build-linux-arm64": "cross-env GOOS=linux GOARCH=arm64 go build -x -o ./build/uptime-kuma-push-arm64 uptime-kuma-push.go",
+ "build-linux-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./build/uptime-kuma-push-armv7 uptime-kuma-push.go",
+ "build-linux-armv6": "cross-env GOOS=linux GOARCH=arm GOARM=6 go build -x -o ./build/uptime-kuma-push-armv6 uptime-kuma-push.go",
+ "build-linux-armv5": "cross-env GOOS=linux GOARCH=arm GOARM=5 go build -x -o ./build/uptime-kuma-push-armv5 uptime-kuma-push.go",
+ "build-linux-riscv64": "cross-env GOOS=linux GOARCH=riscv64 go build -x -o ./build/uptime-kuma-push-riscv64 uptime-kuma-push.go"
+ }
+}
diff --git a/extra/uptime-kuma-push/uptime-kuma-push.go b/extra/uptime-kuma-push/uptime-kuma-push.go
new file mode 100644
index 0000000..69cd1f8
--- /dev/null
+++ b/extra/uptime-kuma-push/uptime-kuma-push.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ os "os"
+ "time"
+)
+
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Fprintln(os.Stderr, "Usage: uptime-kuma-push <url> [<interval>]")
+ os.Exit(1)
+ }
+
+ pushURL := os.Args[1]
+
+ var interval time.Duration
+
+ if len(os.Args) >= 3 {
+ intervalString, err := time.ParseDuration(os.Args[2] + "s")
+ interval = intervalString
+
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Error: Invalid interval", err)
+ os.Exit(1)
+ }
+
+ } else {
+ interval = 60 * time.Second
+ }
+
+ for {
+ _, err := http.Get(pushURL)
+ if err == nil {
+ fmt.Print("Pushed!")
+ } else {
+ fmt.Print("Error: ", err)
+ }
+
+ fmt.Println(" Sleeping for", interval)
+ time.Sleep(interval)
+ }
+}