mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d475ab23f | |||
| 01cb39e18e | |||
| 0d0ef25970 | |||
| 6c766e2001 | |||
| 1c73166120 | |||
| fc5dbc85dc | |||
| 622327cdf6 | |||
| b0a4cd5c42 | |||
| 4e5fb26be1 | |||
| 6fc80bc0fc | |||
| 292667ac29 | |||
| 8c03058f8d | |||
| 25e73d097c | |||
| 1970cd8891 | |||
| 8ae9b2dec1 | |||
| 1662608984 | |||
| 42693983f1 |
@@ -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.3"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.1.3-blue" alt="Kener latest stable version: 3.1.3" /></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.3)</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.3" target="_blank"><code>3.1.3</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.3) 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.3) 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.3)</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.3-alpine" target="_blank"><code>3.1.3-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.3) 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.3) 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.4",
|
||||
"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;
|
||||
|
||||
@@ -8,13 +8,12 @@ const SITE = "./config/site.yaml";
|
||||
const UP = "UP";
|
||||
const DOWN = "DOWN";
|
||||
const DEGRADED = "DEGRADED";
|
||||
const NO_DATA = "NO_DATA";
|
||||
const API_TIMEOUT = 10 * 1000; // 10 seconds
|
||||
const AnalyticsProviders = {
|
||||
GA: "https://unpkg.com/@analytics/google-analytics@1.0.7/dist/@analytics/google-analytics.min.js",
|
||||
AMPLITUDE:
|
||||
"https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js",
|
||||
MIXPANEL:
|
||||
"https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js",
|
||||
AMPLITUDE: "https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js",
|
||||
MIXPANEL: "https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js",
|
||||
};
|
||||
const AllRecordTypes = {
|
||||
A: 1,
|
||||
@@ -102,6 +101,7 @@ const REALTIME = "realtime";
|
||||
const TIMEOUT = "timeout";
|
||||
const ERROR = "error";
|
||||
const MANUAL = "manual";
|
||||
const WEBHOOK = "webhook";
|
||||
|
||||
export {
|
||||
MONITOR,
|
||||
@@ -117,4 +117,6 @@ export {
|
||||
TIMEOUT,
|
||||
ERROR,
|
||||
MANUAL,
|
||||
NO_DATA,
|
||||
WEBHOOK,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
|
||||
import { UP, DOWN, DEGRADED, NO_DATA } from "../constants.js";
|
||||
|
||||
const saltRounds = 10;
|
||||
const DUMMY_SECRET = "DUMMY_SECRET";
|
||||
@@ -153,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) {
|
||||
@@ -199,17 +205,28 @@ export const CreateUpdateMonitor = async (monitor) => {
|
||||
export const GetMonitors = async (data) => {
|
||||
return await db.getMonitors(data);
|
||||
};
|
||||
export const GetMonitorsParsed = async (data) => {
|
||||
let monitors = await db.getMonitors(data);
|
||||
let parsedMonitors = monitors.map((m) => {
|
||||
let parsedMonitor = { ...m };
|
||||
if (!!parsedMonitor.type_data) {
|
||||
parsedMonitor.type_data = JSON.parse(parsedMonitor.type_data);
|
||||
export const GetMonitorsParsed = async (query) => {
|
||||
// Retrieve monitors from the database based on the provided query
|
||||
const rawMonitors = await db.getMonitors(query);
|
||||
|
||||
// Parse type_data if available for each monitor
|
||||
const parsedMonitors = rawMonitors.map((monitor) => {
|
||||
const monitorData = { ...monitor };
|
||||
|
||||
if (monitorData.type_data) {
|
||||
try {
|
||||
monitorData.type_data = JSON.parse(monitorData.type_data);
|
||||
} catch (error) {
|
||||
// Fallback to an empty object if JSON parsing fails
|
||||
monitorData.type_data = {};
|
||||
}
|
||||
} else {
|
||||
parsedMonitor.type_data = {};
|
||||
monitorData.type_data = {};
|
||||
}
|
||||
return parsedMonitor;
|
||||
|
||||
return monitorData;
|
||||
});
|
||||
|
||||
return parsedMonitors;
|
||||
};
|
||||
|
||||
@@ -386,31 +403,32 @@ export const HashString = (str) => {
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
export const InterpolateData = (data, start, anchorStatus, e) => {
|
||||
let finalData = [];
|
||||
let status = anchorStatus || "UP";
|
||||
let end = start;
|
||||
if (!!data && data.length > 0) {
|
||||
end = data[data.length - 1].timestamp;
|
||||
export const InterpolateData = (rawData, startTimestamp, initialStatus, overrideEndTimestamp) => {
|
||||
const interpolatedData = [];
|
||||
let currentStatus = initialStatus || "UP";
|
||||
let endTimestamp = startTimestamp;
|
||||
|
||||
if (rawData && rawData.length > 0) {
|
||||
endTimestamp = rawData[rawData.length - 1].timestamp;
|
||||
}
|
||||
if (overrideEndTimestamp) {
|
||||
endTimestamp = overrideEndTimestamp;
|
||||
}
|
||||
|
||||
if (e) {
|
||||
end = e;
|
||||
}
|
||||
let dataMap = data.reduce((acc, d) => {
|
||||
acc[d.timestamp] = d;
|
||||
return acc;
|
||||
const dataByTimestamp = rawData.reduce((accumulator, entry) => {
|
||||
accumulator[entry.timestamp] = entry;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
for (let i = start; i <= end; i += 60) {
|
||||
let nowData = dataMap[i];
|
||||
if (!!nowData) {
|
||||
status = nowData.status;
|
||||
for (let timestamp = startTimestamp; timestamp <= endTimestamp; timestamp += 60) {
|
||||
const currentEntry = dataByTimestamp[timestamp];
|
||||
if (currentEntry) {
|
||||
currentStatus = currentEntry.status;
|
||||
}
|
||||
|
||||
finalData.push({ timestamp: i, status: status });
|
||||
interpolatedData.push({ timestamp, status: currentStatus });
|
||||
}
|
||||
return finalData;
|
||||
|
||||
return interpolatedData;
|
||||
};
|
||||
|
||||
export const GetLastStatusBefore = async (monitor_tag, timestamp) => {
|
||||
@@ -418,7 +436,7 @@ export const GetLastStatusBefore = async (monitor_tag, timestamp) => {
|
||||
if (data) {
|
||||
return data.status;
|
||||
}
|
||||
return "UP";
|
||||
return NO_DATA;
|
||||
};
|
||||
|
||||
export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, timezoneOffsetMinutes = 0) => {
|
||||
@@ -443,16 +461,13 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
|
||||
UP: 0,
|
||||
DOWN: 0,
|
||||
DEGRADED: 0,
|
||||
latencySum: 0,
|
||||
latencies: [],
|
||||
NO_DATA: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const group = acc[dayGroup];
|
||||
group.total++;
|
||||
group[row.status]++;
|
||||
group.latencySum += row.latency;
|
||||
group.latencies.push(row.latency);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -464,9 +479,7 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
|
||||
UP: group.UP,
|
||||
DOWN: group.DOWN,
|
||||
DEGRADED: group.DEGRADED,
|
||||
avgLatency: group.total > 0 ? Number((group.latencySum / group.total).toFixed(3)) : null,
|
||||
maxLatency: group.latencies.length > 0 ? Number(Math.max(...group.latencies).toFixed(3)) : null,
|
||||
minLatency: group.latencies.length > 0 ? Number(Math.min(...group.latencies).toFixed(3)) : null,
|
||||
NO_DATA: group.NO_DATA,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -497,7 +510,7 @@ export const CreateIncident = async (data) => {
|
||||
|
||||
let newIncident = await db.createIncident(incident);
|
||||
return {
|
||||
incident_id: newIncident[0],
|
||||
incident_id: newIncident.id,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -673,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;
|
||||
}
|
||||
|
||||
+156
-171
@@ -2,211 +2,196 @@
|
||||
// @ts-ignore
|
||||
import db from "$lib/server/db/db.js";
|
||||
import {
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
BeginningOfDay,
|
||||
StatusObj,
|
||||
ParseUptime,
|
||||
GetDayStartTimestampUTC
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
BeginningOfDay,
|
||||
StatusObj,
|
||||
ParseUptime,
|
||||
GetDayStartTimestampUTC,
|
||||
} from "$lib/server/tool.js";
|
||||
import { formatDuration, intervalToDuration } from "date-fns";
|
||||
import { fdm, l, summaryTime } from "$lib/i18n/client";
|
||||
import {
|
||||
GetDataGroupByDayAlternative,
|
||||
InterpolateData,
|
||||
GetLastStatusBefore
|
||||
GetDataGroupByDayAlternative,
|
||||
InterpolateData,
|
||||
GetLastStatusBefore,
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
import { UP, DOWN, DEGRADED, NO_DATA } from "$lib/server/constants.js";
|
||||
|
||||
function getSummaryDuration(numOfMinute, selectedLang) {
|
||||
// Convert minutes to milliseconds and create duration object
|
||||
const duration = intervalToDuration({
|
||||
start: new Date(0),
|
||||
end: new Date(numOfMinute * 60 * 1000)
|
||||
});
|
||||
// Convert minutes to milliseconds and create duration object
|
||||
const duration = intervalToDuration({
|
||||
start: new Date(0),
|
||||
end: new Date(numOfMinute * 60 * 1000),
|
||||
});
|
||||
|
||||
return fdm(duration, selectedLang);
|
||||
return fdm(duration, selectedLang);
|
||||
}
|
||||
function getTimezoneOffset(timeZone) {
|
||||
const formatter = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "short" });
|
||||
const parts = formatter.formatToParts(new Date());
|
||||
const timeZoneOffset = parts.find((part) => part.type === "timeZoneName")?.value;
|
||||
const formatter = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "short" });
|
||||
const parts = formatter.formatToParts(new Date());
|
||||
const timeZoneOffset = parts.find((part) => part.type === "timeZoneName")?.value;
|
||||
|
||||
const match = timeZoneOffset.match(/([+-]\d{2}):?(\d{2})/);
|
||||
if (!match) return 0;
|
||||
const match = timeZoneOffset.match(/([+-]\d{2}):?(\d{2})/);
|
||||
if (!match) return 0;
|
||||
|
||||
const [, hours, minutes] = match;
|
||||
return parseInt(hours) * 60 + parseInt(minutes);
|
||||
const [, hours, minutes] = match;
|
||||
return parseInt(hours) * 60 + parseInt(minutes);
|
||||
}
|
||||
|
||||
function returnStatusClass(val, total, c, barStyle) {
|
||||
// return "api-degraded-10";
|
||||
if (barStyle === undefined || barStyle == "FULL") {
|
||||
return c;
|
||||
} else if (barStyle == "PARTIAL") {
|
||||
let totalHeight = total;
|
||||
let cl = `api-up`;
|
||||
if (val > 0 && val <= 0.1 * totalHeight) {
|
||||
cl = c + "-10";
|
||||
} else if (val > 0.1 * totalHeight && val <= 0.2 * totalHeight) {
|
||||
cl = c + "-20";
|
||||
} else if (val > 0.2 * totalHeight && val <= 0.4 * totalHeight) {
|
||||
cl = c + "-40";
|
||||
} else if (val > 0.4 * totalHeight && val <= 0.6 * totalHeight) {
|
||||
cl = c + "-60";
|
||||
} else if (val > 0.6 * totalHeight && val <= 0.8 * totalHeight) {
|
||||
cl = c + "-80";
|
||||
} else if (val > 0.8 * totalHeight && val < totalHeight) {
|
||||
cl = c + "-90";
|
||||
} else if (val == totalHeight) {
|
||||
cl = c;
|
||||
}
|
||||
return cl;
|
||||
}
|
||||
return c;
|
||||
// return "api-degraded-10";
|
||||
if (barStyle === undefined || barStyle == "FULL") {
|
||||
return c;
|
||||
} else if (barStyle == "PARTIAL") {
|
||||
let totalHeight = total;
|
||||
let cl = `api-up`;
|
||||
if (val > 0 && val <= 0.1 * totalHeight) {
|
||||
cl = c + "-10";
|
||||
} else if (val > 0.1 * totalHeight && val <= 0.2 * totalHeight) {
|
||||
cl = c + "-20";
|
||||
} else if (val > 0.2 * totalHeight && val <= 0.4 * totalHeight) {
|
||||
cl = c + "-40";
|
||||
} else if (val > 0.4 * totalHeight && val <= 0.6 * totalHeight) {
|
||||
cl = c + "-60";
|
||||
} else if (val > 0.6 * totalHeight && val <= 0.8 * totalHeight) {
|
||||
cl = c + "-80";
|
||||
} else if (val > 0.8 * totalHeight && val < totalHeight) {
|
||||
cl = c + "-90";
|
||||
} else if (val == totalHeight) {
|
||||
cl = c;
|
||||
}
|
||||
return cl;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
function getCountOfSimilarStatuesEnd(arr, statusType) {
|
||||
let count = 0;
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (arr[i].status === statusType) {
|
||||
count++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
let count = 0;
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (arr[i].status === statusType) {
|
||||
count++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
const secondsInDay = 24 * 60 * 60;
|
||||
//get offset from utc in minutes
|
||||
const now = GetMinuteStartNowTimestampUTC() + 60;
|
||||
const midnight = BeginningOfDay({ timeZone: localTz });
|
||||
const midnight90DaysAgo = midnight - 90 * 24 * 60 * 60;
|
||||
const NO_DATA = "No Data";
|
||||
const midnightTomorrow = midnight + secondsInDay;
|
||||
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(now) - midnight) / 60);
|
||||
const _90Day = {};
|
||||
let latestTimestamp = 0;
|
||||
const secondsInDay = 24 * 60 * 60;
|
||||
//get offset from utc in minutes
|
||||
const now = GetMinuteStartNowTimestampUTC() + 60;
|
||||
const midnight = BeginningOfDay({ timeZone: localTz });
|
||||
const midnight90DaysAgo = midnight - 90 * 24 * 60 * 60;
|
||||
const NO_DATA = "No Data";
|
||||
const midnightTomorrow = midnight + secondsInDay;
|
||||
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(now) - midnight) / 60);
|
||||
const _90Day = {};
|
||||
let latestTimestamp = 0;
|
||||
|
||||
let dbData = await GetDataGroupByDayAlternative(
|
||||
monitor.tag,
|
||||
midnight90DaysAgo,
|
||||
midnightTomorrow,
|
||||
offsetInMinutes
|
||||
);
|
||||
let totalDegradedCount = 0;
|
||||
let totalDownCount = 0;
|
||||
let totalUpCount = 0;
|
||||
let dbData = await GetDataGroupByDayAlternative(monitor.tag, midnight90DaysAgo, midnightTomorrow, offsetInMinutes);
|
||||
let totalDegradedCount = 0;
|
||||
let totalDownCount = 0;
|
||||
let totalUpCount = 0;
|
||||
|
||||
for (let i = 0; i < dbData.length; i++) {
|
||||
let dayData = dbData[i];
|
||||
let ts = dayData.timestamp;
|
||||
let cssClass = StatusObj.UP;
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = "UP";
|
||||
latestTimestamp = ts;
|
||||
totalDegradedCount += dayData.DEGRADED;
|
||||
totalDownCount += dayData.DOWN;
|
||||
totalUpCount += dayData.UP;
|
||||
for (let i = 0; i < dbData.length; i++) {
|
||||
let dayData = dbData[i];
|
||||
let ts = dayData.timestamp;
|
||||
let cssClass = StatusObj.UP;
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = UP;
|
||||
latestTimestamp = ts;
|
||||
totalDegradedCount += dayData.DEGRADED;
|
||||
totalDownCount += dayData.DOWN;
|
||||
totalUpCount += dayData.UP;
|
||||
|
||||
if (dayData.DEGRADED >= monitor.day_degraded_minimum_count) {
|
||||
cssClass = returnStatusClass(
|
||||
dayData.DEGRADED,
|
||||
dayData.total,
|
||||
StatusObj.DEGRADED,
|
||||
site.barStyle
|
||||
);
|
||||
summaryDuration = getSummaryDuration(dayData.DEGRADED, selectedLang);
|
||||
summaryStatus = "DEGRADED";
|
||||
}
|
||||
if (dayData.DOWN >= monitor.day_down_minimum_count) {
|
||||
cssClass = returnStatusClass(
|
||||
dayData.DOWN,
|
||||
dayData.total,
|
||||
StatusObj.DOWN,
|
||||
site.barStyle
|
||||
);
|
||||
if (dayData.DEGRADED >= monitor.day_degraded_minimum_count) {
|
||||
cssClass = returnStatusClass(dayData.DEGRADED, dayData.total, StatusObj.DEGRADED, site.barStyle);
|
||||
summaryDuration = getSummaryDuration(dayData.DEGRADED, selectedLang);
|
||||
summaryStatus = "DEGRADED";
|
||||
}
|
||||
if (dayData.DOWN >= monitor.day_down_minimum_count) {
|
||||
cssClass = returnStatusClass(dayData.DOWN, dayData.total, StatusObj.DOWN, site.barStyle);
|
||||
|
||||
summaryDuration = getSummaryDuration(dayData.DOWN, selectedLang);
|
||||
summaryStatus = "DOWN";
|
||||
}
|
||||
summaryDuration = getSummaryDuration(dayData.DOWN, selectedLang);
|
||||
summaryStatus = "DOWN";
|
||||
}
|
||||
if (dayData.NO_DATA === dayData.total) {
|
||||
cssClass = StatusObj.NO_DATA;
|
||||
summaryStatus = NO_DATA;
|
||||
}
|
||||
|
||||
_90Day[ts] = {
|
||||
border: true
|
||||
};
|
||||
_90Day[ts] = {
|
||||
border: true,
|
||||
};
|
||||
|
||||
_90Day[ts].timestamp = ts;
|
||||
_90Day[ts].cssClass = cssClass;
|
||||
_90Day[ts].summaryStatus = l(lang, summaryTime(summaryStatus), {
|
||||
status: l(lang, summaryStatus),
|
||||
duration: summaryDuration
|
||||
});
|
||||
_90Day[ts].timestamp = ts;
|
||||
_90Day[ts].cssClass = cssClass;
|
||||
_90Day[ts].summaryStatus = l(lang, summaryTime(summaryStatus), {
|
||||
status: l(lang, summaryStatus),
|
||||
duration: summaryDuration,
|
||||
});
|
||||
|
||||
_90Day[ts].textClass = cssClass.replace(/-\d+$/, "");
|
||||
}
|
||||
let uptime90DayNumerator = totalUpCount + totalDegradedCount;
|
||||
let uptime90DayDenominator = totalUpCount + totalDownCount + totalDegradedCount;
|
||||
_90Day[ts].textClass = cssClass.replace(/-\d+$/, "");
|
||||
}
|
||||
|
||||
//remove degraded from uptime
|
||||
if (monitor.include_degraded_in_downtime === "YES") {
|
||||
uptime90DayNumerator = totalUpCount;
|
||||
}
|
||||
// return _90Day;
|
||||
let uptime90Day = ParseUptime(uptime90DayNumerator, uptime90DayDenominator);
|
||||
let uptime90DayNumerator = totalUpCount + totalDegradedCount;
|
||||
let uptime90DayDenominator = totalUpCount + totalDownCount + totalDegradedCount;
|
||||
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = "UP";
|
||||
//remove degraded from uptime
|
||||
if (monitor.include_degraded_in_downtime === "YES") {
|
||||
uptime90DayNumerator = totalUpCount;
|
||||
}
|
||||
// return _90Day;
|
||||
let uptime90Day = ParseUptime(uptime90DayNumerator, uptime90DayDenominator);
|
||||
|
||||
let summaryColorClass = "api-nodata";
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = "UP";
|
||||
|
||||
let todayDataDb = await db.getMonitoringData(monitor.tag, midnight, midnight + secondsInDay);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, midnight);
|
||||
todayDataDb = InterpolateData(
|
||||
todayDataDb,
|
||||
midnight,
|
||||
anchorStatus,
|
||||
todayDataDb.length == 0 ? midnight + secondsInDay : null
|
||||
);
|
||||
let summaryColorClass = "api-nodata";
|
||||
|
||||
if (site.summaryStyle === "CURRENT") {
|
||||
summaryColorClass = "api-up";
|
||||
let todayDataDb = await db.getMonitoringData(monitor.tag, midnight, midnight + secondsInDay);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, midnight);
|
||||
todayDataDb = InterpolateData(
|
||||
todayDataDb,
|
||||
midnight,
|
||||
anchorStatus,
|
||||
todayDataDb.length == 0 ? midnight + secondsInDay : null,
|
||||
);
|
||||
|
||||
let lastRow = todayDataDb[todayDataDb.length - 1];
|
||||
if (site.summaryStyle === "CURRENT") {
|
||||
summaryColorClass = "api-up";
|
||||
|
||||
if (!!lastRow && lastRow.status == "DEGRADED") {
|
||||
summaryDuration = getSummaryDuration(
|
||||
getCountOfSimilarStatuesEnd(todayDataDb, "DEGRADED"),
|
||||
selectedLang
|
||||
);
|
||||
summaryStatus = "DEGRADED";
|
||||
summaryColorClass = "api-degraded";
|
||||
}
|
||||
if (!!lastRow && lastRow.status == "DOWN") {
|
||||
summaryDuration = getSummaryDuration(
|
||||
getCountOfSimilarStatuesEnd(todayDataDb, "DOWN"),
|
||||
selectedLang
|
||||
);
|
||||
summaryStatus = "DOWN";
|
||||
summaryColorClass = "api-down";
|
||||
}
|
||||
summaryStatus = l(lang, summaryTime(summaryStatus), {
|
||||
status: l(lang, summaryStatus),
|
||||
duration: summaryDuration
|
||||
});
|
||||
} else {
|
||||
let lastData = _90Day[latestTimestamp];
|
||||
summaryColorClass = lastData.cssClass.replace(/-\d+$/, "");
|
||||
summaryDuration = lastData.summaryDuration;
|
||||
summaryStatus = lastData.summaryStatus;
|
||||
}
|
||||
return {
|
||||
_90Day: _90Day,
|
||||
uptime90Day: uptime90Day,
|
||||
summaryStatus: summaryStatus,
|
||||
summaryColorClass: summaryColorClass,
|
||||
barRoundness: site.barRoundness,
|
||||
midnight90DaysAgo: midnight90DaysAgo,
|
||||
midnightTomorrow: midnightTomorrow
|
||||
};
|
||||
let lastRow = todayDataDb[todayDataDb.length - 1];
|
||||
|
||||
if (!!lastRow && lastRow.status == "DEGRADED") {
|
||||
summaryDuration = getSummaryDuration(getCountOfSimilarStatuesEnd(todayDataDb, "DEGRADED"), selectedLang);
|
||||
summaryStatus = "DEGRADED";
|
||||
summaryColorClass = "api-degraded";
|
||||
}
|
||||
if (!!lastRow && lastRow.status == "DOWN") {
|
||||
summaryDuration = getSummaryDuration(getCountOfSimilarStatuesEnd(todayDataDb, "DOWN"), selectedLang);
|
||||
summaryStatus = "DOWN";
|
||||
summaryColorClass = "api-down";
|
||||
}
|
||||
summaryStatus = l(lang, summaryTime(summaryStatus), {
|
||||
status: l(lang, summaryStatus),
|
||||
duration: summaryDuration,
|
||||
});
|
||||
} else {
|
||||
let lastData = _90Day[latestTimestamp];
|
||||
summaryColorClass = lastData.cssClass.replace(/-\d+$/, "");
|
||||
summaryDuration = lastData.summaryDuration;
|
||||
summaryStatus = lastData.summaryStatus;
|
||||
}
|
||||
return {
|
||||
_90Day: _90Day,
|
||||
uptime90Day: uptime90Day,
|
||||
summaryStatus: summaryStatus,
|
||||
summaryColorClass: summaryColorClass,
|
||||
barRoundness: site.barRoundness,
|
||||
midnight90DaysAgo: midnight90DaysAgo,
|
||||
midnightTomorrow: midnightTomorrow,
|
||||
};
|
||||
};
|
||||
export { FetchData };
|
||||
|
||||
+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.4 </span>
|
||||
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.1.7 </span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
export let data;
|
||||
let subHeadings = [];
|
||||
let previousH2 = null;
|
||||
let subPath = data.docFilePath.split(".md")[0];
|
||||
|
||||
function fillSubHeadings() {
|
||||
subHeadings = [];
|
||||
const headings = document.querySelectorAll("#markdown h2, #markdown h3");
|
||||
@@ -48,7 +48,7 @@
|
||||
<svg class="check-btn absolute left-0 top-0" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><path d="M20 6 9 17l-5-5"/></svg>`;
|
||||
button.style = "margin-left: 10px;height:16px;width:16px background-color: transparent; cursor: pointer;";
|
||||
button.onclick = () => {
|
||||
navigator.clipboard.writeText(`https://kener.ing/docs${subPath}#` + heading.id);
|
||||
navigator.clipboard.writeText(`https://kener.ing/docs${data.docFilePath.split(".md")[0]}#` + heading.id);
|
||||
};
|
||||
heading.appendChild(button);
|
||||
});
|
||||
@@ -121,7 +121,7 @@
|
||||
<svelte:head>
|
||||
<title>{data.title}</title>
|
||||
<meta name="description" content={data.description} />
|
||||
<link rel="canonical" href="https://kener.ing/docs{subPath}" />
|
||||
<link rel="canonical" href="https://kener.ing/docs{data.docFilePath.split('.md')[0]}" />
|
||||
</svelte:head>
|
||||
|
||||
<div id="markdown">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
|
||||
<div class="blurry-bg mx-auto max-w-3xl text-center">
|
||||
{#if data.hero.image}
|
||||
<GMI src={data.site.hero.image} classList="m-auto mb-2 h-14 w-14" alt="" srcset="" />
|
||||
<GMI src={data.hero.image} classList="m-auto mb-2 h-14 w-14" alt="" srcset="" />
|
||||
{/if}
|
||||
{#if data.hero.title}
|
||||
<h1
|
||||
|
||||
@@ -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