mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 07:14:56 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d475ab23f | |||
| 01cb39e18e | |||
| 0d0ef25970 | |||
| 6c766e2001 | |||
| 1c73166120 | |||
| fc5dbc85dc | |||
| 622327cdf6 | |||
| b0a4cd5c42 | |||
| 4e5fb26be1 | |||
| 6fc80bc0fc | |||
| 292667ac29 | |||
| 8c03058f8d | |||
| 25e73d097c |
@@ -33,6 +33,11 @@ trim_trailing_whitespace = false
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# CSS & PostCSS files
|
||||
[*.{css,postcss}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Markdown files
|
||||
[*.md]
|
||||
indent_style = space
|
||||
|
||||
+4
-3
@@ -57,9 +57,10 @@ COPY . .
|
||||
|
||||
# TODO: Reevaluate permissions (possibly reduce?)...
|
||||
# Remove docs directory and ensure required directories exist
|
||||
RUN rm -rf src/routes/\(docs\) && \
|
||||
rm -rf static/documentation && \
|
||||
mkdir -p uploads database && \
|
||||
RUN rm -rf src/routes/\(docs\) \
|
||||
static/documentation \
|
||||
static/fonts/lato/full && \
|
||||
mkdir -p uploads database && \
|
||||
# TODO: Consider changing below to `chmod -R u-rwX,g=rX,o= uploads database`
|
||||
chmod -R 750 uploads database
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ npm run dev
|
||||
|
||||
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
|
||||
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.4"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.1.4-blue" alt="Kener latest stable version: 3.1.4" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.6"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.1.6-blue" alt="Kener latest stable version: 3.1.6" /></a>
|
||||
|
||||
#### Available Tags
|
||||
|
||||
@@ -93,38 +93,38 @@ Official Docker images for **Kener** are available on [Docker Hub](https://hub.d
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
|
||||
<td>Latest stable release (aka 3.1.4)</td>
|
||||
<td>Latest stable release (aka 3.1.6)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.4" target="_blank"><code>3.1.4</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.6" target="_blank"><code>3.1.6</code></a></td>
|
||||
<td>Specific release version</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1" target="_blank"><code>3.1</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.1.4) release within that minor version (3.1.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.1.6) release within that minor version (3.1.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
|
||||
<td>Major version tag pointing to the latest stable (3.1.4) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.1.6) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 <strong><em>(smallest image size)</em></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
|
||||
<td>Latest stable release (aka 3.1.4)</td>
|
||||
<td>Latest stable release (aka 3.1.6)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.4-alpine" target="_blank"><code>3.1.4-alpine</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.6-alpine" target="_blank"><code>3.1.6-alpine</code></a></td>
|
||||
<td>Specific release version</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1-alpine" target="_blank"><code>3.1-alpine</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.1.4) release within that minor version (3.1.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.1.6) release within that minor version (3.1.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
|
||||
<td>Major version tag pointing to the latest stable (3.1.4) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.1.6) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -93,10 +93,24 @@ export SMTP_SECURE=0
|
||||
export SMTP_FROM_EMAIL=Some Name <user@example.com>
|
||||
```
|
||||
|
||||
<div class=" note info ">
|
||||
|
||||
Generally, port 465 expects implicit SSL (SMTP_SECURE: 1), while port 587 and port 25 usually use STARTTLS (SMTP_SECURE: 0).
|
||||
|
||||
So, if you are using SMTP on port 465, set SMTP_SECURE: 1; otherwise, for ports 25 or 587, set SMTP_SECURE: 0.
|
||||
|
||||
</div>
|
||||
|
||||
<div class=" note danger ">
|
||||
If you set SMTP variables, Kener will use SMTP to send emails. If you set RESEND_API_KEY, Kener will use resend to send emails. If you do both Kener will use SMTP.
|
||||
</div>
|
||||
|
||||
<div class=" note info ">
|
||||
|
||||
If your SMTP provider does require username and password, you can set `SMTP_USER` and `SMTP_PASS` to `-`.
|
||||
|
||||
</div>
|
||||
|
||||
## Using .env
|
||||
|
||||
You can also use a `.env` file to set these variables. Create a `.env` file in the root of the project and add the variables like below
|
||||
|
||||
@@ -216,6 +216,12 @@ Since the password will be stored as plain text we encourage to use environment
|
||||
|
||||
</div>
|
||||
|
||||
<div class=" note info ">
|
||||
|
||||
If your SMTP provider does require username and password, you can set `SMTP_USER` and `SMTP_PASS` to `-`.
|
||||
|
||||
</div>
|
||||
|
||||
### To
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.1.5",
|
||||
"version": "3.1.7",
|
||||
"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.",
|
||||
|
||||
+62
-60
@@ -1,88 +1,90 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss/base";
|
||||
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background-kener: hsl(0, 0%, 100%);
|
||||
--background-kener-rgba: rgba(255, 255, 255, 0.5);
|
||||
--foreground: 240 10% 4%;
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background-kener: hsl(0, 0%, 100%);
|
||||
--background-kener-rgba: rgba(255, 255, 255, 0.5);
|
||||
|
||||
--muted: 0 0% 92.9%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--foreground: 240 10% 4%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 4%;
|
||||
--muted: 0 0% 92.9%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--card: 0 0% 99%;
|
||||
--card-foreground: 240 10% 4%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 4%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--card: 0 0% 99%;
|
||||
--card-foreground: 240 10% 4%;
|
||||
|
||||
--date-picker-background: hsl(0 0% 99%);
|
||||
--date-picker-foreground: hsl(240 10% 4%);
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--date-picker-background: hsl(0 0% 99%);
|
||||
--date-picker-foreground: hsl(240 10% 4%);
|
||||
|
||||
--secondary: 0 0% 92.9%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 0 0% 92.9%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--ring: 240 10% 4%;
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
--ring: 240 10% 4%;
|
||||
|
||||
.dark {
|
||||
--background: 240 7% 11%;
|
||||
--background-kener: hsl(240 7% 11%);
|
||||
--background-kener-rgba: rgba(30, 30, 26, 0.35);
|
||||
--foreground: 60 10% 86%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 0 0% 40%;
|
||||
.dark {
|
||||
--background: 240 7% 11%;
|
||||
--background-kener: hsl(240 7% 11%);
|
||||
--background-kener-rgba: rgba(30, 30, 26, 0.35);
|
||||
--foreground: 60 10% 86%;
|
||||
|
||||
--popover: 230 9% 14%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 0 0% 40%;
|
||||
|
||||
--card: 230 9% 14%;
|
||||
--card-foreground: 60 10% 86%;
|
||||
--popover: 230 9% 14%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--border: 60 1% 17%;
|
||||
--input: 60 6% 17%;
|
||||
--card: 230 9% 14%;
|
||||
--card-foreground: 60 10% 86%;
|
||||
|
||||
--date-picker-background: hsl(240 4% 16%);
|
||||
--date-picker-foreground: hsl(60 10% 86%);
|
||||
--border: 60 1% 17%;
|
||||
--input: 60 6% 17%;
|
||||
|
||||
--primary: 60 10% 86%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--date-picker-background: hsl(240 4% 16%);
|
||||
--date-picker-foreground: hsl(60 10% 86%);
|
||||
|
||||
--secondary: 240 1% 18%;
|
||||
--secondary-foreground: 60 10% 86%;
|
||||
--primary: 60 10% 86%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 240 4% 16%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--secondary: 240 1% 18%;
|
||||
--secondary-foreground: 60 10% 86%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--accent: 240 4% 16%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--ring: hsl(212.7, 26.8%, 83.9);
|
||||
}
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: hsl(212.7, 26.8%, 83.9);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-hairline.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-hairline.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-hairline.ttf") format("truetype");
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-hairline-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-hairline-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-hairline-italic.ttf") format("truetype");
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-light.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-light.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-light.ttf") format("truetype");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-light-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-light-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-light-italic.ttf") format("truetype");
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-regular.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-regular.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-regular.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-regular-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-regular-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-regular-italic.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-bold.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-bold.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-bold.ttf") format("truetype");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-bold-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-bold-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-bold-italic.ttf") format("truetype");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-black.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-black.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-black.ttf") format("truetype");
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-black-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-black-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-black-italic.ttf") format("truetype");
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -27,6 +27,13 @@
|
||||
isFuture = true;
|
||||
}
|
||||
|
||||
let accordionValue = "incident-0";
|
||||
if ($page.data.site.incidentGroupView == "COLLAPSED") {
|
||||
accordionValue = "incident-collapse";
|
||||
} else if ($page.data.site.incidentGroupView == "EXPANDED") {
|
||||
accordionValue = index;
|
||||
}
|
||||
|
||||
let incidentDateSummary = "";
|
||||
let maintenanceBadge = "";
|
||||
let maintenanceBadgeColor = "";
|
||||
@@ -60,7 +67,7 @@
|
||||
<div class="newincident relative grid w-full grid-cols-12 gap-2 px-0 py-0 last:border-b-0">
|
||||
<div class="col-span-12">
|
||||
<Accordion.Root bind:value={index} class="accor">
|
||||
<Accordion.Item value="incident-0">
|
||||
<Accordion.Item value={accordionValue}>
|
||||
<Accordion.Trigger class="px-4 hover:bg-muted hover:no-underline">
|
||||
<div class="w-full text-left hover:no-underline">
|
||||
<p class="flex gap-x-2 text-xs font-semibold">
|
||||
@@ -133,9 +140,9 @@
|
||||
{f(new Date(comment.commented_at * 1000), "MMMM do yyyy, h:mm:ss a", selectedLang)}
|
||||
</time>
|
||||
|
||||
<p class="mb-4 text-sm font-normal">
|
||||
{comment.comment}
|
||||
</p>
|
||||
<div class="mb-4 text-sm font-normal">
|
||||
{@html comment.comment}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
let footerHTML = "";
|
||||
let homeIncidentCount = 10;
|
||||
let homeIncidentStartTimeWithin = 30;
|
||||
let incidentGroupView = "EXPAND_FIRST";
|
||||
|
||||
let nav = [];
|
||||
let categories = [];
|
||||
let i18n = {
|
||||
@@ -61,6 +63,9 @@
|
||||
if (data.siteData.homeIncidentStartTimeWithin) {
|
||||
homeIncidentStartTimeWithin = data.siteData.homeIncidentStartTimeWithin;
|
||||
}
|
||||
if (data.siteData.incidentGroupView) {
|
||||
incidentGroupView = data.siteData.incidentGroupView;
|
||||
}
|
||||
if (data.siteData.footerHTML) {
|
||||
footerHTML = data.siteData.footerHTML;
|
||||
}
|
||||
@@ -138,7 +143,8 @@
|
||||
formStateIncident = "loading";
|
||||
let resp = await storeSiteData({
|
||||
homeIncidentCount: homeIncidentCount,
|
||||
homeIncidentStartTimeWithin: homeIncidentStartTimeWithin
|
||||
homeIncidentStartTimeWithin: homeIncidentStartTimeWithin,
|
||||
incidentGroupView: incidentGroupView
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
@@ -298,7 +304,7 @@
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitIncident}>
|
||||
<div class="flex max-w-md flex-row justify-start gap-2">
|
||||
<div class="flex flex-row justify-start gap-2">
|
||||
<div class="">
|
||||
<Label for="hero_title">Maximum Number to Show</Label>
|
||||
<Input bind:value={homeIncidentCount} class="mt-2" type="text" id="homeIncidentCount" placeholder="10" />
|
||||
@@ -313,6 +319,33 @@
|
||||
placeholder="2 days"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="incidentGroupView" class="text-sm font-medium">Incident Group View</Label>
|
||||
<Select.Root
|
||||
portal={null}
|
||||
onSelectedChange={(e) => {
|
||||
incidentGroupView = e.value;
|
||||
}}
|
||||
selected={{
|
||||
value: incidentGroupView,
|
||||
label: incidentGroupView.replace("_", " ")
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class=" mt-2 w-[200px]" id="incidentGroupView">
|
||||
<Select.Value bind:value={incidentGroupView} placeholder="Favicon Type" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Favicon Type</Select.Label>
|
||||
<Select.Item value="EXPAND_FIRST" label="EXPAND FIRST" class="text-sm font-medium"
|
||||
>EXPAND FIRST</Select.Item
|
||||
>
|
||||
<Select.Item value="COLLAPSED" label="COLLAPSED" class="text-sm font-medium">COLLAPSED</Select.Item>
|
||||
<Select.Item value="EXPANDED" label="EXPANDED" class="text-sm font-medium">EXPANDED</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<Button type="submit" disabled={formStateIncident === "loading"}>
|
||||
Save
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Settings, Bell, Loader, ArrowDownUp, Grip } from "lucide-svelte";
|
||||
import { Plus, X, Settings, Bell, Loader, ArrowDownUp, Grip, ExternalLink } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { base } from "$app/paths";
|
||||
@@ -469,6 +469,9 @@
|
||||
>
|
||||
<Bell class="inline h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" rel="external" href="{base}/?monitor={monitor.tag}">
|
||||
<ExternalLink class="inline h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" href="#{monitor.tag}">
|
||||
<Settings class="inline h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
+161
-174
@@ -3,12 +3,12 @@ import notification from "./notification/notif.js";
|
||||
|
||||
import moment from "moment";
|
||||
import {
|
||||
GetAllSiteData,
|
||||
GetAllTriggers,
|
||||
CreateIncident,
|
||||
AddIncidentComment,
|
||||
AddIncidentMonitor,
|
||||
InsertNewAlert
|
||||
GetAllSiteData,
|
||||
GetAllTriggers,
|
||||
CreateIncident,
|
||||
AddIncidentComment,
|
||||
AddIncidentMonitor,
|
||||
InsertNewAlert,
|
||||
} from "./controllers/controller.js";
|
||||
|
||||
import db from "./db/db.js";
|
||||
@@ -17,197 +17,184 @@ const TRIGGERED = "TRIGGERED";
|
||||
const RESOLVED = "RESOLVED";
|
||||
|
||||
async function createJSONCommonAlert(monitor, config, alert, severity) {
|
||||
let siteData = await GetAllSiteData();
|
||||
let siteURL = siteData.siteURL;
|
||||
let id = monitor.tag + "-" + alert.id;
|
||||
let alert_name = monitor.name + " " + alert.monitor_status;
|
||||
let source = "Kener";
|
||||
let timestamp = new Date().toISOString();
|
||||
let description = config.description || "Monitor has failed";
|
||||
let status = alert.alert_status;
|
||||
let details = {
|
||||
metric: monitor.name,
|
||||
current_value: alert.health_checks,
|
||||
threshold: config.failureThreshold
|
||||
};
|
||||
let actions = [
|
||||
{
|
||||
text: "View Monitor",
|
||||
url: siteURL + "?monitor=" + monitor.tag
|
||||
}
|
||||
];
|
||||
return {
|
||||
id,
|
||||
alert_name,
|
||||
severity,
|
||||
status,
|
||||
source,
|
||||
timestamp,
|
||||
description,
|
||||
details,
|
||||
actions
|
||||
};
|
||||
let siteData = await GetAllSiteData();
|
||||
let siteURL = siteData.siteURL;
|
||||
let id = monitor.tag + "-" + alert.id;
|
||||
let alert_name = monitor.name + " " + alert.monitor_status;
|
||||
let source = "Kener";
|
||||
let timestamp = new Date().toISOString();
|
||||
let description = config.description || "Monitor has failed";
|
||||
let status = alert.alert_status;
|
||||
let details = {
|
||||
metric: monitor.name,
|
||||
current_value: alert.health_checks,
|
||||
threshold: config.failureThreshold,
|
||||
};
|
||||
let actions = [
|
||||
{
|
||||
text: "View Monitor",
|
||||
url: siteURL + "?monitor=" + monitor.tag,
|
||||
},
|
||||
];
|
||||
return {
|
||||
id,
|
||||
alert_name,
|
||||
severity,
|
||||
status,
|
||||
source,
|
||||
timestamp,
|
||||
description,
|
||||
details,
|
||||
actions,
|
||||
};
|
||||
}
|
||||
|
||||
async function createNewIncident(monitor, alert, commonData) {
|
||||
let startDateTime = moment(alert.created_at).unix();
|
||||
let payload = {
|
||||
start_date_time: startDateTime,
|
||||
title: commonData.alert_name
|
||||
};
|
||||
let startDateTime = moment(alert.created_at).unix();
|
||||
let payload = {
|
||||
start_date_time: startDateTime,
|
||||
title: commonData.alert_name,
|
||||
};
|
||||
|
||||
let update = commonData.description;
|
||||
update =
|
||||
update +
|
||||
`. Monitor Name: ${monitor.name}, Incident Status: ${commonData.status}, Severity: ${commonData.severity}, Monitor Status: ${alert.monitor_status}, Monitor Health Checks: ${alert.health_checks}, Monitor Failure Threshold: ${commonData.details.threshold}, Visit - ${commonData.actions[0].url}`;
|
||||
let update = commonData.description;
|
||||
update =
|
||||
update +
|
||||
`<div style="line-height: 1.5;">
|
||||
<p><strong>Monitor Name:</strong> ${monitor.name}</p>
|
||||
<p><strong>Incident Status:</strong> ${commonData.status}</p>
|
||||
<p><strong>Severity:</strong> ${commonData.severity}</p>
|
||||
<p><strong>Monitor Status:</strong> ${alert.monitor_status}</p>
|
||||
<p><strong>Monitor Health Checks:</strong> ${alert.health_checks}</p>
|
||||
<p><strong>Monitor Failure Threshold:</strong> ${commonData.details.threshold}</p>
|
||||
<p><strong>Visit:</strong> <a href="${commonData.actions[0].url}">${commonData.actions[0].url}</a></p>
|
||||
</div>`;
|
||||
|
||||
let newIncident = await CreateIncident(payload);
|
||||
let newIncident = await CreateIncident(payload);
|
||||
|
||||
//add update to incident
|
||||
await AddIncidentComment(newIncident.incident_id, update, "INVESTIGATING", startDateTime);
|
||||
//add update to incident
|
||||
await AddIncidentComment(newIncident.incident_id, update, "INVESTIGATING", startDateTime);
|
||||
|
||||
//add monitor to incident
|
||||
await AddIncidentMonitor(newIncident.incident_id, monitor.tag, alert.monitor_status);
|
||||
//add monitor to incident
|
||||
await AddIncidentMonitor(newIncident.incident_id, monitor.tag, alert.monitor_status);
|
||||
|
||||
return newIncident;
|
||||
return newIncident;
|
||||
}
|
||||
|
||||
async function closeIncident(alert, comment) {
|
||||
let incident_id = alert.incident_number;
|
||||
return await AddIncidentComment(
|
||||
incident_id,
|
||||
comment,
|
||||
"RESOLVED",
|
||||
moment(alert.updated_at).unix()
|
||||
);
|
||||
let incident_id = alert.incident_number;
|
||||
return await AddIncidentComment(incident_id, comment, "RESOLVED", moment(alert.updated_at).unix());
|
||||
}
|
||||
|
||||
function createClosureComment(alert, commonJSON) {
|
||||
let comment = "The incident has been auto resolved";
|
||||
let downtimeDuration = moment(alert.updated_at).diff(moment(alert.created_at), "minutes");
|
||||
comment = comment + `, Total downtime: ` + downtimeDuration + ` minutes`;
|
||||
return comment;
|
||||
let comment = "The incident has been auto resolved";
|
||||
let downtimeDuration = moment(alert.updated_at).diff(moment(alert.created_at), "minutes");
|
||||
comment = comment + `, Total downtime: ` + downtimeDuration + ` minutes`;
|
||||
return comment;
|
||||
}
|
||||
|
||||
async function alerting(m) {
|
||||
let monitor = await db.getMonitorByTag(m.tag);
|
||||
let siteData = await GetAllSiteData();
|
||||
const triggers = await GetAllTriggers({
|
||||
status: "ACTIVE"
|
||||
});
|
||||
const triggerObj = {};
|
||||
if (!!monitor.down_trigger) {
|
||||
triggerObj.down_trigger = JSON.parse(monitor.down_trigger);
|
||||
}
|
||||
if (!!monitor.degraded_trigger) {
|
||||
triggerObj.degraded_trigger = JSON.parse(monitor.degraded_trigger);
|
||||
}
|
||||
let monitor = await db.getMonitorByTag(m.tag);
|
||||
let siteData = await GetAllSiteData();
|
||||
const triggers = await GetAllTriggers({
|
||||
status: "ACTIVE",
|
||||
});
|
||||
const triggerObj = {};
|
||||
if (!!monitor.down_trigger) {
|
||||
triggerObj.down_trigger = JSON.parse(monitor.down_trigger);
|
||||
}
|
||||
if (!!monitor.degraded_trigger) {
|
||||
triggerObj.degraded_trigger = JSON.parse(monitor.degraded_trigger);
|
||||
}
|
||||
|
||||
for (const key in triggerObj) {
|
||||
if (Object.prototype.hasOwnProperty.call(triggerObj, key)) {
|
||||
const alertConfig = triggerObj[key];
|
||||
const monitor_status = alertConfig.trigger_type;
|
||||
for (const key in triggerObj) {
|
||||
if (Object.prototype.hasOwnProperty.call(triggerObj, key)) {
|
||||
const alertConfig = triggerObj[key];
|
||||
const monitor_status = alertConfig.trigger_type;
|
||||
|
||||
const failureThreshold = alertConfig.failureThreshold;
|
||||
const successThreshold = alertConfig.successThreshold;
|
||||
const monitor_tag = monitor.tag;
|
||||
const alertingChannels = alertConfig.triggers; //array of numbers of trigger ids
|
||||
const createIncident = alertConfig.createIncident === "YES";
|
||||
const allMonitorClients = [];
|
||||
const sendTrigger = alertConfig.active;
|
||||
const severity = alertConfig.severity;
|
||||
if (!sendTrigger) {
|
||||
continue;
|
||||
}
|
||||
if (alertingChannels.length > 0) {
|
||||
for (let i = 0; i < alertingChannels.length; i++) {
|
||||
const triggerID = alertingChannels[i];
|
||||
const trigger = triggers.find((c) => c.id === triggerID);
|
||||
if (!trigger) {
|
||||
console.error(
|
||||
`Triggers ${triggerID} not found in server triggers for monitor ${monitor_tag}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (trigger.trigger_status !== "ACTIVE") {
|
||||
console.error(`Triggers ${triggerID} is not active`);
|
||||
continue;
|
||||
}
|
||||
const notificationClient = new notification(trigger, siteData, monitor);
|
||||
allMonitorClients.push(notificationClient);
|
||||
}
|
||||
}
|
||||
const failureThreshold = alertConfig.failureThreshold;
|
||||
const successThreshold = alertConfig.successThreshold;
|
||||
const monitor_tag = monitor.tag;
|
||||
const alertingChannels = alertConfig.triggers; //array of numbers of trigger ids
|
||||
const createIncident = alertConfig.createIncident === "YES";
|
||||
const allMonitorClients = [];
|
||||
const sendTrigger = alertConfig.active;
|
||||
const severity = alertConfig.severity;
|
||||
if (!sendTrigger) {
|
||||
continue;
|
||||
}
|
||||
if (alertingChannels.length > 0) {
|
||||
for (let i = 0; i < alertingChannels.length; i++) {
|
||||
const triggerID = alertingChannels[i];
|
||||
const trigger = triggers.find((c) => c.id === triggerID);
|
||||
if (!trigger) {
|
||||
console.error(`Triggers ${triggerID} not found in server triggers for monitor ${monitor_tag}`);
|
||||
continue;
|
||||
}
|
||||
if (trigger.trigger_status !== "ACTIVE") {
|
||||
console.error(`Triggers ${triggerID} is not active`);
|
||||
continue;
|
||||
}
|
||||
const notificationClient = new notification(trigger, siteData, monitor);
|
||||
allMonitorClients.push(notificationClient);
|
||||
}
|
||||
}
|
||||
|
||||
let isAffected = await db.consecutivelyStatusFor(
|
||||
monitor_tag,
|
||||
monitor_status,
|
||||
failureThreshold
|
||||
);
|
||||
let alertExists = await db.alertExists(monitor_tag, monitor_status, TRIGGERED);
|
||||
let activeAlert = null;
|
||||
if (alertExists) {
|
||||
activeAlert = await db.getActiveAlert(monitor_tag, monitor_status, TRIGGERED);
|
||||
}
|
||||
if (isAffected && !alertExists) {
|
||||
activeAlert = await InsertNewAlert({
|
||||
monitor_tag: monitor_tag,
|
||||
monitor_status: monitor_status,
|
||||
alert_status: TRIGGERED,
|
||||
health_checks: failureThreshold
|
||||
});
|
||||
let commonJSON = await createJSONCommonAlert(
|
||||
monitor,
|
||||
alertConfig,
|
||||
activeAlert,
|
||||
severity
|
||||
);
|
||||
let isAffected = await db.consecutivelyStatusFor(monitor_tag, monitor_status, failureThreshold);
|
||||
let alertExists = await db.alertExists(monitor_tag, monitor_status, TRIGGERED);
|
||||
let activeAlert = null;
|
||||
if (alertExists) {
|
||||
activeAlert = await db.getActiveAlert(monitor_tag, monitor_status, TRIGGERED);
|
||||
}
|
||||
if (isAffected && !alertExists) {
|
||||
activeAlert = await InsertNewAlert({
|
||||
monitor_tag: monitor_tag,
|
||||
monitor_status: monitor_status,
|
||||
alert_status: TRIGGERED,
|
||||
health_checks: failureThreshold,
|
||||
});
|
||||
let commonJSON = await createJSONCommonAlert(monitor, alertConfig, activeAlert, severity);
|
||||
|
||||
if (allMonitorClients.length > 0) {
|
||||
for (let i = 0; i < allMonitorClients.length; i++) {
|
||||
const client = allMonitorClients[i];
|
||||
client.send(commonJSON);
|
||||
}
|
||||
}
|
||||
if (createIncident) {
|
||||
let incident = await createNewIncident(monitor, activeAlert, commonJSON);
|
||||
if (allMonitorClients.length > 0) {
|
||||
for (let i = 0; i < allMonitorClients.length; i++) {
|
||||
const client = allMonitorClients[i];
|
||||
client.send(commonJSON);
|
||||
}
|
||||
}
|
||||
if (createIncident) {
|
||||
let incident = await createNewIncident(monitor, activeAlert, commonJSON);
|
||||
|
||||
if (!!incident) {
|
||||
//send incident to incident channel
|
||||
await db.addIncidentNumberToAlert(activeAlert.id, incident.incident_id);
|
||||
}
|
||||
}
|
||||
} else if (isAffected && alertExists) {
|
||||
await db.incrementAlertHealthChecks(activeAlert.id);
|
||||
} else if (!isAffected && alertExists) {
|
||||
let isUp = await db.consecutivelyStatusFor(monitor_tag, "UP", successThreshold);
|
||||
if (isUp) {
|
||||
await db.updateAlertStatus(activeAlert.id, RESOLVED);
|
||||
activeAlert.alert_status = RESOLVED;
|
||||
let commonJSON = await createJSONCommonAlert(
|
||||
monitor,
|
||||
alertConfig,
|
||||
activeAlert,
|
||||
severity
|
||||
);
|
||||
if (allMonitorClients.length > 0) {
|
||||
for (let i = 0; i < allMonitorClients.length; i++) {
|
||||
const client = allMonitorClients[i];
|
||||
client.send(commonJSON);
|
||||
}
|
||||
}
|
||||
if (!!activeAlert.incident_number) {
|
||||
let comment = createClosureComment(activeAlert, commonJSON);
|
||||
if (!!incident) {
|
||||
//send incident to incident channel
|
||||
await db.addIncidentNumberToAlert(activeAlert.id, incident.incident_id);
|
||||
}
|
||||
}
|
||||
} else if (isAffected && alertExists) {
|
||||
await db.incrementAlertHealthChecks(activeAlert.id);
|
||||
} else if (!isAffected && alertExists) {
|
||||
let isUp = await db.consecutivelyStatusFor(monitor_tag, "UP", successThreshold);
|
||||
if (isUp) {
|
||||
await db.updateAlertStatus(activeAlert.id, RESOLVED);
|
||||
activeAlert.alert_status = RESOLVED;
|
||||
let commonJSON = await createJSONCommonAlert(monitor, alertConfig, activeAlert, severity);
|
||||
if (allMonitorClients.length > 0) {
|
||||
for (let i = 0; i < allMonitorClients.length; i++) {
|
||||
const client = allMonitorClients[i];
|
||||
client.send(commonJSON);
|
||||
}
|
||||
}
|
||||
if (!!activeAlert.incident_number) {
|
||||
let comment = createClosureComment(activeAlert, commonJSON);
|
||||
|
||||
try {
|
||||
await closeIncident(activeAlert, comment);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
await closeIncident(activeAlert, comment);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default alerting;
|
||||
|
||||
@@ -101,6 +101,7 @@ const REALTIME = "realtime";
|
||||
const TIMEOUT = "timeout";
|
||||
const ERROR = "error";
|
||||
const MANUAL = "manual";
|
||||
const WEBHOOK = "webhook";
|
||||
|
||||
export {
|
||||
MONITOR,
|
||||
@@ -117,4 +118,5 @@ export {
|
||||
ERROR,
|
||||
MANUAL,
|
||||
NO_DATA,
|
||||
WEBHOOK,
|
||||
};
|
||||
|
||||
@@ -154,6 +154,11 @@ const siteDataKeys = [
|
||||
isValid: (value) => parseInt(value) >= 1,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "incidentGroupView",
|
||||
isValid: (value) => typeof value === "string" && value.trim().length > 0,
|
||||
data_type: "string",
|
||||
},
|
||||
];
|
||||
|
||||
export function InsertKeyValue(key, value) {
|
||||
@@ -505,7 +510,7 @@ export const CreateIncident = async (data) => {
|
||||
|
||||
let newIncident = await db.createIncident(incident);
|
||||
return {
|
||||
incident_id: newIncident[0],
|
||||
incident_id: newIncident.id,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -681,6 +686,7 @@ export const GetIncidentsDashboard = async (data) => {
|
||||
|
||||
for (let i = 0; i < incidents.length; i++) {
|
||||
incidents[i].monitors = await GetIncidentMonitors(incidents[i].id);
|
||||
incidents[i].isAutoCreated = await db.alertExistsIncident(incidents[i].id);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import axios from "axios";
|
||||
import { Ping, ExtractIPv6HostAndPort, TCP } from "./ping.js";
|
||||
import { UP, DOWN, DEGRADED } from "./constants.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "./constants.js";
|
||||
import Service from "./services/service.js";
|
||||
import { GetMinuteStartNowTimestampUTC, ReplaceAllOccurrences, GetRequiredSecrets, Wait } from "./tool.js";
|
||||
|
||||
@@ -15,11 +15,6 @@ import DNSResolver from "./dns.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const REALTIME = "realtime";
|
||||
const TIMEOUT = "timeout";
|
||||
const ERROR = "error";
|
||||
const MANUAL = "manual";
|
||||
|
||||
const alertingQueue = new Queue({
|
||||
concurrency: 10, // Number of tasks that can run concurrently
|
||||
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
|
||||
|
||||
+28
-12
@@ -1,5 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
|
||||
import { MANUAL } from "../constants.js";
|
||||
import Knex from "knex";
|
||||
|
||||
class DbImpl {
|
||||
@@ -116,19 +117,20 @@ class DbImpl {
|
||||
qb.select("*")
|
||||
.from("monitoring_data")
|
||||
.where("monitor_tag", monitor_tag)
|
||||
.andWhere("type", "!=", MANUAL)
|
||||
.orderBy("timestamp", "desc")
|
||||
.limit(lastX);
|
||||
})
|
||||
.select(
|
||||
this.knex.raw(
|
||||
"CASE WHEN COUNT(*) <= SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) THEN 1 ELSE 0 END as isAffected",
|
||||
"CASE WHEN COUNT(*) <= SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) THEN 1 ELSE 0 END as is_affected",
|
||||
[status],
|
||||
),
|
||||
)
|
||||
.from("last_records")
|
||||
.first();
|
||||
|
||||
return result.isAffected === 1;
|
||||
return result.is_affected === 1;
|
||||
}
|
||||
|
||||
//insert alert
|
||||
@@ -141,6 +143,17 @@ class DbImpl {
|
||||
});
|
||||
}
|
||||
|
||||
//given incident_number check if there is an alert
|
||||
async alertExistsIncident(incident_number) {
|
||||
const result = await this.knex("monitor_alerts")
|
||||
.count("* as count")
|
||||
.where({
|
||||
incident_number,
|
||||
})
|
||||
.first();
|
||||
return result.count > 0;
|
||||
}
|
||||
|
||||
//check if alert exists given monitor_tag, monitor_status, trigger_status
|
||||
async alertExists(monitor_tag, monitor_status, alert_status) {
|
||||
const result = await this.knex("monitor_alerts")
|
||||
@@ -425,16 +438,19 @@ class DbImpl {
|
||||
}
|
||||
|
||||
async createIncident(data) {
|
||||
return 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,
|
||||
});
|
||||
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,
|
||||
})
|
||||
.returning("*");
|
||||
return incident;
|
||||
}
|
||||
|
||||
//get incidents paginated
|
||||
|
||||
@@ -3,24 +3,33 @@ import nodemailer from "nodemailer";
|
||||
import { HashString } from "../controllers/controller.js";
|
||||
const transports = {};
|
||||
export default function getSMTPTransport(meta) {
|
||||
//convert meta to string and generate has id
|
||||
//convert meta to string and generate has id
|
||||
|
||||
let transportId = "smtp_" + HashString(JSON.stringify(meta));
|
||||
let transportId = "smtp_" + HashString(JSON.stringify(meta));
|
||||
|
||||
if (!!transports[transportId]) {
|
||||
return transports[transportId];
|
||||
}
|
||||
if (!!transports[transportId]) {
|
||||
return transports[transportId];
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: meta.smtp_host,
|
||||
port: Number(meta.smtp_port) || 587,
|
||||
secure: meta.smtp_secure, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: meta.smtp_user,
|
||||
pass: meta.smtp_pass
|
||||
}
|
||||
});
|
||||
transports[transportId] = transporter;
|
||||
let options = {
|
||||
host: meta.smtp_host,
|
||||
port: Number(meta.smtp_port) || 587,
|
||||
secure: meta.smtp_secure, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: meta.smtp_user,
|
||||
pass: meta.smtp_pass,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: !!meta.smtp_secure, // false if smtp_secure is 0
|
||||
},
|
||||
};
|
||||
|
||||
return transporter;
|
||||
if (meta.smtp_user === "-" || meta.smtp_pass === "-") {
|
||||
delete options.auth;
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(options);
|
||||
transports[transportId] = transporter;
|
||||
|
||||
return transporter;
|
||||
}
|
||||
|
||||
+126
-125
@@ -1,154 +1,155 @@
|
||||
// @ts-nocheck
|
||||
import {
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
GetMinuteStartTimestampUTC,
|
||||
ParseUptime,
|
||||
GetDayStartTimestampUTC
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
GetMinuteStartTimestampUTC,
|
||||
ParseUptime,
|
||||
GetDayStartTimestampUTC,
|
||||
} from "./tool.js";
|
||||
import db from "./db/db.js";
|
||||
import { WEBHOOK } from "./constants.js";
|
||||
|
||||
import { GetMonitors, VerifyAPIKey } from "./controllers/controller.js";
|
||||
|
||||
const GetAllTags = async function () {
|
||||
let tags = [];
|
||||
let monitors = [];
|
||||
try {
|
||||
monitors = await GetMonitors({
|
||||
status: "ACTIVE"
|
||||
});
|
||||
tags = monitors.map((monitor) => monitor.tag);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
return tags;
|
||||
let tags = [];
|
||||
let monitors = [];
|
||||
try {
|
||||
monitors = await GetMonitors({
|
||||
status: "ACTIVE",
|
||||
});
|
||||
tags = monitors.map((monitor) => monitor.tag);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
return tags;
|
||||
};
|
||||
const CheckIfValidTag = async function (tag) {
|
||||
let tags = [];
|
||||
try {
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!!!monitor) {
|
||||
throw new Error("not a valid tag");
|
||||
}
|
||||
if (monitor.status != "ACTIVE") {
|
||||
throw new Error("monitor is not active");
|
||||
}
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
let tags = [];
|
||||
try {
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!!!monitor) {
|
||||
throw new Error("not a valid tag");
|
||||
}
|
||||
if (monitor.status != "ACTIVE") {
|
||||
throw new Error("monitor is not active");
|
||||
}
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auth = async function (request) {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
const authToken = authHeader?.replace("Bearer ", "");
|
||||
const authHeader = request.headers.get("authorization");
|
||||
const authToken = authHeader?.replace("Bearer ", "");
|
||||
|
||||
if ((await VerifyAPIKey(authToken)) === false) {
|
||||
return new Error("invalid token");
|
||||
}
|
||||
if ((await VerifyAPIKey(authToken)) === false) {
|
||||
return new Error("invalid token");
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
};
|
||||
const store = async function (data) {
|
||||
const tag = data.tag;
|
||||
//remove Bearer from start in authHeader
|
||||
const tag = data.tag;
|
||||
//remove Bearer from start in authHeader
|
||||
|
||||
const resp = {};
|
||||
if (data.status === undefined || ["UP", "DOWN", "DEGRADED"].indexOf(data.status) === -1) {
|
||||
return { error: "status missing", status: 400 };
|
||||
}
|
||||
if (data.latency === undefined || isNaN(data.latency)) {
|
||||
return { error: "latency missing or not a number", status: 400 };
|
||||
}
|
||||
if (data.timestampInSeconds !== undefined && isNaN(data.timestampInSeconds)) {
|
||||
return { error: "timestampInSeconds not a number", status: 400 };
|
||||
}
|
||||
if (data.timestampInSeconds === undefined) {
|
||||
data.timestampInSeconds = GetNowTimestampUTC();
|
||||
}
|
||||
data.timestampInSeconds = GetMinuteStartTimestampUTC(data.timestampInSeconds);
|
||||
resp.status = data.status;
|
||||
resp.latency = data.latency;
|
||||
resp.type = "webhook";
|
||||
let timestamp = GetMinuteStartNowTimestampUTC();
|
||||
try {
|
||||
//throw error if timestamp is future or older than 90days
|
||||
if (data.timestampInSeconds > timestamp) {
|
||||
throw new Error("timestampInSeconds is in future");
|
||||
}
|
||||
//past 90 days only
|
||||
if (timestamp - data.timestampInSeconds > 90 * 24 * 60 * 60) {
|
||||
throw new Error("timestampInSeconds is older than 90days");
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: err.message, status: 400 };
|
||||
}
|
||||
//check if tag is valid
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!!!monitor) {
|
||||
return { error: "no monitor with tag found", status: 400 };
|
||||
}
|
||||
if (monitor.status != "ACTIVE") {
|
||||
return { error: "monitor with the given tag is not active", status: 400 };
|
||||
}
|
||||
const resp = {};
|
||||
if (data.status === undefined || ["UP", "DOWN", "DEGRADED"].indexOf(data.status) === -1) {
|
||||
return { error: "status missing", status: 400 };
|
||||
}
|
||||
if (data.latency === undefined || isNaN(data.latency)) {
|
||||
return { error: "latency missing or not a number", status: 400 };
|
||||
}
|
||||
if (data.timestampInSeconds !== undefined && isNaN(data.timestampInSeconds)) {
|
||||
return { error: "timestampInSeconds not a number", status: 400 };
|
||||
}
|
||||
if (data.timestampInSeconds === undefined) {
|
||||
data.timestampInSeconds = GetNowTimestampUTC();
|
||||
}
|
||||
data.timestampInSeconds = GetMinuteStartTimestampUTC(data.timestampInSeconds);
|
||||
resp.status = data.status;
|
||||
resp.latency = data.latency;
|
||||
resp.type = WEBHOOK;
|
||||
let timestamp = GetMinuteStartNowTimestampUTC();
|
||||
try {
|
||||
//throw error if timestamp is future or older than 90days
|
||||
if (data.timestampInSeconds > timestamp) {
|
||||
throw new Error("timestampInSeconds is in future");
|
||||
}
|
||||
//past 90 days only
|
||||
if (timestamp - data.timestampInSeconds > 90 * 24 * 60 * 60) {
|
||||
throw new Error("timestampInSeconds is older than 90days");
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: err.message, status: 400 };
|
||||
}
|
||||
//check if tag is valid
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!!!monitor) {
|
||||
return { error: "no monitor with tag found", status: 400 };
|
||||
}
|
||||
if (monitor.status != "ACTIVE") {
|
||||
return { error: "monitor with the given tag is not active", status: 400 };
|
||||
}
|
||||
|
||||
//get the monitor object matching the tag
|
||||
//get the monitor object matching the tag
|
||||
|
||||
await db.insertMonitoringData({
|
||||
monitor_tag: tag,
|
||||
timestamp: data.timestampInSeconds,
|
||||
status: resp.status,
|
||||
latency: resp.latency,
|
||||
type: "webhook"
|
||||
});
|
||||
return { status: 200, message: "success at " + data.timestampInSeconds };
|
||||
await db.insertMonitoringData({
|
||||
monitor_tag: tag,
|
||||
timestamp: data.timestampInSeconds,
|
||||
status: resp.status,
|
||||
latency: resp.latency,
|
||||
type: "webhook",
|
||||
});
|
||||
return { status: 200, message: "success at " + data.timestampInSeconds };
|
||||
};
|
||||
|
||||
const GetMonitorStatusByTag = async function (tag, timestamp) {
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!!!monitor) {
|
||||
return { error: "no monitor with tag found", status: 400 };
|
||||
}
|
||||
if (monitor.status != "ACTIVE") {
|
||||
return { error: "monitor with the given tag is not active", status: 400 };
|
||||
}
|
||||
const resp = {
|
||||
status: null,
|
||||
uptime: null,
|
||||
last_updated_at: null
|
||||
};
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!!!monitor) {
|
||||
return { error: "no monitor with tag found", status: 400 };
|
||||
}
|
||||
if (monitor.status != "ACTIVE") {
|
||||
return { error: "monitor with the given tag is not active", status: 400 };
|
||||
}
|
||||
const resp = {
|
||||
status: null,
|
||||
uptime: null,
|
||||
last_updated_at: null,
|
||||
};
|
||||
|
||||
const { include_degraded_in_downtime } = monitor;
|
||||
const { include_degraded_in_downtime } = monitor;
|
||||
|
||||
let now = GetMinuteStartNowTimestampUTC();
|
||||
if (timestamp !== null && timestamp !== undefined) {
|
||||
now = timestamp;
|
||||
}
|
||||
let start = GetDayStartTimestampUTC(now);
|
||||
let now = GetMinuteStartNowTimestampUTC();
|
||||
if (timestamp !== null && timestamp !== undefined) {
|
||||
now = timestamp;
|
||||
}
|
||||
let start = GetDayStartTimestampUTC(now);
|
||||
|
||||
let dayDataNew = await db.getMonitoringData(tag, start, now);
|
||||
let ups = 0;
|
||||
let downs = 0;
|
||||
let degradeds = 0;
|
||||
let lastData = dayDataNew[dayDataNew.length - 1];
|
||||
let dayDataNew = await db.getMonitoringData(tag, start, now);
|
||||
let ups = 0;
|
||||
let downs = 0;
|
||||
let degradeds = 0;
|
||||
let lastData = dayDataNew[dayDataNew.length - 1];
|
||||
|
||||
for (let i = 0; i < dayDataNew.length; i++) {
|
||||
let row = dayDataNew[i];
|
||||
if (row.status == "UP") {
|
||||
ups++;
|
||||
} else if (row.status == "DEGRADED") {
|
||||
degradeds++;
|
||||
} else if (row.status == "DOWN") {
|
||||
downs++;
|
||||
}
|
||||
}
|
||||
let numerator = ups + degradeds;
|
||||
if (include_degraded_in_downtime === "YES") {
|
||||
numerator = ups;
|
||||
}
|
||||
for (let i = 0; i < dayDataNew.length; i++) {
|
||||
let row = dayDataNew[i];
|
||||
if (row.status == "UP") {
|
||||
ups++;
|
||||
} else if (row.status == "DEGRADED") {
|
||||
degradeds++;
|
||||
} else if (row.status == "DOWN") {
|
||||
downs++;
|
||||
}
|
||||
}
|
||||
let numerator = ups + degradeds;
|
||||
if (include_degraded_in_downtime === "YES") {
|
||||
numerator = ups;
|
||||
}
|
||||
|
||||
resp.uptime = ParseUptime(numerator, ups + degradeds + downs);
|
||||
resp.last_updated_at = Number(lastData.timestamp);
|
||||
resp.status = lastData.status;
|
||||
return { status: 200, ...resp };
|
||||
resp.uptime = ParseUptime(numerator, ups + degradeds + downs);
|
||||
resp.last_updated_at = Number(lastData.timestamp);
|
||||
resp.status = lastData.status;
|
||||
return { status: 200, ...resp };
|
||||
};
|
||||
export { store, auth, CheckIfValidTag, GetAllTags, GetMonitorStatusByTag };
|
||||
|
||||
@@ -92,7 +92,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.1.5 </span>
|
||||
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.1.7 </span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -72,7 +72,13 @@ export async function load({ parent, url }) {
|
||||
include_degraded_in_downtime: monitor.include_degraded_in_downtime,
|
||||
};
|
||||
})
|
||||
.filter((monitor) => !hiddenGroupedMonitorsTags.includes(monitor.tag));
|
||||
.filter((monitor) => !hiddenGroupedMonitorsTags.includes(monitor.tag))
|
||||
.filter((monitor) => {
|
||||
if (pageType == "home") {
|
||||
return monitor.category_name == "Home";
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
@@ -105,7 +111,7 @@ export async function load({ parent, url }) {
|
||||
let isMonitorPage = pageType === "monitor";
|
||||
let isGroupPage = pageType === "group";
|
||||
|
||||
if (isMonitorPage) {
|
||||
if (isMonitorPage && monitorsActive.length > 0) {
|
||||
pageTitle = monitorsActive[0].name + " - " + pageTitle;
|
||||
pageDescription = monitorsActive[0].description;
|
||||
canonical = canonical + "?monitor=" + monitorsActive[0].tag;
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
})
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
|
||||
incidents = resp.incidents.map((incident) => {
|
||||
let i = { ...incident };
|
||||
if (!!!i.end_date_time) {
|
||||
@@ -632,6 +633,7 @@
|
||||
variant="ghost"
|
||||
class="h-5 w-5 p-1"
|
||||
size="icon"
|
||||
disabled={incident.isAutoCreated}
|
||||
on:click={(e) => {
|
||||
showEditModal(incident);
|
||||
}}
|
||||
@@ -903,7 +905,7 @@
|
||||
{#each comments as comment}
|
||||
<div class="flex items-center justify-between gap-2 border-b py-2">
|
||||
<div class="w-full rounded px-2 py-2 {newComment.id == comment.id ? 'bg-input' : ''}">
|
||||
<p class="mb-2 text-xs font-medium">{comment.comment}</p>
|
||||
<p class="mb-2 text-xs font-medium">{@html comment.comment}</p>
|
||||
<div class="flex w-full justify-between gap-x-2">
|
||||
<div class="text-xs font-semibold text-muted-foreground">
|
||||
{#if currentIncident.incident_type == "INCIDENT"}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user