Compare commits

...

13 Commits

Author SHA1 Message Date
Raj Nandan Sharma 5d475ab23f Merge pull request #302 from rajnandan1/fix/incidents-1
Fix/incidents 1
2025-02-19 10:12:08 +05:30
Raj Nandan Sharma 01cb39e18e Bump version to 3.1.7 and update favicon type label; improve incident description formatting 2025-02-19 10:10:31 +05:30
Raj Nandan Sharma 0d0ef25970 Improve documentation for SMTP configuration and add external link button in monitors management 2025-02-19 09:52:09 +05:30
Raj Nandan Sharma 6c766e2001 Enhances incident management and SMTP configuration
Improves incident display and management by introducing configurable incident group views and enhancing comment rendering to support HTML content.

Solves the bug raised in #295 where server crashes when an incident is created from an alert

Refines SMTP email settings by adding TLS configuration and allowing username/password to be optional. #300 and #298

Also, fixes a bug where only home page was being filtered. Now all pages are filtered. #297
2025-02-19 07:05:24 +05:30
Raj Nandan Sharma 1c73166120 Merge pull request #292 from kaffolder7/feature/embed-webfont
feat: embed project webfont
2025-02-18 07:46:56 +05:30
github-actions fc5dbc85dc Auto-generate README.md with release versions 2025-02-18 02:06:31 +00:00
Raj Nandan Sharma 622327cdf6 Merge pull request #294 from rajnandan1/fix/293-and-287
Bump version to 3.1.6 and update affected status handling in database…
2025-02-18 07:22:13 +05:30
Raj Nandan Sharma b0a4cd5c42 Bump version to 3.1.6 and update affected status handling in database queries for pg db reported in #293 and #287 2025-02-18 07:20:08 +05:30
Kyle Affolder 4e5fb26be1 add: CSS rule in EditorConfig 2025-02-17 12:53:29 -05:00
Kyle Affolder 6fc80bc0fc change: ensure full Lato font is kept from build
Ensures that the full variant of the Lato font-family is kept/removed from Docker image build. (Keeps the size small!)
2025-02-17 12:27:32 -05:00
Kyle Affolder 292667ac29 add: Lato webfont
Noticed multiple individuals commenting about insecure/privacy-unfriendly Lato webfont library being served via Google Fonts. I had formerly suggested replacing this with BunnyFonts and was happy to see that added as a placeholder, however, I also understand someone’s comment about this being loaded from an external resource.

This brings that webfont local. Size of webfont files should minimally grow Docker image sizes and I think we should prioritize UI and privacy by including it locally. The font’s licensing is OFL, so we are allowed to package it for distribution with this project.

I’m including both the full font family (for archival purposes) and Latin subset of this font. The Latin variant is used in the Docker image build (since this will apply to the majority of users and keep the Docker image smaller). If users need to extend this with their own subsets, they can always load those as a custom font. :)
2025-02-17 12:26:15 -05:00
Kyle Affolder 8c03058f8d update: combine to avoid multiple processes
Combined to reduce spawning multiple `rm` processes.
2025-02-17 12:21:57 -05:00
github-actions 25e73d097c Auto-generate README.md with release versions 2025-02-17 05:36:58 +00:00
82 changed files with 696 additions and 418 deletions
+5
View File
@@ -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
View File
@@ -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
+9 -9
View File
@@ -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 &nbsp;<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>
+14
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
+90
View File
@@ -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;
}
+11 -4
View File
@@ -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>
+35 -2
View File
@@ -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
+4 -1
View File
@@ -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
View File
@@ -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;
+2
View File
@@ -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,
};
+7 -1
View File
@@ -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 -6
View File
@@ -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
View File
@@ -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
+25 -16
View File
@@ -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
View File
@@ -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 };
+1 -1
View File
@@ -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>
+8 -2
View File
@@ -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"}
+93
View File
@@ -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.