Merge branch 'master' into feature/monitor-postgres-query-result
This commit is contained in:
commit
cf6658f929
93
.github/workflows/beta-release.yml
vendored
Normal file
93
.github/workflows/beta-release.yml
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
name: Beta Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Beta version number (e.g., 2.1.0-beta.2)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
previous_version:
|
||||||
|
description: "Previous version tag for changelog (e.g., 2.1.0-beta.1)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
dry_run:
|
||||||
|
description: "Dry Run (The docker image will not be pushed to registries. PR will still be created.)"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
beta-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 120
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
persist-credentials: true
|
||||||
|
fetch-depth: 0 # Fetch all history for changelog generation
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
- name: Create release branch
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git"
|
||||||
|
# Delete remote branch if it exists
|
||||||
|
git push origin --delete "release-${VERSION}" || true
|
||||||
|
# Delete local branch if it exists
|
||||||
|
git branch -D "release-${VERSION}" || true
|
||||||
|
# For testing purpose
|
||||||
|
# git checkout beta-workflow
|
||||||
|
git checkout -b "release-${VERSION}"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm clean-install --no-fund
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ secrets.GHCR_USERNAME }}
|
||||||
|
password: ${{ secrets.GHCR_TOKEN }}
|
||||||
|
|
||||||
|
- name: Run release-beta
|
||||||
|
env:
|
||||||
|
RELEASE_BETA_VERSION: ${{ inputs.version }}
|
||||||
|
RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }}
|
||||||
|
DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
|
run: npm run release-beta
|
||||||
|
|
||||||
|
- name: Upload dist.tar.gz as artifact
|
||||||
|
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||||
|
with:
|
||||||
|
name: dist-${{ inputs.version }}
|
||||||
|
path: ./tmp/dist.tar.gz
|
||||||
|
retention-days: 90
|
||||||
65
.github/workflows/mark-as-draft-on-requesting-changes.yml
vendored
Normal file
65
.github/workflows/mark-as-draft-on-requesting-changes.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
name: Mark PR as draft when changes are requested
|
||||||
|
|
||||||
|
# pull_request_target is safe here because:
|
||||||
|
# 1. Does not use any external actions; only uses the GitHub CLI via run commands
|
||||||
|
# 2. Has minimal permissions
|
||||||
|
# 3. Doesn't checkout or execute any untrusted code from PRs
|
||||||
|
# 4. Only adds/removes labels or changes the draft status
|
||||||
|
on: # zizmor: ignore[dangerous-triggers]
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- review_submitted
|
||||||
|
- labeled
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mark-draft:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
if: |
|
||||||
|
(
|
||||||
|
github.event.action == 'review_submitted' &&
|
||||||
|
github.event.review.state == 'changes_requested'
|
||||||
|
) || (
|
||||||
|
github.event.action == 'labeled' &&
|
||||||
|
github.event.label.name == 'pr:please address review comments'
|
||||||
|
)
|
||||||
|
steps:
|
||||||
|
- name: Add label on requested changes
|
||||||
|
if: github.event.review.state == 'changes_requested'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh issue edit "${{ github.event.pull_request.number }}" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--add-label "pr:please address review comments"
|
||||||
|
|
||||||
|
- name: Mark PR as draft
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh pr ready "${{ github.event.pull_request.number }}" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--undo || true
|
||||||
|
# || true to ignore the case where the pr is already a draft
|
||||||
|
|
||||||
|
ready-for-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
if: github.event.action == 'ready_for_review'
|
||||||
|
steps:
|
||||||
|
- name: Update labels for review
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh issue edit "${{ github.event.pull_request.number }}" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--remove-label "pr:please address review comments" || true
|
||||||
|
|
||||||
|
gh issue edit "${{ github.event.pull_request.number }}" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--add-label "pr:needs review"
|
||||||
@ -36,6 +36,9 @@ export default defineConfig({
|
|||||||
srcDir: "src",
|
srcDir: "src",
|
||||||
filename: "serviceWorker.ts",
|
filename: "serviceWorker.ts",
|
||||||
strategies: "injectManifest",
|
strategies: "injectManifest",
|
||||||
|
injectManifest: {
|
||||||
|
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024, // 3 MiB
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor", function (table) {
|
||||||
|
table.boolean("save_response").notNullable().defaultTo(false);
|
||||||
|
table.boolean("save_error_response").notNullable().defaultTo(true);
|
||||||
|
table.integer("response_max_length").notNullable().defaultTo(1024); // Default 1KB
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("save_response");
|
||||||
|
table.dropColumn("save_error_response");
|
||||||
|
table.dropColumn("response_max_length");
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.alterTable("heartbeat", function (table) {
|
||||||
|
table.text("response").nullable().defaultTo(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("heartbeat", function (table) {
|
||||||
|
table.dropColumn("response");
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { createRequire } from "module";
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
const pkg = require("../../package.json");
|
const pkg = require("../../package.json");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const childProcess = require("child_process");
|
const childProcess = require("child_process");
|
||||||
@ -58,8 +61,13 @@ function commit(version) {
|
|||||||
throw new Error("commit error");
|
throw new Error("commit error");
|
||||||
}
|
}
|
||||||
|
|
||||||
res = childProcess.spawnSync("git", ["push", "origin", "master"]);
|
// Get the current branch name
|
||||||
console.log(res.stdout.toString().trim());
|
res = childProcess.spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
|
let branchName = res.stdout.toString().trim();
|
||||||
|
console.log("Current branch:", branchName);
|
||||||
|
|
||||||
|
// Git push the branch
|
||||||
|
childProcess.spawnSync("git", ["push", "origin", branchName, "--force"], { stdio: "inherit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4,22 +4,13 @@
|
|||||||
|
|
||||||
import * as childProcess from "child_process";
|
import * as childProcess from "child_process";
|
||||||
|
|
||||||
const ignoreList = [
|
const ignoreList = ["louislam", "CommanderStorm", "UptimeKumaBot", "weblate", "Copilot", "@autofix-ci[bot]"];
|
||||||
"louislam",
|
|
||||||
"CommanderStorm",
|
|
||||||
"UptimeKumaBot",
|
|
||||||
"weblate",
|
|
||||||
"Copilot"
|
|
||||||
];
|
|
||||||
|
|
||||||
const mergeList = [
|
const mergeList = ["Translations Update from Weblate", "Update dependencies"];
|
||||||
"Translations Update from Weblate",
|
|
||||||
"Update dependencies",
|
|
||||||
];
|
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
|
|
||||||
LLM Task: Please help to put above PRs into the following sections based on their content. If a PR fits multiple sections, choose the most relevant one. If a PR doesn't fit any section, place it in "Others". If there are grammatical errors in the PR titles, please correct them. Don't change the PR numbers and authors, and keep the format. Output as markdown.
|
LLM Task: Please help to put above PRs into the following sections based on their content. If a PR fits multiple sections, choose the most relevant one. If a PR doesn't fit any section, place it in "Others". If there are grammatical errors in the PR titles, please correct them. Don't change the PR numbers and authors, and keep the format. Output as markdown file format.
|
||||||
|
|
||||||
Changelog:
|
Changelog:
|
||||||
|
|
||||||
@ -37,7 +28,9 @@ Changelog:
|
|||||||
- Other small changes, code refactoring and comment/doc updates in this repo:
|
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await main();
|
if (import.meta.main) {
|
||||||
|
await main();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Function
|
* Main Function
|
||||||
@ -52,60 +45,63 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Generating changelog since version ${previousVersion}...`);
|
console.log(`Generating changelog since version ${previousVersion}...`);
|
||||||
|
console.log(await generateChangelog(previousVersion));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
/**
|
||||||
const prList = await getPullRequestList(previousVersion);
|
* Generate Changelog
|
||||||
const list = [];
|
* @param {string} previousVersion Previous Version Tag
|
||||||
|
* @returns {Promise<string>} Changelog Content
|
||||||
|
*/
|
||||||
|
export async function generateChangelog(previousVersion) {
|
||||||
|
const prList = await getPullRequestList(previousVersion);
|
||||||
|
const list = [];
|
||||||
|
let content = "";
|
||||||
|
|
||||||
let i = 1;
|
let i = 1;
|
||||||
for (const pr of prList) {
|
for (const pr of prList) {
|
||||||
console.log(`Progress: ${i++}/${prList.length}`);
|
console.log(`Progress: ${i++}/${prList.length}`);
|
||||||
let authorSet = await getAuthorList(pr.number);
|
let authorSet = await getAuthorList(pr.number);
|
||||||
authorSet = await mainAuthorToFront(pr.author.login, authorSet);
|
authorSet = await mainAuthorToFront(pr.author.login, authorSet);
|
||||||
|
|
||||||
if (mergeList.includes(pr.title)) {
|
if (mergeList.includes(pr.title)) {
|
||||||
// Check if it is already in the list
|
// Check if it is already in the list
|
||||||
const existingItem = list.find(item => item.title === pr.title);
|
const existingItem = list.find((item) => item.title === pr.title);
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
existingItem.numbers.push(pr.number);
|
existingItem.numbers.push(pr.number);
|
||||||
for (const author of authorSet) {
|
for (const author of authorSet) {
|
||||||
existingItem.authors.add(author);
|
existingItem.authors.add(author);
|
||||||
// Sort the authors
|
// Sort the authors
|
||||||
existingItem.authors = new Set([ ...existingItem.authors ].sort((a, b) => a.localeCompare(b)));
|
existingItem.authors = new Set([...existingItem.authors].sort((a, b) => a.localeCompare(b)));
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = {
|
|
||||||
numbers: [ pr.number ],
|
|
||||||
title: pr.title,
|
|
||||||
authors: authorSet,
|
|
||||||
};
|
|
||||||
|
|
||||||
list.push(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of list) {
|
const item = {
|
||||||
// Concat pr numbers into a string like #123 #456
|
numbers: [pr.number],
|
||||||
const prPart = item.numbers.map(num => `#${num}`).join(" ");
|
title: pr.title,
|
||||||
|
authors: authorSet,
|
||||||
|
};
|
||||||
|
|
||||||
// Concat authors into a string like @user1 @user2
|
list.push(item);
|
||||||
let authorPart = [ ...item.authors ].map(author => `@${author}`).join(" ");
|
|
||||||
|
|
||||||
if (authorPart) {
|
|
||||||
authorPart = `(Thanks ${authorPart})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`- ${prPart} ${item.title} ${authorPart}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(template);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to get pull request list:", e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const item of list) {
|
||||||
|
// Concat pr numbers into a string like #123 #456
|
||||||
|
const prPart = item.numbers.map((num) => `#${num}`).join(" ");
|
||||||
|
|
||||||
|
// Concat authors into a string like @user1 @user2
|
||||||
|
let authorPart = [...item.authors].map((author) => `@${author}`).join(" ");
|
||||||
|
|
||||||
|
if (authorPart) {
|
||||||
|
authorPart = `(Thanks ${authorPart})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content += `- ${prPart} ${item.title} ${authorPart}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content + "\n" + template;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,28 +110,37 @@ async function main() {
|
|||||||
*/
|
*/
|
||||||
async function getPullRequestList(previousVersion) {
|
async function getPullRequestList(previousVersion) {
|
||||||
// Get the date of previousVersion in YYYY-MM-DD format from git
|
// Get the date of previousVersion in YYYY-MM-DD format from git
|
||||||
const previousVersionDate = childProcess.execSync(`git log -1 --format=%cd --date=short ${previousVersion}`).toString().trim();
|
const previousVersionDate = childProcess
|
||||||
|
.execSync(`git log -1 --format=%cd --date=short ${previousVersion}`)
|
||||||
|
.toString()
|
||||||
|
.trim();
|
||||||
|
|
||||||
if (!previousVersionDate) {
|
if (!previousVersionDate) {
|
||||||
throw new Error(`Unable to find the date of version ${previousVersion}. Please make sure the version tag exists.`);
|
throw new Error(
|
||||||
|
`Unable to find the date of version ${previousVersion}. Please make sure the version tag exists.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ghProcess = childProcess.spawnSync("gh", [
|
const ghProcess = childProcess.spawnSync(
|
||||||
"pr",
|
"gh",
|
||||||
"list",
|
[
|
||||||
"--state",
|
"pr",
|
||||||
"merged",
|
"list",
|
||||||
"--base",
|
"--state",
|
||||||
"master",
|
"merged",
|
||||||
"--search",
|
"--base",
|
||||||
`merged:>=${previousVersionDate}`,
|
"master",
|
||||||
"--json",
|
"--search",
|
||||||
"number,title,author",
|
`merged:>=${previousVersionDate}`,
|
||||||
"--limit",
|
"--json",
|
||||||
"1000"
|
"number,title,author",
|
||||||
], {
|
"--limit",
|
||||||
encoding: "utf-8"
|
"1000",
|
||||||
});
|
],
|
||||||
|
{
|
||||||
|
encoding: "utf-8",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (ghProcess.error) {
|
if (ghProcess.error) {
|
||||||
throw ghProcess.error;
|
throw ghProcess.error;
|
||||||
@ -153,14 +158,8 @@ async function getPullRequestList(previousVersion) {
|
|||||||
* @returns {Promise<Set<string>>} Set of Authors' GitHub Usernames
|
* @returns {Promise<Set<string>>} Set of Authors' GitHub Usernames
|
||||||
*/
|
*/
|
||||||
async function getAuthorList(prID) {
|
async function getAuthorList(prID) {
|
||||||
const ghProcess = childProcess.spawnSync("gh", [
|
const ghProcess = childProcess.spawnSync("gh", ["pr", "view", prID, "--json", "commits"], {
|
||||||
"pr",
|
encoding: "utf-8",
|
||||||
"view",
|
|
||||||
prID,
|
|
||||||
"--json",
|
|
||||||
"commits"
|
|
||||||
], {
|
|
||||||
encoding: "utf-8"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ghProcess.error) {
|
if (ghProcess.error) {
|
||||||
@ -185,7 +184,7 @@ async function getAuthorList(prID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort the set
|
// Sort the set
|
||||||
return new Set([ ...set ].sort((a, b) => a.localeCompare(b)));
|
return new Set([...set].sort((a, b) => a.localeCompare(b)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -197,5 +196,5 @@ async function mainAuthorToFront(mainAuthor, authorSet) {
|
|||||||
if (ignoreList.includes(mainAuthor)) {
|
if (ignoreList.includes(mainAuthor)) {
|
||||||
return authorSet;
|
return authorSet;
|
||||||
}
|
}
|
||||||
return new Set([ mainAuthor, ...authorSet ]);
|
return new Set([mainAuthor, ...authorSet]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,24 +7,28 @@ import {
|
|||||||
checkTagExists,
|
checkTagExists,
|
||||||
checkVersionFormat,
|
checkVersionFormat,
|
||||||
getRepoNames,
|
getRepoNames,
|
||||||
pressAnyKey,
|
execSync,
|
||||||
execSync, uploadArtifacts, checkReleaseBranch,
|
checkReleaseBranch,
|
||||||
|
createDistTarGz,
|
||||||
|
createReleasePR,
|
||||||
} from "./lib.mjs";
|
} from "./lib.mjs";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
|
|
||||||
const repoNames = getRepoNames();
|
const repoNames = getRepoNames();
|
||||||
const version = process.env.RELEASE_BETA_VERSION;
|
const version = process.env.RELEASE_BETA_VERSION;
|
||||||
const githubToken = process.env.RELEASE_GITHUB_TOKEN;
|
const dryRun = process.env.DRY_RUN === "true";
|
||||||
|
const previousVersion = process.env.RELEASE_PREVIOUS_VERSION;
|
||||||
|
const branchName = `release-${version}`;
|
||||||
|
const githubRunId = process.env.GITHUB_RUN_ID;
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
console.log("Dry run mode enabled. No images will be pushed.");
|
||||||
|
}
|
||||||
|
|
||||||
console.log("RELEASE_BETA_VERSION:", version);
|
console.log("RELEASE_BETA_VERSION:", version);
|
||||||
|
|
||||||
if (!githubToken) {
|
// Check if the current branch is "release-{version}"
|
||||||
console.error("GITHUB_TOKEN is required");
|
checkReleaseBranch(branchName);
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the current branch is "release"
|
|
||||||
checkReleaseBranch();
|
|
||||||
|
|
||||||
// Check if the version is a valid semver
|
// Check if the version is a valid semver
|
||||||
checkVersionFormat(version);
|
checkVersionFormat(version);
|
||||||
@ -44,24 +48,34 @@ checkDocker();
|
|||||||
await checkTagExists(repoNames, version);
|
await checkTagExists(repoNames, version);
|
||||||
|
|
||||||
// node extra/beta/update-version.js
|
// node extra/beta/update-version.js
|
||||||
execSync("node ./extra/beta/update-version.js");
|
await import("../beta/update-version.mjs");
|
||||||
|
|
||||||
|
// Create Pull Request (gh pr create will handle pushing the branch)
|
||||||
|
await createReleasePR(version, previousVersion, dryRun, branchName, githubRunId);
|
||||||
|
|
||||||
// Build frontend dist
|
// Build frontend dist
|
||||||
buildDist();
|
buildDist();
|
||||||
|
|
||||||
// Build slim image (rootless)
|
if (!dryRun) {
|
||||||
buildImage(repoNames, [ "beta-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
|
// Build slim image (rootless)
|
||||||
|
buildImage(
|
||||||
|
repoNames,
|
||||||
|
["beta-slim-rootless", ver(version, "slim-rootless")],
|
||||||
|
"rootless",
|
||||||
|
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
|
||||||
|
);
|
||||||
|
|
||||||
// Build full image (rootless)
|
// Build full image (rootless)
|
||||||
buildImage(repoNames, [ "beta-rootless", ver(version, "rootless") ], "rootless");
|
buildImage(repoNames, ["beta-rootless", ver(version, "rootless")], "rootless");
|
||||||
|
|
||||||
// Build slim image
|
// 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:base2-slim");
|
||||||
|
|
||||||
// Build full image
|
// Build full image
|
||||||
buildImage(repoNames, [ "beta", version ], "release");
|
buildImage(repoNames, ["beta", version], "release");
|
||||||
|
} else {
|
||||||
|
console.log("Dry run mode - skipping image build and push.");
|
||||||
|
}
|
||||||
|
|
||||||
await pressAnyKey();
|
// Create dist.tar.gz
|
||||||
|
await createDistTarGz();
|
||||||
// npm run upload-artifacts
|
|
||||||
uploadArtifacts(version, githubToken);
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import * as childProcess from "child_process";
|
import * as childProcess from "child_process";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
|
import { generateChangelog } from "../generate-changelog.mjs";
|
||||||
|
import fs from "fs";
|
||||||
|
import tar from "tar";
|
||||||
|
|
||||||
export const dryRun = process.env.RELEASE_DRY_RUN === "1";
|
export const dryRun = process.env.RELEASE_DRY_RUN === "1";
|
||||||
|
|
||||||
@ -23,16 +26,14 @@ export function checkDocker() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Docker Hub repository name
|
* Get Docker Hub repository name
|
||||||
|
* @returns {string[]} List of repository names
|
||||||
*/
|
*/
|
||||||
export function getRepoNames() {
|
export function getRepoNames() {
|
||||||
if (process.env.RELEASE_REPO_NAMES) {
|
if (process.env.RELEASE_REPO_NAMES) {
|
||||||
// Split by comma
|
// Split by comma
|
||||||
return process.env.RELEASE_REPO_NAMES.split(",").map((name) => name.trim());
|
return process.env.RELEASE_REPO_NAMES.split(",").map((name) => name.trim());
|
||||||
}
|
}
|
||||||
return [
|
return ["louislam/uptime-kuma", "ghcr.io/louislam/uptime-kuma"];
|
||||||
"louislam/uptime-kuma",
|
|
||||||
"ghcr.io/louislam/uptime-kuma",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,15 +58,15 @@ export function buildDist() {
|
|||||||
* @param {string} platform Build platform
|
* @param {string} platform Build platform
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function buildImage(repoNames, tags, target, buildArgs = "", dockerfile = "docker/dockerfile", platform = "linux/amd64,linux/arm64,linux/arm/v7") {
|
export function buildImage(
|
||||||
let args = [
|
repoNames,
|
||||||
"buildx",
|
tags,
|
||||||
"build",
|
target,
|
||||||
"-f",
|
buildArgs = "",
|
||||||
dockerfile,
|
dockerfile = "docker/dockerfile",
|
||||||
"--platform",
|
platform = "linux/amd64,linux/arm64,linux/arm/v7"
|
||||||
platform,
|
) {
|
||||||
];
|
let args = ["buildx", "build", "-f", dockerfile, "--platform", platform];
|
||||||
|
|
||||||
for (let repoName of repoNames) {
|
for (let repoName of repoNames) {
|
||||||
// Add tags
|
// Add tags
|
||||||
@ -74,22 +75,14 @@ export function buildImage(repoNames, tags, target, buildArgs = "", dockerfile =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
args = [
|
args = [...args, "--target", target];
|
||||||
...args,
|
|
||||||
"--target",
|
|
||||||
target,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add build args
|
// Add build args
|
||||||
if (buildArgs) {
|
if (buildArgs) {
|
||||||
args.push("--build-arg", buildArgs);
|
args.push("--build-arg", buildArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
args = [
|
args = [...args, ".", "--push"];
|
||||||
...args,
|
|
||||||
".",
|
|
||||||
"--push",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!dryRun) {
|
if (!dryRun) {
|
||||||
childProcess.spawnSync("docker", args, { stdio: "inherit" });
|
childProcess.spawnSync("docker", args, { stdio: "inherit" });
|
||||||
@ -172,11 +165,13 @@ export function pressAnyKey() {
|
|||||||
console.log("Git Push and Publish the release note on github, then press any key to continue");
|
console.log("Git Push and Publish the release note on github, then press any key to continue");
|
||||||
process.stdin.setRawMode(true);
|
process.stdin.setRawMode(true);
|
||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
return new Promise(resolve => process.stdin.once("data", data => {
|
return new Promise((resolve) =>
|
||||||
process.stdin.setRawMode(false);
|
process.stdin.once("data", (data) => {
|
||||||
process.stdin.pause();
|
process.stdin.setRawMode(false);
|
||||||
resolve();
|
process.stdin.pause();
|
||||||
}));
|
resolve();
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -189,9 +184,9 @@ export function ver(version, identifier) {
|
|||||||
const obj = semver.parse(version);
|
const obj = semver.parse(version);
|
||||||
|
|
||||||
if (obj.prerelease.length === 0) {
|
if (obj.prerelease.length === 0) {
|
||||||
obj.prerelease = [ identifier ];
|
obj.prerelease = [identifier];
|
||||||
} else {
|
} else {
|
||||||
obj.prerelease[0] = [ obj.prerelease[0], identifier ].join("-");
|
obj.prerelease[0] = [obj.prerelease[0], identifier].join("-");
|
||||||
}
|
}
|
||||||
return obj.format();
|
return obj.format();
|
||||||
}
|
}
|
||||||
@ -202,6 +197,7 @@ export function ver(version, identifier) {
|
|||||||
* @param {string} version Version
|
* @param {string} version Version
|
||||||
* @param {string} githubToken GitHub token
|
* @param {string} githubToken GitHub token
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
export function uploadArtifacts(version, githubToken) {
|
export function uploadArtifacts(version, githubToken) {
|
||||||
let args = [
|
let args = [
|
||||||
@ -251,14 +247,117 @@ export function execSync(cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the current branch is "release"
|
* Check if the current branch matches the expected release branch pattern
|
||||||
|
* @param {string} expectedBranch Expected branch name (can be "release" or "release-{version}")
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function checkReleaseBranch() {
|
export function checkReleaseBranch(expectedBranch = "release") {
|
||||||
const res = childProcess.spawnSync("git", [ "rev-parse", "--abbrev-ref", "HEAD" ]);
|
const res = childProcess.spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||||
const branch = res.stdout.toString().trim();
|
const branch = res.stdout.toString().trim();
|
||||||
if (branch !== "release") {
|
if (branch !== expectedBranch) {
|
||||||
console.error(`Current branch is ${branch}, please switch to "release" branch`);
|
console.error(`Current branch is ${branch}, please switch to "${expectedBranch}" branch`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create dist.tar.gz from the dist directory
|
||||||
|
* Similar to "tar -zcvf dist.tar.gz dist", but using nodejs
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function createDistTarGz() {
|
||||||
|
const distPath = "dist";
|
||||||
|
const outputPath = "./tmp/dist.tar.gz";
|
||||||
|
const tmpDir = "./tmp";
|
||||||
|
|
||||||
|
// Ensure tmp directory exists
|
||||||
|
if (!fs.existsSync(tmpDir)) {
|
||||||
|
fs.mkdirSync(tmpDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if dist directory exists
|
||||||
|
if (!fs.existsSync(distPath)) {
|
||||||
|
console.error("Error: dist directory not found");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Creating ${outputPath} from ${distPath}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tar.create(
|
||||||
|
{
|
||||||
|
gzip: true,
|
||||||
|
file: outputPath,
|
||||||
|
},
|
||||||
|
[distPath]
|
||||||
|
);
|
||||||
|
console.log(`Successfully created ${outputPath}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to create tarball: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a draft release PR
|
||||||
|
* @param {string} version Version
|
||||||
|
* @param {string} previousVersion Previous version tag
|
||||||
|
* @param {boolean} dryRun Still create the PR, but add "[DRY RUN]" to the title
|
||||||
|
* @param {string} branchName The branch name to use for the PR head (defaults to "release")
|
||||||
|
* @param {string} githubRunId The GitHub Actions run ID for linking to artifacts
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function createReleasePR(version, previousVersion, dryRun, branchName = "release", githubRunId = null) {
|
||||||
|
const changelog = await generateChangelog(previousVersion);
|
||||||
|
|
||||||
|
const title = dryRun ? `chore: update to ${version} (dry run)` : `chore: update to ${version}`;
|
||||||
|
|
||||||
|
// Build the artifact link - use direct run link if available, otherwise link to workflow file
|
||||||
|
const artifactLink = githubRunId
|
||||||
|
? `https://github.com/louislam/uptime-kuma/actions/runs/${githubRunId}/workflow`
|
||||||
|
: `https://github.com/louislam/uptime-kuma/actions/workflows/beta-release.yml`;
|
||||||
|
|
||||||
|
const body = `## Release ${version}
|
||||||
|
|
||||||
|
This PR prepares the release for version ${version}.
|
||||||
|
|
||||||
|
### Manual Steps Required
|
||||||
|
- [ ] Merge this PR (squash and merge)
|
||||||
|
- [ ] Create a new release on GitHub with the tag \`${version}\`.
|
||||||
|
- [ ] Ask any LLM to categorize the changelog into sections.
|
||||||
|
- [ ] Place the changelog in the release note.
|
||||||
|
- [ ] Download the \`dist.tar.gz\` artifact from the [workflow run](${artifactLink}) and upload it to the release.
|
||||||
|
- [ ] (Beta only) Set prerelease
|
||||||
|
- [ ] Publish the release note on GitHub.
|
||||||
|
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
\`\`\`md
|
||||||
|
${changelog}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Release Artifacts
|
||||||
|
The \`dist.tar.gz\` archive will be available as an artifact in the workflow run.
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create the PR using gh CLI
|
||||||
|
const args = ["pr", "create", "--title", title, "--body", body, "--base", "master", "--head", branchName, "--draft"];
|
||||||
|
|
||||||
|
console.log(`Creating draft PR: ${title}`);
|
||||||
|
|
||||||
|
const result = childProcess.spawnSync("gh", args, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
stdio: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
GH_TOKEN: process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
console.error("Failed to create pull request");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Successfully created draft pull request");
|
||||||
|
}
|
||||||
|
|||||||
1665
package-lock.json
generated
1665
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "2.1.0-beta.1",
|
"version": "2.1.0-beta.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -159,7 +159,7 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
"@fortawesome/free-solid-svg-icons": "~5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
"@fortawesome/vue-fontawesome": "~3.1.3",
|
||||||
"@playwright/test": "~1.39.0",
|
"@playwright/test": "~1.39.0",
|
||||||
"@popperjs/core": "~2.10.2",
|
"@popperjs/core": "~2.10.2",
|
||||||
"@testcontainers/hivemq": "^10.13.1",
|
"@testcontainers/hivemq": "^10.13.1",
|
||||||
@ -194,8 +194,8 @@
|
|||||||
"favico.js": "~0.3.10",
|
"favico.js": "~0.3.10",
|
||||||
"get-port-please": "^3.1.1",
|
"get-port-please": "^3.1.1",
|
||||||
"node-ssh": "~13.1.0",
|
"node-ssh": "~13.1.0",
|
||||||
"postcss-html": "~1.5.0",
|
"postcss-html": "~1.8.1",
|
||||||
"postcss-rtlcss": "~3.7.2",
|
"postcss-rtlcss": "~5.7.1",
|
||||||
"postcss-scss": "~4.0.4",
|
"postcss-scss": "~4.0.4",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prismjs": "~1.30.0",
|
"prismjs": "~1.30.0",
|
||||||
@ -213,11 +213,11 @@
|
|||||||
"vite": "~5.4.15",
|
"vite": "~5.4.15",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-pwa": "^1.1.0",
|
"vite-plugin-pwa": "^1.1.0",
|
||||||
"vue": "~3.4.2",
|
"vue": "~3.5.26",
|
||||||
"vue-chartjs": "~5.2.0",
|
"vue-chartjs": "~5.2.0",
|
||||||
"vue-confirm-dialog": "~1.0.2",
|
"vue-confirm-dialog": "~1.0.2",
|
||||||
"vue-contenteditable": "~3.0.4",
|
"vue-contenteditable": "~3.0.4",
|
||||||
"vue-i18n": "~9.14.3",
|
"vue-i18n": "~11.2.8",
|
||||||
"vue-image-crop-upload": "~3.0.3",
|
"vue-image-crop-upload": "~3.0.3",
|
||||||
"vue-multiselect": "~3.0.0-alpha.2",
|
"vue-multiselect": "~3.0.0-alpha.2",
|
||||||
"vue-prism-editor": "~2.0.0-alpha.2",
|
"vue-prism-editor": "~2.0.0-alpha.2",
|
||||||
|
|||||||
@ -87,10 +87,12 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
|||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
||||||
|
|
||||||
|
const result = list.map((bean) => bean.toJSON());
|
||||||
|
|
||||||
if (toUser) {
|
if (toUser) {
|
||||||
io.to(socket.userID).emit("importantHeartbeatList", monitorID, list, overwrite);
|
io.to(socket.userID).emit("importantHeartbeatList", monitorID, result, overwrite);
|
||||||
} else {
|
} else {
|
||||||
socket.emit("importantHeartbeatList", monitorID, list, overwrite);
|
socket.emit("importantHeartbeatList", monitorID, result, overwrite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -159,31 +159,22 @@ class DomainExpiry extends BeanModel {
|
|||||||
const tld = parseTld(target);
|
const tld = parseTld(target);
|
||||||
|
|
||||||
// Avoid logging for incomplete/invalid input while editing monitors.
|
// Avoid logging for incomplete/invalid input while editing monitors.
|
||||||
if (!tld.domain) {
|
|
||||||
throw new TranslatableError("domain_expiry_unsupported_invalid_domain", { hostname: tld.hostname });
|
|
||||||
}
|
|
||||||
if (!tld.publicSuffix) {
|
|
||||||
throw new TranslatableError("domain_expiry_unsupported_public_suffix", { publicSuffix: tld.publicSuffix });
|
|
||||||
}
|
|
||||||
if (tld.isIp) {
|
if (tld.isIp) {
|
||||||
throw new TranslatableError("domain_expiry_unsupported_is_ip", { hostname: tld.hostname });
|
throw new TranslatableError("domain_expiry_unsupported_is_ip", { hostname: tld.hostname });
|
||||||
}
|
}
|
||||||
|
|
||||||
// No one-letter public suffix exists; treat this as an incomplete/invalid input while typing.
|
// No one-letter public suffix exists; treat this as an incomplete/invalid input while typing.
|
||||||
if (tld.publicSuffix.length < 2) {
|
if (tld.publicSuffix.length < 2) {
|
||||||
throw new TranslatableError("domain_expiry_public_suffix_too_short", { publicSuffix: tld.publicSuffix });
|
throw new TranslatableError("domain_expiry_public_suffix_too_short", { publicSuffix: tld.publicSuffix });
|
||||||
}
|
}
|
||||||
|
if (!tld.isIcann) {
|
||||||
|
throw new TranslatableError("domain_expiry_unsupported_is_icann", {
|
||||||
|
domain: tld.domain,
|
||||||
|
publicSuffix: tld.publicSuffix,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const rdap = await getRdapServer(tld.publicSuffix);
|
const rdap = await getRdapServer(tld.publicSuffix);
|
||||||
if (!rdap) {
|
if (!rdap) {
|
||||||
// Only warn when the monitor actually has domain expiry notifications enabled.
|
|
||||||
// The edit monitor page calls this method frequently while the user is typing.
|
|
||||||
if (Boolean(monitor.domainExpiryNotification)) {
|
|
||||||
log.warn(
|
|
||||||
"domain_expiry",
|
|
||||||
`Domain expiry unsupported for '.${tld.publicSuffix}' because its RDAP endpoint is not listed in the IANA database.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new TranslatableError("domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint", {
|
throw new TranslatableError("domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint", {
|
||||||
publicSuffix: tld.publicSuffix,
|
publicSuffix: tld.publicSuffix,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const zlib = require("node:zlib");
|
||||||
|
const { promisify } = require("node:util");
|
||||||
|
const brotliDecompress = promisify(zlib.brotliDecompress);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
@ -36,8 +39,46 @@ class Heartbeat extends BeanModel {
|
|||||||
important: this._important,
|
important: this._important,
|
||||||
duration: this._duration,
|
duration: this._duration,
|
||||||
retries: this._retries,
|
retries: this._retries,
|
||||||
|
response: this._response,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @param {{ decodeResponse?: boolean }} opts Options for JSON serialization
|
||||||
|
* @returns {Promise<object>} Object ready to parse
|
||||||
|
*/
|
||||||
|
async toJSONAsync(opts) {
|
||||||
|
return {
|
||||||
|
monitorID: this._monitorId,
|
||||||
|
status: this._status,
|
||||||
|
time: this._time,
|
||||||
|
msg: this._msg,
|
||||||
|
ping: this._ping,
|
||||||
|
important: this._important,
|
||||||
|
duration: this._duration,
|
||||||
|
retries: this._retries,
|
||||||
|
response: opts?.decodeResponse ? await Heartbeat.decodeResponseValue(this._response) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode compressed response payload stored in database.
|
||||||
|
* @param {string|null} response Encoded response payload.
|
||||||
|
* @returns {string|null} Decoded response payload.
|
||||||
|
*/
|
||||||
|
static async decodeResponseValue(response) {
|
||||||
|
if (!response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Offload brotli decode from main event loop to libuv thread pool
|
||||||
|
return (await brotliDecompress(Buffer.from(response, "base64"))).toString("utf8");
|
||||||
|
} catch (error) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Heartbeat;
|
module.exports = Heartbeat;
|
||||||
|
|||||||
@ -24,6 +24,8 @@ const {
|
|||||||
PING_PER_REQUEST_TIMEOUT_MIN,
|
PING_PER_REQUEST_TIMEOUT_MIN,
|
||||||
PING_PER_REQUEST_TIMEOUT_MAX,
|
PING_PER_REQUEST_TIMEOUT_MAX,
|
||||||
PING_PER_REQUEST_TIMEOUT_DEFAULT,
|
PING_PER_REQUEST_TIMEOUT_DEFAULT,
|
||||||
|
RESPONSE_BODY_LENGTH_DEFAULT,
|
||||||
|
RESPONSE_BODY_LENGTH_MAX,
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
const {
|
const {
|
||||||
ping,
|
ping,
|
||||||
@ -56,6 +58,9 @@ const { CookieJar } = require("tough-cookie");
|
|||||||
const { HttpsCookieAgent } = require("http-cookie-agent/http");
|
const { HttpsCookieAgent } = require("http-cookie-agent/http");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
const zlib = require("node:zlib");
|
||||||
|
const { promisify } = require("node:util");
|
||||||
|
const brotliCompress = promisify(zlib.brotliCompress);
|
||||||
const DomainExpiry = require("./domain_expiry");
|
const DomainExpiry = require("./domain_expiry");
|
||||||
|
|
||||||
const rootCertificates = rootCertificatesFingerprints();
|
const rootCertificates = rootCertificatesFingerprints();
|
||||||
@ -203,6 +208,11 @@ class Monitor extends BeanModel {
|
|||||||
ping_numeric: this.isPingNumeric(),
|
ping_numeric: this.isPingNumeric(),
|
||||||
ping_count: this.ping_count,
|
ping_count: this.ping_count,
|
||||||
ping_per_request_timeout: this.ping_per_request_timeout,
|
ping_per_request_timeout: this.ping_per_request_timeout,
|
||||||
|
|
||||||
|
// response saving options
|
||||||
|
saveResponse: this.getSaveResponse(),
|
||||||
|
saveErrorResponse: this.getSaveErrorResponse(),
|
||||||
|
responseMaxLength: this.response_max_length ?? RESPONSE_BODY_LENGTH_DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
@ -386,6 +396,22 @@ class Monitor extends BeanModel {
|
|||||||
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
|
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean} Should save response data on success?
|
||||||
|
*/
|
||||||
|
getSaveResponse() {
|
||||||
|
return Boolean(this.save_response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean} Should save response data on error?
|
||||||
|
*/
|
||||||
|
getSaveErrorResponse() {
|
||||||
|
return Boolean(this.save_error_response);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitor
|
* Start monitor
|
||||||
* @param {Server} io Socket server instance
|
* @param {Server} io Socket server instance
|
||||||
@ -620,6 +646,11 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
|
// in the frontend, the save response is only shown if the saveErrorResponse is set
|
||||||
|
if (this.getSaveResponse() && this.getSaveErrorResponse()) {
|
||||||
|
await this.saveResponseData(bean, res.data);
|
||||||
|
}
|
||||||
|
|
||||||
// fallback for if kelog event is not emitted, but we may still have tlsInfo,
|
// fallback for if kelog event is not emitted, but we may still have tlsInfo,
|
||||||
// e.g. if the connection is made through a proxy
|
// e.g. if the connection is made through a proxy
|
||||||
if (this.getUrl()?.protocol === "https:" && tlsInfo.valid === undefined) {
|
if (this.getUrl()?.protocol === "https:" && tlsInfo.valid === undefined) {
|
||||||
@ -931,6 +962,10 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = error.message;
|
bean.msg = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.getSaveErrorResponse() && error?.response?.data !== undefined) {
|
||||||
|
await this.saveResponseData(bean, error.response.data);
|
||||||
|
}
|
||||||
|
|
||||||
// If UP come in here, it must be upside down mode
|
// If UP come in here, it must be upside down mode
|
||||||
// Just reset the retries
|
// Just reset the retries
|
||||||
if (this.isUpsideDown() && bean.status === UP) {
|
if (this.isUpsideDown() && bean.status === UP) {
|
||||||
@ -1024,7 +1059,15 @@ class Monitor extends BeanModel {
|
|||||||
log.debug("monitor", `Failed getting expiration date for domain ${supportInfo.domain}`);
|
log.debug("monitor", `Failed getting expiration date for domain ${supportInfo.domain}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// purposely not logged due to noise. Is accessible via checkMointor
|
if (
|
||||||
|
error.message === "domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint" &&
|
||||||
|
Boolean(this.domainExpiryNotification)
|
||||||
|
) {
|
||||||
|
log.warn(
|
||||||
|
"domain_expiry",
|
||||||
|
`Domain expiry unsupported for '.${error.meta.publicSuffix}' because its RDAP endpoint is not listed in the IANA database.`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1114,6 +1157,35 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save response body to a heartbeat if response saving is enabled.
|
||||||
|
* @param {import("redbean-node").Bean} bean Heartbeat bean to populate.
|
||||||
|
* @param {unknown} data Response payload.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
async saveResponseData(bean, data) {
|
||||||
|
if (data === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData = data;
|
||||||
|
if (typeof responseData !== "string") {
|
||||||
|
try {
|
||||||
|
responseData = JSON.stringify(responseData);
|
||||||
|
} catch (error) {
|
||||||
|
responseData = String(responseData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = this.response_max_length ?? RESPONSE_BODY_LENGTH_DEFAULT;
|
||||||
|
if (responseData.length > maxSize) {
|
||||||
|
responseData = responseData.substring(0, maxSize) + "... (truncated)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offload brotli compression from main event loop to libuv thread pool
|
||||||
|
bean.response = (await brotliCompress(Buffer.from(responseData, "utf8"))).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a request using axios
|
* Make a request using axios
|
||||||
* @param {object} options Options for Axios
|
* @param {object} options Options for Axios
|
||||||
@ -1417,7 +1489,7 @@ class Monitor extends BeanModel {
|
|||||||
* Send a notification about a monitor
|
* Send a notification about a monitor
|
||||||
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
||||||
* @param {Monitor} monitor The monitor to send a notification about
|
* @param {Monitor} monitor The monitor to send a notification about
|
||||||
* @param {Bean} bean Status information about monitor
|
* @param {import("./heartbeat")} bean Status information about monitor
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||||
@ -1435,7 +1507,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
for (let notification of notificationList) {
|
for (let notification of notificationList) {
|
||||||
try {
|
try {
|
||||||
const heartbeatJSON = bean.toJSON();
|
const heartbeatJSON = await bean.toJSONAsync({ decodeResponse: true });
|
||||||
const monitorData = [{ id: monitor.id, active: monitor.active, name: monitor.name }];
|
const monitorData = [{ id: monitor.id, active: monitor.active, name: monitor.name }];
|
||||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||||
@ -1642,6 +1714,16 @@ class Monitor extends BeanModel {
|
|||||||
throw new Error(`Retry interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
throw new Error(`Retry interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.response_max_length !== undefined) {
|
||||||
|
if (this.response_max_length < 0) {
|
||||||
|
throw new Error(`Response max length cannot be less than 0`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.response_max_length > RESPONSE_BODY_LENGTH_MAX) {
|
||||||
|
throw new Error(`Response max length cannot be more than ${RESPONSE_BODY_LENGTH_MAX} bytes`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.type === "ping") {
|
if (this.type === "ping") {
|
||||||
// ping parameters validation
|
// ping parameters validation
|
||||||
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
|
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
|
||||||
|
|||||||
@ -11,6 +11,11 @@ class Discord extends NotificationProvider {
|
|||||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
const okMsg = "Sent Successfully.";
|
const okMsg = "Sent Successfully.";
|
||||||
|
|
||||||
|
// Discord Message Flags
|
||||||
|
// @see https://discord.com/developers/docs/resources/message#message-object-message-flags
|
||||||
|
// This message will not trigger push and desktop notifications
|
||||||
|
const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let config = this.getAxiosConfigWithProxy({});
|
let config = this.getAxiosConfigWithProxy({});
|
||||||
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||||
@ -41,6 +46,9 @@ class Discord extends NotificationProvider {
|
|||||||
if (notification.discordChannelType === "createNewForumPost") {
|
if (notification.discordChannelType === "createNewForumPost") {
|
||||||
discordtestdata.thread_name = notification.postName;
|
discordtestdata.thread_name = notification.postName;
|
||||||
}
|
}
|
||||||
|
if (notification.discordSuppressNotifications) {
|
||||||
|
discordtestdata.flags = SUPPRESS_NOTIFICATIONS_FLAG;
|
||||||
|
}
|
||||||
await axios.post(webhookUrl.toString(), discordtestdata, config);
|
await axios.post(webhookUrl.toString(), discordtestdata, config);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
}
|
}
|
||||||
@ -89,6 +97,9 @@ class Discord extends NotificationProvider {
|
|||||||
if (notification.discordPrefixMessage) {
|
if (notification.discordPrefixMessage) {
|
||||||
discorddowndata.content = notification.discordPrefixMessage;
|
discorddowndata.content = notification.discordPrefixMessage;
|
||||||
}
|
}
|
||||||
|
if (notification.discordSuppressNotifications) {
|
||||||
|
discorddowndata.flags = SUPPRESS_NOTIFICATIONS_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
await axios.post(webhookUrl.toString(), discorddowndata, config);
|
await axios.post(webhookUrl.toString(), discorddowndata, config);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
@ -140,6 +151,9 @@ class Discord extends NotificationProvider {
|
|||||||
if (notification.discordPrefixMessage) {
|
if (notification.discordPrefixMessage) {
|
||||||
discordupdata.content = notification.discordPrefixMessage;
|
discordupdata.content = notification.discordPrefixMessage;
|
||||||
}
|
}
|
||||||
|
if (notification.discordSuppressNotifications) {
|
||||||
|
discordupdata.flags = SUPPRESS_NOTIFICATIONS_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
await axios.post(webhookUrl.toString(), discordupdata, config);
|
await axios.post(webhookUrl.toString(), discordupdata, config);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
|
|||||||
@ -90,6 +90,20 @@ class Slack extends NotificationProvider {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Optional context line for monitor group path (excluding monitor name)
|
||||||
|
const groupPath = monitorJSON?.path?.length > 1 ? monitorJSON.path.slice(0, -1).join(" / ") : "";
|
||||||
|
if (groupPath) {
|
||||||
|
blocks.push({
|
||||||
|
type: "context",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
type: "mrkdwn",
|
||||||
|
text: `_${groupPath}_`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// the body block, containing the details
|
// the body block, containing the details
|
||||||
blocks.push({
|
blocks.push({
|
||||||
type: "section",
|
type: "section",
|
||||||
@ -142,7 +156,7 @@ class Slack extends NotificationProvider {
|
|||||||
|
|
||||||
const baseURL = await setting("primaryBaseURL");
|
const baseURL = await setting("primaryBaseURL");
|
||||||
|
|
||||||
const title = "Uptime Kuma Alert";
|
const title = monitorJSON?.name || "Uptime Kuma Alert";
|
||||||
let data = {
|
let data = {
|
||||||
text: msg,
|
text: msg,
|
||||||
channel: notification.slackchannel,
|
channel: notification.slackchannel,
|
||||||
|
|||||||
@ -863,6 +863,9 @@ let needSetup = false;
|
|||||||
bean.packetSize = monitor.packetSize;
|
bean.packetSize = monitor.packetSize;
|
||||||
bean.maxredirects = monitor.maxredirects;
|
bean.maxredirects = monitor.maxredirects;
|
||||||
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||||
|
bean.save_response = monitor.saveResponse;
|
||||||
|
bean.save_error_response = monitor.saveErrorResponse;
|
||||||
|
bean.response_max_length = monitor.responseMaxLength;
|
||||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||||
bean.pushToken = monitor.pushToken;
|
bean.pushToken = monitor.pushToken;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h4>{{ $t("Certificate Info") }}</h4>
|
<h4>{{ $t("Certificate Info") }}</h4>
|
||||||
{{ $t("Certificate Chain") }}:
|
{{ $t("Certificate Chain:") }}
|
||||||
<div v-if="valid" class="rounded d-inline-flex ms-2 text-white tag-valid">
|
<div v-if="valid" class="rounded d-inline-flex ms-2 text-white tag-valid">
|
||||||
{{ $t("Valid") }}
|
{{ $t("Valid") }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -40,14 +40,13 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="form-text">
|
<i18n-t tag="div" keypath="Examples:" class="form-text">
|
||||||
{{ $t("Examples") }}:
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>/var/run/docker.sock</li>
|
<li><code>/var/run/docker.sock</code></li>
|
||||||
<li>http://localhost:2375</li>
|
<li><code>http://localhost:2375</code></li>
|
||||||
<li>https://localhost:2376 (TLS)</li>
|
<li><code>https://localhost:2376 (TLS)</code></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -83,7 +83,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="selected-count ms-2">
|
<span class="selected-count ms-2">
|
||||||
{{ $tc("selectedMonitorCountMsg", selectedMonitorCount) }}
|
{{ $t("selectedMonitorCountMsg", selectedMonitorCount) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -360,7 +360,7 @@ export default {
|
|||||||
|
|
||||||
this.bulkActionInProgress = true;
|
this.bulkActionInProgress = true;
|
||||||
activeMonitors.forEach((id) => this.$root.getSocket().emit("pauseMonitor", id, () => {}));
|
activeMonitors.forEach((id) => this.$root.getSocket().emit("pauseMonitor", id, () => {}));
|
||||||
this.$root.toastSuccess(this.$tc("pausedMonitorsMsg", activeMonitors.length));
|
this.$root.toastSuccess(this.$t("pausedMonitorsMsg", activeMonitors.length));
|
||||||
this.bulkActionInProgress = false;
|
this.bulkActionInProgress = false;
|
||||||
this.cancelSelectMode();
|
this.cancelSelectMode();
|
||||||
},
|
},
|
||||||
@ -384,7 +384,7 @@ export default {
|
|||||||
|
|
||||||
this.bulkActionInProgress = true;
|
this.bulkActionInProgress = true;
|
||||||
inactiveMonitors.forEach((id) => this.$root.getSocket().emit("resumeMonitor", id, () => {}));
|
inactiveMonitors.forEach((id) => this.$root.getSocket().emit("resumeMonitor", id, () => {}));
|
||||||
this.$root.toastSuccess(this.$tc("resumedMonitorsMsg", inactiveMonitors.length));
|
this.$root.toastSuccess(this.$t("resumedMonitorsMsg", inactiveMonitors.length));
|
||||||
this.bulkActionInProgress = false;
|
this.bulkActionInProgress = false;
|
||||||
this.cancelSelectMode();
|
this.cancelSelectMode();
|
||||||
},
|
},
|
||||||
@ -424,10 +424,10 @@ export default {
|
|||||||
this.bulkActionInProgress = false;
|
this.bulkActionInProgress = false;
|
||||||
|
|
||||||
if (successCount > 0) {
|
if (successCount > 0) {
|
||||||
this.$root.toastSuccess(this.$tc("deletedMonitorsMsg", successCount));
|
this.$root.toastSuccess(this.$t("deletedMonitorsMsg", successCount));
|
||||||
}
|
}
|
||||||
if (errorCount > 0) {
|
if (errorCount > 0) {
|
||||||
this.$root.toastError(this.$tc("bulkDeleteErrorMsg", errorCount));
|
this.$root.toastError(this.$t("bulkDeleteErrorMsg", errorCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cancelSelectMode();
|
this.cancelSelectMode();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center flex-wrap gap-1">
|
||||||
<MonitorListFilterDropdown :filterActive="filterState.status?.length > 0">
|
<MonitorListFilterDropdown :filterActive="filterState.status?.length > 0">
|
||||||
<template #status>
|
<template #status>
|
||||||
<Status v-if="filterState.status?.length === 1" :status="filterState.status[0]" />
|
<Status v-if="filterState.status?.length === 1" :status="filterState.status[0]" />
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ disabled: !monitor.active }">
|
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ disabled: !monitor.active }">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div
|
<div
|
||||||
class="col-6 small-padding"
|
class="col-9 col-xl-6 small-padding"
|
||||||
:class="{
|
:class="{
|
||||||
'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none',
|
'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none',
|
||||||
}"
|
}"
|
||||||
@ -44,7 +44,11 @@
|
|||||||
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
|
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-6">
|
<div
|
||||||
|
v-show="$root.userHeartbeatBar == 'normal'"
|
||||||
|
:key="$root.userHeartbeatBar"
|
||||||
|
class="col-3 col-xl-6"
|
||||||
|
>
|
||||||
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
|
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
<template #item="monitor">
|
<template #item="monitor">
|
||||||
<div class="item" data-testid="monitor">
|
<div class="item" data-testid="monitor">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 small-padding">
|
<div class="col-9 col-xl-6 small-padding">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
v-if="editMode"
|
v-if="editMode"
|
||||||
@ -116,7 +116,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :key="$root.userHeartbeatBar" class="col-6">
|
<div :key="$root.userHeartbeatBar" class="col-3 col-xl-6">
|
||||||
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -231,15 +231,11 @@ export default {
|
|||||||
*/
|
*/
|
||||||
formattedCertExpiryMessage(monitor) {
|
formattedCertExpiryMessage(monitor) {
|
||||||
if (monitor?.element?.validCert && monitor?.element?.certExpiryDaysRemaining) {
|
if (monitor?.element?.validCert && monitor?.element?.certExpiryDaysRemaining) {
|
||||||
return (
|
return this.$t("days", monitor.element.certExpiryDaysRemaining);
|
||||||
monitor.element.certExpiryDaysRemaining +
|
|
||||||
" " +
|
|
||||||
this.$tc("day", monitor.element.certExpiryDaysRemaining)
|
|
||||||
);
|
|
||||||
} else if (monitor?.element?.validCert === false) {
|
} else if (monitor?.element?.validCert === false) {
|
||||||
return this.$t("noOrBadCertificate");
|
return this.$t("noOrBadCertificate");
|
||||||
} else {
|
} else {
|
||||||
return this.$t("Unknown") + " " + this.$tc("day", 2);
|
return this.$t("unknownDays");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -31,12 +31,9 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="form-text mt-3">
|
<i18n-t tag="div" keypath="Example:" class="form-text mt-3">
|
||||||
{{ $t("Examples") }}:
|
<code>ws://chrome.browserless.io/playwright?token=YOUR-API-TOKEN</code>
|
||||||
<ul>
|
</i18n-t>
|
||||||
<li>ws://chrome.browserless.io/playwright?token=YOUR-API-TOKEN</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="tag-monitors" class="form-label">
|
<label for="tag-monitors" class="form-label">
|
||||||
{{ $tc("Monitor", selectedMonitors.length) }}
|
{{ $t("Monitors", selectedMonitors.length) }}
|
||||||
</label>
|
</label>
|
||||||
<div class="tag-monitors-list">
|
<div class="tag-monitors-list">
|
||||||
<router-link
|
<router-link
|
||||||
|
|||||||
@ -85,12 +85,12 @@ export default {
|
|||||||
|
|
||||||
title() {
|
title() {
|
||||||
if (this.type === "1y") {
|
if (this.type === "1y") {
|
||||||
return `1 ${this.$tc("year", 1)}`;
|
return this.$t("years", 1);
|
||||||
}
|
}
|
||||||
if (this.type === "720") {
|
if (this.type === "720") {
|
||||||
return `30 ${this.$tc("day", 30)}`;
|
return this.$t("days", 30);
|
||||||
}
|
}
|
||||||
return `24 ${this.$tc("hour", 24)}`;
|
return this.$t("hours", 24);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -102,6 +102,24 @@
|
|||||||
<label class="form-check-label" for="discord-disable-url">{{ $t("Disable URL in Notification") }}</label>
|
<label class="form-check-label" for="discord-disable-url">{{ $t("Disable URL in Notification") }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
id="discord-suppress-notifications"
|
||||||
|
v-model="$parent.notification.discordSuppressNotifications"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="discord-suppress-notifications">
|
||||||
|
{{ $t("Suppress Notifications") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("discordSuppressNotificationsHelptext") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
@ -112,6 +130,9 @@ export default {
|
|||||||
if (this.$parent.notification.disableUrl === undefined) {
|
if (this.$parent.notification.disableUrl === undefined) {
|
||||||
this.$parent.notification.disableUrl = false;
|
this.$parent.notification.disableUrl = false;
|
||||||
}
|
}
|
||||||
|
if (this.$parent.notification.discordSuppressNotifications === undefined) {
|
||||||
|
this.$parent.notification.discordSuppressNotifications = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="octopush-version" class="form-label">{{ $t("Octopush API Version") }}</label>
|
<label for="octopush-version" class="form-label">{{ $t("Octopush API Version") }}</label>
|
||||||
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
|
<select id="octopush-version" v-model="$parent.notification.octopushVersion" class="form-select">
|
||||||
<option value="2">{{ "octopush" }} ({{ $t("endpoint") }}: api.octopush.com)</option>
|
<option value="2">{{ $t("octopushEndpoint", { url: "api.octopush.com" }) }}</option>
|
||||||
<option value="1">{{ $t("Legacy Octopush-DM") }} ({{ $t("endpoint") }}: www.octopush-dm.com)</option>
|
<option value="1">{{ $t("legacyOctopushEndpoint", { url: "www.octopush-dm.com" }) }}</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
{{ $t("octopushLegacyHint") }}
|
{{ $t("octopushLegacyHint") }}
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
<div class="status">
|
<div class="status">
|
||||||
{{ $t("apiKey-" + item.status) }}
|
{{ $t("apiKey-" + item.status) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="date">{{ $t("Created") }}: {{ item.createdDate }}</div>
|
<div class="date">{{ $t("createdAt", { date: item.createdDate }) }}</div>
|
||||||
<div class="date">
|
<div class="date">
|
||||||
{{ $t("Expires") }}:
|
{{ $t("Expires") }}:
|
||||||
{{ item.expires || $t("Never") }}
|
{{ item.expires || $t("Never") }}
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
<div class="logo d-flex flex-column justify-content-center align-items-center">
|
<div class="logo d-flex flex-column justify-content-center align-items-center">
|
||||||
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
<object class="my-4" width="200" height="200" data="/icon.svg" />
|
||||||
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
<div class="fs-4 fw-bold">Uptime Kuma</div>
|
||||||
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
|
<div>{{ $t("versionIs", { version: $root.info.version }) }}</div>
|
||||||
<div class="frontend-version">{{ $t("Frontend Version") }}: {{ $root.frontendVersion }}</div>
|
<div class="frontend-version">{{ $t("frontendVersionIs", { version: $root.frontendVersion }) }}</div>
|
||||||
|
|
||||||
<div v-if="!$root.isFrontendBackendVersionMatched" class="alert alert-warning mt-4" role="alert">
|
<div v-if="!$root.isFrontendBackendVersionMatched" class="alert alert-warning mt-4" role="alert">
|
||||||
⚠️ {{ $t("Frontend Version do not match backend version!") }}
|
⚠️ {{ $t("Frontend Version do not match backend version!") }}
|
||||||
|
|||||||
@ -64,7 +64,7 @@
|
|||||||
:key="day"
|
:key="day"
|
||||||
class="d-flex align-items-center justify-content-between cert-exp-day-row py-2"
|
class="d-flex align-items-center justify-content-between cert-exp-day-row py-2"
|
||||||
>
|
>
|
||||||
<span>{{ day }} {{ $tc("day", day) }}</span>
|
<span>{{ $t("days", day) }}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-rm-expiry btn btn-outline-danger ms-2 py-1"
|
class="btn-rm-expiry btn btn-outline-danger ms-2 py-1"
|
||||||
@ -79,7 +79,7 @@
|
|||||||
<ActionInput
|
<ActionInput
|
||||||
v-model="tlsExpiryNotifInput"
|
v-model="tlsExpiryNotifInput"
|
||||||
:type="'number'"
|
:type="'number'"
|
||||||
:placeholder="$t('day')"
|
:placeholder="$t('days', 1)"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
:action="() => addTlsExpiryNotifDay(tlsExpiryNotifInput)"
|
:action="() => addTlsExpiryNotifDay(tlsExpiryNotifInput)"
|
||||||
:action-aria-label="$t('Add a new expiry notification day')"
|
:action-aria-label="$t('Add a new expiry notification day')"
|
||||||
@ -102,7 +102,7 @@
|
|||||||
:key="day"
|
:key="day"
|
||||||
class="d-flex align-items-center justify-content-between cert-exp-day-row py-2"
|
class="d-flex align-items-center justify-content-between cert-exp-day-row py-2"
|
||||||
>
|
>
|
||||||
<span>{{ day }} {{ $tc("day", day) }}</span>
|
<span>{{ $t("days", day) }}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-rm-expiry btn btn-outline-danger ms-2 py-1"
|
class="btn-rm-expiry btn btn-outline-danger ms-2 py-1"
|
||||||
@ -117,7 +117,7 @@
|
|||||||
<ActionInput
|
<ActionInput
|
||||||
v-model="domainExpiryNotifInput"
|
v-model="domainExpiryNotifInput"
|
||||||
:type="'number'"
|
:type="'number'"
|
||||||
:placeholder="$t('day')"
|
:placeholder="$t('days', 1)"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
:action="() => addDomainExpiryNotifDay(domainExpiryNotifInput)"
|
:action="() => addDomainExpiryNotifDay(domainExpiryNotifInput)"
|
||||||
:action-aria-label="$t('Add a new expiry notification day')"
|
:action-aria-label="$t('Add a new expiry notification day')"
|
||||||
|
|||||||
@ -4,15 +4,13 @@
|
|||||||
<!-- Change Password -->
|
<!-- Change Password -->
|
||||||
<template v-if="!settings.disableAuth">
|
<template v-if="!settings.disableAuth">
|
||||||
<p>
|
<p>
|
||||||
{{ $t("Current User") }}:
|
|
||||||
<strong>{{ $root.username }}</strong>
|
|
||||||
<button
|
<button
|
||||||
v-if="!settings.disableAuth"
|
v-if="!settings.disableAuth"
|
||||||
id="logout-btn"
|
id="logout-btn"
|
||||||
class="btn btn-danger ms-4 me-2 mb-2"
|
class="btn btn-danger ms-4 me-2 mb-2"
|
||||||
@click="$root.logout"
|
@click="$root.logout"
|
||||||
>
|
>
|
||||||
{{ $t("Logout") }}
|
{{ $t("logoutCurrentUser", { username: $root.username }) }}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<Tag :item="tag" />
|
<Tag :item="tag" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-5 px-1 d-none d-sm-block">
|
<div class="col-5 px-1 d-none d-sm-block">
|
||||||
<div>{{ monitorsByTag(tag.id).length }} {{ $tc("Monitor", monitorsByTag(tag.id).length) }}</div>
|
<div>{{ $t("Monitors", monitorsByTag(tag.id).length) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2 pe-2 pe-lg-3 d-flex justify-content-end">
|
<div class="col-2 pe-2 pe-lg-3 d-flex justify-content-end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
"General": "General",
|
"General": "General",
|
||||||
"Game": "Game",
|
"Game": "Game",
|
||||||
"Primary Base URL": "Primary Base URL",
|
"Primary Base URL": "Primary Base URL",
|
||||||
"Version": "Version",
|
"versionIs": "Version: {version}",
|
||||||
"Check Update On GitHub": "Check Update On GitHub",
|
"Check Update On GitHub": "Check Update On GitHub",
|
||||||
"List": "List",
|
"List": "List",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
@ -33,6 +33,7 @@
|
|||||||
"statusMaintenance": "Maintenance",
|
"statusMaintenance": "Maintenance",
|
||||||
"Maintenance": "Maintenance",
|
"Maintenance": "Maintenance",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
|
"unknownDays": "Unknown days",
|
||||||
"Cannot connect to the socket server": "Cannot connect to the socket server",
|
"Cannot connect to the socket server": "Cannot connect to the socket server",
|
||||||
"Reconnecting...": "Reconnecting...",
|
"Reconnecting...": "Reconnecting...",
|
||||||
"General Monitor Type": "General Monitor Type",
|
"General Monitor Type": "General Monitor Type",
|
||||||
@ -52,12 +53,14 @@
|
|||||||
"Current": "Current",
|
"Current": "Current",
|
||||||
"Uptime": "Uptime",
|
"Uptime": "Uptime",
|
||||||
"Cert Exp.": "Cert Exp.",
|
"Cert Exp.": "Cert Exp.",
|
||||||
"Monitor": "Monitor | Monitors",
|
"Monitors": "{n} Monitor | {n} Monitors",
|
||||||
"now": "now",
|
"now": "now",
|
||||||
"time ago": "{0} ago",
|
"time ago": "{0} ago",
|
||||||
"day": "day | days",
|
"days": "{n} day | {n} days",
|
||||||
"hour": "hour | hours",
|
"hours": "{n} hour | {n} hours",
|
||||||
"year": "year | years",
|
"minutes": "{n} minute | {n} minutes",
|
||||||
|
"minuteShort": "{n} min | {n} min",
|
||||||
|
"years": "{n} year | {n} years",
|
||||||
"Response": "Response",
|
"Response": "Response",
|
||||||
"Ping": "Ping",
|
"Ping": "Ping",
|
||||||
"Monitor Type": "Monitor Type",
|
"Monitor Type": "Monitor Type",
|
||||||
@ -100,6 +103,11 @@
|
|||||||
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||||
"Upside Down Mode": "Upside Down Mode",
|
"Upside Down Mode": "Upside Down Mode",
|
||||||
"Max. Redirects": "Max. Redirects",
|
"Max. Redirects": "Max. Redirects",
|
||||||
|
"saveResponseForNotifications": "Save HTTP Success Response for Notifications",
|
||||||
|
"saveErrorResponseForNotifications": "Save HTTP Error Response for Notifications",
|
||||||
|
"saveResponseDescription": "Stores the HTTP response and makes it available to notification templates as {templateVariable}",
|
||||||
|
"responseMaxLength": "Response Max Length (bytes)",
|
||||||
|
"responseMaxLengthDescription": "Maximum size of response data to store. Set to 0 for unlimited. Larger responses will be truncated. Default: 1024 (1KB)",
|
||||||
"Accepted Status Codes": "Accepted Status Codes",
|
"Accepted Status Codes": "Accepted Status Codes",
|
||||||
"Push URL": "Push URL",
|
"Push URL": "Push URL",
|
||||||
"needPushEvery": "You should call this URL every {0} seconds.",
|
"needPushEvery": "You should call this URL every {0} seconds.",
|
||||||
@ -138,6 +146,7 @@
|
|||||||
"where you intend to implement third-party authentication": "where you intend to implement third-party authentication",
|
"where you intend to implement third-party authentication": "where you intend to implement third-party authentication",
|
||||||
"Please use this option carefully!": "Please use this option carefully!",
|
"Please use this option carefully!": "Please use this option carefully!",
|
||||||
"Logout": "Log out",
|
"Logout": "Log out",
|
||||||
|
"logoutCurrentUser": "Log out {username}",
|
||||||
"Leave": "Leave",
|
"Leave": "Leave",
|
||||||
"I understand, please disable": "I understand, please disable",
|
"I understand, please disable": "I understand, please disable",
|
||||||
"Confirm": "Confirm",
|
"Confirm": "Confirm",
|
||||||
@ -273,7 +282,6 @@
|
|||||||
"records": "records",
|
"records": "records",
|
||||||
"One record": "One record",
|
"One record": "One record",
|
||||||
"steamApiKeyDescriptionAt": "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key at {url}",
|
"steamApiKeyDescriptionAt": "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key at {url}",
|
||||||
"Current User": "Current User",
|
|
||||||
"topic": "Topic",
|
"topic": "Topic",
|
||||||
"topicExplanation": "MQTT topic to monitor",
|
"topicExplanation": "MQTT topic to monitor",
|
||||||
"mqttWebSocketPath": "MQTT WebSocket Path",
|
"mqttWebSocketPath": "MQTT WebSocket Path",
|
||||||
@ -313,8 +321,9 @@
|
|||||||
"dark": "dark",
|
"dark": "dark",
|
||||||
"Post": "Post",
|
"Post": "Post",
|
||||||
"Please input title and content": "Please input title and content",
|
"Please input title and content": "Please input title and content",
|
||||||
"Created": "Created",
|
"createdAt": "Created: {date}",
|
||||||
"Last Updated": "Last Updated",
|
"lastUpdatedAt": "Last Updated: {date}",
|
||||||
|
"lastUpdatedAtFromNow": "Last Updated: {date} ({fromNow})",
|
||||||
"Switch to Light Theme": "Switch to Light Theme",
|
"Switch to Light Theme": "Switch to Light Theme",
|
||||||
"Switch to Dark Theme": "Switch to Dark Theme",
|
"Switch to Dark Theme": "Switch to Dark Theme",
|
||||||
"Show Tags": "Show Tags",
|
"Show Tags": "Show Tags",
|
||||||
@ -349,7 +358,7 @@
|
|||||||
"proxyDescription": "Proxies must be assigned to a monitor to function.",
|
"proxyDescription": "Proxies must be assigned to a monitor to function.",
|
||||||
"enableProxyDescription": "This proxy will not effect on monitor requests until it is activated. You can control temporarily disable the proxy from all monitors by activation status.",
|
"enableProxyDescription": "This proxy will not effect on monitor requests until it is activated. You can control temporarily disable the proxy from all monitors by activation status.",
|
||||||
"setAsDefaultProxyDescription": "This proxy will be enabled by default for new monitors. You can still disable the proxy separately for each monitor.",
|
"setAsDefaultProxyDescription": "This proxy will be enabled by default for new monitors. You can still disable the proxy separately for each monitor.",
|
||||||
"Certificate Chain": "Certificate Chain",
|
"Certificate Chain:": "Certificate Chain:",
|
||||||
"Valid": "Valid",
|
"Valid": "Valid",
|
||||||
"Invalid": "Invalid",
|
"Invalid": "Invalid",
|
||||||
"User": "User",
|
"User": "User",
|
||||||
@ -396,7 +405,7 @@
|
|||||||
"Add a new expiry notification day": "Add a new expiry notification day",
|
"Add a new expiry notification day": "Add a new expiry notification day",
|
||||||
"Remove the expiry notification": "Remove the expiry notification day",
|
"Remove the expiry notification": "Remove the expiry notification day",
|
||||||
"Proxy": "Proxy",
|
"Proxy": "Proxy",
|
||||||
"Date Created": "Date Created",
|
"dateCreatedAtFromNow": "Date Created: {date} ({fromNow})",
|
||||||
"Footer Text": "Footer Text",
|
"Footer Text": "Footer Text",
|
||||||
"RSS Title": "RSS Title",
|
"RSS Title": "RSS Title",
|
||||||
"Leave blank to use status page title": "Leave blank to use status page title",
|
"Leave blank to use status page title": "Leave blank to use status page title",
|
||||||
@ -470,13 +479,13 @@
|
|||||||
"disableCloudflaredNoAuthMsg": "You are in No Auth mode, a password is not required.",
|
"disableCloudflaredNoAuthMsg": "You are in No Auth mode, a password is not required.",
|
||||||
"trustProxyDescription": "Trust 'X-Forwarded-*' headers. If you want to get the correct client IP and your Uptime Kuma is behind a proxy such as Nginx or Apache, you should enable this.",
|
"trustProxyDescription": "Trust 'X-Forwarded-*' headers. If you want to get the correct client IP and your Uptime Kuma is behind a proxy such as Nginx or Apache, you should enable this.",
|
||||||
"wayToGetLineNotifyToken": "You can get an access token from {0}",
|
"wayToGetLineNotifyToken": "You can get an access token from {0}",
|
||||||
"Examples": "Examples",
|
"Examples:": "Examples: {0}",
|
||||||
"supportBaleChatID": "Support Direct Chat / Group / Channel's Chat ID",
|
"supportBaleChatID": "Support Direct Chat / Group / Channel's Chat ID",
|
||||||
"wayToGetBaleChatID": "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id:",
|
"wayToGetBaleChatID": "You can get your chat ID by sending a message to the bot and going to this URL to view the chat_id:",
|
||||||
"wayToGetBaleToken": "You can get a token from {0}.",
|
"wayToGetBaleToken": "You can get a token from {0}.",
|
||||||
"Home Assistant URL": "Home Assistant URL",
|
"Home Assistant URL": "Home Assistant URL",
|
||||||
"Long-Lived Access Token": "Long-Lived Access Token",
|
"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. ",
|
"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 Service": "Notification Service",
|
"Notification Service": "Notification Service",
|
||||||
"default: notify all devices": "default: notify all devices",
|
"default: notify all devices": "default: notify all devices",
|
||||||
"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.",
|
"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.",
|
||||||
@ -485,7 +494,7 @@
|
|||||||
"Event type:": "Event type:",
|
"Event type:": "Event type:",
|
||||||
"Event data:": "Event data:",
|
"Event data:": "Event data:",
|
||||||
"Then choose an action, for example switch the scene to where an RGB light is red.": "Then choose an action, for example switch the scene to where an RGB light is red.",
|
"Then choose an action, for example switch the scene to where an RGB light is red.": "Then choose an action, for example switch the scene to where an RGB light is red.",
|
||||||
"Frontend Version": "Frontend Version",
|
"frontendVersionIs": "Frontend Version: {version}",
|
||||||
"Frontend Version do not match backend version!": "Frontend Version do not match backend version!",
|
"Frontend Version do not match backend version!": "Frontend Version do not match backend version!",
|
||||||
"backupOutdatedWarning": "Deprecated: Since a lot of features were added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
|
"backupOutdatedWarning": "Deprecated: Since a lot of features were added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
|
||||||
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
|
"backupRecommend": "Please backup the volume or the data folder (./data/) directly instead.",
|
||||||
@ -496,7 +505,7 @@
|
|||||||
"startDateTime": "Start Date/Time",
|
"startDateTime": "Start Date/Time",
|
||||||
"endDateTime": "End Date/Time",
|
"endDateTime": "End Date/Time",
|
||||||
"cronExpression": "Cron Expression",
|
"cronExpression": "Cron Expression",
|
||||||
"cronSchedule": "Schedule: ",
|
"cronScheduleDescription": "Schedule: {description}",
|
||||||
"Duration (Minutes)": "Duration (Minutes)",
|
"Duration (Minutes)": "Duration (Minutes)",
|
||||||
"invalidCronExpression": "Invalid Cron Expression: {0}",
|
"invalidCronExpression": "Invalid Cron Expression: {0}",
|
||||||
"recurringInterval": "Interval",
|
"recurringInterval": "Interval",
|
||||||
@ -663,6 +672,8 @@
|
|||||||
"recurringIntervalMessage": "Run once every day | Run once every {0} days",
|
"recurringIntervalMessage": "Run once every day | Run once every {0} days",
|
||||||
"affectedMonitorsDescription": "Select monitors that are affected by current maintenance",
|
"affectedMonitorsDescription": "Select monitors that are affected by current maintenance",
|
||||||
"affectedStatusPages": "Show this maintenance message on selected status pages",
|
"affectedStatusPages": "Show this maintenance message on selected status pages",
|
||||||
|
"Sets end time based on start time": "Sets end time based on start time",
|
||||||
|
"Please set start time first": "Please set start time first",
|
||||||
"noMonitorsSelectedWarning": "You are creating a maintenance without any affected monitors. Are you sure you want to continue?",
|
"noMonitorsSelectedWarning": "You are creating a maintenance without any affected monitors. Are you sure you want to continue?",
|
||||||
"noMonitorsOrStatusPagesSelectedError": "Cannot create maintenance without affected monitors or status pages",
|
"noMonitorsOrStatusPagesSelectedError": "Cannot create maintenance without affected monitors or status pages",
|
||||||
"passwordNotMatchMsg": "The repeat password does not match.",
|
"passwordNotMatchMsg": "The repeat password does not match.",
|
||||||
@ -673,7 +684,6 @@
|
|||||||
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
|
"backupDescription": "You can backup all monitors and notifications into a JSON file.",
|
||||||
"backupDescription2": "Note: history and event data is not included.",
|
"backupDescription2": "Note: history and event data is not included.",
|
||||||
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
|
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",
|
||||||
"endpoint": "endpoint",
|
|
||||||
"octopushAPIKey": "\"API key\" from HTTP API credentials in control panel",
|
"octopushAPIKey": "\"API key\" from HTTP API credentials in control panel",
|
||||||
"octopushLogin": "\"Login\" from HTTP API credentials in control panel",
|
"octopushLogin": "\"Login\" from HTTP API credentials in control panel",
|
||||||
"promosmsLogin": "API Login Name",
|
"promosmsLogin": "API Login Name",
|
||||||
@ -725,7 +735,7 @@
|
|||||||
"apiCredentials": "API credentials",
|
"apiCredentials": "API credentials",
|
||||||
"octopushLegacyHint": "Do you use the legacy version of Octopush (2011-2020) or the new version?",
|
"octopushLegacyHint": "Do you use the legacy version of Octopush (2011-2020) or the new version?",
|
||||||
"Check octopush prices": "Check octopush prices {0}.",
|
"Check octopush prices": "Check octopush prices {0}.",
|
||||||
"octopushPhoneNumber": "Phone number (intl format, eg : +33612345678) ",
|
"octopushPhoneNumber": "Phone number (intl format, eg : +33612345678)",
|
||||||
"octopushSMSSender": "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
|
"octopushSMSSender": "SMS Sender Name : 3-11 alphanumeric characters and space (a-zA-Z0-9)",
|
||||||
"LunaSea Device ID": "LunaSea Device ID",
|
"LunaSea Device ID": "LunaSea Device ID",
|
||||||
"Apprise URL": "Apprise URL",
|
"Apprise URL": "Apprise URL",
|
||||||
@ -875,7 +885,8 @@
|
|||||||
"From Name/Number": "From Name/Number",
|
"From Name/Number": "From Name/Number",
|
||||||
"Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.",
|
"Leave blank to use a shared sender number.": "Leave blank to use a shared sender number.",
|
||||||
"Octopush API Version": "Octopush API Version",
|
"Octopush API Version": "Octopush API Version",
|
||||||
"Legacy Octopush-DM": "Legacy Octopush-DM",
|
"octopushEndpoint": "octopush (endpoint: {url})",
|
||||||
|
"legacyOctopushEndpoint": "Legacy Octopush-DM (endpoint: {url})",
|
||||||
"ntfy Topic": "ntfy Topic",
|
"ntfy Topic": "ntfy Topic",
|
||||||
"Server URL should not contain the nfty topic": "Server URL should not contain the nfty topic",
|
"Server URL should not contain the nfty topic": "Server URL should not contain the nfty topic",
|
||||||
"onebotHttpAddress": "OneBot HTTP Address",
|
"onebotHttpAddress": "OneBot HTTP Address",
|
||||||
@ -1254,6 +1265,8 @@
|
|||||||
"Matomo": "Matomo",
|
"Matomo": "Matomo",
|
||||||
"Umami": "Umami",
|
"Umami": "Umami",
|
||||||
"Disable URL in Notification": "Disable URL in Notification",
|
"Disable URL in Notification": "Disable URL in Notification",
|
||||||
|
"Suppress Notifications": "Suppress Notifications",
|
||||||
|
"discordSuppressNotificationsHelptext": "When enabled, messages will be posted to the channel but won't trigger push or desktop notifications for recipients.",
|
||||||
"Ip Family": "IP Family",
|
"Ip Family": "IP Family",
|
||||||
"ipFamilyDescriptionAutoSelect": "Uses the {happyEyeballs} for determining the IP family.",
|
"ipFamilyDescriptionAutoSelect": "Uses the {happyEyeballs} for determining the IP family.",
|
||||||
"Happy Eyeballs algorithm": "Happy Eyeballs algorithm",
|
"Happy Eyeballs algorithm": "Happy Eyeballs algorithm",
|
||||||
@ -1284,9 +1297,8 @@
|
|||||||
"domainExpiryDescription": "Trigger notification when domain names expires in:",
|
"domainExpiryDescription": "Trigger notification when domain names expires in:",
|
||||||
"domain_expiry_unsupported_monitor_type": "Domain expiry monitoring is not supported for this monitor type",
|
"domain_expiry_unsupported_monitor_type": "Domain expiry monitoring is not supported for this monitor type",
|
||||||
"domain_expiry_unsupported_missing_target": "No valid domain or hostname is configured for this monitor",
|
"domain_expiry_unsupported_missing_target": "No valid domain or hostname is configured for this monitor",
|
||||||
"domain_expiry_unsupported_invalid_domain": "The configured value \"{hostname}\" is not a valid domain name",
|
|
||||||
"domain_expiry_public_suffix_too_short": "\".{publicSuffix}\" is too short for a top level domain",
|
"domain_expiry_public_suffix_too_short": "\".{publicSuffix}\" is too short for a top level domain",
|
||||||
"domain_expiry_unsupported_public_suffix": "The domain \"{domain}\" does not have a valid public suffix",
|
"domain_expiry_unsupported_is_icann": "The domain \"{domain}\" is not a candidate for domain expiry monitoring, because its public suffix \".{publicSuffix}\" is not ICAN",
|
||||||
"domain_expiry_unsupported_is_ip": "\"{hostname}\" is an IP address. Domain expiry monitoring requires a domain name",
|
"domain_expiry_unsupported_is_ip": "\"{hostname}\" is an IP address. Domain expiry monitoring requires a domain name",
|
||||||
"domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint": "Domain expiry monitoring is not available for \".{publicSuffix}\" because no RDAP service is listed by IANA",
|
"domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint": "Domain expiry monitoring is not available for \".{publicSuffix}\" because no RDAP service is listed by IANA",
|
||||||
"minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.",
|
"minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.",
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
<MonitorList :scrollbar="true" />
|
<MonitorList :scrollbar="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="container" class="col-12 col-md-7 col-xl-8 mb-3">
|
<div ref="container" class="col-12 col-md-7 col-xl-8 mb-3 gx-0">
|
||||||
<!-- Add :key to disable vue router re-use the same component -->
|
<!-- Add :key to disable vue router re-use the same component -->
|
||||||
<router-view :key="$route.fullPath" :calculatedHeight="height" />
|
<router-view :key="$route.fullPath" :calculatedHeight="height" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -194,7 +194,7 @@
|
|||||||
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
|
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
|
||||||
>
|
>
|
||||||
<h4 class="col-4 col-sm-12">{{ pingTitle(true) }}</h4>
|
<h4 class="col-4 col-sm-12">{{ pingTitle(true) }}</h4>
|
||||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ 24 }} {{ $tc("hour", 24) }})</p>
|
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("hours", 24) }})</p>
|
||||||
<span class="col-4 col-sm-12 num">
|
<span class="col-4 col-sm-12 num">
|
||||||
<CountUp :value="avgPing" />
|
<CountUp :value="avgPing" />
|
||||||
</span>
|
</span>
|
||||||
@ -203,7 +203,7 @@
|
|||||||
<!-- Uptime (24-hour) -->
|
<!-- Uptime (24-hour) -->
|
||||||
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||||
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
||||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ 24 }} {{ $tc("hour", 24) }})</p>
|
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("hours", 24) }})</p>
|
||||||
<span class="col-4 col-sm-12 num">
|
<span class="col-4 col-sm-12 num">
|
||||||
<Uptime :monitor="monitor" type="24" />
|
<Uptime :monitor="monitor" type="24" />
|
||||||
</span>
|
</span>
|
||||||
@ -212,7 +212,7 @@
|
|||||||
<!-- Uptime (30-day) -->
|
<!-- Uptime (30-day) -->
|
||||||
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||||
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
||||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ 30 }} {{ $tc("day", 30) }})</p>
|
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("days", 30) }})</p>
|
||||||
<span class="col-4 col-sm-12 num">
|
<span class="col-4 col-sm-12 num">
|
||||||
<Uptime :monitor="monitor" type="720" />
|
<Uptime :monitor="monitor" type="720" />
|
||||||
</span>
|
</span>
|
||||||
@ -221,7 +221,7 @@
|
|||||||
<!-- Uptime (1-year) -->
|
<!-- Uptime (1-year) -->
|
||||||
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||||
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
||||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ 1 }} {{ $tc("year", 1) }})</p>
|
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("years", 1) }})</p>
|
||||||
<span class="col-4 col-sm-12 num">
|
<span class="col-4 col-sm-12 num">
|
||||||
<Uptime :monitor="monitor" type="1y" />
|
<Uptime :monitor="monitor" type="1y" />
|
||||||
</span>
|
</span>
|
||||||
@ -236,7 +236,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<span class="col-4 col-sm-12 num">
|
<span class="col-4 col-sm-12 num">
|
||||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">
|
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">
|
||||||
{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}
|
{{ $t("days", tlsInfo.certInfo.daysRemaining) }}
|
||||||
</a>
|
</a>
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
v-if="tlsInfo.hostnameMatchMonitorUrl === false"
|
v-if="tlsInfo.hostnameMatchMonitorUrl === false"
|
||||||
@ -254,7 +254,7 @@
|
|||||||
)
|
)
|
||||||
</p>
|
</p>
|
||||||
<span class="col-4 col-sm-12 num">
|
<span class="col-4 col-sm-12 num">
|
||||||
{{ domainInfo.daysRemaining }} {{ $tc("day", domainInfo.daysRemaining) }}
|
{{ $t("days", domainInfo.daysRemaining) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -374,7 +374,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="delete-children-checkbox">
|
<label class="form-check-label" for="delete-children-checkbox">
|
||||||
{{ $t("deleteChildrenMonitors", childrenCount, { count: childrenCount }) }}
|
{{ $t("deleteChildrenMonitors", childrenCount) }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -123,16 +123,13 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Single Maintenance Window -->
|
|
||||||
<template v-if="maintenance.strategy === 'single'"></template>
|
|
||||||
|
|
||||||
<template v-if="maintenance.strategy === 'cron'">
|
<template v-if="maintenance.strategy === 'cron'">
|
||||||
<!-- Cron -->
|
<!-- Cron -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="cron" class="form-label">
|
<label for="cron" class="form-label">
|
||||||
{{ $t("cronExpression") }}
|
{{ $t("cronExpression") }}
|
||||||
</label>
|
</label>
|
||||||
<p>{{ $t("cronSchedule") }}{{ cronDescription }}</p>
|
<p>{{ $t("cronScheduleDescription", { description: cronDescription }) }}</p>
|
||||||
<input
|
<input
|
||||||
id="cron"
|
id="cron"
|
||||||
v-model="maintenance.cron"
|
v-model="maintenance.cron"
|
||||||
@ -167,7 +164,7 @@
|
|||||||
|
|
||||||
<template v-if="maintenance.intervalDay >= 1">
|
<template v-if="maintenance.intervalDay >= 1">
|
||||||
({{
|
({{
|
||||||
$tc("recurringIntervalMessage", maintenance.intervalDay, [
|
$t("recurringIntervalMessage", maintenance.intervalDay, [
|
||||||
maintenance.intervalDay,
|
maintenance.intervalDay,
|
||||||
])
|
])
|
||||||
}})
|
}})
|
||||||
@ -331,6 +328,102 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="maintenance.strategy === 'single'">
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 15 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 15"
|
||||||
|
@click="setQuickDuration(15)"
|
||||||
|
>
|
||||||
|
{{ $t("minuteShort", 15) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 30 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 30"
|
||||||
|
@click="setQuickDuration(30)"
|
||||||
|
>
|
||||||
|
{{ $t("minuteShort", 30) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 60 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 60"
|
||||||
|
@click="setQuickDuration(60)"
|
||||||
|
>
|
||||||
|
{{ $t("hours", 1) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 120 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 120"
|
||||||
|
@click="setQuickDuration(120)"
|
||||||
|
>
|
||||||
|
{{ $t("hours", 2) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 240 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 240"
|
||||||
|
@click="setQuickDuration(240)"
|
||||||
|
>
|
||||||
|
{{ $t("hours", 4) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 480 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 480"
|
||||||
|
@click="setQuickDuration(480)"
|
||||||
|
>
|
||||||
|
{{ $t("hours", 8) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 720 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 720"
|
||||||
|
@click="setQuickDuration(720)"
|
||||||
|
>
|
||||||
|
{{ $t("hours", 12) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 1440 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 1440"
|
||||||
|
@click="setQuickDuration(1440)"
|
||||||
|
>
|
||||||
|
{{ $t("hours", 24) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">{{ $t("Sets end time based on start time") }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -511,6 +604,22 @@ export default {
|
|||||||
hasStatusPages() {
|
hasStatusPages() {
|
||||||
return this.showOnAllPages || this.selectedStatusPages.length > 0;
|
return this.showOnAllPages || this.selectedStatusPages.length > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the current duration in minutes between start and end dates
|
||||||
|
* @returns {number|null} Duration in minutes, or null if dates are invalid
|
||||||
|
*/
|
||||||
|
currentDurationMinutes() {
|
||||||
|
if (!this.maintenance.dateRange?.[0] || !this.maintenance.dateRange?.[1]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const start = new Date(this.maintenance.dateRange[0]);
|
||||||
|
const end = new Date(this.maintenance.dateRange[1]);
|
||||||
|
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.round((end.getTime() - start.getTime()) / 60000);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$route.fullPath"() {
|
"$route.fullPath"() {
|
||||||
@ -570,6 +679,19 @@ export default {
|
|||||||
this.selectedStatusPages = [];
|
this.selectedStatusPages = [];
|
||||||
|
|
||||||
if (this.isAdd) {
|
if (this.isAdd) {
|
||||||
|
// Get current date/time in local timezone
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourLater = new Date(now.getTime() + 60 * 60000);
|
||||||
|
|
||||||
|
const formatDateTime = (date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(date.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
|
||||||
this.maintenance = {
|
this.maintenance = {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
@ -578,7 +700,7 @@ export default {
|
|||||||
cron: "30 3 * * *",
|
cron: "30 3 * * *",
|
||||||
durationMinutes: 60,
|
durationMinutes: 60,
|
||||||
intervalDay: 1,
|
intervalDay: 1,
|
||||||
dateRange: [],
|
dateRange: [formatDateTime(now), formatDateTime(oneHourLater)],
|
||||||
timeRange: [
|
timeRange: [
|
||||||
{
|
{
|
||||||
hours: 2,
|
hours: 2,
|
||||||
@ -591,7 +713,7 @@ export default {
|
|||||||
],
|
],
|
||||||
weekdays: [],
|
weekdays: [],
|
||||||
daysOfMonth: [],
|
daysOfMonth: [],
|
||||||
timezoneOption: null,
|
timezoneOption: "SAME_AS_SERVER",
|
||||||
};
|
};
|
||||||
} else if (this.isEdit || this.isClone) {
|
} else if (this.isEdit || this.isClone) {
|
||||||
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
||||||
@ -655,6 +777,30 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set quick duration for single maintenance
|
||||||
|
* Calculates end time based on start time + duration in minutes
|
||||||
|
* @param {number} minutes Duration in minutes
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
setQuickDuration(minutes) {
|
||||||
|
if (!this.maintenance.dateRange[0]) {
|
||||||
|
this.$root.toastError(this.$t("Please set start time first"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDate = new Date(this.maintenance.dateRange[0]);
|
||||||
|
const endDate = new Date(startDate.getTime() + minutes * 60000);
|
||||||
|
|
||||||
|
const year = endDate.getFullYear();
|
||||||
|
const month = String(endDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(endDate.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(endDate.getHours()).padStart(2, "0");
|
||||||
|
const mins = String(endDate.getMinutes()).padStart(2, "0");
|
||||||
|
|
||||||
|
this.maintenance.dateRange[1] = `${year}-${month}-${day}T${hours}:${mins}`;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle form submission - show confirmation if no monitors selected
|
* Handle form submission - show confirmation if no monitors selected
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
|
|||||||
@ -1514,6 +1514,89 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
monitor.type === 'http' ||
|
||||||
|
monitor.type === 'keyword' ||
|
||||||
|
monitor.type === 'json-query'
|
||||||
|
"
|
||||||
|
class="my-3"
|
||||||
|
>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="saveErrorResponse"
|
||||||
|
v-model="monitor.saveErrorResponse"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="saveErrorResponse">
|
||||||
|
{{ $t("saveErrorResponseForNotifications") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
<i18n-t keypath="saveResponseDescription" tag="div" class="form-text">
|
||||||
|
<template #templateVariable>
|
||||||
|
<code>heartbeatJSON.response</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(monitor.type === 'http' ||
|
||||||
|
monitor.type === 'keyword' ||
|
||||||
|
monitor.type === 'json-query') &&
|
||||||
|
monitor.saveErrorResponse
|
||||||
|
"
|
||||||
|
class="my-3"
|
||||||
|
>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="saveResponse"
|
||||||
|
v-model="monitor.saveResponse"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="saveResponse">
|
||||||
|
{{ $t("saveResponseForNotifications") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
<i18n-t keypath="saveResponseDescription" tag="div" class="form-text">
|
||||||
|
<template #templateVariable>
|
||||||
|
<code>heartbeatJSON.response</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(monitor.type === 'http' ||
|
||||||
|
monitor.type === 'keyword' ||
|
||||||
|
monitor.type === 'json-query') &&
|
||||||
|
(monitor.saveResponse || monitor.saveErrorResponse)
|
||||||
|
"
|
||||||
|
class="my-3"
|
||||||
|
>
|
||||||
|
<label for="responseMaxLength" class="form-label">
|
||||||
|
{{ $t("responseMaxLength") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="responseMaxLength"
|
||||||
|
v-model="monitor.responseMaxLength"
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("responseMaxLengthDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="acceptedStatusCodes" class="form-label">
|
<label for="acceptedStatusCodes" class="form-label">
|
||||||
{{ $t("Accepted Status Codes") }}
|
{{ $t("Accepted Status Codes") }}
|
||||||
@ -2184,6 +2267,9 @@ const monitorDefaults = {
|
|||||||
domainExpiryNotification: true,
|
domainExpiryNotification: true,
|
||||||
maxredirects: 10,
|
maxredirects: 10,
|
||||||
accepted_statuscodes: ["200-299"],
|
accepted_statuscodes: ["200-299"],
|
||||||
|
saveResponse: false,
|
||||||
|
saveErrorResponse: true,
|
||||||
|
responseMaxLength: 1024,
|
||||||
dns_resolve_type: "A",
|
dns_resolve_type: "A",
|
||||||
dns_resolve_server: "1.1.1.1",
|
dns_resolve_server: "1.1.1.1",
|
||||||
docker_container: "",
|
docker_container: "",
|
||||||
|
|||||||
@ -342,14 +342,20 @@
|
|||||||
|
|
||||||
<!-- Incident Date -->
|
<!-- Incident Date -->
|
||||||
<div class="date mt-3">
|
<div class="date mt-3">
|
||||||
{{ $t("Date Created") }}: {{ $root.datetime(incident.createdDate) }} ({{
|
{{
|
||||||
dateFromNow(incident.createdDate)
|
$t("dateCreatedAtFromNow", {
|
||||||
}})
|
date: $root.datetime(incident.createdDate),
|
||||||
|
fromNow: dateFromNow(incident.createdDate),
|
||||||
|
})
|
||||||
|
}}
|
||||||
<br />
|
<br />
|
||||||
<span v-if="incident.lastUpdatedDate">
|
<span v-if="incident.lastUpdatedDate">
|
||||||
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{
|
{{
|
||||||
dateFromNow(incident.lastUpdatedDate)
|
$t("lastUpdatedAtFromNow", {
|
||||||
}})
|
date: $root.datetime(incident.lastUpdatedDate),
|
||||||
|
fromNow: dateFromNow(incident.lastUpdatedDate),
|
||||||
|
})
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -572,9 +578,9 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="refresh-info mb-2">
|
<div class="refresh-info mb-2">
|
||||||
<div>{{ $t("Last Updated") }}: {{ lastUpdateTimeDisplay }}</div>
|
<div>{{ $t("lastUpdatedAt", { date: lastUpdateTimeDisplay }) }}</div>
|
||||||
<div data-testid="update-countdown-text">
|
<div data-testid="update-countdown-text">
|
||||||
{{ $tc("statusPageRefreshIn", [updateCountdownText]) }}
|
{{ $t("statusPageRefreshIn", [updateCountdownText]) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -10,8 +10,8 @@
|
|||||||
*/
|
*/
|
||||||
var _a;
|
var _a;
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = 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.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.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.CONSOLE_STYLE_FgViolet = 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.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 = 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 = void 0;
|
||||||
const dayjs_1 = require("dayjs");
|
const dayjs_1 = require("dayjs");
|
||||||
const jsonata = require("jsonata");
|
const jsonata = require("jsonata");
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
@ -43,6 +43,8 @@ exports.PING_COUNT_DEFAULT = 1;
|
|||||||
exports.PING_PER_REQUEST_TIMEOUT_MIN = 1;
|
exports.PING_PER_REQUEST_TIMEOUT_MIN = 1;
|
||||||
exports.PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
exports.PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
||||||
exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
|
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_Reset = "\x1b[0m";
|
||||||
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
|
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
|
||||||
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
|
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
|
||||||
|
|||||||
11
src/util.ts
11
src/util.ts
@ -66,6 +66,17 @@ export const PING_PER_REQUEST_TIMEOUT_MIN = 1;
|
|||||||
export const PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
export const PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
||||||
export const PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
|
export const PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response body length cutoff used by default (10kb)
|
||||||
|
* (measured in bytes)
|
||||||
|
*/
|
||||||
|
export const RESPONSE_BODY_LENGTH_DEFAULT = 1024;
|
||||||
|
/**
|
||||||
|
* Maximum allowed response body length to store (1mb)
|
||||||
|
* (measured in bytes)
|
||||||
|
*/
|
||||||
|
export const RESPONSE_BODY_LENGTH_MAX = 1024 * 1024;
|
||||||
|
|
||||||
// Console colors
|
// Console colors
|
||||||
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
|
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
|
||||||
export const CONSOLE_STYLE_Reset = "\x1b[0m";
|
export const CONSOLE_STYLE_Reset = "\x1b[0m";
|
||||||
|
|||||||
@ -96,58 +96,26 @@ describe("Domain Expiry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Domain Parsing", () => {
|
describe("Domain Parsing", () => {
|
||||||
test("throws error for invalid domain (no domain part)", async () => {
|
test("throws error for IP address (isIp check)", async () => {
|
||||||
const monitor = {
|
const monitor = {
|
||||||
type: "http",
|
type: "http",
|
||||||
url: "https://",
|
url: "https://127.0.0.1",
|
||||||
domainExpiryNotification: true,
|
domainExpiryNotification: true,
|
||||||
};
|
};
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
async () => await DomainExpiry.checkSupport(monitor),
|
async () => await DomainExpiry.checkSupport(monitor),
|
||||||
(error) => {
|
(error) => {
|
||||||
assert.strictEqual(error.constructor.name, "TranslatableError");
|
assert.strictEqual(error.constructor.name, "TranslatableError");
|
||||||
assert.strictEqual(error.message, "domain_expiry_unsupported_invalid_domain");
|
assert.strictEqual(error.message, "domain_expiry_unsupported_is_ip");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws error for IPv4 address instead of domain", async () => {
|
test("throws error for too short suffix(example.a)", async () => {
|
||||||
const monitor = {
|
const monitor = {
|
||||||
type: "http",
|
type: "http",
|
||||||
url: "https://192.168.1.1",
|
url: "https://example.a",
|
||||||
domainExpiryNotification: true,
|
|
||||||
};
|
|
||||||
await assert.rejects(
|
|
||||||
async () => await DomainExpiry.checkSupport(monitor),
|
|
||||||
(error) => {
|
|
||||||
assert.strictEqual(error.constructor.name, "TranslatableError");
|
|
||||||
assert.strictEqual(error.message, "domain_expiry_unsupported_invalid_domain");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throws error for IPv6 address", async () => {
|
|
||||||
const monitor = {
|
|
||||||
type: "http",
|
|
||||||
url: "https://[2001:db8::1]",
|
|
||||||
domainExpiryNotification: true,
|
|
||||||
};
|
|
||||||
await assert.rejects(
|
|
||||||
async () => await DomainExpiry.checkSupport(monitor),
|
|
||||||
(error) => {
|
|
||||||
assert.strictEqual(error.constructor.name, "TranslatableError");
|
|
||||||
assert.strictEqual(error.message, "domain_expiry_unsupported_invalid_domain");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throws error for single-letter TLD", async () => {
|
|
||||||
const monitor = {
|
|
||||||
type: "http",
|
|
||||||
url: "https://example.x",
|
|
||||||
domainExpiryNotification: true,
|
domainExpiryNotification: true,
|
||||||
};
|
};
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
@ -159,6 +127,22 @@ describe("Domain Expiry", () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("throws error for non-ICANN TLD (e.g. .local)", async () => {
|
||||||
|
const monitor = {
|
||||||
|
type: "http",
|
||||||
|
url: "https://example.local",
|
||||||
|
domainExpiryNotification: true,
|
||||||
|
};
|
||||||
|
await assert.rejects(
|
||||||
|
async () => await DomainExpiry.checkSupport(monitor),
|
||||||
|
(error) => {
|
||||||
|
assert.strictEqual(error.constructor.name, "TranslatableError");
|
||||||
|
assert.strictEqual(error.message, "domain_expiry_unsupported_is_icann");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Edge Cases & RDAP Support", () => {
|
describe("Edge Cases & RDAP Support", () => {
|
||||||
@ -205,22 +189,6 @@ describe("Domain Expiry", () => {
|
|||||||
assert.strictEqual(supportInfo.domain, "example.com");
|
assert.strictEqual(supportInfo.domain, "example.com");
|
||||||
assert.strictEqual(supportInfo.tld, "com");
|
assert.strictEqual(supportInfo.tld, "com");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws error for unsupported TLD without RDAP endpoint", async () => {
|
|
||||||
const monitor = {
|
|
||||||
type: "http",
|
|
||||||
url: "https://example.localhost",
|
|
||||||
domainExpiryNotification: true,
|
|
||||||
};
|
|
||||||
await assert.rejects(
|
|
||||||
async () => await DomainExpiry.checkSupport(monitor),
|
|
||||||
(error) => {
|
|
||||||
assert.strictEqual(error.constructor.name, "TranslatableError");
|
|
||||||
assert.strictEqual(error.message, "domain_expiry_unsupported_unsupported_tld_no_rdap_endpoint");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
36
test/backend-test/test-monitor-response.js
Normal file
36
test/backend-test/test-monitor-response.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const { describe, test } = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const Monitor = require("../../server/model/monitor");
|
||||||
|
const Heartbeat = require("../../server/model/heartbeat");
|
||||||
|
const { RESPONSE_BODY_LENGTH_DEFAULT } = require("../../src/util");
|
||||||
|
|
||||||
|
describe("Monitor response saving", () => {
|
||||||
|
test("getSaveResponse and getSaveErrorResponse parse booleans", () => {
|
||||||
|
const monitor = Object.create(Monitor.prototype);
|
||||||
|
monitor.save_response = 1;
|
||||||
|
monitor.save_error_response = 0;
|
||||||
|
|
||||||
|
assert.strictEqual(monitor.getSaveResponse(), true);
|
||||||
|
assert.strictEqual(monitor.getSaveErrorResponse(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saveResponseData stores and truncates response", async () => {
|
||||||
|
const monitor = Object.create(Monitor.prototype);
|
||||||
|
monitor.response_max_length = 5;
|
||||||
|
|
||||||
|
const bean = {};
|
||||||
|
await monitor.saveResponseData(bean, "abcdef");
|
||||||
|
|
||||||
|
assert.strictEqual(await Heartbeat.decodeResponseValue(bean.response), "abcde... (truncated)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saveResponseData stringifies objects", async () => {
|
||||||
|
const monitor = Object.create(Monitor.prototype);
|
||||||
|
monitor.response_max_length = RESPONSE_BODY_LENGTH_DEFAULT;
|
||||||
|
|
||||||
|
const bean = {};
|
||||||
|
await monitor.saveResponseData(bean, { ok: true });
|
||||||
|
|
||||||
|
assert.strictEqual(await Heartbeat.decodeResponseValue(bean.response), JSON.stringify({ ok: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -105,4 +105,34 @@ test.describe("Monitor Form", () => {
|
|||||||
|
|
||||||
await screenshot(testInfo, page);
|
await screenshot(testInfo, page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("save response settings persist", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await selectMonitorType(page, "http");
|
||||||
|
|
||||||
|
const friendlyName = "Example HTTP Save Response";
|
||||||
|
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||||
|
await page.getByTestId("url-input").fill("https://www.example.com/");
|
||||||
|
|
||||||
|
// Expect error response save enabled by default
|
||||||
|
await expect(page.getByLabel("Save HTTP Error Response for Notifications")).toBeChecked();
|
||||||
|
|
||||||
|
await page.getByLabel("Save HTTP Success Response for Notifications").check();
|
||||||
|
await page.getByLabel("Save HTTP Error Response for Notifications").uncheck();
|
||||||
|
await page.getByLabel("Response Max Length (bytes)").fill("2048");
|
||||||
|
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*");
|
||||||
|
|
||||||
|
await page.getByRole("link", { name: "Edit" }).click();
|
||||||
|
await page.waitForURL("/edit/*");
|
||||||
|
|
||||||
|
await expect(page.getByLabel("Save HTTP Success Response for Notifications")).toBeHidden();
|
||||||
|
await expect(page.getByLabel("Save HTTP Error Response for Notifications")).not.toBeChecked();
|
||||||
|
await expect(page.getByLabel("Response Max Length (bytes)")).toHaveValue("2048");
|
||||||
|
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user