Compare commits

..

14 Commits

Author SHA1 Message Date
Louis Lam
df23942f65 chore: remove @aws-sdk, @azure packages [3.0.X] (#7102) 2026-03-06 22:35:46 +08:00
Louis Lam
f154fcf8da chore: merge changes to 3.0.X (#7104) 2026-03-06 22:30:07 +08:00
Louis Lam
7a16d803d3 Manual merge 2026-03-06 22:28:23 +08:00
Louis Lam
73d3573198 Merge branch 'master' into 3.0.X-merge
# Conflicts:
#	.github/workflows/validate.yml
#	extra/rdap-dns.json
#	extra/release/final.mjs
#	package-lock.json
#	package.json
#	src/util.js
2026-03-06 22:26:21 +08:00
Dream
9df8a957c7 fix: Explicitly pushing status=down now bypasses retry logic and goes directly to DOWN (#6595)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-07 09:52:25 +01:00
Louis Lam
baaf14c594 chore: Merge Changes from master to 3.0.X (#6806) 2026-01-25 07:15:54 +08:00
Louis Lam
b3388f5bb7 Regenerate package-lock.json 2026-01-25 07:06:43 +08:00
Louis Lam
3f1866f658 Merge branch 'master' into 3.0.X-merge
# Conflicts:
#	.github/workflows/auto-test.yml
#	CONTRIBUTING.md
#	extra/build-healthcheck.js
#	extra/release/beta.mjs
#	extra/release/lib.mjs
#	extra/sort-contributors.js
#	package-lock.json
#	package.json
#	src/util.js
#	tsconfig-backend.json
2026-01-25 07:02:20 +08:00
Louis Lam
57f5414d79 [3.0.0] Merge changes from master (#6634) 2026-01-07 16:02:20 +08:00
Louis Lam
0b174ef25a Match new playwright version 2026-01-07 16:01:07 +08:00
Louis Lam
9cfa0f483d Update lock file 2026-01-07 15:55:31 +08:00
Louis Lam
f28cba8388 Merge branch 'master' into 3.0.X-merge
# Conflicts:
#	.github/workflows/auto-test.yml
#	.github/workflows/validate.yml
#	CONTRIBUTING.md
#	package-lock.json
#	package.json
#	src/util.js
2026-01-07 15:53:59 +08:00
Louis Lam
ac5781d711 Update playwright from ~1.39.0 to ~1.56.1 (#6321) 2025-11-08 02:58:58 +08:00
Louis Lam
93fc8e463f [3.0.0] Project Upgrade (#6310) 2025-11-05 21:54:55 +08:00
45 changed files with 1582 additions and 4415 deletions

View File

@@ -6,7 +6,7 @@ concurrency:
on:
push:
branches: [master, 1.23.X, 3.0.0]
branches: [master, 1.23.X, 3.0.X]
pull_request:
permissions: {}
@@ -21,11 +21,7 @@ jobs:
matrix:
os: [macos-latest, ubuntu-22.04, windows-latest, ubuntu-22.04-arm]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
node: [20, 24]
# Also test non-LTS, but only on Ubuntu.
include:
- os: ubuntu-22.04
node: 25
node: [24, 25]
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
@@ -55,37 +51,6 @@ jobs:
HEADLESS_TEST: 1
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
armv7-simple-test:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
node: [20, 22]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
with:
platforms: linux/arm/v7
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
- name: Test on ARMv7 using Docker with QEMU
run: |
docker run --rm --platform linux/arm/v7 \
-v $PWD:/workspace \
-w /workspace \
arm32v7/node:${{ matrix.node }} \
npm clean-install --no-fund --production
check-linters:
runs-on: ubuntu-latest
permissions:
@@ -103,10 +68,10 @@ jobs:
path: node_modules
key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
- name: Use Node.js 20
- name: Use Node.js 24
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 20
node-version: 24
- run: npm clean-install --no-fund
- run: npm run lint:prod
@@ -115,7 +80,7 @@ jobs:
permissions:
contents: read
env:
PLAYWRIGHT_VERSION: ~1.39.0
PLAYWRIGHT_VERSION: ~1.56.0
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -131,7 +96,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 22
node-version: 24
- run: npm clean-install --no-fund
- name: Rebuild native modules for ARM64

View File

@@ -1,49 +0,0 @@
name: Build Docker Push Image
on:
schedule:
# Runs at 2:00 AM UTC on the 1st of every month
- cron: "0 2 1 * *"
workflow_dispatch: # Allow manual trigger
permissions: {}
jobs:
build-docker-push:
# Only run on the original repository, not on forks
if: github.repository == 'louislam/uptime-kuma'
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
contents: read
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Use Node.js 20
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 20
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
- name: Install cross-env
run: npm install -g cross-env
- name: Build and push Docker image
working-directory: extra/uptime-kuma-push
run: npm run build-docker

View File

@@ -7,6 +7,7 @@ on:
branches:
- master
- 1.23.X
- 3.0.X
workflow_dispatch:
permissions: {}

View File

@@ -1,26 +0,0 @@
# CLAUDE.md
This is `guardrail` — part of BlackRoad OS.
> GuardRail — BlackRoad uptime monitoring. Forked from Uptime Kuma.
## Owner
BlackRoad OS, Inc. — Proprietary. All rights reserved.
## AI Instructions
- Part of BlackRoad ecosystem (615+ repos, 15 orgs)
- Primary git: RoadCode (Gitea). GitHub is mirror.
- All code proprietary unless marked otherwise
- Brand: black bg, white text, gradient accents
- Fonts: Space Grotesk, Inter, JetBrains Mono
- Language: JavaScript
## Memory System
- Check codex: `memory-codex.sh search "<problem>"`
- Log actions: `memory-system.sh log <action> <entity> "<details>"`
- Broadcast: `memory-til-broadcast.sh broadcast <cat> "<learning>"`
## Collaboration
- Register: `memory-collaboration.sh register`
- Claim: `memory-collaboration.sh claim "<task>"`
- Board: `memory-collaboration.sh board`

View File

@@ -288,7 +288,7 @@ you can finally start the app. The goal is to make the Uptime Kuma installation
as easy as installing a mobile app.
- Easy to install for non-Docker users
- no native build dependency is needed (for `x86_64`/`armv7`/`arm64`)
- no native build dependency is needed (for `x86_64`/`arm64`)
- no extra configuration and
- no extra effort required to get it running
@@ -469,7 +469,7 @@ We have a few procedures we follow. These are documented here:
- <details><summary><b>Set up a Docker Builder</b> (click to expand)</summary>
<p>
- amd64, armv7 using local.
- amd64 using local.
- arm64 using remote arm64 cpu, as the emulator is too slow and can no longer
pass the `npm ci` command.
1. Add the public key to the remote server.
@@ -483,7 +483,7 @@ We have a few procedures we follow. These are documented here:
3. Create a new builder.
```bash
docker buildx create --name kuma-builder --platform linux/amd64,linux/arm/v7
docker buildx create --name kuma-builder --platform linux/amd64
docker buildx use kuma-builder
docker buildx inspect --bootstrap
```
@@ -516,8 +516,7 @@ We have a few procedures we follow. These are documented here:
These Items need to be checked:
- [ ] Check all tags is fine on
<https://hub.docker.com/r/louislam/uptime-kuma/tags>
- [ ] Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 /
armv7)
- [ ] Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64)
- [ ] Try clean installation with Node.js
</p>

View File

@@ -59,7 +59,7 @@ export default defineConfig({
// Run your local dev server before starting the tests.
webServer: {
command: `node extra/remove-playwright-test-data.js && cross-env NODE_ENV=development node server/server.js --port=${port} --data-dir=./data/playwright-test`,
command: `node extra/remove-playwright-test-data.js && cross-env NODE_ENV=development node --import=tsx server/server.js --port=${port} --data-dir=./data/playwright-test`,
url,
reuseExistingServer: false,
cwd: "../",

View File

@@ -1,22 +1,15 @@
############################################
# Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
############################################
FROM golang:1-buster
FROM golang:1-trixie
WORKDIR /app
ARG TARGETPLATFORM
COPY ./extra/ ./extra/
## Switch to archive.debian.org
RUN sed -i '/^deb/s/^/#/' /etc/apt/sources.list \
&& echo "deb http://archive.debian.org/debian buster main contrib non-free" | tee -a /etc/apt/sources.list \
&& echo "deb http://archive.debian.org/debian-security buster/updates main contrib non-free" | tee -a /etc/apt/sources.list \
&& echo "deb http://archive.debian.org/debian buster-updates main contrib non-free" | tee -a /etc/apt/sources.list
# Compile healthcheck.go
RUN apt update && \
apt --yes --no-install-recommends install curl && \
curl -sL https://deb.nodesource.com/setup_18.x | bash && \
curl -sL https://deb.nodesource.com/setup_24.x | bash && \
apt --yes --no-install-recommends install nodejs && \
node ./extra/build-healthcheck.js $TARGETPLATFORM && \
apt --yes remove nodejs

View File

@@ -1,5 +1,5 @@
# Download Apprise deb package
FROM node:22-bookworm-slim AS download-apprise
FROM node:24-trixie-slim AS download-apprise
WORKDIR /app
COPY ./extra/download-apprise.mjs ./download-apprise.mjs
RUN apt update && \
@@ -9,7 +9,7 @@ RUN apt update && \
# Base Image (Slim)
# If the image changed, the second stage image should be changed too
FROM node:22-bookworm-slim AS base2-slim
FROM node:24-trixie-slim AS base3-slim
ARG TARGETPLATFORM
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
@@ -35,7 +35,6 @@ RUN apt update && \
apt --yes autoremove
# 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)
# 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
@@ -47,7 +46,7 @@ RUN apt update && \
# Install cloudflared
RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main' | tee /etc/apt/sources.list.d/cloudflared.list && \
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | tee /etc/apt/sources.list.d/cloudflared.list && \
apt update && \
apt install --yes --no-install-recommends cloudflared && \
cloudflared version && \
@@ -62,8 +61,7 @@ COPY ./docker/etc/sudoers /etc/sudoers
# Full Base Image
# MariaDB, Chromium and fonts
# Make sure to reuse the slim image here. Uncomment the above line if you want to build it from scratch.
# FROM base2-slim AS base2
FROM louislam/uptime-kuma:base2-slim AS base2
FROM louislam/uptime-kuma:base3-slim AS base3
ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1
RUN apt update && \
apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \

View File

@@ -3,7 +3,7 @@ version: "3.8"
services:
uptime-kuma:
container_name: uptime-kuma-dev
image: louislam/uptime-kuma:nightly2
image: louislam/uptime-kuma:nightly3
volumes:
#- ./data:/app/data
- ../server:/app/server

View File

@@ -1,16 +1,16 @@
ARG BASE_IMAGE=louislam/uptime-kuma:base2
ARG BASE_IMAGE=louislam/uptime-kuma:base3
############################################
# Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, otherwise it will be super slow where it is building the armv7 healthcheck
# Check file: builder-go.dockerfile
############################################
FROM louislam/uptime-kuma:builder-go AS build_healthcheck
FROM louislam/uptime-kuma:builder-go3 AS build_healthcheck
############################################
# Build in Node.js
############################################
FROM louislam/uptime-kuma:base2 AS build
FROM louislam/uptime-kuma:base3 AS build
USER node
WORKDIR /app
@@ -59,7 +59,7 @@ USER node
############################################
# Build an image for testing pr
############################################
FROM louislam/uptime-kuma:base2 AS pr-test2
FROM louislam/uptime-kuma:base3 AS pr-test2
WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
@@ -92,7 +92,7 @@ CMD ["npm", "run", "start-pr-test"]
############################################
# Upload the artifact to Github
############################################
FROM louislam/uptime-kuma:base2 AS upload-artifact
FROM louislam/uptime-kuma:base3 AS upload-artifact
WORKDIR /
RUN apt update && \
apt --yes install curl file

View File

@@ -1,5 +1,4 @@
const childProcess = require("child_process");
const fs = require("fs");
const platform = process.argv[2];
if (!platform) {
@@ -7,22 +6,5 @@ if (!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);

View File

@@ -1,55 +0,0 @@
/*
* ⚠️ ⚠️ ⚠️ ⚠️ 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();

View File

@@ -61,14 +61,14 @@ if (!dryRun) {
repoNames,
["beta-slim-rootless", ver(version, "slim-rootless")],
"rootless",
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
"BASE_IMAGE=louislam/uptime-kuma:base3-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");
buildImage(repoNames, ["beta-slim", ver(version, "slim")], "release", "BASE_IMAGE=louislam/uptime-kuma:base3-slim");
// Build full image
buildImage(repoNames, ["beta", version], "release");

View File

@@ -59,24 +59,24 @@ if (!dryRun) {
// Build slim image (rootless)
buildImage(
repoNames,
["2-slim-rootless", ver(version, "slim-rootless")],
["3-slim-rootless", ver(version, "slim-rootless")],
"rootless",
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
"BASE_IMAGE=louislam/uptime-kuma:base3-slim"
);
// Build full image (rootless)
buildImage(repoNames, ["2-rootless", ver(version, "rootless")], "rootless");
buildImage(repoNames, ["3-rootless", ver(version, "rootless")], "rootless");
// Build slim image
buildImage(
repoNames,
["next-slim", "2-slim", ver(version, "slim")],
["next-slim", "3-slim", ver(version, "slim")],
"release",
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
"BASE_IMAGE=louislam/uptime-kuma:base3-slim"
);
// Build full image
buildImage(repoNames, ["next", "2", version], "release");
buildImage(repoNames, ["next", "3", version], "release");
} else {
console.log("Dry run mode - skipping image build and push.");
}

View File

@@ -64,7 +64,7 @@ export function buildImage(
target,
buildArgs = "",
dockerfile = "docker/dockerfile",
platform = "linux/amd64,linux/arm64,linux/arm/v7"
platform = "linux/amd64,linux/arm64"
) {
let args = ["buildx", "build", "-f", dockerfile, "--platform", platform];

4431
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "uptime-kuma",
"version": "2.2.1",
"version": "3.0.0-beta.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/louislam/uptime-kuma.git"
},
"engines": {
"node": ">= 20.4.0"
"node": ">= 24.0.0"
},
"scripts": {
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
@@ -20,31 +20,27 @@
"lint:prod": "npm run lint:js-prod && npm run lint:style",
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
"start": "npm run start-server",
"start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
"start-server-dev:watch": "cross-env NODE_ENV=development node --watch server/server.js",
"start-server": "tsx server/server.js",
"start-server-dev": "cross-env NODE_ENV=development tsx server/server.js",
"start-server-dev:watch": "cross-env NODE_ENV=development tsx --watch server/server.js",
"build": "vite build --config ./config/vite.config.js",
"test": "npm run test-backend && npm run test-e2e",
"test-with-build": "npm run build && npm test",
"test-backend": "node test/test-backend.mjs",
"test-backend-22": "cross-env TEST_BACKEND=1 node --test --test-reporter=spec \"test/backend-test/**/*.js\"",
"test-backend-20": "cross-env TEST_BACKEND=1 node --test --test-reporter=spec test/backend-test",
"test-backend": "cross-env TEST_BACKEND=1 node --import=tsx --test --test-reporter=spec \"test/backend-test/**/*.js\"",
"test-e2e": "playwright test --config ./config/playwright.config.js",
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
"playwright-show-report": "playwright show-report ./private/playwright-report",
"tsc": "tsc --project ./tsconfig-backend.json",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"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-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",
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:base3 --target base3 . --push",
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:base3-slim --target base3-slim . --push",
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:builder-go3 . --push",
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly3 --target nightly .",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test3 --target pr-test3 . --push",
"upload-artifacts": "node extra/release/upload-artifacts.mjs",
"upload-artifacts-beta": "node extra/release/upload-artifacts-beta.mjs",
"setup": "git checkout 2.2.1 && npm ci --omit dev --no-audit && npm run download-dist",
"setup": "git checkout 2.2.0 && npm ci --omit dev --no-audit && 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",
@@ -60,11 +56,9 @@
"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.mjs && npm install && npm run dev",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
"deploy-demo-server": "node extra/deploy-demo-server.js",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly3",
"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",
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js",
"generate-changelog": "node ./extra/generate-changelog.mjs"
},
@@ -111,7 +105,7 @@
"kafkajs": "~2.2.4",
"knex": "~3.1.0",
"limiter": "~2.1.0",
"liquidjs": "~10.25.0",
"liquidjs": "~10.24.0",
"marked": "~14.1.4",
"mitt": "~3.0.1",
"mongodb": "~4.17.2",
@@ -129,7 +123,7 @@
"password-hash": "~1.2.2",
"pg": "~8.11.6",
"pg-connection-string": "~2.6.4",
"playwright-core": "~1.39.0",
"playwright-core": "~1.56.1",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.2",
"promisify-child-process": "~4.1.2",
@@ -150,7 +144,9 @@
"tough-cookie": "~4.1.4",
"validator": "~13.15.26",
"web-push": "~3.6.7",
"ws": "~8.19.0"
"tsx": "~4.20.6",
"ws": "~8.19.0",
"zod": "~4.1.12"
},
"devDependencies": {
"@actions/github": "~6.0.1",
@@ -158,7 +154,7 @@
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.1.3",
"@playwright/test": "~1.39.0",
"@playwright/test": "~1.56.1",
"@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.28.0",
"@testcontainers/mariadb": "^10.28.0",
@@ -174,7 +170,7 @@
"@vitejs/plugin-vue": "~5.0.5",
"@vue/compiler-sfc": "~3.4.38",
"@vuepic/vue-datepicker": "~3.4.8",
"aedes": "~1.0.0",
"aedes": "^0.46.3",
"bootstrap": "5.1.3",
"chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4",
@@ -182,14 +178,16 @@
"core-js": "~3.26.1",
"cronstrue": "~2.24.0",
"cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.5",
"dompurify": "~3.3.2",
"dompurify": "~3.2.7",
"eslint": "~8.14.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "~46.4.6",
"eslint-plugin-vue": "~8.7.1",
"eslint-plugin-vue-scoped-css": "~2.7.2",
"favico.js": "~0.3.10",
"get-port-please": "^3.2.0",
"node-ssh": "~13.1.0",
"postcss-html": "~1.8.1",
"postcss-rtlcss": "~5.7.1",
@@ -223,5 +221,12 @@
"vuedraggable": "~4.1.0",
"wait-on": "^7.2.0",
"whatwg-url": "~12.0.1"
},
"overrides": {
"@aws-sdk/credential-providers": "npm:useless-module@1.0.0",
"tedious": {
"@azure/identity": "npm:useless-module@1.0.0",
"@azure/keyvault-keys": "npm:useless-module@1.0.0"
}
}
}

View File

@@ -275,6 +275,7 @@ class Database {
// See: https://github.com/knex/knex/issues/3176#issuecomment-3389054899
min: 0,
max: 20,
propagateCreateError: false,
acquireTimeoutMillis: acquireConnectionTimeout,
afterCreate: (rawConn, done) => {
this.initSQLite(rawConn, testMode)

View File

@@ -2059,7 +2059,7 @@ class Monitor extends BeanModel {
}
const parentActive = await Monitor.isParentActive(parent.id);
return parent.active === 1 && parentActive;
return parent.active && parentActive;
}
/**

View File

@@ -1,249 +0,0 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class Fluxer extends NotificationProvider {
name = "fluxer";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
const fluxerDisplayName = notification.fluxerUsername || "Uptime Kuma";
const webhookUrl = new URL(notification.fluxerWebhookUrl);
// Check if the webhook has an avatar
let webhookHasAvatar = true;
try {
const webhookInfo = await axios.get(webhookUrl.toString(), config);
webhookHasAvatar = !!webhookInfo.data.avatar;
} catch (e) {
// If we can't verify, we assume he has an avatar to avoid forcing the default avatar
webhookHasAvatar = true;
}
const messageFormat =
notification.fluxerMessageFormat || (notification.fluxerUseMessageTemplate ? "custom" : "normal");
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let content = msg;
if (messageFormat === "minimalist") {
content = "Test: " + msg;
} else if (messageFormat === "custom") {
const customMessage = notification.fluxerMessageTemplate?.trim() || "";
if (customMessage !== "") {
content = await this.renderTemplate(customMessage, msg, monitorJSON, heartbeatJSON);
}
}
let fluxertestdata = {
username: fluxerDisplayName,
content: content,
};
if (!webhookHasAvatar) {
fluxertestdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
await axios.post(webhookUrl.toString(), fluxertestdata, config);
return okMsg;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
let addess = this.extractAddress(monitorJSON);
// Minimalist: status + name only (is down / is up; no "back up" — may be first trigger)
if (messageFormat === "minimalist") {
const content =
heartbeatJSON["status"] === DOWN
? "🔴 " + monitorJSON["name"] + " is down."
: "🟢 " + monitorJSON["name"] + " is up.";
let payload = {
username: fluxerDisplayName,
content: content,
};
if (!webhookHasAvatar) {
payload.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
await axios.post(webhookUrl.toString(), payload, config);
return okMsg;
}
// Custom template: send only content (no embeds)
const useCustomTemplate =
messageFormat === "custom" && (notification.fluxerMessageTemplate?.trim() || "") !== "";
if (useCustomTemplate) {
const content = await this.renderTemplate(
notification.fluxerMessageTemplate.trim(),
msg,
monitorJSON,
heartbeatJSON
);
let payload = {
username: fluxerDisplayName,
content: content,
};
if (!webhookHasAvatar) {
payload.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
await axios.post(webhookUrl.toString(), payload, config);
return okMsg;
}
if (heartbeatJSON["status"] === DOWN) {
const wentOfflineTimestamp = Math.floor(new Date(heartbeatJSON["time"]).getTime() / 1000);
let fluxerdowndata = {
username: fluxerDisplayName,
embeds: [
{
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌",
color: 16711680,
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
...(!notification.disableUrl && addess
? [
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: addess,
},
]
: []),
{
name: "Went Offline",
// F for full date/time
value: `<t:${wentOfflineTimestamp}:F>`,
},
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
},
{
name: "Error",
value: heartbeatJSON["msg"] == null ? "N/A" : heartbeatJSON["msg"],
},
],
},
],
};
if (!webhookHasAvatar) {
fluxerdowndata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.fluxerPrefixMessage) {
fluxerdowndata.content = notification.fluxerPrefixMessage;
}
await axios.post(webhookUrl.toString(), fluxerdowndata, config);
return okMsg;
} else if (heartbeatJSON["status"] === UP) {
const backOnlineTimestamp = Math.floor(new Date(heartbeatJSON["time"]).getTime() / 1000);
let downtimeDuration = null;
let wentOfflineTimestamp = null;
if (heartbeatJSON["lastDownTime"]) {
wentOfflineTimestamp = Math.floor(new Date(heartbeatJSON["lastDownTime"]).getTime() / 1000);
downtimeDuration = this.formatDuration(backOnlineTimestamp - wentOfflineTimestamp);
}
let fluxerupdata = {
username: fluxerDisplayName,
embeds: [
{
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅",
color: 65280,
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
...(!notification.disableUrl && addess
? [
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: addess,
},
]
: []),
...(wentOfflineTimestamp
? [
{
name: "Went Offline",
// F for full date/time
value: `<t:${wentOfflineTimestamp}:F>`,
},
]
: []),
...(downtimeDuration
? [
{
name: "Downtime Duration",
value: downtimeDuration,
},
]
: []),
// Show server timezone for parity with the DOWN notification embed
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
},
...(heartbeatJSON["ping"] != null
? [
{
name: "Ping",
value: heartbeatJSON["ping"] + " ms",
},
]
: []),
],
},
],
};
if (!webhookHasAvatar) {
fluxerupdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.fluxerPrefixMessage) {
fluxerupdata.content = notification.fluxerPrefixMessage;
}
await axios.post(webhookUrl.toString(), fluxerupdata, config);
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Format duration as human-readable string (e.g., "1h 23m", "45m 30s")
* TODO: Update below to `Intl.DurationFormat("en", { style: "short" }).format(duration)` once we are on a newer node version
* @param {number} timeInSeconds The time in seconds to format a duration for
* @returns {string} The formatted duration
*/
formatDuration(timeInSeconds) {
const hours = Math.floor(timeInSeconds / 3600);
const minutes = Math.floor((timeInSeconds % 3600) / 60);
const seconds = timeInSeconds % 60;
const durationParts = [];
if (hours > 0) {
durationParts.push(`${hours}h`);
}
if (minutes > 0) {
durationParts.push(`${minutes}m`);
}
if (seconds > 0 && hours === 0) {
// Only show seconds if less than an hour
durationParts.push(`${seconds}s`);
}
return durationParts.length > 0 ? durationParts.join(" ") : "0s";
}
}
module.exports = Fluxer;

View File

@@ -12,7 +12,6 @@ const CallMeBot = require("./notification-providers/call-me-bot");
const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Fluxer = require("./notification-providers/fluxer");
const Elks = require("./notification-providers/46elks");
const Feishu = require("./notification-providers/feishu");
const Notifery = require("./notification-providers/notifery");
@@ -118,7 +117,6 @@ class Notification {
new SMSC(),
new DingDing(),
new Discord(),
new Fluxer(),
new Elks(),
new Feishu(),
new FreeMobile(),

View File

@@ -52,6 +52,10 @@ router.all("/api/push/:pushToken", async (request, response) => {
let statusString = request.query.status || "up";
const statusFromParam = statusString === "up" ? UP : DOWN;
// Check if status=down was explicitly provided (not defaulting to "up")
// When explicitly pushing down, bypass retry logic and go directly to DOWN
const isExplicitDown = request.query.status === "down";
// Validate ping value - max 100 billion ms (~3.17 years)
// Fits safely in both BIGINT and FLOAT(20,2)
const MAX_PING_MS = 100000000000;
@@ -85,7 +89,14 @@ router.all("/api/push/:pushToken", async (request, response) => {
msg = "Monitor under maintenance";
bean.status = MAINTENANCE;
} else {
determineStatus(statusFromParam, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
determineStatus(
statusFromParam,
previousHeartbeat,
monitor.maxretries,
monitor.isUpsideDown(),
bean,
isExplicitDown
);
}
// Calculate uptime
@@ -129,7 +140,7 @@ router.all("/api/push/:pushToken", async (request, response) => {
Monitor.sendStats(io, monitor.id, monitor.user_id);
try {
new Prometheus(monitor, await monitor.getTags()).update(bean, undefined);
new Prometheus(monitor, []).update(bean, undefined);
} catch (e) {
log.error("prometheus", "Please submit an issue to our GitHub repo. Prometheus update error: ", e.message);
}
@@ -571,13 +582,21 @@ router.get("/api/badge/:id/response", cache("5 minutes"), async (request, respon
* @param {number} maxretries - The maximum number of retries allowed.
* @param {boolean} isUpsideDown - Indicates if the monitor is upside down.
* @param {object} bean - The new heartbeat object.
* @param {boolean} isExplicitDown - If status=down was explicitly pushed, bypass retries.
* @returns {void}
*/
function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, bean) {
function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, bean, isExplicitDown) {
if (isUpsideDown) {
status = flipStatus(status);
}
// If status=down was explicitly pushed, bypass retry logic and go directly to DOWN
if (isExplicitDown && status === DOWN) {
bean.retries = 0;
bean.status = DOWN;
return;
}
if (previousHeartbeat) {
if (previousHeartbeat.status === UP && status === DOWN) {
// Going Down

View File

@@ -3,6 +3,8 @@
* node "server/server.js"
* DO NOT require("./server") in other modules, it likely creates circular dependency!
*/
import { genSecret, getRandomInt, isDev, log, sleep } from "../src/util";
console.log("Welcome to Uptime Kuma");
// As the log function need to use dayjs, it should be very top
@@ -46,7 +48,6 @@ if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
}
const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config");
process.title = "uptime-kuma";

View File

@@ -215,7 +215,6 @@ export default {
bale: "Bale",
Bitrix24: "Bitrix24",
discord: "Discord",
fluxer: "Fluxer",
GoogleChat: "Google Chat (Google Workspace)",
gorush: "Gorush",
gotify: "Gotify",

View File

@@ -310,15 +310,15 @@ export default {
// Show ping values if it was up in this period
avgPingData.push({
x,
y: datapoint.up > 0 && datapoint.avgPing != null ? datapoint.avgPing : null,
y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.avgPing : null,
});
minPingData.push({
x,
y: datapoint.up > 0 && datapoint.avgPing != null ? datapoint.minPing : null,
y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.minPing : null,
});
maxPingData.push({
x,
y: datapoint.up > 0 && datapoint.avgPing != null ? datapoint.maxPing : null,
y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.maxPing : null,
});
downData.push({
x,

View File

@@ -1,10 +1,11 @@
<template>
<div class="mb-3">
<label for="discord-webhook-url" class="form-label">{{ $t("Discord Webhook URL") }}</label>
<HiddenInput
<input
id="discord-webhook-url"
v-model="$parent.notification.discordWebhookUrl"
type="text"
class="form-control"
required
autocomplete="false"
/>
@@ -143,13 +144,11 @@
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
import TemplatedTextarea from "../TemplatedTextarea.vue";
export default {
components: {
TemplatedTextarea,
HiddenInput,
},
mounted() {
if (!this.$parent.notification.discordChannelType) {

View File

@@ -1,87 +0,0 @@
<template>
<div class="mb-3">
<label for="fluxer-webhook-url" class="form-label">{{ $t("Fluxer Webhook URL") }}</label>
<HiddenInput
id="fluxer-webhook-url"
v-model="$parent.notification.fluxerWebhookUrl"
type="url"
required
autocomplete="false"
/>
<div class="form-text">
{{ $t("wayToGetFluxerURL") }}
</div>
</div>
<div class="mb-3">
<label for="fluxer-username" class="form-label">{{ $t("Bot Display Name") }}</label>
<input
id="fluxer-username"
v-model="$parent.notification.fluxerUsername"
type="text"
class="form-control"
autocomplete="false"
:placeholder="$root.appName"
/>
</div>
<div class="mb-3">
<label for="fluxer-prefix-message" class="form-label">{{ $t("Prefix Custom Message") }}</label>
<input
id="fluxer-prefix-message"
v-model="$parent.notification.fluxerPrefixMessage"
type="text"
class="form-control"
autocomplete="false"
:placeholder="$t('Hello @everyone is...')"
/>
</div>
<div class="mb-3">
<label for="fluxer-message-format" class="form-label">{{ $t("fluxerMessageFormat") }}</label>
<select id="fluxer-message-format" v-model="$parent.notification.fluxerMessageFormat" class="form-select">
<option value="normal">{{ $t("fluxerMessageFormatNormal") }}</option>
<option value="minimalist">{{ $t("fluxerMessageFormatMinimalist") }}</option>
<option value="custom">{{ $t("fluxerMessageFormatCustom") }}</option>
</select>
</div>
<div v-show="$parent.notification.fluxerMessageFormat === 'custom'">
<div class="mb-3">
<label for="fluxer-message-template" class="form-label">{{ $t("fluxerMessageTemplate") }}</label>
<TemplatedTextarea
id="fluxer-message-template"
v-model="$parent.notification.fluxerMessageTemplate"
:required="false"
placeholder=""
></TemplatedTextarea>
<div class="form-text">{{ $t("fluxerUseMessageTemplateDescription") }}</div>
</div>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
import TemplatedTextarea from "../TemplatedTextarea.vue";
export default {
components: {
TemplatedTextarea,
HiddenInput,
},
mounted() {
if (!this.$parent.notification.fluxerChannelType) {
this.$parent.notification.fluxerChannelType = "channel";
}
if (this.$parent.notification.disableUrl === undefined) {
this.$parent.notification.disableUrl = false;
}
// Message format: default "normal"; migrate from old checkbox
if (typeof this.$parent.notification.fluxerMessageFormat === "undefined") {
const hadCustom =
this.$parent.notification.fluxerUseMessageTemplate === true ||
!!this.$parent.notification.fluxerMessageTemplate?.trim();
this.$parent.notification.fluxerMessageFormat = hadCustom ? "custom" : "normal";
}
},
};
</script>

View File

@@ -38,7 +38,7 @@
</div>
<div class="mb-3">
<label for="notificationService" class="form-label">{{ $t("Notification Action") }}</label>
<label for="notificationService" class="form-label">{{ $t("Notification Service") }}</label>
<input
id="notificationService"
v-model="$parent.notification.notificationService"
@@ -48,7 +48,13 @@
/>
<div class="form-text">
<p>{{ $t("homeAssistantNotificationActionHelptext") }}</p>
<p>
{{
$t(
'A list of Notification Services can be found in Home Assistant under "Developer Tools > Services" search for "notification" to find your device/phone name.'
)
}}
</p>
<p>{{ $t("Automations can optionally be triggered in Home Assistant:") }}</p>
<p>
{{ $t("Trigger type:") }}

View File

@@ -11,7 +11,6 @@ import CallMeBot from "./CallMeBot.vue";
import SMSC from "./SMSC.vue";
import DingDing from "./DingDing.vue";
import Discord from "./Discord.vue";
import Fluxer from "./Fluxer.vue";
import Elks from "./46elks.vue";
import Feishu from "./Feishu.vue";
import FreeMobile from "./FreeMobile.vue";
@@ -106,7 +105,6 @@ const NotificationFormList = {
smsir: SMSIR,
DingDing: DingDing,
discord: Discord,
fluxer: Fluxer,
Elks: Elks,
Feishu: Feishu,
FreeMobile: FreeMobile,

View File

@@ -1611,14 +1611,5 @@
"360messengerWayToGetUrlAndToken": "Можете да получите вашия API ключ за 360messenger от {0}.",
"360messengerWayToWriteRecipient": "Въведете един или повече телефонни номера в международен формат без водещ плюс (напр. {0}). Разделете отделните номера със запетая.",
"GlobalpingMultipleLocationsError": "Не се поддържат множество местоположения, моля, използвайте едно местоположение за всеки монитор.",
"GlobalpingLocationDescription": "Полето за местоположение приема континенти, държави, региони, градове, ASN, интернет доставчици или облачни региони. Можете да комбинирате филтри с {plus} (напр. {amazonPlusGermany} или {comcastPlusCalifornia}). Ако латентността е важен показател, използвайте филтри, за да стесните местоположението до малък регион, за да избегнете пикове, и за по-добра стабилност задайте филтъра {datacenter}. {fullDocs}.",
"fluxerMessageFormat": "Формат на съобщението",
"fluxerMessageFormatNormal": "Нормално (с вграден rich)",
"fluxerMessageFormatCustom": "Персонализиран шаблон",
"fluxerUseMessageTemplate": "Използвай персонализиран шаблон за съобщение",
"fluxerMessageTemplate": "Шаблон за съобщение",
"Fluxer Webhook URL": "Fluxer URL адрес за уебкука",
"fluxerMessageFormatMinimalist": "Минималистичен (кратък статус)",
"fluxerUseMessageTemplateDescription": "Ако е активирано, съобщението ще бъде изпратено с помощта на персонализиран шаблон (LiquidJS). Оставете празно, за да използвате Uptime Kuma формат, който е по подразбиране.",
"wayToGetFluxerURL": "Можете да получите, като отидете в настройките на целевия канал > Уеб куки > Създаване на уеб кука > Копиране на URL адрес на уеб кука."
"GlobalpingLocationDescription": "Полето за местоположение приема континенти, държави, региони, градове, ASN, интернет доставчици или облачни региони. Можете да комбинирате филтри с {plus} (напр. {amazonPlusGermany} или {comcastPlusCalifornia}). Ако латентността е важен показател, използвайте филтри, за да стесните местоположението до малък регион, за да избегнете пикове, и за по-добра стабилност задайте филтъра {datacenter}. {fullDocs}."
}

View File

@@ -514,9 +514,9 @@
"Home Assistant URL": "Home Assistant URL",
"Long-Lived Access Token": "Long-Lived Access Token",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token.",
"Notification Action": "Notification Action",
"Notification Service": "Notification Service",
"default: notify all devices": "default: notify all devices",
"homeAssistantNotificationActionHelptext": "A list of Notification Actions can be found in Home Assistant under \"Settings > Developer Tools > Actions\". Search for \"notify\" to find your actions. Enter only the part after \"notify.\", e.g. for the action \"notify.mobile_app_xyz\" enter \"mobile_app_xyz\". For built-in mobile notifications, look for \"Send a notification via mobile_app_xyz\" (not \"Send a notification\").",
"A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.": "A list of Notification Services can be found in Home Assistant under \"Developer Tools > Services\" search for \"notification\" to find your device/phone name.",
"Automations can optionally be triggered in Home Assistant:": "Automations can optionally be triggered in Home Assistant:",
"Trigger type:": "Trigger type:",
"Event type:": "Event type:",
@@ -1358,15 +1358,6 @@
"discordUseMessageTemplate": "Use custom message template",
"discordUseMessageTemplateDescription": "If enabled, the message will be sent using a custom template (LiquidJS). Leave blank to use the default Uptime Kuma format.",
"discordMessageTemplate": "Message Template",
"fluxerMessageFormat": "Message Format",
"fluxerMessageFormatNormal": "Normal (rich embeds)",
"fluxerMessageFormatMinimalist": "Minimalist (short status)",
"fluxerMessageFormatCustom": "Custom template",
"fluxerUseMessageTemplate": "Use custom message template",
"fluxerUseMessageTemplateDescription": "If enabled, the message will be sent using a custom template (LiquidJS). Leave blank to use the default Uptime Kuma format.",
"fluxerMessageTemplate": "Message Template",
"Fluxer Webhook URL": "Fluxer Webhook URL",
"wayToGetFluxerURL": "You can get this by going to the target channel's settings > Webhooks > Create Webhook > Copy Webhook URL.",
"Ip Family": "IP Family",
"ipFamilyDescriptionAutoSelect": "Uses the {happyEyeballs} for determining the IP family.",
"Happy Eyeballs algorithm": "Happy Eyeballs algorithm",

View File

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

View File

@@ -155,7 +155,7 @@
"Skip existing": "Omitir existente",
"Overwrite": "Sobrescribir",
"Options": "Opciones",
"Keep both": "Mantener ambos",
"Keep both": "Manténer ambos",
"Tags": "Etiquetas",
"Add New below or Select...": "Agregar nuevo a continuación o seleccionar…",
"Tag with this name already exist.": "Una etiqueta con este nombre ya existe.",
@@ -510,7 +510,7 @@
"Octopush API Version": "Versión API Octopush",
"From Name/Number": "De Nombre/Número",
"Recipient Number": "Número de Destinatario",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "El token de acceso de larga duración puede crearse haciendo clic en el nombre de su perfil (abajo a la izquierda), desplazándose hasta la parte inferior y luego haciendo clic en Crear token.",
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "El token de acceso de larga duración se puede crear haciendo clic en el nombre de su perfil (abajo a la izquierda) y desplazándose hasta la parte inferior y luego haciendo clic en Crear token. ",
"backupOutdatedWarning": "Obsoleto: dado que se agregaron muchas funciones y esta función de copia de seguridad no se mantiene desde hace un tiempo, no puede generar ni restaurar una copia de seguridad completa.",
"Optional": "Opcional",
"loadingError": "No se pueden obtener los datos, inténtelo de nuevo más tarde.",
@@ -597,7 +597,7 @@
"checkPrice": "Consultar {0} precios:",
"apiCredentials": "Credenciales de API",
"Check octopush prices": "Consulta los precios de octopush {0}.",
"octopushPhoneNumber": "Número de teléfono (en formato internacional, ejemplo: +33612345678)",
"octopushPhoneNumber": "Número de teléfono (en formato internacional, ejemplo: +33612345678) ",
"octopushSMSSender": "Nombre de Remitente del SMS: 3-11 caracteres alfanuméricos y espacio (a-zA-Z0-9)",
"LunaSea Device ID": "ID Dispositivo LunaSea",
"goAlert": "GoAlert",
@@ -649,7 +649,7 @@
"alertaEnvironment": "Entorno",
"PushDeer Key": "Key de PushDeer",
"onebotSafetyTips": "Por seguridad, deberías colocara el token de acceso",
"wayToGetClickSendSMSToken": "Puedes obtener Usuario de API y llave de API {aquí}.",
"wayToGetClickSendSMSToken": "Puedes obtener Nombre de Usuario de la API y la llave {aquí}.",
"Apprise URL": "URL Apprise",
"gorush": "Gorush",
"squadcast": "Squadcast",
@@ -680,7 +680,7 @@
"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. Si estas usando HTML, evita espacios al principio para prevenir problemas de formato.",
"markdownSupported": "Sintaxis de Markdown soportada",
"Server Address": "Dirección del Servidor",
"Learn More": "Aprende Más",
"Pick a RR-Type...": "Seleccione un Tipo RR…",
@@ -846,7 +846,7 @@
"toastSuccessTimeout": "Tiempo de espera para notificaciones de éxito",
"toastErrorTimeout": "Tiempo de espera para notificaciones de error",
"setupDatabaseChooseDatabase": "¿Qué base de datos te gustaría usar?",
"setupDatabaseEmbeddedMariaDB": "No necesitas configurar nada. Esta imagen de Docker tiene incorporado y configurado MariaDB para ti automáticamente. Uptime Kuma se conectará a esta base de datos a través de un socket Unix.",
"setupDatabaseEmbeddedMariaDB": "No necesitas configurar nada. Esta imagen de Docker tiene incorporada y configurada MariaDB automáticamente para ti. Uptime Kuma se conectará a esta base de datos a través de un socket Unix.",
"setupDatabaseMariaDB": "Conectarse a una base de datos MariaDB externa. Debe configurar la información de conexión a la base de datos.",
"setupDatabaseSQLite": "Un archivo de base de datos simple, recomendado para despliegues a pequeña escala. Antes de la versión 2.0.0, Uptime Kuma utilizaba SQLite como base de datos predeterminada.",
"dbName": "Nombre de la Base de Datos",
@@ -854,7 +854,7 @@
"authIncorrectCreds": "Nombre de usuario o contraseña incorrectos.",
"2faEnabled": "2FA habilitado.",
"2faDisabled": "2FA deshabilitado.",
"liquidIntroduction": "El plantillaje se logra a través del lenguaje de plantillas Liquid. Consulte {0} para obtener instrucciones de uso.",
"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": "solo disponible para notificaciones FUNCIONAL/CAÍDO/Caducidad de certificado",
"emailTemplateMsg": "mensaje de la notificación",
"emailTemplateLimitedToUpDownNotification": "sólo disponible para latidos FUNCIONAL/CAÍDO, de lo contrario nulo",
@@ -939,7 +939,7 @@
"threemaSenderIdentity": "ID de Gateway",
"threemaSenderIdentityFormat": "8 caracteres, generalmente comienza con *",
"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}",
"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 emisor del SMS",
"smspartnerApiurl": "Puedes encontrar tu clave API en tu panel de control en {0}",
@@ -1213,19 +1213,19 @@
"Number of retry attempts if webhook fails": "Número de intentos de reintento (cada 60180 segundos) si el webhook falla.",
"Maximum Retries": "Máximo de reintentos",
"sipsakPingWarning": "Para poder utilizar el monitor de SIP Options Ping, necesitas instalar Uptime Kuma sin Docker e instalar el cliente Sipsak en tu servidor.",
"Plausible": "Admisible",
"Plausible": "Plausible",
"Select All": "Seleccionar todo",
"Deselect All": "Desmarcar todo",
"HTTP Method": "Método HTTP",
"webhookPostMethodDesc": "POST es adecuado para la mayoría de servidores HTTP.",
"webhookPostMethodDesc": "POST es aceptado para la mayoría de servidores HTTP.",
"resendApiKey": "Reenviar la llave API",
"deleteGroupMsg": "¿Está seguro que quiere eliminar este grupo?",
"deleteGroupMsg": "¿Estás seguro de querer eliminar este grupo?",
"settingsDomainExpiry": "Expiración de dominio",
"labelDomainExpiry": "Exp. de dominio",
"message": "mensaje",
"domainExpiryDescription": "Lanzar notificación cuando el nombre de dominio expire en:",
"year": "año | años",
"descriptionHelpText": "Mostrar en el panel de control interno. Se permite markdown limpio (conserva el espacio y la sangría) antes de mostrarse.",
"descriptionHelpText": "Se muestra en el panel principal interno. El código Markdown está permitido y se senea (preserva los espacios y las identaciones) antes de mostrarse.",
"json_value": "Valor JSON",
"Press Enter to add node": "Pulsa Enter para añadir el nodo",
"resendApiHelp": "Crear una llave de API aquí {0}",
@@ -1235,7 +1235,7 @@
"wsCodeDescription": "Para más información acerca de los códigos de estado, por favor consulta {rfc6455}",
"Subprotocol(s)": "Subprotocolo(s)",
"certHostnameMismatch": "El nombre de host del certificado no coincide con la URL del monitor.",
"twilioMessagingServiceSID": "SID del servicio de Mensajería (opcional)",
"twilioMessagingServiceSID": "Servicio de mensajería SID (opcional)",
"resendFromEmail": "Correo electrónico del remitente",
"resendLeaveBlankForDefaultSubject": "Deja en blanco para utilizar el asunto por defecto",
"ignoreSecWebsocketAcceptHeaderDescription": "En caso de que la actualización del websocket sea satisfactoria, permite al servidor no responder con la cabecera Sec-WebSocket-Accept.",
@@ -1261,7 +1261,7 @@
"systemServiceDescriptionWindows": "Comprueba si el gestor de servicios {service_name} de Windows está ejecutándose",
"invalidURL": "URL no válida",
"Clone Maintenance": "Clonar Mantenimiento",
"ariaPauseMaintenance": "Pausar este cronograma de mantenimiento",
"ariaPauseMaintenance": "Pausar este horario de mantenimiento",
"systemServiceName": "Nombre del servicio",
"systemService": "Servicio del sistema",
"systemServiceCommandHint": "Comando utilizado: {command}",
@@ -1276,15 +1276,15 @@
"Browser not supported": "Navegador no permitido",
"labelDomainNameExpiryNotification": "Notificación de expiración de dominio",
"Duration (Minutes)": "Duración (Minutos)",
"ariaResumeMaintenance": "Reanudar este cronograma de mantenimiento",
"ariaCloneMaintenance": "Crear una copia de este cronograma de mantenimiento",
"ariaEditMaintenance": "Editar este cronograma de mantenimiento",
"ariaDeleteMaintenance": "Eliminar este cronograma de mantenimiento",
"ariaResumeMaintenance": "Reanudar este horario de mantenimiento",
"ariaCloneMaintenance": "Crear una copia de este horario de mantenimiento",
"ariaEditMaintenance": "Editar este horario de mantenimiento",
"ariaDeleteMaintenance": "Eliminar este horario de mantenimiento",
"SMTP Security": "Seguridad SMTP",
"Ignore STARTTLS": "Ignorar STARTTLS",
"Use STARTTLS": "Utilizar STARTTLS",
"twilloMessagingServiceSIDHelptext": "Ingrese el SID del Servicio de Mensajería aquí si está usando {twillo_messaging_service_help_link} para administrar los remitentes y características",
"webhookGetMethodDesc": "GET envía los datos como parámetros de consulta y no permite la configuración del cuerpo de la consulta. Útil para disparar monitores PUSH de Uptime Kuma.",
"twilloMessagingServiceSIDHelptext": "Introduce el SID de tu servicio de mensajería si utilizas {twillo_messaging_service_help_link} para genstionar los remitentes y las características",
"webhookGetMethodDesc": "GET envía los datos como parámetros de la búsqueda y no permite configurar un cuerpo de mensaje. Útil para disparar los monitores Push de Uptime Kuma.",
"showOnlyLastHeartbeat": "Mostrar solo el último latido",
"Analytics Type": "Tipo de analíticas",
"Google": "Google",
@@ -1298,7 +1298,7 @@
"checkPriceAt": "Comprueba los precios de {service} en {url}",
"noMonitorsOrStatusPagesSelectedError": "No se puede crear un mantenimiento sin monitores afectados o páginas de estado",
"noMonitorsSelectedWarning": "Estás creando un mantenimiento sin ningún monitor afectado. ¿Estás seguro de que deseas continuar?",
"deleteChildrenMonitors": "Tambien elimina los monitores hijos directos y sus descendientes si los tuvieran | Tambien elimina todos los {count} monitores hijos directos y sus descendientes si los tuvieran",
"deleteChildrenMonitors": "Borra también los sub-monitores y sus descendientes si los tuvieran|Borra también todos los {count} sub-monitores directos y sus descendientes si los tuvieran",
"OptionalParameters": "Parámetros Opcionales",
"aliyun-template-requirements-and-parameters": "La plantilla de SMS de aliyun debe de contener los siguientes parámetros: {parameters}",
"aliyun-template-optional-parameters": "Parámetros opcionales: {parameters}",
@@ -1310,7 +1310,7 @@
"enableSSL": "Habilitar SSL/TLS",
"mariadbCaCertificateLabel": "Certificado CA",
"unknownDays": "Días desconocidos",
"No incidents recorded": "No se registraron incidentes",
"No incidents recorded": "No se registraron incidentes.",
"Load More": "Cargar más",
"mariadbUseSSLHelptext": "Habilita el uso de una conexión cifrada a tu base de datos. Requerido para la mayoría de las bases de datos en la nube.",
"mariadbCaCertificateHelptext": "Pegue el certificado CA en formato PEM para utilizarlo con certificados autofirmados. Déjelo en blanco si su base de datos utiliza un certificado firmado por una CA pública.",
@@ -1329,48 +1329,5 @@
"Only retry if status code check fails": "Reintentar solo si la comprobación del código de estado falla",
"retryOnlyOnStatusCodeFailureDescription": "Si está habilitado, los reintentos solo se realizarán cuando falle la comprobación del código de estado HTTP (por ejemplo, si el servidor está caído). Si la comprobación del código de estado es correcta pero falla la consulta JSON, el monitor se marcará como inactivo inmediatamente, sin reintentos.",
"responseMaxLengthDescription": "Tamaño máximo de los datos de la respuesta que se van a almacenar. Establezca 0 para ilimitado. Las respuestas más grandes se truncarán. Valor por defecto: 1024 (1KB)",
"logoutCurrentUser": "Cerrar sesión de {username}",
"Incident description": "Descripción del incidente",
"twilioApiKeyHelptext": "La llave de la API es opcional pero recomendada. Puede proporcionar el SID de la cuenta y el Token de Autorizacion desde la consola de Twilio o el SID de la cuenta y la llave de la API junto con el secreto de la llave de la API",
"monitorTypeGameServer": "Servidor de Juego",
"monitorTypeDatabase": "Tipo de Monitor de Base de Datos",
"monitorTypeSpecial": "Especial",
"Recipient Numbers": "Números de destinatarios",
"Incident not found or access denied": "No se encontró incidente o acceso denegado",
"Past Incidents": "Incidentes pasados",
"Incident title": "Título del incidente",
"example": "Ejemplo",
"Result": "Resultado",
"lastUpdatedAt": "Última actualización: {date}",
"Actions": "Acciones",
"selectAllMonitorsAria": "Seleccionar todos los monitores",
"deselectAllMonitorsAria": "Deseleccionar todos los monitores",
"lastUpdatedAtFromNow": "Última actualización: {date} ({fromNow})",
"See Jira Cloud Docs": "Ver la documentación de Jura Cloud",
"Cloud ID": "ID de nube",
"API Token": "Token de API",
"templateAvailableVariables": "Variables disponibles",
"selectMonitorMsg": "Selecciona monitores para realizar acciones",
"Examples:": "Ejemplos: {0}",
"Pinned incidents are shown prominently on the status page": "Incidentes marcados se muestran prominentemente en la pagina de estado",
"Edit Incident": "Editar incidente",
"Please input title": "Por favor ingresa título",
"Resolve": "Resolver",
"Resolved": "Resuelto",
"createdAt": "Creado: {date}",
"deleteIncidentMsg": "Estas seguro que quieres eliminar este incidente?",
"Certificate Chain:": "Cadena de certificado:",
"Please input content": "Por favor ingresa contenido",
"dateCreatedAtFromNow": "Fecha de creación: {date} ({fromNow})",
"360messengerAuthToken": "Clave de la API 360messenger",
"360messengerGroupId": "ID de grupo 360messenger",
"360messengerGroupList": "Grupos de WhatsApp",
"ntfyUseTemplateDescription": "Habilite esta opción para personalizar los títulos y mensajes de notificación mediante plantillas LiquidJS",
"ntfyCustomTitle": "Plantilla de título personalizado",
"ntfyCustomMessage": "Plantilla de mensaje personalizado",
"ntfyNotificationTemplateFallback": "Dejar en blanco para utilizar el formato predeterminado de Uptime Kuma",
"Screenshot Delay": "Retraso de captura de pantalla (espera {milliseconds})",
"milliseconds": "{n} milisegundos | {n} milisegundos",
"snmpV3Username": "Nombre de usuario SNMPv3",
"ntfyUseTemplate": "Personalizar plantillas de notificación"
"logoutCurrentUser": "Cerrar sesión de {username}"
}

View File

@@ -1611,14 +1611,5 @@
"360messengerWayToWriteRecipient": "Saisissez un ou plusieurs numéros de téléphone au format international sans signe plus (par exemple : {0}). Séparez les numéros par des virgules.",
"monitorTypeSpecial": "Spécial",
"monitorTypeGameServer": "Serveur de jeu",
"monitorTypeDatabase": "Sonde de Type base de données",
"fluxerMessageFormatNormal": "Normal (intégrations riches)",
"fluxerUseMessageTemplate": "Utiliser un modèle de message personnalisé",
"Fluxer Webhook URL": "URL du Webhook de Fluxer",
"wayToGetFluxerURL": "Vous pouvez obtenir cela en allant dans les paramètres du canal cible > Webhooks > Créer un webhook > Copier lURL du webhook.",
"fluxerMessageFormat": "Format du message",
"fluxerMessageFormatCustom": "Modèle personnalisé",
"fluxerMessageTemplate": "Modèle de message",
"fluxerMessageFormatMinimalist": "Minimalist (statut court",
"fluxerUseMessageTemplateDescription": "Si activé, le message sera envoyé en utilisant un modèle personnalisé (LiquidJS). Laissez vide pour utiliser le format Uptime Kuma par défaut."
"monitorTypeDatabase": "Sonde de Type base de données"
}

View File

@@ -1377,7 +1377,7 @@
"TLS Alert Spec": "RFC 8446",
"Suppress Notifications": "Fógraí a Chosc",
"discordSuppressNotificationsHelptext": "Nuair a bheidh sé cumasaithe, cuirfear teachtaireachtaí chuig an gcainéal ach ní spreagfar fógraí brú ná fógraí deisce do fhaighteoirí.",
"domain_expiry_unsupported_is_icann": "Ní iarrthóir é an fearann \"{domain}\" le haghaidh monatóireachta ar dhul in éag fearainn, toisc nach bhfuil a iarmhír phoiblí \".{publicSuffix}\" á bhainistiú ag ICANN",
"domain_expiry_unsupported_is_icann": "Ní iarrthóir é an fearann \"{domain}\" le haghaidh monatóireachta ar dhul in éag fearainn, toisc nach bhfuil a iarmhír phoiblí \".{publicSuffix}\" ICAN",
"notificationUniversal": "Uilíoch",
"notificationChatPlatforms": "Ardáin Comhrá",
"notificationPushServices": "Seirbhísí Brúigh",
@@ -1540,30 +1540,5 @@
"GlobalpingIpFamilyInfo": "An leagan IP le húsáid. Ní cheadaítear é seo ach amháin má tá an sprioc ina hainm óstach.",
"GlobalpingResolverInfo": "Seoladh IPv4/IPv6 nó Ainm Fearainn Cáilithe go Lán (FQDN). Is é an réamhshocrú ná réiteoir líonra áitiúil an tóireadóra. Is féidir leat an freastalaí réiteora a athrú am ar bith.",
"Jira Service Management": "Bainistíocht Seirbhíse Jira",
"Google Apps Script Webhook URL": "URL Gréasáin-chrúca Script Google Apps",
"360messengerEnableSendToGroup": "Cumasaigh seoltaí chuig grúpa(í) WhatsApp",
"360messengerAuthToken": "Eochair API 360messenger",
"360messengerRecipient": "Uimhir(í) theileafóin an fhaighteora",
"360messengerGroupId": "Aitheantas Grúpa 360messenger",
"360messengerUseTemplate": "Úsáid teimpléad teachtaireachta saincheaptha",
"360messengerTemplate": "Teimpléad Teachtaireachta 360messenger",
"360messengerGroupList": "Grúpaí WhatsApp",
"360messengerSelectGroupList": "Roghnaigh grúpa le cur leis",
"360messengerSelectedGroupID": "Aitheantais Ghrúpa Roghnaithe",
"360messengerCustomMessageTemplate": "Teimpléad teachtaireachta saincheaptha",
"360messengerEnableCustomMessage": "Cumasaigh teimpléad teachtaireachta saincheaptha in ionad an teachtaireachta réamhshocraithe.",
"360messengerMessageTemplate": "Teimpléad teachtaireachta",
"360messengerWayToGetUrlAndToken": "Is féidir leat d'eochair API 360messenger a fháil ó {0}.",
"360messengerErrorNoApiKey": "Cuir isteach deochair API 360messenger ar dtús.",
"360messengerErrorNoGroups": "Ní bhfuarthas aon ghrúpaí WhatsApp don chuntas seo.",
"360messengerErrorApi": "Ní féidir liosta na ngrúpaí WhatsApp a luchtú (Earráid {statusCode}: {message}).",
"360messengerErrorGeneric": "Ní féidir an liosta grúpa WhatsApp a luchtú: {message}",
"GlobalpingLocationDescription": "Glacann an réimse suímh le hilchríocha, tíortha, réigiúin, cathracha, ASNanna, ISPanna, nó réigiúin scamall. Is féidir leat scagairí a chomhcheangal le {plus} (m.sh. {amazonPlusGermany} nó {comcastPlusCalifornia}). Más méadracht thábhachtach í an mhoill, bain úsáid as scagairí chun an suíomh a chúngú síos go réigiún beag chun spící a sheachaint agus socraigh an scagaire {datacenter} ar mhaithe le cobhsaíocht níos fearr. {fullDocs}.",
"GlobalpingMultipleLocationsError": "Ní thacaítear le hilshuíomhanna, bain úsáid as suíomh amháin do gach monatóir le do thoil.",
"360messengerWayToWriteRecipient": "Cuir isteach uimhir theileafóin amháin nó níos mó i bhformáid idirnáisiúnta gan móide tosaigh (m.sh. {0}). Scar uimhreacha iolracha le camóga.",
"signalUseTemplate": "Úsáid teimpléad teachtaireachta saincheaptha",
"signalUseTemplateDescription": "Má tá sé cumasaithe, seolfar an teachtaireacht ag baint úsáide as teimpléad saincheaptha. Is féidir leat teimpléadú Liquid a úsáid chun formáid an fhógra a shaincheapadh.",
"monitorTypeGameServer": "Freastalaí Cluiche",
"monitorTypeDatabase": "Cineál Monatóra Bunachar Sonraí",
"monitorTypeSpecial": "Speisialta"
"Google Apps Script Webhook URL": "URL Gréasáin-chrúca Script Google Apps"
}

View File

@@ -1275,8 +1275,5 @@
"Ignore STARTTLS": "Ignora STARTTLS",
"Use STARTTLS": "Utilizza STARTTLS",
"Enter the list of nodes": "Inserisci l'elenco dei nodi di gestione RabbitMQ",
"Press Enter to add node": "Premi Invio per aggiungere il nodo",
"enableSSL": "Abilita SSL/TLS",
"mariadbUseSSLHelptext": "Abilita per usare una connessione criptata per il tuo database. Richiesto dalla maggior parte dei database cloud.",
"mariadbCaCertificateLabel": "Certificato CA"
"Press Enter to add node": "Premi Invio per aggiungere il nodo"
}

View File

@@ -3,72 +3,5 @@
"setupDatabaseChooseDatabase": "Kuru datubāzi izmantosiet?",
"setupDatabaseEmbeddedMariaDB": "Jums nav nekas jādara. Docker imidžā ir iebūvēta un automātiski konfigurēta MariaDB datubāze. Uptime Kuma pieslēgsies šai datubāzei izmantojot unix soketu.",
"setupDatabaseSQLite": "Vienkāršs datu bāzes fails, iesakāms maza izmēra risinājumiem. Pirms versijas v2.0.0 SQLite bija noklusējuma datubāze.",
"setupDatabaseMariaDB": "Pieslēgties ārējai MariaDB datubāzei. Jums būs jākonfigurē datubāzes pieslēgšanās informācija.",
"Name": "Nosaukums",
"Ping": "Ping",
"Dashboard": "Panelis",
"dbName": "Datubāzes nosaukums",
"enableSSL": "Iespējot SSL/TLS",
"Settings": "Iestatījumi",
"Help": "Palīdzība",
"New Update": "Jauns atjauninājums",
"Language": "Valoda",
"Appearance": "Izskats",
"Theme": "Tēma",
"General": "Vispārīgi",
"Game": "Spēle",
"mariadbCaCertificateLabel": "CA sertifikāts",
"Primary Base URL": "Galvenais bāzes URL",
"Check Update On GitHub": "Pārbaudīt atjauninājumu GitHub",
"List": "Saraksts",
"Home": "Sākums",
"Add": "Pievienot",
"Add New Monitor": "Pievienot jaunu monitoru",
"Quick Stats": "Ātrā statistika",
"Down": "Nedarbojas",
"Pending": "Rindā",
"statusMaintenance": "Tehniskā apkope",
"Maintenance": "Tehniskā apkope",
"Unknown": "Nezināms",
"unknownDays": "Nezināms dienu skaits",
"Cannot connect to the socket server": "Nevar izveidot savienojumu ar soketa serveri",
"Reconnecting...": "Savienojas...",
"General Monitor Type": "Vispārīgais monitora veids",
"pauseDashboardHome": "Pauze",
"Pause": "Pauze",
"DateTime": "Datums un laiks",
"Specific Monitor Type": "Specifiskais monitora veids",
"settingUpDatabaseMSG": "Tiek uzstādīta datubāze. Uzgaidiet, lūdzu, tas var prasīt laiku.",
"mariadbUseSSLHelptext": "Iespējot šifrētu savienojumu ar jūsu datubāzi. Nepieciešams lielākajai daļai mākoņdatubāžu.",
"mariadbCaCertificateHelptext": "Ielīmējiet CA sertifikātu PEM formātā, lai to izmantotu ar pašparakstītiem sertifikātiem. Atstājiet tukšu, ja jūsu datubāze izmanto publiskas sertifikācijas iestādes parakstītu sertifikātu.",
"Passive Monitor Type": "Pasīvais monitora veids",
"markdownSupported": "Tiek atbalstīta Markdown sintakse. Ja izmantojat HTML, izvairieties no atstarpēm rindas sākumā, lai novērstu formatēšanas problēmas.",
"versionIs": "Versija: {version}",
"monitorTypeGameServer": "Spēļu serveris",
"monitorTypeDatabase": "Datubāzes monitora veids",
"monitorTypeSpecial": "Specifisks",
"Message": "Ziņa",
"No incidents recorded": "Nav reģistrētu incidentu",
"Load More": "Ielādēt vairāk",
"Loading...": "Ielāde...",
"No important events": "Nav svarīgu notikumu",
"Resume": "Turpināt",
"Edit": "Labot",
"Delete": "Dzēst",
"Current": "Pašreizējais",
"Uptime": "Darbības laiks",
"Cert Exp.": "Sert. term.",
"Monitors": "{n} monitors | {n} monitori",
"now": "tagad",
"time ago": "pirms {0}",
"days": "{n} diena | {n} dienas",
"hours": "{n} stunda | {n} stundas",
"minutes": "{n} minūte | {n} minūtes",
"minuteShort": "{n} minūte | {n} minūtes",
"years": "{n} gads| {n} gadi",
"Response": "Atbilde",
"Pin this incident": "Piespraust šo incidentu",
"Monitor Type": "Monitora tips",
"Up": "Darbojas",
"Status": "Status"
"setupDatabaseMariaDB": "Pieslēgties ārējai MariaDB datubāzei. Jums būs jākonfigurē datubāzes pieslēgšanās informācija."
}

View File

@@ -994,7 +994,7 @@
"and": "en",
"snmpCommunityStringHelptext": "Deze string fungeert als een wachtwoord om toegang tot SNMP-apparaten te verifiëren en te beheren. Match het met de configuratie van uw SNMP-apparaat.",
"groupOnesenderDesc": "Zorg ervoor dat de GroupID juist is. Om een bericht naar een groep te sturen, bijvoorbeeld: 628123456789-342345",
"privateOnesenderDesc": "Zorg ervoor dat het telefoonnummer juist is. Om een bericht te sturen naar een privénummer, bijvoorbeeld: 628123456789",
"privateOnesenderDesc": "Zorg ervoor dat het telefoonnummer juist is. Om een bericht te sturen naar een privenummer, bijvoorbeeld: 628123456789",
"now": "nu",
"time ago": "{0} geleden",
"-year": "-jaar",
@@ -1007,7 +1007,7 @@
"Host Onesender": "Host Onesender",
"Token Onesender": "Token Onesender",
"Recipient Type": "Ontvanger Type",
"Private Number": "Privénummer",
"Private Number": "Privenummer",
"Group ID": "Groep ID",
"wayToGetOnesenderUrlandToken": "U kunt de URL en Token krijgen door naar de Onesender website te gaan. Meer informatie {0}",
"Add Remote Browser": "Externe browser toevoegen",
@@ -1551,27 +1551,5 @@
"expectedTlsAlertDescription": "Selecteer de TLS-waarschuwing waarvan u verwacht dat de server deze retourneert. Gebruik {code} om te verifiëren dat mTLS-eindpunten verbindingen weigeren zonder clientcertificaten. Zie {link} voor details.",
"Protocol": "Protocol",
"domain_expiry_unsupported_missing_target": "Geen geldige domeinnaam of hostnaam is ingesteld voor deze monitor",
"Endpoint": "Endpoint",
"signalUseTemplate": "Gebruik een persoonlijk berichtensjabloon",
"signalUseTemplateDescription": "Indien ingeschakeld, wordt het bericht verzonden met een persoonlijk sjabloon. U kunt Liquid-templating gebruiken om het meldingsformaat aan te passen.",
"360messengerAuthToken": "360messenger API Sleutel",
"360messengerRecipient": "Telefoonnummer (s) van de ontvanger",
"360messengerGroupId": "360messenger Groep ID",
"360messengerUseTemplate": "Gebruik een persoonlijk berichtensjabloon",
"360messengerTemplate": "360messenger berichtensjabloon",
"360messengerGroupList": "WhatsApp groepen",
"360messengerSelectGroupList": "Selecteer een groep om toe te voegen",
"360messengerSelectedGroupID": "Geselecteerde Groep ID(s)",
"360messengerEnableSendToGroup": "Schakel versturen naar WhatsApp groep(en) in",
"360messengerCustomMessageTemplate": "Persoonlijk berichtensjabloon",
"360messengerEnableCustomMessage": "Schakel een persoonlijk berichtensjabloon in i.p.v. het standaardbericht.",
"360messengerMessageTemplate": "Berichtensjabloon",
"teamsEnableTags": "Inclusief tags",
"teamsEnableTagsDescription": "Indien ingeschakeld zal het bericht de monitortags bevatten.",
"certificateExpiryNotificationHelp": "De hoeveelheid dagen op voorhand kan in de instellingen worden geconfigureerd.",
"matrixUseTemplate": "Gebruik een persoonlijk berichtensjabloon",
"matrixUseTemplateDescription": "Indien ingeschakeld, wordt het bericht verzonden met een persoonlijk sjabloon.",
"monitorTypeGameServer": "Spelserver",
"monitorTypeDatabase": "Databank Monitor Type",
"monitorTypeSpecial": "Speciaal"
"Endpoint": "Endpoint"
}

View File

@@ -1578,14 +1578,5 @@
"360messengerWayToWriteRecipient": "Insira um ou mais números de telefone no formato internacional, sem o sinal de mais inicial (por exemplo, {0}). Separe vários números com vírgulas.",
"360messengerErrorApi": "Não foi possível carregar a lista de grupos do WhatsApp (Erro {statusCode}: {message}).",
"GlobalpingMultipleLocationsError": "Não é possível realizar várias localizações; utilize uma única localização para cada monitor.",
"GlobalpingLocationDescription": "O campo de localização aceita continentes, países, regiões, cidades, ASNs, ISPs ou regiões de nuvem. Você pode combinar filtros com {plus} (por exemplo, {amazonPlusGermany} ou {comcastPlusCalifornia}). Se a latência for uma métrica importante, use filtros para restringir a localização a uma região pequena para evitar picos e, para maior estabilidade, defina o filtro {datacenter}. {fullDocs}.",
"fluxerMessageFormat": "Formato da mensagem",
"fluxerMessageFormatNormal": "Normal (rich embeds)",
"fluxerMessageFormatMinimalist": "Minimalista (status curto)",
"fluxerUseMessageTemplate": "Use um modelo de mensagem personalizado",
"fluxerMessageTemplate": "Modelo de mensagem",
"fluxerMessageFormatCustom": "Modelo personalizado",
"fluxerUseMessageTemplateDescription": "Se ativada, a mensagem será enviada usando um modelo personalizado (LiquidJS). Deixe em branco para usar o formato padrão do Uptime Kuma.",
"Fluxer Webhook URL": "URL do Webhook do Fluxer",
"wayToGetFluxerURL": "Você pode obter essa informação acessando as configurações do canal de destino > Webhooks > Criar Webhook > Copiar URL do Webhook."
"GlobalpingLocationDescription": "O campo de localização aceita continentes, países, regiões, cidades, ASNs, ISPs ou regiões de nuvem. Você pode combinar filtros com {plus} (por exemplo, {amazonPlusGermany} ou {comcastPlusCalifornia}). Se a latência for uma métrica importante, use filtros para restringir a localização a uma região pequena para evitar picos e, para maior estabilidade, defina o filtro {datacenter}. {fullDocs}."
}

View File

@@ -1431,7 +1431,7 @@
"legacyOctopushEndpoint": "Staršia verzia Octopush-DM (koncový bod: {url})",
"Suppress Notifications": "Stlmiť oznámenia",
"discordSuppressNotificationsHelptext": "Ak je táto funkcia povolená, správy budú odosielané do kanála, ale nebudú spúšťať push alebo desktopové notifikácie pre príjemcov.",
"domain_expiry_unsupported_is_icann": "Doména „{domain}“ nie je kandidátom na monitorovanie vypršania platnosti domény, pretože jej verejná prípona „.{publicSuffix}“ nie je spravovaná organizáciou ICANN",
"domain_expiry_unsupported_is_icann": "Doména „{domain}“ nie je kandidátom na monitorovanie vypršania platnosti domény, pretože jej verejná prípona „.{publicSuffix}“ nie je ICAN",
"snmpV3Username": "Používateľské meno SNMPv3",
"WeCom Mentioned Mobile List Description": "Zadajte telefónne čísla, ktoré chcete označiť. Viac čísel oddeľte čiarkami. Použite {'@'}all, aby ste označili všetkých.",
"WeCom Mentioned Mobile List": "WeCom zoznam zmienených",
@@ -1546,28 +1546,5 @@
"certificateExpiryNotificationHelp": "Počet dní vopred je možné nastaviť v nastaveniach.",
"signalUseTemplate": "Použite vlastnú šablónu správy",
"signalUseTemplateDescription": "Ak je táto funkcia povolená, správa bude odoslaná pomocou vlastnej šablóny. Na prispôsobenie formátu oznámenia môžete použiť šablóny Liquid.",
"domainExpiryNotificationHelp": "Počet dní vopred je možné nastaviť v nastaveniach.",
"monitorTypeDatabase": "Typ monitoru databázy",
"monitorTypeGameServer": "Herný server",
"monitorTypeSpecial": "Špeciálny",
"360messengerAuthToken": "API kľúč 360messenger",
"360messengerRecipient": "Telefónne číslo/a príjemcu",
"360messengerGroupId": "360messenger ID skupiny",
"360messengerUseTemplate": "Použite vlastnú šablónu správy",
"360messengerTemplate": "Šablóna správy 360messenger",
"360messengerGroupList": "Skupiny WhatsApp",
"360messengerSelectGroupList": "Vyberte skupinu, ktorú chcete pridať",
"360messengerSelectedGroupID": "Vybrané ID skupiny/ín",
"360messengerEnableSendToGroup": "Povoliť odosielanie do skupín WhatsApp",
"360messengerCustomMessageTemplate": "Šablóna vlastnej správy",
"360messengerEnableCustomMessage": "Povoliť vlastnú šablónu správy namiesto predvolenej správy.",
"360messengerMessageTemplate": "Šablóna správy",
"360messengerWayToWriteRecipient": "Zadajte jedno alebo viacero telefónnych čísel v medzinárodnom formáte bez predpony plus (napr. {0}). Viacero čísel oddeľte čiarkami.",
"360messengerErrorNoApiKey": "Najskôr zadajte svoj API kľúč 360messenger.",
"360messengerErrorApi": "Nie je možné načítať zoznam skupín WhatsApp (Chyba {statusCode}: {message}).",
"360messengerErrorGeneric": "Nie je možné načítať zoznam skupín WhatsApp: {message}",
"GlobalpingMultipleLocationsError": "Viacnásobné polohy nie sú podporované, pre každý monitor použite jednu polohu.",
"360messengerWayToGetUrlAndToken": "API kľúč pre 360messenger môžete získať na {0}.",
"360messengerErrorNoGroups": "Pre tento účet neboli nájdené žiadne skupiny WhatsApp.",
"GlobalpingLocationDescription": "Do poľa polohy môžete zadávať kontinenty, krajiny, regióny, mestá, ASN, ISP alebo cloudové regióny. Filtre môžete kombinovať pomocou znaku {plus} (napr. {amazonPlusGermany} alebo {comcastPlusCalifornia}). Ak je dôležitým ukazovateľom latencia, použite filtre na zúženie polohy na malý región, aby ste sa vyhli výkyvom, a pre lepšiu stabilitu nastavte filter {datacenter}. {fullDocs}."
"domainExpiryNotificationHelp": "Počet dní vopred je možné nastaviť v nastaveniach."
}

View File

@@ -1,493 +0,0 @@
"use strict";
/*!
// Common Util for frontend and backend
//
// DOT NOT MODIFY util.js!
// Need to run "npm run tsc" to compile if there are any changes.
//
// Backend uses the compiled file util.js
// Frontend uses util.ts
*/
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.RESPONSE_BODY_LENGTH_MAX = exports.RESPONSE_BODY_LENGTH_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.INCIDENT_PAGE_SIZE = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = void 0;
const dayjs_1 = require("dayjs");
const jsonata = require("jsonata");
exports.isDev = process.env.NODE_ENV === "development";
exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
const dayjs = exports.isNode ? require("dayjs") : dayjs_1.default;
exports.appName = "Uptime Kuma";
exports.DOWN = 0;
exports.UP = 1;
exports.PENDING = 2;
exports.MAINTENANCE = 3;
exports.STATUS_PAGE_ALL_DOWN = 0;
exports.STATUS_PAGE_ALL_UP = 1;
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
exports.STATUS_PAGE_MAINTENANCE = 3;
exports.SQL_DATE_FORMAT = "YYYY-MM-DD";
exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
exports.MAX_INTERVAL_SECOND = 2073600;
exports.MIN_INTERVAL_SECOND = 1;
exports.INCIDENT_PAGE_SIZE = 10;
exports.PING_PACKET_SIZE_MIN = 1;
exports.PING_PACKET_SIZE_MAX = 65500;
exports.PING_PACKET_SIZE_DEFAULT = 56;
exports.PING_GLOBAL_TIMEOUT_MIN = 1;
exports.PING_GLOBAL_TIMEOUT_MAX = 300;
exports.PING_GLOBAL_TIMEOUT_DEFAULT = 10;
exports.PING_COUNT_MIN = 1;
exports.PING_COUNT_MAX = 100;
exports.PING_COUNT_DEFAULT = 1;
exports.PING_PER_REQUEST_TIMEOUT_MIN = 1;
exports.PING_PER_REQUEST_TIMEOUT_MAX = 60;
exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
exports.RESPONSE_BODY_LENGTH_DEFAULT = 1024;
exports.RESPONSE_BODY_LENGTH_MAX = 1024 * 1024;
exports.CONSOLE_STYLE_Reset = "\x1b[0m";
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
exports.CONSOLE_STYLE_Underscore = "\x1b[4m";
exports.CONSOLE_STYLE_Blink = "\x1b[5m";
exports.CONSOLE_STYLE_Reverse = "\x1b[7m";
exports.CONSOLE_STYLE_Hidden = "\x1b[8m";
exports.CONSOLE_STYLE_FgBlack = "\x1b[30m";
exports.CONSOLE_STYLE_FgRed = "\x1b[31m";
exports.CONSOLE_STYLE_FgGreen = "\x1b[32m";
exports.CONSOLE_STYLE_FgYellow = "\x1b[33m";
exports.CONSOLE_STYLE_FgBlue = "\x1b[34m";
exports.CONSOLE_STYLE_FgMagenta = "\x1b[35m";
exports.CONSOLE_STYLE_FgCyan = "\x1b[36m";
exports.CONSOLE_STYLE_FgWhite = "\x1b[37m";
exports.CONSOLE_STYLE_FgGray = "\x1b[90m";
exports.CONSOLE_STYLE_FgOrange = "\x1b[38;5;208m";
exports.CONSOLE_STYLE_FgLightGreen = "\x1b[38;5;119m";
exports.CONSOLE_STYLE_FgLightBlue = "\x1b[38;5;117m";
exports.CONSOLE_STYLE_FgViolet = "\x1b[38;5;141m";
exports.CONSOLE_STYLE_FgBrown = "\x1b[38;5;130m";
exports.CONSOLE_STYLE_FgPink = "\x1b[38;5;219m";
exports.CONSOLE_STYLE_BgBlack = "\x1b[40m";
exports.CONSOLE_STYLE_BgRed = "\x1b[41m";
exports.CONSOLE_STYLE_BgGreen = "\x1b[42m";
exports.CONSOLE_STYLE_BgYellow = "\x1b[43m";
exports.CONSOLE_STYLE_BgBlue = "\x1b[44m";
exports.CONSOLE_STYLE_BgMagenta = "\x1b[45m";
exports.CONSOLE_STYLE_BgCyan = "\x1b[46m";
exports.CONSOLE_STYLE_BgWhite = "\x1b[47m";
exports.CONSOLE_STYLE_BgGray = "\x1b[100m";
const consoleModuleColors = [
exports.CONSOLE_STYLE_FgCyan,
exports.CONSOLE_STYLE_FgGreen,
exports.CONSOLE_STYLE_FgLightGreen,
exports.CONSOLE_STYLE_FgBlue,
exports.CONSOLE_STYLE_FgLightBlue,
exports.CONSOLE_STYLE_FgMagenta,
exports.CONSOLE_STYLE_FgOrange,
exports.CONSOLE_STYLE_FgViolet,
exports.CONSOLE_STYLE_FgBrown,
exports.CONSOLE_STYLE_FgPink,
];
const consoleLevelColors = {
INFO: exports.CONSOLE_STYLE_FgCyan,
WARN: exports.CONSOLE_STYLE_FgYellow,
ERROR: exports.CONSOLE_STYLE_FgRed,
DEBUG: exports.CONSOLE_STYLE_FgGray,
};
exports.badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue",
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
defaultCertExpValueSuffix: " days",
defaultCertExpLabelSuffix: "h",
defaultCertExpireWarnDays: "14",
defaultCertExpireDownDays: "7",
};
function flipStatus(s) {
if (s === exports.UP) {
return exports.DOWN;
}
if (s === exports.DOWN) {
return exports.UP;
}
return s;
}
exports.flipStatus = flipStatus;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
exports.sleep = sleep;
function ucfirst(str) {
if (!str) {
return str;
}
const firstLetter = str.substr(0, 1);
return firstLetter.toUpperCase() + str.substr(1);
}
exports.ucfirst = ucfirst;
function debug(msg) {
exports.log.log("", "DEBUG", msg);
}
exports.debug = debug;
class Logger {
constructor() {
this.hideLog = {
info: [],
warn: [],
error: [],
debug: [],
};
if (typeof process !== "undefined" && process.env.UPTIME_KUMA_HIDE_LOG) {
const list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map((v) => v.toLowerCase());
for (const pair of list) {
const values = pair.split(/_(.*)/s);
if (values.length >= 2) {
this.hideLog[values[0]].push(values[1]);
}
}
this.debug("server", "UPTIME_KUMA_HIDE_LOG is set");
this.debug("server", this.hideLog);
}
}
log(module, level, ...msg) {
if (level === "DEBUG" && !exports.isDev) {
return;
}
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return;
}
module = module.toUpperCase();
let now;
if (dayjs.tz) {
now = dayjs.tz(new Date()).format();
}
else {
now = dayjs().format();
}
if (process.env.UPTIME_KUMA_LOG_FORMAT === "json") {
const msgString = msg
.map((m) => {
if (typeof m === "string") {
return m;
}
else {
try {
return JSON.stringify(m);
}
catch (_a) {
return String(m);
}
}
})
.join(" ");
console.log(JSON.stringify({
time: now,
module: module,
level: level,
msg: msgString,
}));
return;
}
const levelColor = consoleLevelColors[level];
const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
let timePart;
let modulePart;
let levelPart;
if (exports.isNode) {
switch (level) {
case "DEBUG":
timePart = exports.CONSOLE_STYLE_FgGray + now + exports.CONSOLE_STYLE_Reset;
break;
default:
timePart = exports.CONSOLE_STYLE_FgCyan + now + exports.CONSOLE_STYLE_Reset;
break;
}
modulePart = "[" + moduleColor + module + exports.CONSOLE_STYLE_Reset + "]";
levelPart = levelColor + `${level}:` + exports.CONSOLE_STYLE_Reset;
}
else {
timePart = now;
modulePart = `[${module}]`;
levelPart = `${level}:`;
}
switch (level) {
case "ERROR":
console.error(timePart, modulePart, levelPart, ...msg);
break;
case "WARN":
console.warn(timePart, modulePart, levelPart, ...msg);
break;
case "INFO":
console.info(timePart, modulePart, levelPart, ...msg);
break;
case "DEBUG":
if (exports.isDev) {
console.debug(timePart, modulePart, levelPart, ...msg);
}
break;
default:
console.log(timePart, modulePart, levelPart, ...msg);
break;
}
}
info(module, ...msg) {
this.log(module, "INFO", ...msg);
}
warn(module, ...msg) {
this.log(module, "WARN", ...msg);
}
error(module, ...msg) {
this.log(module, "ERROR", ...msg);
}
debug(module, ...msg) {
this.log(module, "DEBUG", ...msg);
}
exception(module, exception, ...msg) {
this.log(module, "ERROR", ...msg, exception);
}
}
exports.log = new Logger();
function polyfill() {
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str, newStr) {
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
return this.replace(str, newStr);
}
return this.replace(new RegExp(str, "g"), newStr);
};
}
}
exports.polyfill = polyfill;
class TimeLogger {
constructor() {
this.startTime = dayjs().valueOf();
}
print(name) {
if (exports.isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
}
}
}
exports.TimeLogger = TimeLogger;
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
exports.getRandomArbitrary = getRandomArbitrary;
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
exports.getRandomInt = getRandomInt;
const getRandomBytes = (typeof window !== "undefined" && window.crypto
?
function () {
return (numBytes) => {
const randomBytes = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i += 65536) {
window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
}
return randomBytes;
};
}
:
function () {
return require("crypto").randomBytes;
})();
function getCryptoRandomInt(min, max) {
const range = max - min;
if (range >= Math.pow(2, 32)) {
console.log("Warning! Range is too large.");
}
let tmpRange = range;
let bitsNeeded = 0;
let bytesNeeded = 0;
let mask = 1;
while (tmpRange > 0) {
if (bitsNeeded % 8 === 0) {
bytesNeeded += 1;
}
bitsNeeded += 1;
mask = (mask << 1) | 1;
tmpRange = tmpRange >>> 1;
}
const randomBytes = getRandomBytes(bytesNeeded);
let randomValue = 0;
for (let i = 0; i < bytesNeeded; i++) {
randomValue |= randomBytes[i] << (8 * i);
}
randomValue = randomValue & mask;
if (randomValue <= range) {
return min + randomValue;
}
else {
return getCryptoRandomInt(min, max);
}
}
exports.getCryptoRandomInt = getCryptoRandomInt;
function genSecret(length = 64) {
let secret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charsLength = chars.length;
for (let i = 0; i < length; i++) {
secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1));
}
return secret;
}
exports.genSecret = genSecret;
function getMonitorRelativeURL(id) {
return "/dashboard/" + id;
}
exports.getMonitorRelativeURL = getMonitorRelativeURL;
function parseTimeObject(time) {
if (!time) {
return {
hours: 0,
minutes: 0,
};
}
const array = time.split(":");
if (array.length < 2) {
throw new Error("parseVueDatePickerTimeFormat: Invalid Time");
}
const obj = {
hours: parseInt(array[0]),
minutes: parseInt(array[1]),
seconds: 0,
};
if (array.length >= 3) {
obj.seconds = parseInt(array[2]);
}
return obj;
}
exports.parseTimeObject = parseTimeObject;
function parseTimeFromTimeObject(obj) {
if (!obj) {
return obj;
}
let result = "";
result += obj.hours.toString().padStart(2, "0") + ":" + obj.minutes.toString().padStart(2, "0");
if (obj.seconds) {
result += ":" + obj.seconds.toString().padStart(2, "0");
}
return result;
}
exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
function isoToUTCDateTime(input) {
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
}
exports.isoToUTCDateTime = isoToUTCDateTime;
function utcToISODateTime(input) {
return dayjs.utc(input).toISOString();
}
exports.utcToISODateTime = utcToISODateTime;
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs.utc(input).local().format(format);
}
exports.utcToLocal = utcToLocal;
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs(input).utc().format(format);
}
exports.localToUTC = localToUTC;
function intHash(str, length = 10) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash += str.charCodeAt(i);
}
return ((hash % length) + length) % length;
}
exports.intHash = intHash;
async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue) {
let response;
try {
response = JSON.parse(data);
}
catch (_a) {
response =
(typeof data === "object" || typeof data === "number") && !Buffer.isBuffer(data) ? data : data.toString();
}
try {
response = jsonPath ? await jsonata(jsonPath).evaluate(response) : response;
if (response === null || response === undefined) {
throw new Error("Empty or undefined response. Check query syntax and response structure");
}
if (Array.isArray(response)) {
const responseStr = JSON.stringify(response);
const truncatedResponse = responseStr.length > 25 ? responseStr.substring(0, 25) + "...]" : responseStr;
throw new Error("JSON query returned the array " +
truncatedResponse +
", but a primitive value is required. " +
"Modify your query to return a single value via [0] to get the first element or use an aggregation like $count(), $sum() or $boolean().");
}
if (typeof response === "object" || response instanceof Date || typeof response === "function") {
throw new Error(`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`);
}
let jsonQueryExpression;
switch (jsonPathOperator) {
case ">":
case ">=":
case "<":
case "<=":
jsonQueryExpression = `$number($.value) ${jsonPathOperator} $number($.expected)`;
break;
case "!=":
jsonQueryExpression = "$.value != $.expected";
break;
case "==":
jsonQueryExpression = "$.value = $.expected";
break;
case "contains":
jsonQueryExpression = "$contains($.value, $.expected)";
break;
default:
throw new Error(`Invalid condition ${jsonPathOperator}`);
}
const expression = jsonata(jsonQueryExpression);
const status = await expression.evaluate({
value: response.toString(),
expected: expectedValue.toString(),
});
if (status === undefined) {
throw new Error("Query evaluation returned undefined. Check query syntax and the structure of the response data");
}
return {
status,
response,
};
}
catch (err) {
response = JSON.stringify(response);
response = response && response.length > 50 ? `${response.substring(0, 100)}… (truncated)` : response;
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
}
}
exports.evaluateJsonQuery = evaluateJsonQuery;
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = {
http: "url",
keyword: "url",
"json-query": "url",
"real-browser": "url",
"websocket-upgrade": "url",
port: "hostname",
ping: "hostname",
"grpc-keyword": "grpcUrl",
dns: "hostname",
smtp: "hostname",
snmp: "hostname",
gamedig: "hostname",
steam: "hostname",
mqtt: "hostname",
radius: "hostname",
"tailscale-ping": "hostname",
"sip-options": "hostname",
};

View File

@@ -17,7 +17,7 @@ import * as timezone from "dayjs/plugin/timezone";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as utc from "dayjs/plugin/utc";
import * as jsonata from "jsonata";
import jsonata from "jsonata";
export const isDev = process.env.NODE_ENV === "development";
export const isNode = typeof process !== "undefined" && process?.versions?.node;

View File

@@ -1,50 +0,0 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
/**
* Extracts the ping value filtering logic from PingChart.vue pushDatapoint().
* This mirrors the condition: datapoint.up > 0 && datapoint.avgPing != null
* @param {object} datapoint Datapoint with up, avgPing, minPing, maxPing
* @returns {number|null} The avgPing value or null if filtered out
*/
function filterPingValue(datapoint) {
return datapoint.up > 0 && datapoint.avgPing != null ? datapoint.avgPing : null;
}
describe("PingChart pushDatapoint filtering", () => {
test("avgPing of 0 should be rendered, not filtered out (#7143)", () => {
const datapoint = { up: 1, down: 0, avgPing: 0, minPing: 0, maxPing: 0 };
const result = filterPingValue(datapoint);
assert.strictEqual(result, 0, "avgPing of 0 must not be converted to null");
});
test("avgPing of 1 should be rendered", () => {
const datapoint = { up: 1, down: 0, avgPing: 1, minPing: 1, maxPing: 1 };
const result = filterPingValue(datapoint);
assert.strictEqual(result, 1);
});
test("avgPing of null should be filtered out", () => {
const datapoint = { up: 1, down: 0, avgPing: null, minPing: null, maxPing: null };
const result = filterPingValue(datapoint);
assert.strictEqual(result, null);
});
test("avgPing of undefined should be filtered out", () => {
const datapoint = { up: 1, down: 0, avgPing: undefined, minPing: undefined, maxPing: undefined };
const result = filterPingValue(datapoint);
assert.strictEqual(result, null);
});
test("datapoint with no up counts should be filtered out", () => {
const datapoint = { up: 0, down: 1, avgPing: 5, minPing: 5, maxPing: 5 };
const result = filterPingValue(datapoint);
assert.strictEqual(result, null);
});
test("normal ping value with up count should be rendered", () => {
const datapoint = { up: 3, down: 0, avgPing: 42, minPing: 30, maxPing: 55 };
const result = filterPingValue(datapoint);
assert.strictEqual(result, 42);
});
});

View File

@@ -1,12 +0,0 @@
import * as childProcess from "child_process";
const version = parseInt(process.version.slice(1).split(".")[0]);
/**
* Since Node.js 22 introduced a different "node --test" command with glob, we need to run different test commands based on the Node.js version.
*/
if (version < 22) {
childProcess.execSync("npm run test-backend-20", { stdio: "inherit" });
} else {
childProcess.execSync("npm run test-backend-22", { stdio: "inherit" });
}

View File

@@ -1,7 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"esModuleInterop": false
},
"files": ["./src/util.ts"]
}