Merge pull request #340 from rajnandan1/release/3.2.2

Release/3.2.2
This commit is contained in:
Raj Nandan Sharma
2025-03-08 22:57:28 +05:30
committed by GitHub
27 changed files with 653 additions and 543 deletions

View File

@@ -77,6 +77,7 @@ Copy and update the translation file in the `locales` folder. The translation fi
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Started %startedAt, will last for %lastedFor more": "Started %startedAt, will last for %lastedFor more",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",

View File

@@ -9,28 +9,29 @@ const databaseType = databaseURLParts[0];
const databasePath = databaseURLParts[1];
const knexOb = {
migrations: {
directory: "./migrations"
},
seeds: {
directory: "./seeds"
}
migrations: {
directory: "./migrations",
},
seeds: {
directory: "./seeds",
},
databaseType,
};
if (databaseType === "sqlite") {
knexOb.client = "better-sqlite3";
knexOb.connection = {
filename: databasePath
};
knexOb.useNullAsDefault = true;
knexOb.client = "better-sqlite3";
knexOb.connection = {
filename: databasePath,
};
knexOb.useNullAsDefault = true;
} else if (databaseType === "postgresql") {
knexOb.client = "pg";
knexOb.connection = databaseURL;
knexOb.client = "pg";
knexOb.connection = databaseURL;
} else if (databaseType === "mysql") {
knexOb.client = "mysql2";
knexOb.connection = databaseURL;
knexOb.client = "mysql2";
knexOb.connection = databaseURL;
} else {
console.error("Invalid database type");
process.exit(1);
console.error("Invalid database type");
process.exit(1);
}
export default knexOb;

View File

