Updates documentation and Dockerfile configuration
Updates documentation to reflect the new directory structure. The documentation now correctly references images in the `/documentation` directory. Removes the `src/static/documentation` directory in the Dockerfile.
@@ -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>
|
||||
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -163,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) {
|
||||
@@ -623,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"
|
||||
|
||||
@@ -339,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
|
||||
|
||||
@@ -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 -->
|
||||
@@ -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>
|
||||
|
||||
|
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 |
|
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 |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
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 |
|
Before Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |