Merge pull request #289 from rajnandan1/feature/category-status
Feature/category status
@@ -58,7 +58,8 @@ COPY . .
|
||||
# TODO: Reevaluate permissions (possibly reduce?)...
|
||||
# Remove docs directory and ensure required directories exist
|
||||
RUN rm -rf src/routes/\(docs\) && \
|
||||
mkdir -p uploads database && \
|
||||
rm -rf src/static/documentation && \
|
||||
mkdir -p uploads database && \
|
||||
# TODO: Consider changing below to `chmod -R u-rwX,g=rX,o= uploads database`
|
||||
chmod -R 750 uploads database
|
||||
|
||||
|
||||
@@ -64,24 +64,24 @@ Body of the webhook will be sent as below:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mockoon-9",
|
||||
"alert_name": "Mockoon DOWN",
|
||||
"severity": "critical",
|
||||
"status": "TRIGGERED",
|
||||
"source": "Kener",
|
||||
"timestamp": "2024-11-27T04:55:00.369Z",
|
||||
"description": "🚨 **Service Alert**: Check the details below",
|
||||
"details": {
|
||||
"metric": "Mockoon",
|
||||
"current_value": 1,
|
||||
"threshold": 1
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"text": "View Monitor",
|
||||
"url": "https://kener.ing/monitor-mockoon"
|
||||
}
|
||||
]
|
||||
"id": "mockoon-9",
|
||||
"alert_name": "Mockoon DOWN",
|
||||
"severity": "critical",
|
||||
"status": "TRIGGERED",
|
||||
"source": "Kener",
|
||||
"timestamp": "2024-11-27T04:55:00.369Z",
|
||||
"description": "🚨 **Service Alert**: Check the details below",
|
||||
"details": {
|
||||
"metric": "Mockoon",
|
||||
"current_value": 1,
|
||||
"threshold": 1
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"text": "View Monitor",
|
||||
"url": "https://kener.ing/monitor-mockoon"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -108,7 +108,7 @@ The discord message when alert is `TRIGGERED` will look like this
|
||||
|
||||
The discord message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||

|
||||
|
||||
### Slack
|
||||
|
||||
@@ -118,7 +118,7 @@ The slack message when alert is `TRIGGERED` will look like this
|
||||
|
||||
The slack message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||

|
||||
|
||||
### Add Alerts to Monitors
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ A small text that will be shown below the title.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ A small text that will be shown below the title.
|
||||
|
||||
You can navigation links to other urls. You can add as many as you want.
|
||||
|
||||

|
||||

|
||||
|
||||
### Icon
|
||||
|
||||
@@ -45,7 +45,7 @@ The title of the link.
|
||||
|
||||
The URL to redirect to when the link is clicked.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ API monitors are used to monitor APIs. You can use API monitors to monitor the u
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64);
|
||||
let decodedResp = atob(responseDataBase64)
|
||||
//if the response is a json object
|
||||
//let jsonResp = JSON.parse(decodedResp)
|
||||
```
|
||||
@@ -79,61 +79,61 @@ let decodedResp = atob(responseDataBase64);
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the status code is 2XX then the status is UP, if the status code is 5XX then the status is DOWN. If the response contains the word `Unknown Error` then the status is DOWN. If the response time is greater than 2000 then the status is DEGRADED.
|
||||
|
||||
```javascript
|
||||
(async function (statusCode, responseTime, responseDataBase64) {
|
||||
const resp = atob(responseDataBase64); //convert base64 to string
|
||||
;(async function (statusCode, responseTime, responseDataBase64) {
|
||||
const resp = atob(responseDataBase64) //convert base64 to string
|
||||
|
||||
let status = "DOWN";
|
||||
let status = "DOWN"
|
||||
|
||||
//if the status code is 2XX then the status is UP
|
||||
if (/^[2]\d{2}$/.test(statusCode)) {
|
||||
status = "UP";
|
||||
if (responseTime > 2000) {
|
||||
status = "DEGRADED";
|
||||
}
|
||||
}
|
||||
//if the status code is 2XX then the status is UP
|
||||
if (/^[2]\d{2}$/.test(statusCode)) {
|
||||
status = "UP"
|
||||
if (responseTime > 2000) {
|
||||
status = "DEGRADED"
|
||||
}
|
||||
}
|
||||
|
||||
//if the status code is 5XX then the status is DOWN
|
||||
if (/^[5]\d{2}$/.test(statusCode)) status = "DOWN";
|
||||
//if the status code is 5XX then the status is DOWN
|
||||
if (/^[5]\d{2}$/.test(statusCode)) status = "DOWN"
|
||||
|
||||
if (resp.includes("Unknown Error")) {
|
||||
status = "DOWN";
|
||||
}
|
||||
if (resp.includes("Unknown Error")) {
|
||||
status = "DOWN"
|
||||
}
|
||||
|
||||
return {
|
||||
status: status,
|
||||
latency: responseTime
|
||||
};
|
||||
});
|
||||
return {
|
||||
status: status,
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This next example shows how to call another API withing eval. It is scrapping the second last script tag from the response and checking if the heading is "No recent issues" then the status is UP else it is DOWN.
|
||||
|
||||
```javascript
|
||||
(async function raj(statusCode, responseTime, responseDataBase64) {
|
||||
let htmlString = atob(responseDataBase64);
|
||||
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g);
|
||||
if (scriptTags && scriptTags.length >= 2) {
|
||||
// Extract the second last script tag's src attribute
|
||||
const secondLastScript = scriptTags[scriptTags.length - 2];
|
||||
const srcMatch = secondLastScript.match(/src="([^"]+)"/);
|
||||
const secondLastScriptSrc = srcMatch ? srcMatch[1] : null;
|
||||
;(async function raj(statusCode, responseTime, responseDataBase64) {
|
||||
let htmlString = atob(responseDataBase64)
|
||||
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g)
|
||||
if (scriptTags && scriptTags.length >= 2) {
|
||||
// Extract the second last script tag's src attribute
|
||||
const secondLastScript = scriptTags[scriptTags.length - 2]
|
||||
const srcMatch = secondLastScript.match(/src="([^"]+)"/)
|
||||
const secondLastScriptSrc = srcMatch ? srcMatch[1] : null
|
||||
|
||||
let jsResp = await fetch(secondLastScriptSrc); //api call
|
||||
let jsRespText = await jsResp.text();
|
||||
//check if heading":"No recent issues" exists
|
||||
let noRecentIssues = jsRespText.indexOf('heading":"No recent issues"');
|
||||
if (noRecentIssues != -1) {
|
||||
return {
|
||||
status: "UP",
|
||||
latency: responseTime
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: "DOWN",
|
||||
latency: responseTime
|
||||
};
|
||||
});
|
||||
let jsResp = await fetch(secondLastScriptSrc) //api call
|
||||
let jsRespText = await jsResp.text()
|
||||
//check if heading":"No recent issues" exists
|
||||
let noRecentIssues = jsRespText.indexOf('heading":"No recent issues"')
|
||||
if (noRecentIssues != -1) {
|
||||
return {
|
||||
status: "UP",
|
||||
latency: responseTime
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: "DOWN",
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Examples
|
||||
@@ -153,7 +153,7 @@ This is an example to monitor google every 5 minute.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -180,7 +180,7 @@ export SOME_TOKEN=some-token-example
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -201,7 +201,7 @@ Example showing setting up a POST request every minute with a timeout of 2 secon
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -228,7 +228,7 @@ export SERVICE_SECRET=secret2_secret
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ DNS monitors are used to monitor DNS servers. Verify DNS queries for your server
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
40
docs/monitors-group.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Group Monitors | Kener
|
||||
description: Learn how to set up and work with Group monitors in kener.
|
||||
---
|
||||
|
||||
# Group Monitors
|
||||
|
||||
Group monitors are used to monitor multiple monitors at once. You can use Group monitors to monitor multiple monitors at once and get notified when they are down.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Timeout
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The timeout is used to define the time in milliseconds after which the group monitor should timeout.
|
||||
|
||||
Let us say the group monitor runs every minute, it will expect in the same minute all the other monitors to finish. It will wait till the timeout for them to complete. If not completed within that timeout, it will be marked as down.
|
||||
|
||||
## Monitors
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
You can add as many monitors as you want to monitor. The minimum number of monitors required is 2. The monitor can be any type of monitor.
|
||||
|
||||
## Hide
|
||||
|
||||
You can hide the monitors that are part of the group monitor. If you hide the monitors, the monitors inside the group will not be shown in the home page.
|
||||
|
||||
<div class="note">
|
||||
The group status will be the worst status of the monitors in the group.
|
||||
</div>
|
||||
@@ -9,7 +9,7 @@ Ping monitors are used to monitor livenees of your servers. You can use Ping mon
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -32,29 +32,29 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64));
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive;
|
||||
}, true);
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive
|
||||
}, true)
|
||||
|
||||
return {
|
||||
status: alive ? "UP" : "DOWN",
|
||||
latency: latencyTotal / arrayOfPings.length
|
||||
};
|
||||
});
|
||||
return {
|
||||
status: alive ? "UP" : "DOWN",
|
||||
latency: latencyTotal / arrayOfPings.length
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64);
|
||||
let jsonResp = JSON.parse(decodedResp);
|
||||
console.log(jsonResp);
|
||||
let decodedResp = atob(responseDataBase64)
|
||||
let jsonResp = JSON.parse(decodedResp)
|
||||
console.log(jsonResp)
|
||||
/*
|
||||
[
|
||||
{
|
||||
@@ -109,28 +109,28 @@ The input to the eval function is a base64 encoded string. You will have to deco
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
|
||||
|
||||
```javascript
|
||||
(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64));
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
|
||||
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive;
|
||||
}, true);
|
||||
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive
|
||||
}, true)
|
||||
|
||||
let avgLatency = latencyTotal / arrayOfPings.length;
|
||||
let avgLatency = latencyTotal / arrayOfPings.length
|
||||
|
||||
if (areAllOpen && avgLatency > 10) {
|
||||
return {
|
||||
status: "DEGRADED",
|
||||
latency: avgLatency
|
||||
};
|
||||
}
|
||||
if (areAllOpen && avgLatency > 10) {
|
||||
return {
|
||||
status: "DEGRADED",
|
||||
latency: avgLatency
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: areAllOpen ? "UP" : "DOWN",
|
||||
latency: avgLatency
|
||||
};
|
||||
});
|
||||
return {
|
||||
status: areAllOpen ? "UP" : "DOWN",
|
||||
latency: avgLatency
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ TCP monitors are used to monitor the livenees of your servers. You can use TCP m
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -32,33 +32,33 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64));
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
if (ping.status === "open") {
|
||||
return acc && true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
if (ping.status === "open") {
|
||||
return acc && true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, true)
|
||||
|
||||
return {
|
||||
status: alive ? "UP" : "DOWN",
|
||||
latency: latencyTotal / arrayOfPings.length
|
||||
};
|
||||
});
|
||||
return {
|
||||
status: alive ? "UP" : "DOWN",
|
||||
latency: latencyTotal / arrayOfPings.length
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64);
|
||||
let jsonResp = JSON.parse(decodedResp);
|
||||
console.log(jsonResp);
|
||||
let decodedResp = atob(responseDataBase64)
|
||||
let jsonResp = JSON.parse(decodedResp)
|
||||
console.log(jsonResp)
|
||||
/*
|
||||
[
|
||||
{
|
||||
@@ -104,32 +104,32 @@ The input to the eval function is a base64 encoded string. You will have to deco
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
|
||||
|
||||
```javascript
|
||||
(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64));
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
|
||||
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
|
||||
if (ping.status === "open") {
|
||||
return acc && true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
|
||||
if (ping.status === "open") {
|
||||
return acc && true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, true)
|
||||
|
||||
let avgLatency = latencyTotal / arrayOfPings.length;
|
||||
let avgLatency = latencyTotal / arrayOfPings.length
|
||||
|
||||
if (areAllOpen && avgLatency > 10) {
|
||||
return {
|
||||
status: "DEGRADED",
|
||||
latency: avgLatency
|
||||
};
|
||||
}
|
||||
if (areAllOpen && avgLatency > 10) {
|
||||
return {
|
||||
status: "DEGRADED",
|
||||
latency: avgLatency
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: areAllOpen ? "UP" : "DOWN",
|
||||
latency: avgLatency
|
||||
};
|
||||
});
|
||||
return {
|
||||
status: areAllOpen ? "UP" : "DOWN",
|
||||
latency: avgLatency
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -13,7 +13,7 @@ Click on the ➕ to add a monitor.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ Example: `Kener - Open-Source and Modern looking Node.js Status Page for Effortl
|
||||
|
||||
```html
|
||||
<title>
|
||||
Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management
|
||||
Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management
|
||||
</title>
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## Site Name
|
||||
|
||||
@@ -33,7 +33,7 @@ Example: `Kener - Open-Source and Modern looking Node.js Status Page for Effortl
|
||||
|
||||
This will be shown as a brand name on the status page on the nav bar top left.
|
||||
|
||||

|
||||

|
||||
|
||||
## Home Location
|
||||
|
||||
|
||||
@@ -1,166 +1,166 @@
|
||||
{
|
||||
"sidebar": [
|
||||
{
|
||||
"sectionTitle": "Getting Started",
|
||||
"children": [
|
||||
{
|
||||
"title": "Introduction",
|
||||
"link": "/docs/home",
|
||||
"file": "/home.md"
|
||||
},
|
||||
{
|
||||
"title": "Get Started",
|
||||
"link": "/docs/quick-start",
|
||||
"file": "/quick-start.md"
|
||||
},
|
||||
{
|
||||
"title": "Concepts",
|
||||
"link": "/docs/concepts",
|
||||
"file": "/concepts.md"
|
||||
},
|
||||
"sidebar": [
|
||||
{
|
||||
"sectionTitle": "Getting Started",
|
||||
"children": [
|
||||
{
|
||||
"title": "Introduction",
|
||||
"link": "/docs/home",
|
||||
"file": "/home.md"
|
||||
},
|
||||
{
|
||||
"title": "Get Started",
|
||||
"link": "/docs/quick-start",
|
||||
"file": "/quick-start.md"
|
||||
},
|
||||
{
|
||||
"title": "Concepts",
|
||||
"link": "/docs/concepts",
|
||||
"file": "/concepts.md"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Deployment",
|
||||
"link": "/docs/deployment",
|
||||
"file": "/deployment.md"
|
||||
},
|
||||
{
|
||||
"title": "Databases",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Guides",
|
||||
"children": [
|
||||
{
|
||||
"title": "Setup Environment",
|
||||
"link": "/docs/environment-vars",
|
||||
"file": "/environment-vars.md"
|
||||
},
|
||||
{
|
||||
"title": "Use Badges",
|
||||
"link": "/docs/status-badges",
|
||||
"file": "/status-badges.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Monitors",
|
||||
"link": "/docs/monitors",
|
||||
"file": "/monitors.md"
|
||||
},
|
||||
{
|
||||
"title": "API/Website Monitor",
|
||||
"link": "/docs/monitors-api",
|
||||
"file": "/monitors-api.md"
|
||||
},
|
||||
{
|
||||
"title": "Ping Monitor",
|
||||
"link": "/docs/monitors-ping",
|
||||
"file": "/monitors-ping.md"
|
||||
},
|
||||
{
|
||||
"title": "TCP Monitor",
|
||||
"link": "/docs/monitors-tcp",
|
||||
"file": "/monitors-tcp.md"
|
||||
},
|
||||
{
|
||||
"title": "DNS Monitor",
|
||||
"link": "/docs/monitors-dns",
|
||||
"file": "/monitors-dns.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Triggers",
|
||||
"link": "/docs/triggers",
|
||||
"file": "/triggers.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Site",
|
||||
"link": "/docs/site",
|
||||
"file": "/site.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Github",
|
||||
"link": "/docs/gh-setup",
|
||||
"file": "/gh-setup.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup SEO",
|
||||
"link": "/docs/seo",
|
||||
"file": "/seo.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Home",
|
||||
"link": "/docs/home-page",
|
||||
"file": "/home-page.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Theme",
|
||||
"link": "/docs/theme",
|
||||
"file": "/theme.md"
|
||||
},
|
||||
{
|
||||
"title": "View Alerts",
|
||||
"link": "/docs/alerts",
|
||||
"file": "/alerts.md"
|
||||
},
|
||||
{
|
||||
"title": "API Keys",
|
||||
"link": "/docs/apikeys",
|
||||
"file": "/apikeys.md"
|
||||
},
|
||||
{
|
||||
"title": "Deployment",
|
||||
"link": "/docs/deployment",
|
||||
"file": "/deployment.md"
|
||||
},
|
||||
{
|
||||
"title": "Databases",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Guides",
|
||||
"children": [
|
||||
{
|
||||
"title": "Setup Environment",
|
||||
"link": "/docs/environment-vars",
|
||||
"file": "/environment-vars.md"
|
||||
},
|
||||
{
|
||||
"title": "Use Badges",
|
||||
"link": "/docs/status-badges",
|
||||
"file": "/status-badges.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Monitors",
|
||||
"link": "/docs/monitors",
|
||||
"file": "/monitors.md"
|
||||
},
|
||||
{
|
||||
"title": "API/Website Monitor",
|
||||
"link": "/docs/monitors-api",
|
||||
"file": "/monitors-api.md"
|
||||
},
|
||||
{
|
||||
"title": "Ping Monitor",
|
||||
"link": "/docs/monitors-ping",
|
||||
"file": "/monitors-ping.md"
|
||||
},
|
||||
{
|
||||
"title": "TCP Monitor",
|
||||
"link": "/docs/monitors-tcp",
|
||||
"file": "/monitors-tcp.md"
|
||||
},
|
||||
{
|
||||
"title": "DNS Monitor",
|
||||
"link": "/docs/monitors-dns",
|
||||
"file": "/monitors-dns.md"
|
||||
},
|
||||
{
|
||||
"title": "Group Monitor",
|
||||
"link": "/docs/monitors-group",
|
||||
"file": "/monitors-group.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Triggers",
|
||||
"link": "/docs/triggers",
|
||||
"file": "/triggers.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Site",
|
||||
"link": "/docs/site",
|
||||
"file": "/site.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup SEO",
|
||||
"link": "/docs/seo",
|
||||
"file": "/seo.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Home",
|
||||
"link": "/docs/home-page",
|
||||
"file": "/home-page.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Theme",
|
||||
"link": "/docs/theme",
|
||||
"file": "/theme.md"
|
||||
},
|
||||
{
|
||||
"title": "View Alerts",
|
||||
"link": "/docs/alerts",
|
||||
"file": "/alerts.md"
|
||||
},
|
||||
{
|
||||
"title": "API Keys",
|
||||
"link": "/docs/apikeys",
|
||||
"file": "/apikeys.md"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Incident Management",
|
||||
"link": "/docs/incident-management",
|
||||
"file": "/incident-management.md"
|
||||
},
|
||||
{
|
||||
"title": "Embed",
|
||||
"link": "/docs/embed",
|
||||
"file": "/embed.md"
|
||||
},
|
||||
{
|
||||
"title": "Custom JS/CSS",
|
||||
"link": "/docs/custom-js-css-guide",
|
||||
"file": "/custom-js-css-guide.md"
|
||||
},
|
||||
{
|
||||
"title": "Internationalization",
|
||||
"link": "/docs/i18n",
|
||||
"file": "/i18n.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "API Reference",
|
||||
"children": [
|
||||
{
|
||||
"title": "Kener APIs",
|
||||
"link": "/docs/kener-apis",
|
||||
"file": "/kener-apis.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Help",
|
||||
"children": [
|
||||
{
|
||||
"title": "Fonts",
|
||||
"link": "/docs/custom-fonts",
|
||||
"file": "/custom-fonts.md"
|
||||
},
|
||||
{
|
||||
"title": "Changelogs",
|
||||
"link": "/docs/changelogs",
|
||||
"file": "/changelogs.md"
|
||||
},
|
||||
{
|
||||
"title": "Roadmap",
|
||||
"link": "/docs/roadmap",
|
||||
"file": "/roadmap.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
{
|
||||
"title": "Incident Management",
|
||||
"link": "/docs/incident-management",
|
||||
"file": "/incident-management.md"
|
||||
},
|
||||
{
|
||||
"title": "Embed",
|
||||
"link": "/docs/embed",
|
||||
"file": "/embed.md"
|
||||
},
|
||||
{
|
||||
"title": "Custom JS/CSS",
|
||||
"link": "/docs/custom-js-css-guide",
|
||||
"file": "/custom-js-css-guide.md"
|
||||
},
|
||||
{
|
||||
"title": "Internationalization",
|
||||
"link": "/docs/i18n",
|
||||
"file": "/i18n.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "API Reference",
|
||||
"children": [
|
||||
{
|
||||
"title": "Kener APIs",
|
||||
"link": "/docs/kener-apis",
|
||||
"file": "/kener-apis.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Help",
|
||||
"children": [
|
||||
{
|
||||
"title": "Fonts",
|
||||
"link": "/docs/custom-fonts",
|
||||
"file": "/custom-fonts.md"
|
||||
},
|
||||
{
|
||||
"title": "Changelogs",
|
||||
"link": "/docs/changelogs",
|
||||
"file": "/changelogs.md"
|
||||
},
|
||||
{
|
||||
"title": "Roadmap",
|
||||
"link": "/docs/roadmap",
|
||||
"file": "/roadmap.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ Kener provides you with the ability to customize the theme of your status page.
|
||||
|
||||
## Home Page Pattern
|
||||
|
||||
Kener can show a subtle pattern in all your pages. It is either sqaure or dots. Right now you cannot modify the color of the pattern. However, you can disable it by choosing none
|
||||
Kener can show a subtle pattern in all your pages. It is either square or dots. Right now you cannot modify the color of the pattern. However, you can disable it by choosing none
|
||||
|
||||
---
|
||||
|
||||
@@ -35,13 +35,13 @@ You can change how the bars and summary of a monitor looks like.
|
||||
|
||||
The status bar will be a gradient from green to red/yellow based on the status of the monitor.
|
||||
|
||||

|
||||

|
||||
|
||||
#### Full
|
||||
|
||||
The status bar will be a solid color based on the status of the monitor.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -51,11 +51,11 @@ Adjust the roundness of the status bar.
|
||||
|
||||
#### SHARP
|
||||
|
||||

|
||||

|
||||
|
||||
#### ROUNDED
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -97,7 +97,7 @@ You can add custom CSS to your status page. This will be added to the head of th
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Triggers are used to trigger actions based on the status of your monitors. You c
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ Webhook triggers are used to send a HTTP POST request to a URL when a monitor go
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -72,24 +72,24 @@ Body of the webhook will be sent as below:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mockoon-9",
|
||||
"alert_name": "Mockoon DOWN",
|
||||
"severity": "critical",
|
||||
"status": "TRIGGERED",
|
||||
"source": "Kener",
|
||||
"timestamp": "2024-11-27T04:55:00.369Z",
|
||||
"description": "🚨 **Service Alert**: Check the details below",
|
||||
"details": {
|
||||
"metric": "Mockoon",
|
||||
"current_value": 1,
|
||||
"threshold": 1
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"text": "View Monitor",
|
||||
"url": "https://kener.ing/monitor-mockoon"
|
||||
}
|
||||
]
|
||||
"id": "mockoon-9",
|
||||
"alert_name": "Mockoon DOWN",
|
||||
"severity": "critical",
|
||||
"status": "TRIGGERED",
|
||||
"source": "Kener",
|
||||
"timestamp": "2024-11-27T04:55:00.369Z",
|
||||
"description": "🚨 **Service Alert**: Check the details below",
|
||||
"details": {
|
||||
"metric": "Mockoon",
|
||||
"current_value": 1,
|
||||
"threshold": 1
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"text": "View Monitor",
|
||||
"url": "https://kener.ing/monitor-mockoon"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -118,7 +118,7 @@ Discord triggers are used to send a message to a discord channel when a monitor
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -147,7 +147,7 @@ The discord message when alert is `TRIGGERED` will look like this
|
||||
|
||||
The discord message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||

|
||||
|
||||
## Slack
|
||||
|
||||
@@ -155,7 +155,7 @@ Slack triggers are used to send a message to a slack channel when a monitor goes
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -185,7 +185,7 @@ The slack message when alert is `TRIGGERED` will look like this
|
||||
|
||||
The slack message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||

|
||||
|
||||
## Email
|
||||
|
||||
@@ -193,7 +193,7 @@ Email triggers are used to send an email when a monitor goes down or up. Kener s
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -250,11 +250,11 @@ Subject of the email when `RESOLVED`
|
||||
|
||||
The emaik message when alert is `TRIGGERED` will look like this
|
||||
|
||||

|
||||

|
||||
|
||||
The emaik message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -278,9 +278,9 @@ Set the URL to `https://api.telegram.org/bot[BOT_TOKEN]/sendMessage`. Replace [B
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_id": "[CHAT_ID]", // Replace [CHAT_ID] with your chat id
|
||||
"text": "<b>${alert_name}</b>\n\n<b>Severity:</b> <code>${severity}</code>\n<b>Status:</b> ${status}\n<b>Source:</b> Kener\n<b>Time:</b> ${timestamp}\n\n📌 <b>Details:</b>\n- <b>Metric:</b>${metric}\n- <b>Current Value:</b> <code>${current_value}</code>\n- <b>Threshold:</b> <code>${threshold}</code>\n\n🔍 <a href=\"${action_url}\">${action_text}</a>",
|
||||
"parse_mode": "HTML"
|
||||
"chat_id": "[CHAT_ID]", // Replace [CHAT_ID] with your chat id
|
||||
"text": "<b>${alert_name}</b>\n\n<b>Severity:</b> <code>${severity}</code>\n<b>Status:</b> ${status}\n<b>Source:</b> Kener\n<b>Time:</b> ${timestamp}\n\n📌 <b>Details:</b>\n- <b>Metric:</b>${metric}\n- <b>Current Value:</b> <code>${current_value}</code>\n- <b>Threshold:</b> <code>${threshold}</code>\n\n🔍 <a href=\"${action_url}\">${action_text}</a>",
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.1.0",
|
||||
"version": "3.1.2",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
|
||||
|
||||
57
src/docs.css
@@ -1,69 +1,82 @@
|
||||
code:not([class^="language-"]) {
|
||||
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs dark:bg-gray-800;
|
||||
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.sidebar-item.active,
|
||||
.sidebar-item:hover {
|
||||
color: #ed702d;
|
||||
color: #ed702d;
|
||||
}
|
||||
|
||||
.w-585px {
|
||||
width: 585px;
|
||||
width: 585px;
|
||||
}
|
||||
|
||||
main {
|
||||
scroll-behavior: smooth;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.kener-home-links a {
|
||||
text-decoration: none;
|
||||
background-color: var(--bg-background);
|
||||
text-decoration: none;
|
||||
background-color: var(--bg-background);
|
||||
}
|
||||
.kener-home-links > div:hover {
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 0 8px 1.5px #3e9a4b;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 0 8px 1.5px #3e9a4b;
|
||||
}
|
||||
|
||||
.accm input:checked ~ div {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
.accm input ~ div {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.accm input {
|
||||
visibility: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.accmt {
|
||||
background-color: hsl(223, 10%, 14%);
|
||||
background-color: hsl(223, 10%, 14%);
|
||||
}
|
||||
|
||||
.accm input:checked ~ .showaccm span:first-child {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
.accm input:checked ~ .showaccm span:last-child {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
.accm input ~ .showaccm span:first-child {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
.accm input ~ .showaccm span:last-child {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
background: transparent !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.note {
|
||||
@apply mt-4 rounded-md border bg-background p-3 text-sm shadow-sm;
|
||||
@apply mt-4 rounded-md border bg-background p-3 text-sm shadow-sm;
|
||||
}
|
||||
.note.danger {
|
||||
border: 1px solid #e3342f;
|
||||
color: #e3342f;
|
||||
border: 1px solid #e3342f;
|
||||
color: #e3342f;
|
||||
}
|
||||
|
||||
.note.info {
|
||||
border: 1px solid #3490dc;
|
||||
color: #3490dc;
|
||||
border: 1px solid #3490dc;
|
||||
color: #3490dc;
|
||||
}
|
||||
|
||||
.copybtn .copy-btn {
|
||||
transform: scale(1);
|
||||
}
|
||||
.copybtn .check-btn {
|
||||
transform: scale(0);
|
||||
}
|
||||
.copybtn:focus .copy-btn {
|
||||
transform: scale(0);
|
||||
}
|
||||
.copybtn:focus .check-btn {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
let formState = "idle";
|
||||
|
||||
export let newMonitor;
|
||||
|
||||
if (!!newMonitor && newMonitor.monitor_type == "PING" && !!!newMonitor.pingConfig.hosts) {
|
||||
newMonitor.pingConfig.hosts = [];
|
||||
}
|
||||
@@ -250,17 +251,34 @@
|
||||
invalidFormMessage = "Invalid Name Server";
|
||||
return;
|
||||
}
|
||||
//atleast one value should be present
|
||||
//at least one value should be present
|
||||
if (
|
||||
!newMonitor.dnsConfig.values ||
|
||||
!Array.isArray(newMonitor.dnsConfig.values) ||
|
||||
newMonitor.dnsConfig.values.length === 0
|
||||
) {
|
||||
invalidFormMessage = "Atleast one value is required";
|
||||
invalidFormMessage = "At least one value is required";
|
||||
return;
|
||||
}
|
||||
newMonitor.type_data = JSON.stringify(newMonitor.dnsConfig);
|
||||
} else if (newMonitor.monitor_type === "GROUP") {
|
||||
let selectedOnly = newMonitor.groupConfig.monitors.filter((monitor) => monitor.selected);
|
||||
if (selectedOnly.length < 2) {
|
||||
invalidFormMessage = "Select at least 2 monitors";
|
||||
return;
|
||||
}
|
||||
//timeout >=1000
|
||||
if (newMonitor.groupConfig.timeout < 1000) {
|
||||
invalidFormMessage = "Timeout should be greater than or equal to 1000";
|
||||
return;
|
||||
}
|
||||
newMonitor.type_data = JSON.stringify({
|
||||
monitors: selectedOnly,
|
||||
timeout: parseInt(newMonitor.groupConfig.timeout),
|
||||
hideMonitors: newMonitor.groupConfig.hideMonitors
|
||||
});
|
||||
}
|
||||
|
||||
formState = "loading";
|
||||
|
||||
try {
|
||||
@@ -385,7 +403,7 @@
|
||||
id="logo"
|
||||
type="file"
|
||||
disabled={loadingLogo}
|
||||
accept=".jpg, .jpeg, .png"
|
||||
accept=".jpg, .jpeg, .png, .svg"
|
||||
on:change={(e) => {
|
||||
handleFileChangeLogo(e);
|
||||
}}
|
||||
@@ -504,6 +522,7 @@
|
||||
<Select.Item value="PING" label="PING" class="text-sm font-medium">PING</Select.Item>
|
||||
<Select.Item value="DNS" label="DNS" class="text-sm font-medium">DNS</Select.Item>
|
||||
<Select.Item value="TCP" label="TCP" class="text-sm font-medium">TCP</Select.Item>
|
||||
<Select.Item value="GROUP" label="GROUP" class="text-sm font-medium">GROUP</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
@@ -868,6 +887,54 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if newMonitor.monitor_type == "GROUP"}
|
||||
<div class="mt-4 grid grid-cols-6 gap-2">
|
||||
<div class="col-span-6 mb-1">
|
||||
<Label for="timeout">
|
||||
Timeout(ms)
|
||||
<span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input bind:value={newMonitor.groupConfig.timeout} class="w-40" id="timeout" />
|
||||
<p class="my-1 text-xs text-muted-foreground">
|
||||
Maximum Time in milliseconds it will wait for all the monitors to resolved in that particular timestamp
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-span-6">
|
||||
<Label class="text-sm">Monitors</Label>
|
||||
</div>
|
||||
{#each newMonitor.groupConfig.monitors as monitor}
|
||||
<div class="col-span-3">
|
||||
<label class="cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
on:change={(e) => {
|
||||
monitor.selected = e.target.checked;
|
||||
}}
|
||||
checked={monitor.selected}
|
||||
/>
|
||||
{monitor.name}
|
||||
<span class="text-muted-foreground">({monitor.tag})</span>
|
||||
</label>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="col-span-6 mt-2 rounded-sm border p-2">
|
||||
<label class="cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
on:change={(e) => {
|
||||
newMonitor.groupConfig.hideMonitors = e.target.checked;
|
||||
}}
|
||||
checked={newMonitor.groupConfig.hideMonitors}
|
||||
/>
|
||||
Hide Grouped Monitors from Home View
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-span-6 mt-2">
|
||||
<ul class="text-xs font-medium leading-5 text-muted-foreground">
|
||||
<li>- The group status will be the worst status of the monitors in the group.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
//broadcast a custom event named blockScroll
|
||||
if (!!isMounted) {
|
||||
const noScrollEvent = new CustomEvent("noScroll", {
|
||||
detail: showAddMonitor
|
||||
detail: showAddMonitor || draggableMenu || shareMenusToggle
|
||||
});
|
||||
window.dispatchEvent(noScrollEvent);
|
||||
|
||||
@@ -87,10 +87,29 @@
|
||||
nameServer: "8.8.8.8",
|
||||
matchType: "ANY",
|
||||
values: []
|
||||
}
|
||||
},
|
||||
groupConfig: createGroupConfig({
|
||||
monitors: [],
|
||||
timeout: 10000,
|
||||
hideMonitors: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function createGroupConfig(groupConfig) {
|
||||
let eligibleMonitors = monitors
|
||||
.filter((m) => m.monitor_type != "GROUP" && m.status == "ACTIVE")
|
||||
.map((m) => {
|
||||
let isSelected = false;
|
||||
if (groupConfig.monitors) {
|
||||
isSelected = !!groupConfig.monitors.find((tm) => tm.id == m.id);
|
||||
}
|
||||
|
||||
return { id: m.id, name: m.name, tag: m.tag, selected: isSelected };
|
||||
});
|
||||
return { monitors: eligibleMonitors, timeout: groupConfig.timeout, hideMonitors: groupConfig.hideMonitors };
|
||||
}
|
||||
|
||||
function showUpdateMonitorSheet(m) {
|
||||
resetNewMonitor();
|
||||
newMonitor = { ...newMonitor, ...m };
|
||||
@@ -103,6 +122,8 @@
|
||||
newMonitor.dnsConfig = JSON.parse(newMonitor.type_data);
|
||||
} else if (newMonitor.monitor_type == "TCP") {
|
||||
newMonitor.tcpConfig = JSON.parse(newMonitor.type_data);
|
||||
} else if (newMonitor.monitor_type == "GROUP") {
|
||||
newMonitor.groupConfig = createGroupConfig(JSON.parse(newMonitor.type_data));
|
||||
}
|
||||
showAddMonitor = true;
|
||||
}
|
||||
@@ -142,7 +163,7 @@
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ action: "getTriggers", data: { status: "ACTIVE" } })
|
||||
body: JSON.stringify({ action: "getTriggers", data: {} })
|
||||
});
|
||||
triggers = await apiResp.json();
|
||||
} catch (error) {
|
||||
@@ -439,7 +460,13 @@
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<Button variant="secondary" class="h-8 w-8 p-2 " on:click={() => openAlertMenu(monitor)}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="h-8 w-8 p-2 {monitor.down_trigger?.active || monitor.degraded_trigger?.active
|
||||
? 'text-yellow-500'
|
||||
: ''}"
|
||||
on:click={() => openAlertMenu(monitor)}
|
||||
>
|
||||
<Bell class="inline h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" href="#{monitor.tag}">
|
||||
@@ -596,7 +623,7 @@
|
||||
<p class="col-span-4 mt-2 text-sm font-medium">Choose Triggers</p>
|
||||
{#each triggers as trigger}
|
||||
<div class="col-span-1 overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
<label class="cursor-pointer">
|
||||
<label class="cursor-pointer" class:line-through={trigger.trigger_status != "ACTIVE"}>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="text-sm"
|
||||
|
||||
@@ -8,18 +8,7 @@
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
import {
|
||||
Share2,
|
||||
Link,
|
||||
CopyCheck,
|
||||
Code,
|
||||
Settings,
|
||||
TrendingUp,
|
||||
Percent,
|
||||
Loader,
|
||||
ChevronLeft,
|
||||
ChevronRight
|
||||
} from "lucide-svelte";
|
||||
import { Share2, ArrowRight, Settings, TrendingUp, Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { afterUpdate } from "svelte";
|
||||
@@ -33,7 +22,6 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let monitor;
|
||||
|
||||
export let localTz;
|
||||
export let lang;
|
||||
export let embed = false;
|
||||
@@ -224,6 +212,16 @@
|
||||
>
|
||||
<Share2 class="h-4 w-4 " />
|
||||
</Button>
|
||||
{#if monitor.monitor_type === "GROUP"}
|
||||
<Button
|
||||
class="bounce-right h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
variant="link"
|
||||
href="{base}?group={monitor.tag}"
|
||||
rel="external"
|
||||
>
|
||||
<ArrowRight class="arrow h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ const alertingQueue = new Queue({
|
||||
});
|
||||
const apiQueue = 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)
|
||||
timeout: 2 * 60 * 1000, // Timeout in ms after which a task will be considered as failed (optional)
|
||||
autostart: true, // Automatically start the queue (optional)
|
||||
});
|
||||
|
||||
@@ -75,29 +75,6 @@ async function manualIncident(monitor) {
|
||||
return manualData;
|
||||
}
|
||||
|
||||
const tcpCall = async (hosts, tcpEval, tag) => {
|
||||
let arrayOfPings = [];
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
const host = hosts[i];
|
||||
arrayOfPings.push(await TCP(host.type, host.host, host.port, host.timeout));
|
||||
}
|
||||
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString("base64");
|
||||
|
||||
let evalResp = undefined;
|
||||
|
||||
try {
|
||||
evalResp = await eval(tcpEval + `("${respBase64}")`);
|
||||
} catch (error) {
|
||||
console.log(`Error in tcpEval for ${tag}`, error.message);
|
||||
}
|
||||
//reduce to get the status
|
||||
return {
|
||||
status: evalResp.status,
|
||||
latency: evalResp.latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
};
|
||||
|
||||
const Minuter = async (monitor) => {
|
||||
let realTimeData = {};
|
||||
let manualData = {};
|
||||
@@ -132,6 +109,8 @@ const Minuter = async (monitor) => {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "DNS") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "GROUP") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
|
||||
}
|
||||
|
||||
manualData = await manualIncident(monitor);
|
||||
|
||||
@@ -79,6 +79,30 @@ class DbImpl {
|
||||
return data;
|
||||
}
|
||||
|
||||
async getLastStatusBeforeCombined(monitor_tags_arr, timestamp) {
|
||||
//monitor_tag_arr is an array of string
|
||||
return await this.knex("monitoring_data")
|
||||
.select(
|
||||
"timestamp",
|
||||
this.knex.raw("COUNT(*) as total_entries"),
|
||||
this.knex.raw("AVG(latency) as latency"),
|
||||
this.knex.raw(`
|
||||
CASE
|
||||
WHEN SUM(CASE WHEN status = 'DOWN' THEN 1 ELSE 0 END) > 0 THEN 'DOWN'
|
||||
WHEN SUM(CASE WHEN status = 'DEGRADED' THEN 1 ELSE 0 END) > 0 THEN 'DEGRADED'
|
||||
ELSE 'UP'
|
||||
END as status
|
||||
`),
|
||||
)
|
||||
.whereIn("monitor_tag", monitor_tags_arr)
|
||||
.where("timestamp", "=", timestamp)
|
||||
.groupBy("timestamp")
|
||||
.having("total_entries", "=", monitor_tags_arr.length)
|
||||
.orderBy("timestamp", "desc")
|
||||
.limit(1)
|
||||
.first();
|
||||
}
|
||||
|
||||
async background() {
|
||||
//clear data older than 90 days
|
||||
let ninetyDaysAgo = GetMinuteStartNowTimestampUTC() - 86400 * 100;
|
||||
@@ -272,6 +296,14 @@ class DbImpl {
|
||||
if (!!data.id) {
|
||||
query = query.andWhere("id", data.id);
|
||||
}
|
||||
if (!!data.tag) {
|
||||
query = query.andWhere("tag", data.tag);
|
||||
}
|
||||
if (!!data.tags) {
|
||||
query = query.andWhere((builder) => {
|
||||
builder.whereIn("tag", data.tags);
|
||||
});
|
||||
}
|
||||
return await query.orderBy("id", "desc");
|
||||
}
|
||||
|
||||
@@ -307,7 +339,14 @@ class DbImpl {
|
||||
|
||||
//get all alerts with given status
|
||||
async getTriggers(data) {
|
||||
return await this.knex("triggers").where("trigger_status", data.status).orderBy("id", "desc");
|
||||
let query = this.knex("triggers").whereRaw("1=1");
|
||||
if (!!data.status) {
|
||||
query = query.andWhere("trigger_status", data.status);
|
||||
}
|
||||
if (!!data.id) {
|
||||
query = query.andWhere("id", data.id);
|
||||
}
|
||||
return await query.orderBy("id", "desc");
|
||||
}
|
||||
|
||||
//get trigger by id
|
||||
|
||||
37
src/lib/server/services/groupCall.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import axios from "axios";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences, Wait } from "../tool.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import db from "../db/db.js";
|
||||
|
||||
async function waitForDataAndReturn(arr, ts, d, maxTime) {
|
||||
await Wait(d);
|
||||
let data = await db.getLastStatusBeforeCombined(arr, ts);
|
||||
if (data) {
|
||||
data.type = REALTIME;
|
||||
return data;
|
||||
} else if (d > maxTime) {
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
type: TIMEOUT,
|
||||
};
|
||||
} else {
|
||||
return await waitForDataAndReturn(arr, ts, d * 2, maxTime);
|
||||
}
|
||||
}
|
||||
|
||||
class GroupCall {
|
||||
monitor;
|
||||
|
||||
constructor(monitor, timestamp) {
|
||||
this.monitor = monitor;
|
||||
}
|
||||
|
||||
async execute(startOfMinute) {
|
||||
let tagArr = this.monitor.type_data.monitors.map((m) => m.tag);
|
||||
return await waitForDataAndReturn(tagArr, startOfMinute, 500, this.monitor.type_data.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupCall;
|
||||
@@ -3,6 +3,7 @@ import ApiCall from "./apiCall.js";
|
||||
import PingCall from "./pingCall.js";
|
||||
import TcpCall from "./tcpCall.js";
|
||||
import DnsCall from "./dnsCall.js";
|
||||
import GroupCall from "./groupCall.js";
|
||||
|
||||
class Service {
|
||||
service;
|
||||
@@ -18,13 +19,15 @@ class Service {
|
||||
this.service = new DnsCall(monitor);
|
||||
} else if (monitor.monitor_type === "NONE") {
|
||||
this.service = null;
|
||||
} else if (monitor.monitor_type === "GROUP") {
|
||||
this.service = new GroupCall(monitor);
|
||||
} else {
|
||||
console.log("Invalid monitor.monitor_type ", monitor.monitor_type);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
async execute() {
|
||||
return await this.service.execute();
|
||||
async execute(...p) {
|
||||
return await this.service.execute(...p);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,68 +14,68 @@ const jobs = [];
|
||||
process.env.TZ = "UTC";
|
||||
|
||||
const jobSchedule = async () => {
|
||||
const activeMonitors = (await GetMonitorsParsed({ status: "ACTIVE" })).map((m) => {
|
||||
return {
|
||||
...m,
|
||||
hash: "Tag: " + m.tag + " Hash:" + HashString(JSON.stringify(m))
|
||||
};
|
||||
});
|
||||
const activeMonitors = (await GetMonitorsParsed({ status: "ACTIVE" })).map((m) => {
|
||||
return {
|
||||
...m,
|
||||
hash: "Tag: " + m.tag + " Hash:" + HashString(JSON.stringify(m)),
|
||||
};
|
||||
});
|
||||
|
||||
//stop and remove jobs not present in activeMonitors
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
const job = jobs[i];
|
||||
const monitor = activeMonitors.find((m) => m.hash === job.name);
|
||||
if (!monitor) {
|
||||
job.stop();
|
||||
jobs.splice(i, 1);
|
||||
i--;
|
||||
console.log("REMOVING JOB WITH NAME " + job.name);
|
||||
}
|
||||
}
|
||||
//stop and remove jobs not present in activeMonitors
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
const job = jobs[i];
|
||||
const monitor = activeMonitors.find((m) => m.hash === job.name);
|
||||
if (!monitor) {
|
||||
job.stop();
|
||||
jobs.splice(i, 1);
|
||||
i--;
|
||||
console.log("REMOVING JOB WITH NAME " + job.name);
|
||||
}
|
||||
}
|
||||
|
||||
//add new jobs from activeMonitors if not present already in jobs
|
||||
for (let i = 0; i < activeMonitors.length; i++) {
|
||||
const monitor = activeMonitors[i];
|
||||
const job = jobs.find((j) => j.name === monitor.hash);
|
||||
if (!job) {
|
||||
const j = Cron(
|
||||
monitor.cron,
|
||||
async () => {
|
||||
await Minuter(monitor);
|
||||
},
|
||||
{
|
||||
protect: true,
|
||||
name: monitor.hash
|
||||
}
|
||||
);
|
||||
console.log("ADDING NEW JOB WITH NAME " + j.name);
|
||||
j.trigger();
|
||||
jobs.push(j);
|
||||
}
|
||||
}
|
||||
//add new jobs from activeMonitors if not present already in jobs
|
||||
for (let i = 0; i < activeMonitors.length; i++) {
|
||||
const monitor = activeMonitors[i];
|
||||
const job = jobs.find((j) => j.name === monitor.hash);
|
||||
if (!job) {
|
||||
const j = Cron(
|
||||
monitor.cron,
|
||||
async () => {
|
||||
await Minuter(monitor);
|
||||
},
|
||||
{
|
||||
protect: true,
|
||||
name: monitor.hash,
|
||||
},
|
||||
);
|
||||
console.log("ADDING NEW JOB WITH NAME " + j.name);
|
||||
j.trigger();
|
||||
jobs.push(j);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function Startup() {
|
||||
const mainJob = Cron(
|
||||
"* * * * *",
|
||||
async () => {
|
||||
await jobSchedule();
|
||||
},
|
||||
{
|
||||
protect: true,
|
||||
name: "Main Job"
|
||||
}
|
||||
);
|
||||
const mainJob = Cron(
|
||||
"* * * * *",
|
||||
async () => {
|
||||
await jobSchedule();
|
||||
},
|
||||
{
|
||||
protect: true,
|
||||
name: "Main Job",
|
||||
},
|
||||
);
|
||||
|
||||
mainJob.trigger();
|
||||
mainJob.trigger();
|
||||
|
||||
figlet("Kener is UP!", function (err, data) {
|
||||
if (err) {
|
||||
console.log("Something went wrong...");
|
||||
return;
|
||||
}
|
||||
console.log(data);
|
||||
});
|
||||
figlet("Kener is UP!", function (err, data) {
|
||||
if (err) {
|
||||
console.log("Something went wrong...");
|
||||
return;
|
||||
}
|
||||
console.log(data);
|
||||
});
|
||||
}
|
||||
|
||||
// Call Startup() if not imported as a module
|
||||
@@ -83,7 +83,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
if (process.argv[1] === __filename) {
|
||||
Startup();
|
||||
Startup();
|
||||
}
|
||||
|
||||
export default Startup;
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
import "../../docs.css";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Sun from "lucide-svelte/icons/sun";
|
||||
import Menu from "lucide-svelte/icons/menu";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import { clickOutsideAction, slide } from "svelte-legos";
|
||||
import Moon from "lucide-svelte/icons/moon";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
@@ -44,9 +47,15 @@
|
||||
tableOfContents = e.detail.rightbar;
|
||||
}
|
||||
}
|
||||
let sideBarHidden = true;
|
||||
//if desktop show sidebar by default
|
||||
|
||||
let isMounted = false;
|
||||
onMount(() => {
|
||||
setTheme();
|
||||
if (window.innerWidth > 768) {
|
||||
sideBarHidden = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -74,7 +83,7 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
|
||||
</svelte:head>
|
||||
<div class="dark">
|
||||
<nav class="z-2 fixed left-0 right-0 top-0 z-30 h-16 bg-card">
|
||||
<nav class="fixed left-0 right-0 top-0 z-40 h-16 bg-card">
|
||||
<div class="mx-auto h-full border-b bg-card px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-full items-center justify-between">
|
||||
<!-- Logo/Brand -->
|
||||
@@ -83,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.0 </span>
|
||||
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.1.2 </span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -104,10 +113,18 @@
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="md:hidden">
|
||||
<button type="button" class="hover: text-muted-foreground">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-2 hover:text-muted-foreground"
|
||||
on:click={() => {
|
||||
sideBarHidden = !sideBarHidden;
|
||||
}}
|
||||
>
|
||||
{#if sideBarHidden}
|
||||
<Menu class="h-6 w-6" />
|
||||
{:else}
|
||||
<X class="h-6 w-6" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,34 +132,40 @@
|
||||
</nav>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="z-2 fixed bottom-0 left-0 top-16 w-72 overflow-y-auto">
|
||||
<nav class="border-r bg-card p-6">
|
||||
<!-- Getting Started Section -->
|
||||
{#each sidebar as item}
|
||||
<div class="mb-4">
|
||||
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{item.sectionTitle}
|
||||
</h3>
|
||||
<div class="">
|
||||
{#each item.children as child}
|
||||
<a
|
||||
href={child.link.startsWith("/") ? base + child.link : child.link}
|
||||
class="sidebar-item group flex items-center rounded-md px-3 py-2 text-sm font-medium {!!child.active
|
||||
? 'active'
|
||||
: ''}"
|
||||
>
|
||||
{child.title}
|
||||
</a>
|
||||
{/each}
|
||||
{#if !sideBarHidden}
|
||||
<aside
|
||||
transition:slide={{ direction: "left", duration: 200 }}
|
||||
class="fixed bottom-0 left-0 top-16 z-30 w-72 overflow-y-auto md:block"
|
||||
class:hidden={sideBarHidden}
|
||||
>
|
||||
<nav class="border-r bg-card p-6">
|
||||
<!-- Getting Started Section -->
|
||||
{#each sidebar as item}
|
||||
<div class="mb-4">
|
||||
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{item.sectionTitle}
|
||||
</h3>
|
||||
<div class="">
|
||||
{#each item.children as child}
|
||||
<a
|
||||
href={child.link.startsWith("/") ? base + child.link : child.link}
|
||||
class="sidebar-item group flex items-center rounded-md px-3 py-2 text-sm font-medium {!!child.active
|
||||
? 'active'
|
||||
: ''}"
|
||||
>
|
||||
{child.title}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="z-2 dark relative ml-72 min-h-screen pt-16">
|
||||
<div class="mx-auto max-w-5xl px-4 py-10 sm:px-6 lg:px-8 lg:pr-64">
|
||||
<main class="dark relative z-10 min-h-screen pt-16 md:ml-72">
|
||||
<div class="mx-auto max-w-5xl px-4 py-10 sm:px-6 md:px-8 md:pr-64">
|
||||
<!-- Content Header -->
|
||||
<div
|
||||
class="prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
|
||||
@@ -152,7 +175,7 @@
|
||||
</div>
|
||||
</main>
|
||||
{#if tableOfContents.length > 0}
|
||||
<div class="blurry-bg fixed bottom-0 right-0 top-16 hidden w-64 overflow-y-auto px-6 py-10 lg:block">
|
||||
<div class="blurry-bg fixed bottom-0 right-0 top-16 z-50 hidden w-64 overflow-y-auto px-6 py-10 lg:block">
|
||||
<h4 class="mb-3 text-sm font-semibold uppercase tracking-wider">On this page</h4>
|
||||
<nav class="space-y-2">
|
||||
{#each tableOfContents as item}
|
||||
|
||||
@@ -1,63 +1,153 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { afterNavigate } from "$app/navigation";
|
||||
export let data;
|
||||
let subHeadings = [];
|
||||
let previousH2 = null;
|
||||
function fillSubHeadings() {
|
||||
subHeadings = [];
|
||||
const headings = document.querySelectorAll("#markdown h2, #markdown h3");
|
||||
headings.forEach((heading) => {
|
||||
let id = heading.textContent.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
if (heading.tagName === "H2") {
|
||||
previousH2 = id;
|
||||
} else {
|
||||
id = `${previousH2}-${id}`;
|
||||
}
|
||||
heading.id = id;
|
||||
subHeadings.push({
|
||||
id,
|
||||
text: heading.textContent,
|
||||
level: heading.tagName === "H2" ? 2 : 3
|
||||
});
|
||||
});
|
||||
}
|
||||
import { onMount } from "svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-svelte";
|
||||
import { afterNavigate } from "$app/navigation";
|
||||
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");
|
||||
headings.forEach((heading) => {
|
||||
let id = heading.textContent.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
if (heading.tagName === "H2") {
|
||||
previousH2 = id;
|
||||
} else {
|
||||
id = `${previousH2}-${id}`;
|
||||
}
|
||||
heading.id = id;
|
||||
subHeadings.push({
|
||||
id,
|
||||
text: heading.textContent,
|
||||
level: heading.tagName === "H2" ? 2 : 3
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
afterNavigate(() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("pagechange", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
docFilePath: data.docFilePath
|
||||
}
|
||||
})
|
||||
);
|
||||
fillSubHeadings();
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("rightbar", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
rightbar: subHeadings
|
||||
}
|
||||
})
|
||||
);
|
||||
hljs.highlightAll();
|
||||
});
|
||||
onMount(async () => {});
|
||||
let nextPath;
|
||||
let previousPath;
|
||||
|
||||
function scrollToId(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
const y = element.getBoundingClientRect().top + window.pageYOffset - 100;
|
||||
window.scrollTo({ top: y, behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
|
||||
//function to iterate all h1,h2 nd h3 and add a button inside them
|
||||
function addCopyButton() {
|
||||
const headings = document.querySelectorAll("#markdown h1, #markdown h2, #markdown h3");
|
||||
headings.forEach((heading) => {
|
||||
const button = document.createElement("button");
|
||||
button.classList.add("copybtn");
|
||||
button.classList.add("relative");
|
||||
button.innerHTML = `<svg class="copy-btn left-0 top-0 absolute" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#777" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
<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);
|
||||
};
|
||||
heading.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function handleRightBarClick(e) {
|
||||
if (e.target.tagName === "A") {
|
||||
e.preventDefault();
|
||||
const id = e.target.getAttribute("href").slice(1);
|
||||
scrollToId(id);
|
||||
}
|
||||
}
|
||||
|
||||
//function to find next and previous path
|
||||
function findNextAndPreviousPath() {
|
||||
let structure = data.siteStructure.sidebar;
|
||||
for (let i = 0; i < structure.length; i++) {
|
||||
let children = structure[i].children;
|
||||
|
||||
for (let j = 0; j < children.length; j++) {
|
||||
if (children[j].file === data.docFilePath) {
|
||||
if (j > 0) {
|
||||
previousPath = children[j - 1];
|
||||
}
|
||||
if (j < children.length - 1) {
|
||||
nextPath = children[j + 1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterNavigate(() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("pagechange", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
docFilePath: data.docFilePath
|
||||
}
|
||||
})
|
||||
);
|
||||
fillSubHeadings();
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("rightbar", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
rightbar: subHeadings
|
||||
}
|
||||
})
|
||||
);
|
||||
hljs.highlightAll();
|
||||
//if hash present scroll to hash
|
||||
if (location.hash) {
|
||||
scrollToId(location.hash.slice(1));
|
||||
}
|
||||
//if hash change scroll
|
||||
window.addEventListener("hashchange", () => {
|
||||
scrollToId(location.hash.slice(1));
|
||||
});
|
||||
window.addEventListener("click", (e) => {
|
||||
handleRightBarClick(e);
|
||||
});
|
||||
addCopyButton();
|
||||
findNextAndPreviousPath();
|
||||
});
|
||||
onMount(async () => {});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.title}</title>
|
||||
<meta name="description" content={data.description} />
|
||||
<link rel="canonical" href="https://kener.ing/docs{data.docFilePath.split('.md')[0]}" />
|
||||
<title>{data.title}</title>
|
||||
<meta name="description" content={data.description} />
|
||||
<link rel="canonical" href="https://kener.ing/docs{subPath}" />
|
||||
</svelte:head>
|
||||
|
||||
<div id="markdown">
|
||||
{@html data.md}
|
||||
{@html data.md}
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-2">
|
||||
<div class="col-span-1">
|
||||
{#if previousPath}
|
||||
<Button variant="outline" class="no-underline" href={previousPath.link}>
|
||||
<ArrowLeft class="mr-2 h-4 w-4" />
|
||||
<span>{previousPath.title}</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-span-1 flex justify-end">
|
||||
{#if nextPath}
|
||||
<Button variant="outline" class="no-underline" href={nextPath.link}>
|
||||
<span>{nextPath.title}</span>
|
||||
<ArrowRight class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#markdown {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
#markdown {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,17 +7,77 @@ function removeTags(str) {
|
||||
if (str === null || str === "") return false;
|
||||
else str = str.toString();
|
||||
|
||||
// Regular expression to identify HTML tags in
|
||||
// the input string. Replacing the identified
|
||||
// HTML tag with a null string.
|
||||
return str.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
export async function load({ parent, url }) {
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
|
||||
async function returnTypeOfMonitorsPageMeta(url) {
|
||||
const query = url.searchParams;
|
||||
const requiredCategory = query.get("category") || "Home";
|
||||
let filter = {
|
||||
status: "ACTIVE",
|
||||
};
|
||||
|
||||
let pageType = "home";
|
||||
let group;
|
||||
if (!!query.get("category") && query.get("category") !== "Home") {
|
||||
filter.category_name = query.get("category");
|
||||
pageType = "category";
|
||||
}
|
||||
|
||||
if (!!query.get("monitor")) {
|
||||
filter.tag = query.get("monitor");
|
||||
pageType = "monitor";
|
||||
}
|
||||
|
||||
if (!!query.get("group")) {
|
||||
let g = await GetMonitors({ status: "ACTIVE", tag: query.get("group") });
|
||||
if (g.length > 0) {
|
||||
group = g[0];
|
||||
let typeData = JSON.parse(g[0].type_data);
|
||||
let groupPresentMonitorTags = typeData.monitors.map((monitor) => monitor.tag);
|
||||
filter.tags = groupPresentMonitorTags;
|
||||
pageType = "group";
|
||||
}
|
||||
}
|
||||
|
||||
let monitors = await GetMonitors(filter);
|
||||
return { monitors, pageType, group };
|
||||
}
|
||||
|
||||
export async function load({ parent, url }) {
|
||||
const query = url.searchParams;
|
||||
|
||||
let { monitors, pageType, group } = await returnTypeOfMonitorsPageMeta(url);
|
||||
let hiddenGroupedMonitorsTags = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
if (pageType === "home" && monitors[i].monitor_type === "GROUP") {
|
||||
let typeData = JSON.parse(monitors[i].type_data);
|
||||
if (typeData.monitors && typeData.hideMonitors) {
|
||||
hiddenGroupedMonitorsTags = hiddenGroupedMonitorsTags.concat(typeData.monitors.map((monitor) => monitor.tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
monitors = monitors
|
||||
.map((monitor) => {
|
||||
return {
|
||||
tag: monitor.tag,
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
monitor_type: monitor.monitor_type,
|
||||
image: monitor.image,
|
||||
category_name: monitor.category_name,
|
||||
day_degraded_minimum_count: monitor.day_degraded_minimum_count,
|
||||
day_down_minimum_count: monitor.day_down_minimum_count,
|
||||
id: monitor.id,
|
||||
image: monitor.image,
|
||||
include_degraded_in_downtime: monitor.include_degraded_in_downtime,
|
||||
};
|
||||
})
|
||||
.filter((monitor) => !hiddenGroupedMonitorsTags.includes(monitor.tag));
|
||||
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
let hero = siteData.hero;
|
||||
|
||||
let pageTitle = siteData.title;
|
||||
let canonical = siteData.siteURL;
|
||||
let pageDescription = "";
|
||||
@@ -25,48 +85,66 @@ export async function load({ parent, url }) {
|
||||
if (descDb) {
|
||||
pageDescription = descDb.value;
|
||||
}
|
||||
|
||||
monitors = SortMonitor(siteData.monitorSort, monitors);
|
||||
const monitorsActive = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (!!!query.get("monitor") && !!monitors[i].category_name && monitors[i].category_name !== requiredCategory) {
|
||||
continue;
|
||||
}
|
||||
if (query.get("monitor") && query.get("monitor") !== monitors[i].tag) {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].default_status;
|
||||
|
||||
let data = await FetchData(siteData, monitors[i], parentData.localTz, parentData.selectedLang, parentData.lang);
|
||||
monitors[i].pageData = data;
|
||||
|
||||
monitors[i].activeIncidents = [];
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
|
||||
let startWithin = moment().subtract(siteData.homeIncidentStartTimeWithin, "days").unix();
|
||||
let endWithin = moment().add(siteData.homeIncidentStartTimeWithin, "days").unix();
|
||||
let allOpenIncidents = await GetIncidentsOpenHome(siteData.homeIncidentCount, startWithin, endWithin);
|
||||
|
||||
//if not home page
|
||||
let isCategoryPage = !!query.get("category") && query.get("category") !== "Home";
|
||||
let isMonitorPage = !!query.get("monitor");
|
||||
if (isMonitorPage && monitorsActive.length > 0) {
|
||||
let isCategoryPage = pageType === "category";
|
||||
let isMonitorPage = pageType === "monitor";
|
||||
let isGroupPage = pageType === "group";
|
||||
|
||||
if (isMonitorPage) {
|
||||
pageTitle = monitorsActive[0].name + " - " + pageTitle;
|
||||
pageDescription = monitorsActive[0].description;
|
||||
canonical = canonical + "?monitor=" + monitorsActive[0].tag;
|
||||
hero = {
|
||||
title: monitorsActive[0].name,
|
||||
subtitle: monitorsActive[0].description,
|
||||
image: monitorsActive[0].image,
|
||||
};
|
||||
}
|
||||
|
||||
if (isGroupPage) {
|
||||
pageTitle = group.name + " - " + pageTitle;
|
||||
pageDescription = group.description;
|
||||
canonical = canonical + "?group=" + group.tag;
|
||||
|
||||
hero = {
|
||||
title: group.name,
|
||||
subtitle: group.description,
|
||||
image: group.image,
|
||||
};
|
||||
}
|
||||
|
||||
//if category page
|
||||
if (isCategoryPage) {
|
||||
let allCategories = siteData.categories;
|
||||
let selectedCategory = allCategories.find((category) => category.name === requiredCategory);
|
||||
let selectedCategory = allCategories.find((category) => category.name === query.get("category"));
|
||||
if (selectedCategory) {
|
||||
pageTitle = selectedCategory.name + " - " + pageTitle;
|
||||
pageDescription = selectedCategory.description;
|
||||
canonical = canonical + "?category=" + requiredCategory;
|
||||
canonical = canonical + "?category=" + query.get("category");
|
||||
|
||||
hero = {
|
||||
title: selectedCategory.name,
|
||||
subtitle: selectedCategory.description,
|
||||
image: selectedCategory.image,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (isCategoryPage || isMonitorPage) {
|
||||
if (isCategoryPage || isMonitorPage || isGroupPage) {
|
||||
let eligibleTags = monitorsActive.map((monitor) => monitor.tag);
|
||||
//filter incidents that have monitor_tag in monitors
|
||||
allOpenIncidents = allOpenIncidents.filter((incident) => {
|
||||
@@ -105,11 +183,11 @@ export async function load({ parent, url }) {
|
||||
monitors: monitorsActive,
|
||||
allRecentIncidents,
|
||||
allRecentMaintenances,
|
||||
categoryName: requiredCategory,
|
||||
isCategoryPage: isCategoryPage,
|
||||
isMonitorPage: isMonitorPage,
|
||||
pageType,
|
||||
pageTitle: pageTitle,
|
||||
pageDescription: removeTags(pageDescription),
|
||||
hero,
|
||||
categoryName: query.get("category"),
|
||||
canonical,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,23 +32,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let isHome = !data.isCategoryPage && !data.isMonitorPage;
|
||||
|
||||
if (data.isCategoryPage) {
|
||||
let category = data.site.categories.find((e) => e.name == data.categoryName);
|
||||
if (!!category) {
|
||||
data.site.hero.title = category.name;
|
||||
data.site.hero.subtitle = category.description;
|
||||
}
|
||||
} else if (data.isMonitorPage) {
|
||||
let monitor = data.monitors[0];
|
||||
if (!!monitor) {
|
||||
data.site.hero.title = monitor.name;
|
||||
data.site.hero.subtitle = monitor.description;
|
||||
data.site.hero.image = monitor.image;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
pageLoaded = true;
|
||||
});
|
||||
@@ -79,39 +62,40 @@
|
||||
{/if}
|
||||
</svelte:head>
|
||||
<div class="mt-12"></div>
|
||||
{#if data.site.hero}
|
||||
{#if data.hero}
|
||||
<section class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
|
||||
<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.site.hero.image}
|
||||
{#if data.hero.image}
|
||||
<GMI src={data.site.hero.image} classList="m-auto mb-2 h-14 w-14" alt="" srcset="" />
|
||||
{/if}
|
||||
{#if data.site.hero.title}
|
||||
{#if data.hero.title}
|
||||
<h1
|
||||
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-tight text-transparent"
|
||||
>
|
||||
{@html data.site.hero.title}
|
||||
{@html data.hero.title}
|
||||
</h1>
|
||||
{/if}
|
||||
{#if data.site.hero.subtitle}
|
||||
{#if data.hero.subtitle}
|
||||
<h2 class="mx-auto mt-4 max-w-xl sm:text-xl">
|
||||
{@html data.site.hero.subtitle}
|
||||
{@html data.hero.subtitle}
|
||||
</h2>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{#if !isHome}
|
||||
{#if data.pageType != "home"}
|
||||
<section class="mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="bounce-left h-8 justify-start pl-1.5"
|
||||
on:click={() => {
|
||||
if (data.isCategoryPage) {
|
||||
if (data.pageType == "category") {
|
||||
return window.history.back();
|
||||
}
|
||||
if (data.isMonitorPage) {
|
||||
} else if (data.pageType == "monitor") {
|
||||
return (window.location.href = `${base}/`);
|
||||
} else if (data.pageType == "group") {
|
||||
return (window.location.href = `${base}/`);
|
||||
}
|
||||
}}
|
||||
@@ -222,7 +206,7 @@
|
||||
</Card.Root>
|
||||
</section>
|
||||
{/if}
|
||||
{#if data.site.categories && isHome}
|
||||
{#if data.site.categories && data.pageType == "home"}
|
||||
<section
|
||||
class="relative z-10 mx-auto mb-8 w-full max-w-[890px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
|
||||
>
|
||||
|
||||
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 670 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
BIN
static/documentation/m_group.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 168 B After Width: | Height: | Size: 168 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 300 B After Width: | Height: | Size: 300 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
BIN
static/in_1.png
|
Before Width: | Height: | Size: 127 KiB |
BIN
static/in_2.png
|
Before Width: | Height: | Size: 212 KiB |
BIN
static/in_3.png
|
Before Width: | Height: | Size: 100 KiB |
BIN
static/in_4.png
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 460 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 547 KiB |
|
Before Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 655 KiB |
|
Before Width: | Height: | Size: 677 KiB |
|
Before Width: | Height: | Size: 508 KiB |
BIN
static/ss.png
|
Before Width: | Height: | Size: 420 KiB |
BIN
static/ss2.png
|
Before Width: | Height: | Size: 70 KiB |
BIN
static/ss3.png
|
Before Width: | Height: | Size: 9.1 KiB |