@@ -1,6 +1,6 @@
{
"name": "kener",
"version": "3.2.1",
"version": "3.2.2",
"private": false,
"license": "MIT",
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",

View File

@@ -41,7 +41,8 @@
.squares-pattern::after {
content: "";
position: absolute;
background-image: linear-gradient(#444cf7 1px, transparent 1px),
background-image:
linear-gradient(#444cf7 1px, transparent 1px),
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
-webkit-mask-image: linear-gradient(
180deg,
@@ -87,7 +88,8 @@
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-image: linear-gradient(#616fbf 1px, transparent 1px),
background-image:
linear-gradient(#616fbf 1px, transparent 1px),
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
}
@@ -476,7 +478,8 @@ textarea::placeholder {
}
.mesh {
background-image: linear-gradient(
background-image:
linear-gradient(
323deg,
rgba(255, 255, 255, 0.01) 0%,
rgba(255, 255, 255, 0.01) 16.667%,
@@ -607,7 +610,8 @@ textarea::placeholder {
}
.dark .mesh {
background-image: linear-gradient(
background-image:
linear-gradient(
158deg,
rgba(84, 84, 84, 0.03) 0%,
rgba(84, 84, 84, 0.03) 20%,
@@ -821,7 +825,8 @@ textarea::placeholder {
}
.button-77:active:not(:disabled):after {
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
background-image:
linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
linear-gradient(92.83deg, #ff7426 0, #f93a13 100%);
bottom: 4px;
left: 4px;
@@ -872,3 +877,31 @@ textarea::placeholder {
transform: rotate(15deg);
}
}
.dots-animation::after {
content: ".";
animation: dots 1.5s steps(4) infinite;
}
@keyframes dots {
0% {
content: ".";
}
16% {
content: "..";
}
32% {
content: "...";
}
48% {
content: "....";
}
64% {
content: "...";
}
80% {
content: "..";
}
100% {
content: ".";
}
}

View File

@@ -1,16 +1,16 @@
<script>
import { formatDistanceToNow, formatDistance } from "date-fns";
import { Settings } from "lucide-svelte";
import { Settings, ArrowRight } from "lucide-svelte";
import * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import { Tooltip } from "bits-ui";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
export let incident;
export let index;
export let lang;
export let selectedLang = "en";
let startTime = new Date(incident.start_date_time * 1000);
let endTime = new Date();
let nowTime = new Date();
@@ -18,13 +18,17 @@
endTime = new Date(incident.end_date_time * 1000);
}
let incidentType = incident.incident_type;
const lastedFor = fd(startTime, endTime, selectedLang);
const startedAt = fdn(startTime, selectedLang);
const lastedFor = fd(startTime, endTime, $page.data.selectedLang);
const remainingTime = fd(nowTime, endTime, $page.data.selectedLang);
const startedAt = fdn(startTime, $page.data.selectedLang);
let isFuture = false;
//is future incident
let incidentTimeStatus = "";
if (nowTime < startTime) {
isFuture = true;
incidentTimeStatus = "YET_TO_START";
} else if (nowTime >= startTime && nowTime <= endTime) {
incidentTimeStatus = "ONGOING";
} else if (nowTime > endTime) {
incidentTimeStatus = "COMPLETED";
}
let accordionValue = "incident-0";
@@ -37,30 +41,36 @@
let incidentDateSummary = "";
let maintenanceBadge = "";
let maintenanceBadgeColor = "";
if (!isFuture && incident.state != "RESOLVED") {
incidentDateSummary = l(lang, "Started %startedAt, still ongoing", {
if (incidentTimeStatus == "YET_TO_START") {
incidentDateSummary = l($page.data.lang, "Starts %startedAt", { startedAt });
if (incidentType === "MAINTENANCE") {
incidentDateSummary = l($page.data.lang, "Starts %startedAt, will last for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
}
} else if (incidentTimeStatus == "ONGOING") {
incidentDateSummary = l($page.data.lang, "Started %startedAt, still ongoing", {
startedAt
});
maintenanceBadge = "Maintenance in Progress";
maintenanceBadgeColor = "text-maintenance-in-progress";
} else if (!isFuture && incident.state == "RESOLVED") {
incidentDateSummary = l(lang, "Started %startedAt, lasted for %lastedFor", {
if (incidentType === "MAINTENANCE") {
incidentDateSummary = l($page.data.lang, "Started %startedAt, will last for %lastedFor more", {
startedAt,
lastedFor: remainingTime
});
maintenanceBadge = "Maintenance in Progress";
maintenanceBadgeColor = "text-maintenance-in-progress";
}
} else if (incidentTimeStatus == "COMPLETED") {
incidentDateSummary = l($page.data.lang, "Started %startedAt, lasted for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Maintenance Completed";
maintenanceBadgeColor = "text-maintenance-completed";
} else if (isFuture && incident.state != "RESOLVED") {
incidentDateSummary = l(lang, "Starts %startedAt", { startedAt });
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
} else if (isFuture && incident.state == "RESOLVED") {
incidentDateSummary = l(lang, "Starts %startedAt, will last for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
}
</script>
@@ -73,11 +83,11 @@
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
<span class="badge-{incident.state}">
{l(lang, incident.state)}
{l($page.data.lang, incident.state)}
</span>
{:else if incidentType == "MAINTENANCE"}
<span class="{maintenanceBadgeColor} ">
{l(lang, maintenanceBadge)}
{l($page.data.lang, maintenanceBadge)}
</span>
{/if}
{#if $page.data.isLoggedIn}
@@ -91,20 +101,48 @@
{/if}
</p>
<p class="scroll-m-20 text-lg font-medium tracking-tight">
<p class="font-medium">
{incident.title}
</p>
{#if !!incidentDateSummary}
<p class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground">
{incidentDateSummary}
</p>
<Tooltip.Root openDelay={100} side="bottom" align="end">
<Tooltip.Trigger
class=" text-ellipsis whitespace-nowrap text-xs font-medium tracking-normal text-muted-foreground"
>
{incidentDateSummary}
</Tooltip.Trigger>
<Tooltip.Content class=" z-20 mt-11" side="bottom" align="end">
<div
class="items-center justify-center rounded border bg-primary px-1.5 py-1 text-xs font-medium text-primary-foreground shadow-popover"
>
{f(
new Date(incident.start_date_time * 1000),
"MMMM do yyyy, h:mm:ss a",
$page.data.selectedLang,
$page.data.localTz
)}
{#if incident.end_date_time}
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
{f(
new Date(incident.end_date_time * 1000),
"MMMM do yyyy, h:mm:ss a",
$page.data.selectedLang,
$page.data.localTz
)}
{:else}
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
<span class="dots-animation inline-block w-6 text-left"></span>
{/if}
</div>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</div>
</Accordion.Trigger>
<Accordion.Content>
<div class="px-4 pt-2">
{#if incident.monitors.length > 0}
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
{#each incident.monitors as monitor}
<div class="tag-affected-text flex gap-x-2 rounded-md bg-secondary px-1 py-1 pr-2">
<div
@@ -123,7 +161,7 @@
</div>
{/if}
<p class="my-3 text-xs font-semibold uppercase text-muted-foreground">
{l(lang, "Updates")}
{l($page.data.lang, "Updates")}
</p>
{#if incident.comments.length > 0}
{#if incidentType == "INCIDENT"}
@@ -133,14 +171,14 @@
<div
class="absolute top-0 w-28 -translate-x-32 rounded border bg-secondary px-1.5 py-1 text-center text-xs font-semibold"
>
{l(lang, comment.state)}
{l($page.data.lang, comment.state)}
</div>
<time class=" mb-1 text-sm font-medium leading-none text-muted-foreground">
{f(
new Date(comment.commented_at * 1000),
"MMMM do yyyy, h:mm:ss a",
selectedLang,
$page.data.selectedLang,
$page.data.localTz
)}
</time>
@@ -159,7 +197,7 @@
{f(
new Date(comment.commented_at * 1000),
"MMMM do yyyy, h:mm:ss a",
selectedLang,
$page.data.selectedLang,
$page.data.localTz
)}
</time>
@@ -173,7 +211,7 @@
{/if}
{:else}
<p class="text-sm font-medium">
{l(lang, "No Updates Yet")}
{l($page.data.lang, "No Updates Yet")}
</p>
{/if}
</div>

View File

@@ -149,6 +149,14 @@
}
}
}
if (newTrigger.trigger_meta.has_webhook_body) {
if (newTrigger.trigger_meta.webhook_body == "") {
invalidFormMessage = "Webhook Body is required";
formState = "idle";
return;
}
}
}
//newTrigger.name present not empty
if (newTrigger.name == "") {
@@ -423,7 +431,7 @@
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</div>

View File

@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
"Started %startedAt, will last for %lastedFor more": "Begann vor etwa %startedAt und wird noch etwa %lastedFor dauern",
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
"Starts %startedAt": "Beginnt in %startedAt",
"Status OK": "Status OK",

View File

@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
"Started %startedAt, will last for %lastedFor more": "Startede for cirka %startedAt siden, vil vare i cirka %lastedFor mere",
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
"Starts %startedAt": "Starter om %startedAt",
"Status OK": "Status OK",

View File

@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Started %startedAt, will last for %lastedFor more": "Started %startedAt, will last for %lastedFor more",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",

View File

@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
"Started %startedAt, will last for %lastedFor more": "Commencé il y a environ %startedAt, durera environ %lastedFor de plus",
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
"Starts %startedAt": "Commence dans %startedAt",
"Status OK": "Statut OK",

View File

@@ -48,6 +48,7 @@
"Standard": "मानक",
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
"Started %startedAt, will last for %lastedFor more": "%startedAt शुरू होगा, %lastedFor और चलेगा",
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
"Starts %startedAt": "%startedAt में शुरू होगा",
"Status OK": "स्थिति ठीक है",

View File

@@ -48,6 +48,7 @@
"Standard": "標準",
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
"Started %startedAt, will last for %lastedFor more": "%startedAt 前に開始し、さらに %lastedFor 続きます",
"Starts %startedAt, will last for %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
"Starts %startedAt": "%startedAt 後に開始",
"Status OK": "ステータス正常",

View File

@@ -48,6 +48,7 @@
"Standard": "기본",
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
"Started %startedAt, will last for %lastedFor more": "%startedAt 전에 시작되었으며, %lastedFor 더 지속될 예정입니다",
"Starts %startedAt, will last for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
"Starts %startedAt": "%startedAt 에 시작되었습니다",
"Status OK": "양호",

View File

@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
"Started %startedAt, will last for %lastedFor more": "Startet %startedAt, vil vare i %lastedFor til",
"Starts %startedAt, will last for %lastedFor": "Starter %startedAt, vil vare i %lastedFor",
"Starts %startedAt": "Starter %startedAt",
"Status OK": "Status OK",

View File

@@ -48,6 +48,7 @@
"Standard": "Standaard",
"Started %startedAt, lasted for %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
"Started %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
"Started %startedAt, will last for %lastedFor more": "Begint %startedAt, zal nog ongeveer %lastedFor duren",
"Starts %startedAt, will last for %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
"Starts %startedAt": "Begint over %startedAt",
"Status OK": "Status OK",

View File

@@ -48,6 +48,7 @@
"Standard": "Padrão",
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
"Started %startedAt, will last for %lastedFor more": "Iniciará em %startedAt, durará mais %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Terá início em %startedAt, com duração de %lastedFor",
"Starts %startedAt": "Inicia em %startedAt",
"Status OK": "Status OK",

View File

@@ -48,6 +48,7 @@
"Standard": "Стандартная",
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
"Started %startedAt, will last for %lastedFor more": "Начнется около %startedAt, продлится еще около %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
"Starts %startedAt": "Начнется через %startedAt",
"Status OK": "Статус ОК",

View File

@@ -48,6 +48,7 @@
"Standard": "Standart",
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
"Started %startedAt, will last for %lastedFor more": "%startedAt başladı, %lastedFor daha sürecek",
"Starts %startedAt, will last for %lastedFor": "%startedAt başlıyor, %lastedFor sürecek",
"Starts %startedAt": "%startedAt başlıyor",
"Status OK": "Durum UYGUN",

View File

@@ -48,6 +48,7 @@
"Standard": "Tiêu chuẩn",
"Started %startedAt, lasted for %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
"Started %startedAt, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
"Started %startedAt, will last for %lastedFor more": "Bắt đầu khoảng %startedAt trước, sẽ kéo dài thêm khoảng %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
"Starts %startedAt": "Bắt đầu trong %startedAt",
"Status OK": "Trạng thái OK",

View File

@@ -48,6 +48,7 @@
"Standard": "标准",
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
"Started %startedAt, will last for %lastedFor more": "大约 %startedAt 前开始,将持续约 %lastedFor",
"Starts %startedAt, will last for %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
"Starts %startedAt": "%startedAt 后开始",
"Status OK": "状态正常",

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
import { GetMinuteStartNowTimestampUTC, GetDbType } from "../tool.js";
import { MANUAL, SIGNAL } from "../constants.js";
import Knex from "knex";
@@ -458,20 +458,39 @@ class DbImpl {
}
async createIncident(data) {
const [incident] = await this.knex("incidents")
.insert({
title: data.title,
start_date_time: data.start_date_time,
end_date_time: data.end_date_time,
status: data.status,
state: data.state,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
incident_type: data.incident_type,
incident_source: data.incident_source,
})
.returning("*");
return incident;
const dbType = GetDbType(); // sqlite, postgresql, mysql
// Common insert data
const insertData = {
title: data.title,
start_date_time: data.start_date_time,
end_date_time: data.end_date_time,
status: data.status,
state: data.state,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
incident_type: data.incident_type,
incident_source: data.incident_source,
};
// PostgreSQL supports returning clause
if (dbType === "postgresql") {
const [incident] = await this.knex("incidents").insert(insertData).returning("*");
return incident;
}
// MySQL and SQLite need different approaches
else {
// Insert and get the ID
const result = await this.knex("incidents").insert(insertData);
// Different handling for MySQL vs SQLite
let id = result[0];
// Fetch the newly created record
const incident = await this.knex("incidents").where("id", id).first();
return incident;
}
}
//get incidents paginated

View File

@@ -1,93 +1,99 @@
// @ts-nocheck
class Discord {
url;
headers;
method;
siteData;
monitorData;
url;
headers;
method;
siteData;
monitorData;
constructor(url, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener"
};
constructor(url, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener",
};
this.url = url;
this.headers = Object.assign(kenerHeader, {});
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
}
this.url = url;
this.headers = Object.assign(kenerHeader, {});
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
}
transformData(data) {
let siteURL = this.siteData.siteURL;
let logo =
this.siteData.siteURL +
(!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "") +
this.siteData.logo;
transformData(data) {
let siteURL = this.siteData.siteURL;
let color = 13250616; //down;
if (data.severity === "warning") {
color = 15125089;
}
if (data.status === "RESOLVED") {
color = 5156244;
}
return {
username: this.siteData.siteName,
avatar_url: logo,
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
embeds: [
{
title: data.alert_name,
description: data.description,
url: data.actions[0].url,
color: color,
fields: [
{
name: "Monitor",
value: data.details.metric,
inline: false
},
{
name: "Alert ID",
value: data.id,
inline: false
},
{
name: "Current Value",
value: data.details.current_value,
inline: true
},
{
name: "Threshold",
value: data.details.threshold,
inline: true
}
],
footer: {
text: "Kener",
icon_url: logo
},
timestamp: data.timestamp
}
]
};
}
//remove / if there at the end of the url
if (siteURL.endsWith("/")) {
siteURL = siteURL.slice(0, -1);
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.transformData(data))
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
if (!this.siteData.logo.startsWith("http"))
this.siteData.logo = `${siteURL}${!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : ""}${this.siteData.logo}`;
let logo = this.siteData.logo;
let color = 13250616; //down;
if (data.severity === "warning") {
color = 15125089;
}
if (data.status === "RESOLVED") {
color = 5156244;
}
return {
username: this.siteData.siteName,
avatar_url: logo,
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
embeds: [
{
title: data.alert_name,
description: data.description,
url: data.actions[0].url,
color: color,
fields: [
{
name: "Monitor",
value: data.details.metric,
inline: false,
},
{
name: "Alert ID",
value: data.id,
inline: false,
},
{
name: "Current Value",
value: data.details.current_value,
inline: true,
},
{
name: "Threshold",
value: data.details.threshold,
inline: true,
},
],
footer: {
text: "Kener",
icon_url: logo,
},
timestamp: data.timestamp,
},
],
};
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.transformData(data)),
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
}
export default Discord;

View File

@@ -3,92 +3,90 @@
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
class Webhook {
url;
headers;
method;
siteData;
monitorData;
trigger_meta;
url;
headers;
method;
siteData;
monitorData;
trigger_meta;
constructor(trigger_meta, method, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener/3.0.0"
};
let headers = trigger_meta.headers;
this.trigger_meta = trigger_meta;
this.url = trigger_meta.url;
this.headers = kenerHeader;
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
this.headers[header.key] = header.value;
}
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
constructor(trigger_meta, method, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener/3.2.2",
};
let headers = trigger_meta.headers;
this.trigger_meta = trigger_meta;
this.url = trigger_meta.url;
this.headers = kenerHeader;
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
this.headers[header.key] = header.value;
}
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
let envSecrets = GetRequiredSecrets(
`${this.url} ${JSON.stringify(this.headers)} ${JSON.stringify(this.trigger_meta.webhook_body)}`
);
//replace secrets in url and headers
let envSecrets = GetRequiredSecrets(
`${this.url} ${JSON.stringify(this.headers)} ${JSON.stringify(this.trigger_meta.webhook_body)}`,
);
//replace secrets in url and headers
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.url = ReplaceAllOccurrences(this.url, secret.find, secret.replace);
this.headers = JSON.parse(
ReplaceAllOccurrences(JSON.stringify(this.headers), secret.find, secret.replace)
);
}
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.url = ReplaceAllOccurrences(this.url, secret.find, secret.replace);
this.headers = JSON.parse(ReplaceAllOccurrences(JSON.stringify(this.headers), secret.find, secret.replace));
}
if (!!this.trigger_meta.has_webhook_body && !!this.trigger_meta.webhook_body) {
envSecrets = GetRequiredSecrets(this.trigger_meta.webhook_body);
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.trigger_meta.webhook_body = ReplaceAllOccurrences(
this.trigger_meta.webhook_body,
secret.find,
secret.replace
);
}
}
}
if (!!this.trigger_meta.has_webhook_body && !!this.trigger_meta.webhook_body) {
envSecrets = GetRequiredSecrets(this.trigger_meta.webhook_body);
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.trigger_meta.webhook_body = ReplaceAllOccurrences(
this.trigger_meta.webhook_body,
secret.find,
secret.replace,
);
}
}
}
transformData(data) {
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
let body = this.trigger_meta.webhook_body;
body = ReplaceAllOccurrences(body, "${id}", data.id);
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
body = ReplaceAllOccurrences(body, "${severity}", data.severity);
body = ReplaceAllOccurrences(body, "${status}", data.status);
body = ReplaceAllOccurrences(body, "${source}", data.source);
body = ReplaceAllOccurrences(body, "${timestamp}", data.timestamp);
body = ReplaceAllOccurrences(body, "${description}", data.description);
body = ReplaceAllOccurrences(body, "${metric}", data.details.metric);
body = ReplaceAllOccurrences(body, "${current_value}", data.details.current_value);
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
return body;
}
transformData(data) {
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
let body = this.trigger_meta.webhook_body;
body = ReplaceAllOccurrences(body, "${id}", data.id);
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
body = ReplaceAllOccurrences(body, "${severity}", data.severity);
body = ReplaceAllOccurrences(body, "${status}", data.status);
body = ReplaceAllOccurrences(body, "${source}", data.source);
body = ReplaceAllOccurrences(body, "${timestamp}", data.timestamp);
body = ReplaceAllOccurrences(body, "${description}", data.description);
body = ReplaceAllOccurrences(body, "${metric}", data.details.metric);
body = ReplaceAllOccurrences(body, "${current_value}", data.details.current_value);
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
return body;
}
type() {
return "webhook";
}
type() {
return "webhook";
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.transformData(data)
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.transformData(data),
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
}
export default Webhook;

View File

@@ -1,325 +1,318 @@
// @ts-nocheck
import { AllRecordTypes } from "./constants.js";
import knexOb from "../../../knexfile.js";
import dotenv from "dotenv";
dotenv.config();
const IsValidURL = function (url) {
return /^(http|https):\/\/[^ "]+$/.test(url);
return /^(http|https):\/\/[^ "]+$/.test(url);
};
const IsStringURLSafe = function (str) {
const regex = /^[A-Za-z0-9\-_.~]+$/;
return regex.test(str);
const regex = /^[A-Za-z0-9\-_.~]+$/;
return regex.test(str);
};
const IsValidHTTPMethod = function (method) {
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
};
//return given timestamp in UTC
const GetNowTimestampUTC = function () {
//use js date instead of moment
const now = new Date();
const timestamp = now.getTime();
return Math.floor(timestamp / 1000);
//use js date instead of moment
const now = new Date();
const timestamp = now.getTime();
return Math.floor(timestamp / 1000);
};
//return given timestamp minute start timestamp in UTC
const GetMinuteStartTimestampUTC = function (timestamp) {
//use js date instead of moment
const now = new Date(timestamp * 1000);
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
//use js date instead of moment
const now = new Date(timestamp * 1000);
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0,
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
};
//return current timestamp minute start timestamp in UTC
const GetMinuteStartNowTimestampUTC = function () {
//use js date instead of moment
const now = new Date();
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
//use js date instead of moment
const now = new Date();
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0,
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
};
//return given timestamp day start timestamp in UTC
const GetDayStartTimestampUTC = function (timestamp) {
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayStart = new Date(
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0)
);
const dayStartTimestamp = dayStart.getTime();
return Math.floor(dayStartTimestamp / 1000);
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayStart = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0));
const dayStartTimestamp = dayStart.getTime();
return Math.floor(dayStartTimestamp / 1000);
};
const GetDayEndTimestampUTC = function (timestamp) {
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayEnd = new Date(
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999)
);
const dayEndTimestamp = dayEnd.getTime();
return Math.floor(dayEndTimestamp / 1000) + 60;
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayEnd = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999));
const dayEndTimestamp = dayEnd.getTime();
return Math.floor(dayEndTimestamp / 1000) + 60;
};
const DurationInMinutes = function (start, end) {
return Math.floor((end - start) / 60);
return Math.floor((end - start) / 60);
};
const GetDayStartWithOffset = function (timeStampInSeconds, offsetInMinutes) {
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
//have to figure out when to add a day
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
//have to figure out when to add a day
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
//if xtm - 330 > 1 day , add a day to xtm - 330
//if xtm - 330 > 1 day , add a day to xtm - 330
if (offsetInMinutes < 0) {
//add one day to dayStartThen
dayStartThen = dayStartThen + 24 * 60 * 60;
}
return dayStartThen + offsetInMinutes * 60;
if (offsetInMinutes < 0) {
//add one day to dayStartThen
dayStartThen = dayStartThen + 24 * 60 * 60;
}
return dayStartThen + offsetInMinutes * 60;
};
const BeginningOfDay = (options = {}) => {
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
hour: "numeric",
minute: "numeric",
second: "numeric"
}).formatToParts(date);
const hour = parseInt(parts.find((i) => i.type === "hour").value);
const minute = parseInt(parts.find((i) => i.type === "minute").value);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(
1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000)
);
return dt.getTime() / 1000;
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
hour: "numeric",
minute: "numeric",
second: "numeric",
}).formatToParts(date);
const hour = parseInt(parts.find((i) => i.type === "hour").value);
const minute = parseInt(parts.find((i) => i.type === "minute").value);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000));
return dt.getTime() / 1000;
};
const BeginningOfMinute = (options = {}) => {
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
second: "numeric"
}).formatToParts(date);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(1000 * Math.floor((date - second * 1000) / 1000));
return dt.getTime() / 1000;
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
second: "numeric",
}).formatToParts(date);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(1000 * Math.floor((date - second * 1000) / 1000));
return dt.getTime() / 1000;
};
const ValidateIpAddress = function (input) {
// Check if input is a valid IPv4 address
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IPv4";
}
// Check if input is a valid IPv4 address
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IPv4";
}
// Check if input is a valid IPv6 address
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
if (ipv6Regex.test(input)) {
return "IPv6";
}
// Check if input is a valid IPv6 address
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
if (ipv6Regex.test(input)) {
return "IPv6";
}
// Check if input is a valid domain name
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "Domain Name";
}
// Check if input is a valid domain name
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "Domain Name";
}
// If none of the above conditions match, the input is invalid
return "Invalid";
// If none of the above conditions match, the input is invalid
return "Invalid";
};
function checkIfDuplicateExists(arr) {
return new Set(arr).size !== arr.length;
return new Set(arr).size !== arr.length;
}
function GetWordsStartingWithDollar(text) {
const regex = /\$\w+/g;
const wordsArray = text.match(regex);
return wordsArray || [];
const regex = /\$\w+/g;
const wordsArray = text.match(regex);
return wordsArray || [];
}
const StatusObj = {
UP: "api-up",
DEGRADED: "api-degraded",
DOWN: "api-down",
NO_DATA: "api-nodata"
UP: "api-up",
DEGRADED: "api-degraded",
DOWN: "api-down",
NO_DATA: "api-nodata",
};
// @ts-ignore
const ParseUptime = function (up, all) {
if (all === 0) return String("-");
if (up == 0) return String("0");
if (up == all) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
//return 50% as 50% and not 50.0000%
if (((up / all) * 100) % 10 == 0) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
return String(((up / all) * parseFloat(100)).toFixed(4));
if (all === 0) return String("-");
if (up == 0) return String("0");
if (up == all) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
//return 50% as 50% and not 50.0000%
if (((up / all) * 100) % 10 == 0) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
return String(((up / all) * parseFloat(100)).toFixed(4));
};
const ParsePercentage = function (n) {
if (isNaN(n)) return "-";
if (n == 0) {
return "0";
}
if (n == 100) {
return "100";
}
return n.toFixed(4);
if (isNaN(n)) return "-";
if (n == 0) {
return "0";
}
if (n == 100) {
return "100";
}
return n.toFixed(4);
};
//valid domain name
function IsValidHost(domain) {
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
}
//valid nameserver
function IsValidNameServer(nameServer) {
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
}
//valid dns record type
function IsValidRecordType(recordType) {
return AllRecordTypes.hasOwnProperty(recordType);
return AllRecordTypes.hasOwnProperty(recordType);
}
function ReplaceAllOccurrences(originalString, searchString, replacement) {
const regex = new RegExp(`\\${searchString}`, "g");
const replacedString = originalString.replace(regex, replacement);
return replacedString;
const regex = new RegExp(`\\${searchString}`, "g");
const replacedString = originalString.replace(regex, replacement);
return replacedString;
}
function GetRequiredSecrets(str) {
let envSecrets = [];
const requiredSecrets = GetWordsStartingWithDollar(str).map((x) => x.substr(1));
for (const [key, value] of Object.entries(process.env)) {
if (requiredSecrets.indexOf(key) !== -1) {
envSecrets.push({
find: `$${key}`,
replace: value
});
}
}
return envSecrets;
let envSecrets = [];
const requiredSecrets = GetWordsStartingWithDollar(str).map((x) => x.substr(1));
for (const [key, value] of Object.entries(process.env)) {
if (requiredSecrets.indexOf(key) !== -1) {
envSecrets.push({
find: `$${key}`,
replace: value,
});
}
}
return envSecrets;
}
function ValidateMonitorAlerts(alerts) {
if (!alerts) {
console.log("Alerts object is not provided.");
return false;
}
// if down degraded not present return false
if (!alerts.hasOwnProperty("DOWN") && !alerts.hasOwnProperty("DEGRADED")) {
console.log("Alerts object does not have DOWN or DEGRADED properties.");
return false;
}
let statues = Object.keys(alerts);
//can be either DOWN or DEGRADED
let validKeys = ["DOWN", "DEGRADED"];
for (let i = 0; i < statues.length; i++) {
const keyName = statues[i];
if (!validKeys.includes(keyName)) {
console.log(`Invalid key found in alerts: ${keyName}`);
return false;
}
}
for (const key in alerts) {
if (Object.prototype.hasOwnProperty.call(alerts, key)) {
const element = alerts[key];
const triggers = element.triggers;
//if triggers not present return false
if (!element.hasOwnProperty("triggers")) {
console.log(`Triggers not present for key: ${key}`);
return false;
}
//if length of triggers is 0 return false
if (triggers.length === 0) {
console.log(`Triggers length is 0 for key: ${key}`);
return false;
}
if (
!element.hasOwnProperty("failureThreshold") ||
!element.hasOwnProperty("successThreshold")
) {
console.log(`Thresholds not present for key: ${key}`);
return false;
}
if (
element.hasOwnProperty("failureThreshold") &&
!Number.isInteger(element.failureThreshold)
) {
console.log(`Failure threshold is not an integer for key: ${key}`);
return false;
}
if (
element.hasOwnProperty("successThreshold") &&
!Number.isInteger(element.successThreshold)
) {
console.log(`Success threshold is not an integer for key: ${key}`);
return false;
}
if (element.hasOwnProperty("failureThreshold") && element.failureThreshold <= 0) {
console.log(`Failure threshold is less than or equal to 0 for key: ${key}`);
return false;
}
if (element.hasOwnProperty("successThreshold") && element.successThreshold <= 0) {
console.log(`Success threshold is less than or equal to 0 for key: ${key}`);
return false;
}
}
}
return true;
if (!alerts) {
console.log("Alerts object is not provided.");
return false;
}
// if down degraded not present return false
if (!alerts.hasOwnProperty("DOWN") && !alerts.hasOwnProperty("DEGRADED")) {
console.log("Alerts object does not have DOWN or DEGRADED properties.");
return false;
}
let statues = Object.keys(alerts);
//can be either DOWN or DEGRADED
let validKeys = ["DOWN", "DEGRADED"];
for (let i = 0; i < statues.length; i++) {
const keyName = statues[i];
if (!validKeys.includes(keyName)) {
console.log(`Invalid key found in alerts: ${keyName}`);
return false;
}
}
for (const key in alerts) {
if (Object.prototype.hasOwnProperty.call(alerts, key)) {
const element = alerts[key];
const triggers = element.triggers;
//if triggers not present return false
if (!element.hasOwnProperty("triggers")) {
console.log(`Triggers not present for key: ${key}`);
return false;
}
//if length of triggers is 0 return false
if (triggers.length === 0) {
console.log(`Triggers length is 0 for key: ${key}`);
return false;
}
if (!element.hasOwnProperty("failureThreshold") || !element.hasOwnProperty("successThreshold")) {
console.log(`Thresholds not present for key: ${key}`);
return false;
}
if (element.hasOwnProperty("failureThreshold") && !Number.isInteger(element.failureThreshold)) {
console.log(`Failure threshold is not an integer for key: ${key}`);
return false;
}
if (element.hasOwnProperty("successThreshold") && !Number.isInteger(element.successThreshold)) {
console.log(`Success threshold is not an integer for key: ${key}`);
return false;
}
if (element.hasOwnProperty("failureThreshold") && element.failureThreshold <= 0) {
console.log(`Failure threshold is less than or equal to 0 for key: ${key}`);
return false;
}
if (element.hasOwnProperty("successThreshold") && element.successThreshold <= 0) {
console.log(`Success threshold is less than or equal to 0 for key: ${key}`);
return false;
}
}
}
return true;
}
function GenerateRandomColor() {
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
return randomColor;
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
return randomColor;
}
//wait for x ms promise
function Wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
function MaskString(str) {
return "*".repeat(str.length - 4) + str.slice(-4);
return "*".repeat(str.length - 4) + str.slice(-4);
}
function GetDbType() {
//sqlite, postgresql, mysql
return knexOb.databaseType;
}
export {
IsValidURL,
IsValidHTTPMethod,
GetMinuteStartTimestampUTC,
GetNowTimestampUTC,
GetDayStartTimestampUTC,
GetMinuteStartNowTimestampUTC,
DurationInMinutes,
GetDayStartWithOffset,
BeginningOfDay,
IsStringURLSafe,
ValidateIpAddress,
checkIfDuplicateExists,
GetWordsStartingWithDollar,
StatusObj,
ParseUptime,
ParsePercentage,
IsValidHost,
IsValidRecordType,
IsValidNameServer,
ReplaceAllOccurrences,
GetRequiredSecrets,
ValidateMonitorAlerts,
GenerateRandomColor,
BeginningOfMinute,
Wait,
MaskString
IsValidURL,
IsValidHTTPMethod,
GetMinuteStartTimestampUTC,
GetNowTimestampUTC,
GetDayStartTimestampUTC,
GetMinuteStartNowTimestampUTC,
DurationInMinutes,
GetDayStartWithOffset,
BeginningOfDay,
IsStringURLSafe,
ValidateIpAddress,
checkIfDuplicateExists,
GetWordsStartingWithDollar,
StatusObj,
ParseUptime,
ParsePercentage,
IsValidHost,
IsValidRecordType,
IsValidNameServer,
ReplaceAllOccurrences,
GetRequiredSecrets,
ValidateMonitorAlerts,
GenerateRandomColor,
BeginningOfMinute,
Wait,
MaskString,
GetDbType,
};

View File

@@ -110,7 +110,7 @@
<!-- Document Icon - Replace with your own logo -->
<img src="https://kener.ing/logo.png" class="h-8 w-8" alt="" />
<span class="text-xl font-medium">Kener Documentation</span>
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.2.1 </span>
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.2.2 </span>
</a>
</div>

View File

@@ -633,7 +633,6 @@
variant="ghost"
class="h-5 w-5 p-1"
size="icon"
disabled={incident.isAutoCreated}
on:click={(e) => {
showEditModal(incident);
}}

View File

@@ -2,67 +2,67 @@ import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
plugins: [
require("@tailwindcss/typography")
// ...
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
}
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
},
fontFamily: {
sans: [...fontFamily.sans]
}
}
}
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
plugins: [
require("@tailwindcss/typography"),
// ...
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: [...fontFamily.sans],
},
},
},
};
export default config;