mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 07:14:56 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b4ca67cf0 | |||
| 53936e19d4 | |||
| b58d00c5a6 | |||
| 41db0c45cd | |||
| 5361b551eb | |||
| eb87647466 | |||
| d9b900f28d | |||
| 198bd6c723 | |||
| 1831e6309f | |||
| 007f5149d2 | |||
| 1a094e3511 | |||
| db1ddc1292 | |||
| 535e3bb36a | |||
| 785237e55e | |||
| e6bf47a859 | |||
| 93ae1711f1 | |||
| af97812beb | |||
| 78cf22ade9 | |||
| b4a461d93a | |||
| 5f33c02064 | |||
| 46c1a392b8 | |||
| c6880c5df6 | |||
| 646da94ef9 | |||
| 36ede93dce | |||
| 9ed35589f7 | |||
| e946dd18e0 | |||
| 51cad598c1 | |||
| 0595f23c18 | |||
| b88816706b | |||
| 80edaae023 | |||
| 883c46b064 | |||
| 4248cc31f2 | |||
| 5f81c6d61a | |||
| 00d82cc817 | |||
| a9edfde162 | |||
| 468d7f445d | |||
| 4cc74deece | |||
| f9831490af | |||
| a055a616eb | |||
| 8009a2cbc4 | |||
| 693cc149cf | |||
| d126fe0bce | |||
| 4cc49cce9e | |||
| f7cb4cd805 | |||
| a2bd9883d1 | |||
| 10a079c304 | |||
| 65f12f1082 | |||
| c15f855c72 | |||
| 3d38483022 | |||
| 57dc1e7175 | |||
| ead68c929f |
@@ -5,6 +5,7 @@ KENER_BASE_PATH=""
|
||||
RESEND_API_KEY=
|
||||
ORIGIN=http://localhost:3000
|
||||
DATABASE_URL=sqlite://./database/kener.sqlite.db
|
||||
TZ=UTC
|
||||
RESEND_SENDER_EMAIL=Accounts <accounts@resend.dev>
|
||||
|
||||
# DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Contributing to Kener
|
||||
|
||||
Thank you for considering contributing to our project! Here are some guidelines to help you get started.
|
||||
|
||||
---
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. Fork the repository and clone it locally.
|
||||
2. Create a new branch for your feature or bug fix:
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
3. Make your changes and commit them:
|
||||
```bash
|
||||
git commit -m 'Describe your changes'
|
||||
```
|
||||
4. Push your changes to your fork:
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
5. Create a pull request to the `main` branch.
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
2. Create a `.env` file in the root of the project and add the following:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
2. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
3. Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is available in the `docs` folder. You can view it by going to [http://localhost:3000/docs/home](http://localhost:3000/docs/home) in your browser.
|
||||
|
||||
## Where to Start
|
||||
|
||||
1. Check out the [roadmap items](https://kener.ing/docs/roadmap/)
|
||||
2. Add language support by following the [i18n guide](https://kener.ing/docs/i18n/)
|
||||
+6
-1
@@ -15,8 +15,10 @@ RUN apt-get update && apt-get install -y \
|
||||
iputils-ping && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the timezone environment variable
|
||||
# Set the timezone environment variable and the application environment
|
||||
ARG KENER_BASE_PATH=
|
||||
ENV TZ=Etc/UTC
|
||||
ENV KENER_BASE_PATH=${KENER_BASE_PATH}
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
@@ -28,6 +30,9 @@ RUN npm install && npm cache clean --force
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# remove dir src/routes/(docs)
|
||||
RUN rm -rf src/routes/\(docs\)
|
||||
|
||||
# Ensure /app/uploads and /app/database have rw permissions
|
||||
RUN mkdir -p /app/uploads /app/database && \
|
||||
chmod -R 777 /app/uploads /app/database
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Kener - A Beautiful Status Page System
|
||||
# Kener - Stunning Status Pages
|
||||
|
||||
<p align="center">
|
||||
<img src="https://kener.ing/newbg.png?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
|
||||
@@ -25,11 +25,8 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
#### [👉 Visit a live server](https://kener.ing)
|
||||
|
||||
#### [👉 Quick Start](https://kener.ing/docs/quick-start)
|
||||
|
||||
#### [👉 Documentation](https://kener.ing/docs/home)
|
||||
| [🌍 Live Server](https://kener.ing) | [🎉 Quick Start](https://kener.ing/docs/quick-start) | [🗄 Documentation](https://kener.ing/docs/home) |
|
||||
| ----------------------------------- | ---------------------------------------------------- | ----------------------------------------------- |
|
||||
|
||||
## What is Kener?
|
||||
|
||||
@@ -41,10 +38,29 @@ It comes with all the basic asks for a status page. It is open-source and free t
|
||||
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a cheaply available domain.
|
||||
|
||||
<div align="left">
|
||||
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
|
||||
</div>
|
||||
|
||||
## Installation
|
||||
|
||||
### Manual
|
||||
|
||||
```shell
|
||||
# Clone the repository
|
||||
git clone https://github.com/rajnandan1/kener.git
|
||||
cd kener
|
||||
npm install
|
||||
cp .env.example .env
|
||||
npm run build
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
The latest image is available on DockerHub at [`rajnandan1/kener:latest`](https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest).
|
||||
Download and use the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
|
||||
|
||||
### One Click
|
||||
|
||||
[](https://railway.com/template/spSvic?referralCode=1Pn7vs)
|
||||
|
||||
## Features
|
||||
|
||||
Here are some of the features that you get out of the box. Please read the documentation to know how to use them.
|
||||
@@ -88,6 +104,12 @@ Here are some of the features that you get out of the box. Please read the docum
|
||||
- Auto SEO and Social Media ready
|
||||
- Server Side Rendering
|
||||
|
||||
|
||||
<div align="left">
|
||||
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
|
||||
</div>
|
||||
|
||||
|
||||
## Technologies used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
@@ -103,6 +125,10 @@ If you are using Kener and want to support me, you can do so by sponsoring me on
|
||||
|
||||

|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to contribute to Kener, please read the [Contributing Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#rajnandan1/kener&Date)
|
||||
|
||||
@@ -27,6 +27,10 @@ DNS Monitor is used to check the status of a DNS server. It can be configured to
|
||||
|
||||
Ping Monitor is used to check the status of a server. It can be configured to check the status of the server at different intervals.
|
||||
|
||||
### Data Interpolation
|
||||
|
||||
Kener interpolates the data for missing intervals. For example, suppose if you have a cron monitor of 5 minutes and it has run twice at 10AM and 10:05AM and the monitor is `DOWN` at 10AM. Kener will interpolate the data for 10:01AM, 10:02AM, 10:03AM and 10:04AM as `DOWN`. Similarly, if the monitor is UP at 10:05AM, Kener will interpolate the data for 10:06AM, 10:07AM, 10:08AM and 10:09AM as UP. Similarly for missing data points it will consider the previous data point. If there is no previous data point, it will consider it as `UP`.
|
||||
|
||||
---
|
||||
|
||||
## Triggers
|
||||
|
||||
@@ -29,4 +29,14 @@ Do not forget to add the base path if you are using a subpath. For example, if y
|
||||
|
||||
### Inline CSS
|
||||
|
||||
To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS there. Do not include `<style>` tags.
|
||||
To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS there.
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
<div class="note danger">
|
||||
Do not include <style> tags.
|
||||
</div>
|
||||
|
||||
+21
-13
@@ -90,10 +90,6 @@ docker run \
|
||||
|
||||
Or use **Docker Compose** with the example [docker-compose.yaml](https://raw.githubusercontent.com/rajnandan1/kener/main/docker-compose.yml)
|
||||
|
||||
#### Base path
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path. Read more about it [here](/docs/environment-vars/#kener-base-path)
|
||||
|
||||
#### Postgres
|
||||
|
||||
```bash
|
||||
@@ -118,17 +114,29 @@ docker run \
|
||||
rajnandan1/kener
|
||||
```
|
||||
|
||||
#### Base Path
|
||||
#### Base path
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path. Read more about it [here](/docs/environment-vars/#kener-base-path).
|
||||
|
||||
<div class="note info">
|
||||
|
||||
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
|
||||
- Important: This env variable should be present during both build and run time
|
||||
|
||||
</div>
|
||||
|
||||
Let us say you are running kener on a subpath `/status`. You can set the base path like this:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/uploads:/app/uploads \
|
||||
-e "KENER_BASE_PATH=/status" \
|
||||
-e "KENER_SECRET_KEY=somesecretkey" \
|
||||
-e "DATABASE_URL=mysql://root:password@mysql-container-2.orb.local:3306/kener-2" \
|
||||
-e "ORIGIN=https://www.example.com" \
|
||||
rajnandan1/kener
|
||||
docker build --build-arg KENER_BASE_PATH=/status -t kener .
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 --env-file .env -v $(pwd)/database:/app/uploads -v $(pwd)/database:/app/database kener
|
||||
```
|
||||
|
||||
## Railway
|
||||
|
||||
You can deploy Kener on [Railway](https://railway.app) with a single click.
|
||||
|
||||
[](https://railway.com/template/spSvic?referralCode=1Pn7vs)
|
||||
|
||||
+4
-4
@@ -33,8 +33,8 @@ You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `monitor`: The monitor url
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
|
||||
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
|
||||
@@ -79,8 +79,8 @@ Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
|
||||
### Demo
|
||||
|
||||
|
||||
@@ -33,14 +33,6 @@ Defaults to 3000 if not specified
|
||||
export PORT=4242
|
||||
```
|
||||
|
||||
## GH_TOKEN
|
||||
|
||||
A github token to read issues and create labels. This is required for **incident management**
|
||||
|
||||
```bash
|
||||
export GH_TOKEN=your-github-token
|
||||
```
|
||||
|
||||
## KENER_BASE_PATH
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path.
|
||||
|
||||
+8
-3
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Kener - A Sveltekit NodeJS Status Page System
|
||||
description: Kener is an open-source Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents.
|
||||
title: Kener Documentation
|
||||
description: Kener is a feature-rich and modern status page system built with SvelteKit and NodeJS. It is open-source and free to use.
|
||||
---
|
||||
|
||||
# Kener - A Feature Rich & Modern Status Page
|
||||
@@ -59,7 +59,12 @@ Kener is status page system built with Sveltekit and NodeJS. It does not try to
|
||||
|
||||
It comes with all the basic asks for a status page. It is open-source and free to use.
|
||||
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a cheaply available domain.
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a
|
||||
cheaply available domain.
|
||||
|
||||
## Quick Deployment
|
||||
|
||||
[](https://railway.com/template/spSvic?referralCode=1Pn7vs)
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
+6
-6
@@ -59,7 +59,7 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
"MONITORING": "MONITORING",
|
||||
"No Data": "No Data",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Incident On %date": "No Incident On %date",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
@@ -69,10 +69,10 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
"Share": "Share",
|
||||
"Share this monitor using a link with others": "Share this monitor using a link with others",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Started about %startedAt ago, lasted for about %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Started about %startedAt ago, still ongoing",
|
||||
"Starts in %startedAt": "Starts in %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Starts in %startedAt, will last for about %lastedFor",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Started about %startedAt, still ongoing",
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
|
||||
"Status": "Status",
|
||||
"Status OK": "Status OK",
|
||||
"Theme": "Theme",
|
||||
@@ -129,7 +129,7 @@ Open `src/lib/i18n/client.js` and add the locale code to the `locales` array.
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale";
|
||||
```
|
||||
|
||||
This should be valid for the date-fns library.
|
||||
This should be valid for the date-fns library. You can find the list of locales [here](https://github.com/date-fns/date-fns/blob/9bb51691f201c3ec05ab832acbc5d478f2e5c47a/docs/i18nLocales.md).
|
||||
|
||||
Next add a mapping in the `locales` object.
|
||||
|
||||
|
||||
+33
-7
@@ -45,13 +45,13 @@ The headers are used to define the headers that should be sent with the request.
|
||||
|
||||
The eval is used to define the JavaScript code that should be used to evaluate the response. It is optional and has be a valid JavaScript code.
|
||||
|
||||
This is a anonymous JS function, by default it looks like this.
|
||||
This is an anonymous JS function, it should return a **Promise**, that resolves or rejects to `{status, latency}`, by default it looks like this.
|
||||
|
||||
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and latency(number)
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
(function (statusCode, responseTime, responseDataBase64) {
|
||||
(async function (statusCode, responseTime, responseDataBase64) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
let status = 'DOWN'
|
||||
if(statusCodeShort >=2 && statusCodeShort <= 3) {
|
||||
@@ -74,16 +74,12 @@ let decodedResp = atob(responseDataBase64);
|
||||
//let jsonResp = JSON.parse(decodedResp)
|
||||
```
|
||||
|
||||
<div class="note danger">
|
||||
The eval is validated against (200, 1000, "e30=") which translates to (200, 1000, '{}') within the function. So whatever function you write should be able to handle these values otherwise you won't be able to save the monitor.
|
||||
</div>
|
||||
|
||||
#### Example
|
||||
|
||||
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
|
||||
(function (statusCode, responseTime, responseDataBase64) {
|
||||
(async function (statusCode, responseTime, responseDataBase64) {
|
||||
const resp = atob(responseDataBase64); //convert base64 to string
|
||||
|
||||
let status = "DOWN";
|
||||
@@ -110,6 +106,36 @@ The following example shows how to use the eval function to evaluate the respons
|
||||
});
|
||||
```
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
|
||||
### Website Monitor
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ The image is used to display a logo or an image for the monitor. It is displayed
|
||||
|
||||
## Description
|
||||
|
||||
The description is used to describe the monitor. It is displayed on the status page and in the admin panel. It is optional.
|
||||
The description is used to describe the monitor. It is displayed on the status page and in the admin panel. It is optional. You can also write html in the description and can use tailwind classes.
|
||||
|
||||
## Cron
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ description: Roadmap for Kener
|
||||
|
||||
☑ ~~Handle uploaded images for docker containers~~
|
||||
|
||||
☐ Introduce sitemap.xml
|
||||
☑ ~~Introduce sitemap.xml~~
|
||||
|
||||
☑ ~~Migrate from moment to date-fns for status page~~ (moment remains in dashboard. Will remove later)
|
||||
|
||||
|
||||
@@ -90,3 +90,17 @@ Add font url
|
||||
### Font Family
|
||||
|
||||
Add font family name
|
||||
|
||||
### Custom CSS
|
||||
|
||||
You can add custom CSS to your status page. This will be added to the head of the page. You can add custom CSS to your status page. This will be added to the head of the page.
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
<div class="note danger">
|
||||
Do not include <style> tags.
|
||||
</div>
|
||||
|
||||
+40
-14
@@ -93,20 +93,24 @@ Body of the webhook will be sent as below:
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Description |
|
||||
| --------------------- | ----------------------------------------------------------- |
|
||||
| id | Unique ID of the alert |
|
||||
| alert_name | Name of the alert |
|
||||
| severity | Severity of the alert. Can be `critical`, `warn` |
|
||||
| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` |
|
||||
| source | Source of the alert. Can be `Kener` |
|
||||
| timestamp | Timestamp of the alert |
|
||||
| description | Description of the alert. This you can customize. See below |
|
||||
| details | Details of the alert. |
|
||||
| details.metric | Name of the monitor |
|
||||
| details.current_value | Current value of the monitor |
|
||||
| details.threshold | Alert trigger threshold of the monitor |
|
||||
| actions | Actions to be taken. Link to view the monitor. |
|
||||
| Key | Description | Variable |
|
||||
| --------------------- | ----------------------------------------------------------- | ----------------------------- |
|
||||
| id | Unique ID of the alert | ${id} |
|
||||
| alert_name | Name of the alert | ${alert_name} |
|
||||
| severity | Severity of the alert. Can be `critical`, `warn` | ${severity} |
|
||||
| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` | ${status} |
|
||||
| source | Source of the alert. Can be `Kener` | ${source} |
|
||||
| timestamp | Timestamp of the alert | ${timestamp} |
|
||||
| description | Description of the alert. This you can customize. See below | ${description} |
|
||||
| details | Details of the alert. | ${details} |
|
||||
| details.metric | Name of the monitor | ${details.metric} |
|
||||
| details.current_value | Current value of the monitor | ${current_value} |
|
||||
| details.threshold | Alert trigger threshold of the monitor | ${threshold} |
|
||||
| actions | Actions to be taken. Link to view the monitor. | ${action_text}, ${action_url} |
|
||||
|
||||
### Custom Body
|
||||
|
||||
You can customize the body of the webhook. You can use the variables mentioned above. If you are not using a json body then please make sure you are using the right content-type by setting custom headers. See examples below.
|
||||
|
||||
## Discord
|
||||
|
||||
@@ -250,3 +254,25 @@ Click on the ⚙️ to edit the trigger.
|
||||
### Deactivate Trigger
|
||||
|
||||
You can deactivate the trigger by switching the toggle to off. You cannot send message to a deactivated trigger. Any monitor with this trigger will not send any notifications.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Telegram
|
||||
|
||||
You can use the webhook trigger to send a message to a telegram channel. Enable `Use a custom webhook body`.
|
||||
|
||||
Set the URL to `https://api.telegram.org/bot[BOT_TOKEN]/sendMessage`. Replace [BOT_TOKEN] with your bot token.
|
||||
|
||||
```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"
|
||||
}
|
||||
```
|
||||
|
||||
If you want to send a message to a group, then replace `[CHAT_ID]` with the group id.
|
||||
|
||||
You can also use environment variables to store the bot token and chat id. In that case the URL will be `https://api.telegram.org/bot$BOT_TOKEN/sendMessage`. In the body you can use `"chat_id": "$CHAT_ID"`. Make sure you have set the `BOT_TOKEN` and `CHAT_ID` in the <a href="/docs/environment-vars#secrets">environment variables</a>.
|
||||
|
||||
@@ -4,6 +4,7 @@ import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import express from "express";
|
||||
import Startup from "./src/lib/server/startup.js";
|
||||
import { GetSiteMap } from "./src/lib/server/controllers/controller.js";
|
||||
import fs from "fs-extra";
|
||||
import knex from "knex";
|
||||
import knexOb from "./knexfile.js";
|
||||
@@ -24,6 +25,10 @@ app.use((req, res, next) => {
|
||||
app.get(base + "/healthcheck", (req, res) => {
|
||||
res.end("ok");
|
||||
});
|
||||
app.get(base + "/sitemap.xml", async (req, res) => {
|
||||
res.header("Content-Type", "application/xml");
|
||||
res.send(await GetSiteMap());
|
||||
});
|
||||
//part /uploads server static files from static/uploads
|
||||
|
||||
//set env variable for upload path
|
||||
|
||||
Generated
+13
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kener",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
@@ -65,6 +65,7 @@
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-awesome-color-picker": "^3.1.4",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte-dnd-action": "^0.9.55",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
@@ -7015,6 +7016,16 @@
|
||||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-dnd-action": {
|
||||
"version": "0.9.55",
|
||||
"resolved": "https://registry.npmjs.org/svelte-dnd-action/-/svelte-dnd-action-0.9.55.tgz",
|
||||
"integrity": "sha512-ljAp1VFwigYCGb+eqnjt+kIkzawGtpUJ/KjapXYkzVZMkoofOWtnU3DVnnx0fo7CpcS4iPfPR7gM6cixh7iQfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"svelte": ">=3.23.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-hmr": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.7",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
|
||||
@@ -59,6 +59,7 @@
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-awesome-color-picker": "^3.1.4",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte-dnd-action": "^0.9.55",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
|
||||
+128
-1
@@ -149,7 +149,132 @@ function IsValidNameServer(nameServer) {
|
||||
const IsValidURL = function (url) {
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
};
|
||||
function ValidateCronExpression(cronExp) {
|
||||
// Check if expression is provided and is a string
|
||||
if (!cronExp || typeof cronExp !== "string") {
|
||||
return { isValid: false, message: "Cron expression must be a non-empty string" };
|
||||
}
|
||||
|
||||
// Split the expression into its components
|
||||
const fields = cronExp.trim().split(/\s+/);
|
||||
|
||||
// Standard cron should have 5 or 6 fields
|
||||
// minute hour day-of-month month day-of-week [year]
|
||||
if (fields.length < 5 || fields.length > 6) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "Cron expression must have 5 or 6 fields"
|
||||
};
|
||||
}
|
||||
|
||||
// Define field constraints
|
||||
const fieldConstraints = [
|
||||
{ name: "minute", min: 0, max: 59 },
|
||||
{ name: "hour", min: 0, max: 23 },
|
||||
{ name: "day", min: 1, max: 31 },
|
||||
{ name: "month", min: 1, max: 12 },
|
||||
{ name: "weekday", min: 0, max: 6 },
|
||||
{ name: "year", min: 1970, max: 2099 } // Optional field
|
||||
];
|
||||
|
||||
// Valid characters in cron expressions
|
||||
const validChars = /^[\d/*,\-]+$/;
|
||||
|
||||
// Validate each field
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i];
|
||||
const constraint = fieldConstraints[i];
|
||||
|
||||
// Check for valid characters
|
||||
if (!validChars.test(field)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid characters in ${constraint.name} field`
|
||||
};
|
||||
}
|
||||
|
||||
// Handle special characters
|
||||
if (field === "*") {
|
||||
continue; // Asterisk is valid for all fields
|
||||
}
|
||||
|
||||
// Handle lists (comma-separated values)
|
||||
if (field.includes(",")) {
|
||||
const values = field.split(",");
|
||||
for (const value of values) {
|
||||
if (!isValidRange(value, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${value}`
|
||||
};
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle ranges (with hyphens)
|
||||
if (field.includes("-")) {
|
||||
const [start, end] = field.split("-").map(Number);
|
||||
if (
|
||||
start == null ||
|
||||
end == null ||
|
||||
start < constraint.min ||
|
||||
end > constraint.max ||
|
||||
start > end
|
||||
) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${field}`
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle steps (with forward slash)
|
||||
if (field.includes("/")) {
|
||||
const [range, step] = field.split("/");
|
||||
if (range !== "*" && !isValidRange(range, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${range}`
|
||||
};
|
||||
}
|
||||
if (!isValidRange(step, 1, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid step value in ${constraint.name} field: ${step}`
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle plain numbers
|
||||
if (!isValidRange(field, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${field}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, message: "Valid cron expression" };
|
||||
}
|
||||
|
||||
function isValidRange(value, min, max) {
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && num >= min && num <= max;
|
||||
}
|
||||
function SortMonitor(monitorSort, resp) {
|
||||
let monitors = [];
|
||||
if (!!monitorSort && monitorSort.length > 0) {
|
||||
monitors = monitorSort.map((id) => resp.find((m) => m.id == id)).filter((m) => !!m);
|
||||
//append any new monitors
|
||||
monitors = [...monitors, ...resp.filter((m) => !monitorSort.includes(m.id))];
|
||||
} else {
|
||||
monitors = resp;
|
||||
}
|
||||
return monitors;
|
||||
}
|
||||
export {
|
||||
siteDataExtractFromDb,
|
||||
storeSiteData,
|
||||
@@ -157,5 +282,7 @@ export {
|
||||
ValidateIpAddress,
|
||||
IsValidHost,
|
||||
IsValidNameServer,
|
||||
IsValidURL
|
||||
IsValidURL,
|
||||
ValidateCronExpression,
|
||||
SortMonitor
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
>
|
||||
{#if !isFuture && incident.state != "RESOLVED"}
|
||||
<span>
|
||||
{l(lang, "Started about %startedAt ago, still ongoing", {
|
||||
{l(lang, "Started about %startedAt, still ongoing", {
|
||||
startedAt
|
||||
})}
|
||||
</span>
|
||||
@@ -53,19 +53,19 @@
|
||||
<span>
|
||||
{l(
|
||||
lang,
|
||||
"Started about %startedAt ago, lasted for about %lastedFor",
|
||||
"Started %startedAt, lasted for %lastedFor",
|
||||
{ startedAt, lastedFor }
|
||||
)}
|
||||
</span>
|
||||
{:else if isFuture && incident.state != "RESOLVED"}
|
||||
<span>
|
||||
{l(lang, "Starts in %startedAt", { startedAt })}
|
||||
{l(lang, "Starts %startedAt", { startedAt })}
|
||||
</span>
|
||||
{:else if isFuture && incident.state == "RESOLVED"}
|
||||
<span>
|
||||
{l(
|
||||
lang,
|
||||
"Starts in %startedAt, will last for about %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor",
|
||||
{ startedAt, lastedFor }
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
import {
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
ValidateCronExpression,
|
||||
IsValidHost,
|
||||
IsValidNameServer
|
||||
} from "$lib/clientTools.js";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const defaultEval = `(function (statusCode, responseTime, responseData) {
|
||||
const defaultEval = `(async function (statusCode, responseTime, responseData) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
return {
|
||||
@@ -80,25 +81,25 @@
|
||||
|
||||
let invalidFormMessage = "";
|
||||
|
||||
function isValidEval() {
|
||||
async function isValidEval() {
|
||||
try {
|
||||
let evalResp = eval(newMonitor.apiConfig.eval + `(200, 1000, "e30=")`);
|
||||
|
||||
if (
|
||||
evalResp === undefined ||
|
||||
evalResp === null ||
|
||||
evalResp.status === undefined ||
|
||||
evalResp.status === null ||
|
||||
evalResp.latency === undefined ||
|
||||
evalResp.latency === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// let evalResp = await eval(newMonitor.apiConfig.eval + `(200, 1000, "e30=")`);
|
||||
new Function(newMonitor.apiConfig.eval);
|
||||
return true; // The code is valid
|
||||
// if (
|
||||
// evalResp === undefined ||
|
||||
// evalResp === null ||
|
||||
// evalResp.status === undefined ||
|
||||
// evalResp.status === null ||
|
||||
// evalResp.latency === undefined ||
|
||||
// evalResp.latency === null
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
} catch (error) {
|
||||
invalidFormMessage = error.message + " in eval.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const IsValidURL = function (url) {
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
@@ -130,13 +131,11 @@
|
||||
|
||||
//validating cron
|
||||
const cron = newMonitor.cron;
|
||||
const cronRegex =
|
||||
/^((\*|([0-5]?\d)(-[0-5]?\d)?(\/[1-9]\d*)?|([0-5]?\d(,[0-5]?\d)*))\s+){4}(\*|([0-7](,[0-7])*|([0-7]-[0-7])))$/;
|
||||
if (!cronRegex.test(cron)) {
|
||||
invalidFormMessage = "Invalid Cron";
|
||||
let cronValidation = ValidateCronExpression(cron);
|
||||
if (cronValidation.isValid === false) {
|
||||
invalidFormMessage = "Cron invalid: " + cronValidation.message;
|
||||
return;
|
||||
}
|
||||
|
||||
//if monitor type is API
|
||||
if (newMonitor.monitor_type === "API") {
|
||||
//validating url
|
||||
@@ -155,18 +154,28 @@
|
||||
return;
|
||||
}
|
||||
|
||||
//if evali ends with semicolor throw error
|
||||
if (!!newMonitor.apiConfig.eval && newMonitor.apiConfig.eval.endsWith(";")) {
|
||||
invalidFormMessage = "Eval should not end with semicolon";
|
||||
return;
|
||||
}
|
||||
|
||||
//validating eval
|
||||
if (!!newMonitor.apiConfig.eval && !isValidEval()) {
|
||||
invalidFormMessage = invalidFormMessage + "Invalid eval";
|
||||
return;
|
||||
}
|
||||
if (!!newMonitor.apiConfig.eval) {
|
||||
newMonitor.apiConfig.eval = newMonitor.apiConfig.eval.trim();
|
||||
if (newMonitor.apiConfig.eval.endsWith(";")) {
|
||||
invalidFormMessage = "Eval should not end with semicolon";
|
||||
return;
|
||||
}
|
||||
//has to start with ( and end with )
|
||||
if (
|
||||
!newMonitor.apiConfig.eval.startsWith("(") ||
|
||||
!newMonitor.apiConfig.eval.endsWith(")")
|
||||
) {
|
||||
invalidFormMessage =
|
||||
"Eval should start with ( and end with ). It is an anonymous function";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await isValidEval())) {
|
||||
invalidFormMessage = invalidFormMessage + "Invalid eval";
|
||||
return;
|
||||
}
|
||||
}
|
||||
newMonitor.type_data = JSON.stringify(newMonitor.apiConfig);
|
||||
} else if (newMonitor.monitor_type === "PING") {
|
||||
//validating hostsV4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Settings, Bell, Loader } from "lucide-svelte";
|
||||
import { Plus, X, Settings, Bell, Loader, ArrowDownUp, Grip } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { base } from "$app/paths";
|
||||
@@ -8,10 +8,14 @@
|
||||
import { onMount } from "svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { storeSiteData, SortMonitor } from "$lib/clientTools.js";
|
||||
import { dndzone } from "svelte-dnd-action";
|
||||
import { flip } from "svelte/animate";
|
||||
|
||||
export let categories = [];
|
||||
export let colorDown = "#777";
|
||||
export let colorDegraded = "#777";
|
||||
export let monitorSort = [];
|
||||
let monitors = [];
|
||||
let status = "ACTIVE";
|
||||
let showAddMonitor = false;
|
||||
@@ -88,11 +92,13 @@
|
||||
body: JSON.stringify({ action: "getMonitors", data: { status: status } })
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
monitors = resp.map((m) => {
|
||||
resp = resp.map((m) => {
|
||||
m.down_trigger = JSON.parse(m.down_trigger);
|
||||
m.degraded_trigger = JSON.parse(m.degraded_trigger);
|
||||
return m;
|
||||
});
|
||||
|
||||
monitors = SortMonitor(monitorSort, resp);
|
||||
} catch (error) {
|
||||
alert("Err2or: " + error);
|
||||
} finally {
|
||||
@@ -178,6 +184,19 @@
|
||||
}
|
||||
shareMenusToggle = true;
|
||||
}
|
||||
const flipDurationMs = 200;
|
||||
function handleSort(e) {
|
||||
dropTargetStyle = {
|
||||
border: "1px solid transparent"
|
||||
};
|
||||
monitors = e.detail.items;
|
||||
monitorSort = monitors.map((m) => m.id);
|
||||
storeSiteData({
|
||||
monitorSort: JSON.stringify(monitorSort)
|
||||
});
|
||||
}
|
||||
let dropTargetStyle;
|
||||
let draggableMenu = false;
|
||||
</script>
|
||||
|
||||
{#if showAddMonitor}
|
||||
@@ -190,7 +209,51 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if draggableMenu}
|
||||
<div
|
||||
class="moldal-container fixed left-0 top-0 z-50 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-fit w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
draggableMenu = false;
|
||||
}}
|
||||
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
|
||||
>
|
||||
<X class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Rearrange Monitors</h2>
|
||||
<div
|
||||
class="mt-4"
|
||||
use:dndzone={{ items: monitors, flipDurationMs, dropTargetStyle }}
|
||||
on:consider={handleSort}
|
||||
on:finalize={handleSort}
|
||||
>
|
||||
{#each monitors as monitor (monitor.id)}
|
||||
<div
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
class="mb-2 rounded-md bg-card p-2"
|
||||
>
|
||||
<Grip class="mr-2 inline h-4 w-4" />
|
||||
{#if !!monitor.image}
|
||||
<img
|
||||
src={base + monitor.image}
|
||||
alt={monitor.name}
|
||||
class="mr-1 inline-block h-4 w-4"
|
||||
/>
|
||||
{/if}
|
||||
{monitor.name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4 flex justify-between">
|
||||
<div class="flex w-40">
|
||||
<Select.Root
|
||||
@@ -219,10 +282,21 @@
|
||||
<Loader class="ml-2 mt-2 inline h-6 w-6 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<Button on:click={showAddMonitorSheet}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
Add Monitor
|
||||
</Button>
|
||||
<div>
|
||||
{#if status == "ACTIVE"}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
on:click={() => (draggableMenu = !draggableMenu)}
|
||||
>
|
||||
<ArrowDownUp class=" " />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={showAddMonitorSheet}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
Add Monitor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -240,7 +314,7 @@
|
||||
{monitor.name}
|
||||
</Card.Title>
|
||||
{#if !!monitor.description}
|
||||
<Card.Description>{monitor.description}</Card.Description>
|
||||
<Card.Description>{@html monitor.description}</Card.Description>
|
||||
{/if}
|
||||
<div class="absolute right-2 top-0.5">
|
||||
<Button
|
||||
|
||||
@@ -40,16 +40,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// function handleFileChangeLogo(event) {
|
||||
// const file = event.target.files[0];
|
||||
// if (file) {
|
||||
// const reader = new FileReader();
|
||||
// reader.onload = () => {
|
||||
// siteInformation.logo = reader.result;
|
||||
// };
|
||||
// reader.readAsDataURL(file);
|
||||
// }
|
||||
// }
|
||||
function handleFileChangeFavicon(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
@@ -89,7 +79,7 @@
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
siteInformation.logo = base + "/uploads/" + result.filename;
|
||||
siteInformation.logo = "/uploads/" + result.filename;
|
||||
} else {
|
||||
uploadLogoStatus = "Failed to upload file.";
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
url: "",
|
||||
headers: [],
|
||||
to: "",
|
||||
from: data.RESEND_SENDER_EMAIL || ""
|
||||
from: data.RESEND_SENDER_EMAIL || "",
|
||||
webhook_body: "",
|
||||
has_webhook_body: false
|
||||
}
|
||||
};
|
||||
let invalidFormMessage = "";
|
||||
@@ -41,7 +43,9 @@
|
||||
url: "",
|
||||
headers: [],
|
||||
to: "",
|
||||
from: data.RESEND_SENDER_EMAIL || ""
|
||||
from: data.RESEND_SENDER_EMAIL || "",
|
||||
webhook_body: "",
|
||||
has_webhook_body: false
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -94,6 +98,23 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (newTrigger.trigger_type == "webhook") {
|
||||
//headers elements key value should not be empty
|
||||
if (newTrigger.trigger_meta.headers.length > 0) {
|
||||
for (let i = 0; i < newTrigger.trigger_meta.headers.length; i++) {
|
||||
if (newTrigger.trigger_meta.headers[i].key == "") {
|
||||
invalidFormMessage = "Header Key is required";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
if (newTrigger.trigger_meta.headers[i].value == "") {
|
||||
invalidFormMessage = "Header Value is required";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//newTrigger.name present not empty
|
||||
if (newTrigger.name == "") {
|
||||
invalidFormMessage = "Trigger Name is required";
|
||||
@@ -202,6 +223,30 @@
|
||||
onMount(() => {
|
||||
loadData();
|
||||
});
|
||||
let placeholderWebhookBody = JSON.stringify(
|
||||
{
|
||||
id: "${id}",
|
||||
alert_name: "${alert_name}",
|
||||
severity: "${severity}",
|
||||
status: "${status}",
|
||||
source: "${source}",
|
||||
timestamp: "${timestamp}",
|
||||
description: "${description}",
|
||||
details: {
|
||||
metric: "${metric}",
|
||||
current_value: "${current_value}",
|
||||
threshold: "${threshold}"
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
text: "${action_text}",
|
||||
url: "${action_url}"
|
||||
}
|
||||
]
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="mt-4 flex justify-between">
|
||||
@@ -419,7 +464,7 @@
|
||||
{#if newTrigger.trigger_type == "webhook"}
|
||||
<div class="mt-4 w-full">
|
||||
<Label for="url">Add Optional Headers for Webhooks</Label>
|
||||
<div class="grid grid-cols-6 gap-2">
|
||||
<div class="mt-2 grid grid-cols-6 gap-2">
|
||||
{#each newTrigger.trigger_meta.headers as header, index}
|
||||
<div class="col-span-2">
|
||||
<Input
|
||||
@@ -464,6 +509,41 @@
|
||||
<Plus class="mr-1 h-4 w-4" /> Add Headers
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_webhook_body =
|
||||
e.target.checked;
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={newTrigger.trigger_meta.has_webhook_body}
|
||||
/>
|
||||
Use a Custom Webhook Body
|
||||
</label>
|
||||
{#if !!newTrigger.trigger_meta.has_webhook_body}
|
||||
<p class="my-2 text-xs text-muted-foreground">
|
||||
You can use a custom webhook body. The body should be a
|
||||
valid for the Content-Type header you have set. The default
|
||||
is JSON. There are <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#webhook-body"
|
||||
>variables</a
|
||||
>
|
||||
that you can use for the webhook body. To use a variable wrap
|
||||
it like
|
||||
<code>${variable}</code>
|
||||
</p>
|
||||
<div class="w-full">
|
||||
<textarea
|
||||
bind:value={newTrigger.trigger_meta.webhook_body}
|
||||
class="mt-2 h-[500px] w-full rounded-sm border p-2"
|
||||
placeholder={placeholderWebhookBody}
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "email"}
|
||||
<div class="mt-4 w-full">
|
||||
|
||||
@@ -277,10 +277,7 @@
|
||||
class="text-api-up truncate text-xs font-semibold text-{monitor.pageData
|
||||
.summaryColorClass}"
|
||||
>
|
||||
{l(lang, summaryTime(monitor.pageData.summaryStatus), {
|
||||
status: monitor.pageData.summaryStatus,
|
||||
duration: monitor.pageData.summaryDuration
|
||||
})}
|
||||
{monitor.pageData.summaryStatus}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +301,6 @@
|
||||
on:click={(e) => {
|
||||
dailyDataGetter(e, bar, incidents[ts]);
|
||||
}}
|
||||
style="transition: opacity {bar.ij * 2 + 100}ms ease-in;"
|
||||
href="#"
|
||||
class="oneline h-[34px] w-[6px]
|
||||
{bar.border ? 'opacity-100' : 'opacity-20'} pb-1"
|
||||
@@ -335,10 +331,7 @@
|
||||
selectedLang
|
||||
)}
|
||||
-
|
||||
{l(lang, summaryTime(bar.summaryStatus), {
|
||||
status: bar.summaryStatus,
|
||||
duration: bar.summaryDuration
|
||||
})}
|
||||
{bar.summaryStatus}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -388,9 +381,12 @@
|
||||
>
|
||||
<p>
|
||||
<span class="text-{bar.cssClass}"> ● </span>
|
||||
{new Date(
|
||||
bar.timestamp * 1000
|
||||
).toLocaleTimeString()}
|
||||
|
||||
{f(
|
||||
new Date(bar.timestamp * 1000),
|
||||
"hh:mm a",
|
||||
selectedLang
|
||||
)}
|
||||
</p>
|
||||
{#if bar.status != "NO_DATA"}
|
||||
<p class="pl-2">
|
||||
|
||||
@@ -169,10 +169,7 @@
|
||||
{l(lang, "Embed")}
|
||||
</h2>
|
||||
<p class="mb-1 text-xs text-muted-foreground">
|
||||
{@html l(
|
||||
lang,
|
||||
"Embed this moni2tor using <script> or <iframe> in your app."
|
||||
)}
|
||||
{l(lang, "Embed this monitor using <script> or <iframe> in your app.")}
|
||||
</p>
|
||||
<div class="mb-4 grid grid-cols-2 gap-2">
|
||||
<div class="col-span-1">
|
||||
|
||||
+4
-13
@@ -1,9 +1,9 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale";
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr } from "date-fns/locale";
|
||||
|
||||
const locales = { ru, en: enUS, hi, de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko };
|
||||
const locales = { ru, en: enUS, hi, de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko, "pt-BR": ptBR, tr };
|
||||
|
||||
const f = function (date, formatStr, locale) {
|
||||
return format(date, formatStr, {
|
||||
@@ -19,7 +19,7 @@ const fdn = function (start, locale) {
|
||||
};
|
||||
const fdm = function (duration, locale) {
|
||||
return formatDuration(duration, {
|
||||
format: ["hours", "minutes"],
|
||||
format: ["days", "hours", "minutes"],
|
||||
zero: false,
|
||||
delimiter: " ",
|
||||
locale: locales[locale]
|
||||
@@ -27,15 +27,7 @@ const fdm = function (duration, locale) {
|
||||
};
|
||||
|
||||
const l = function (sessionLangMap, key, args = {}) {
|
||||
const keys = key.split(".");
|
||||
let obj = sessionLangMap;
|
||||
|
||||
for (const keyPart of keys) {
|
||||
obj = obj?.[keyPart];
|
||||
if (!obj) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let obj = sessionLangMap[key];
|
||||
|
||||
// Replace placeholders in the string using the args object
|
||||
if (obj && typeof obj === "string") {
|
||||
@@ -44,7 +36,6 @@ const l = function (sessionLangMap, key, args = {}) {
|
||||
return args[argKey] !== undefined ? args[argKey] : placeholder;
|
||||
});
|
||||
}
|
||||
|
||||
return obj || key;
|
||||
};
|
||||
const summaryTime = function (summaryStatus) {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "ÜBERWACHUNG",
|
||||
"No Data": "Keine Daten",
|
||||
"No Incidents": "Keine Vorfälle",
|
||||
"No Incident On %date": "Kein Vorfall am %date",
|
||||
"No Incident in %date": "Kein Vorfall am %date",
|
||||
"No Monitor Found": "Kein Monitor gefunden",
|
||||
"No Updates Yet": "Noch keine Updates",
|
||||
"Ongoing Incidents": "Laufende Vorfälle",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "Teilen",
|
||||
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
|
||||
"Starts in %startedAt": "Beginnt in %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
|
||||
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
|
||||
"Starts %startedAt": "Beginnt in %startedAt",
|
||||
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
|
||||
"Status": "Status",
|
||||
"Status OK": "Status OK",
|
||||
"Theme": "Thema",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "OVERVÅGNING",
|
||||
"No Data": "Ingen Data",
|
||||
"No Incidents": "Ingen Hændelser",
|
||||
"No Incident On %date": "Ingen Hændelse Den %date",
|
||||
"No Incident in %date": "Ingen Hændelse Den %date",
|
||||
"No Monitor Found": "Ingen Monitor Fundet",
|
||||
"No Updates Yet": "Ingen Opdateringer Endnu",
|
||||
"Ongoing Incidents": "Igangværende Hændelser",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "Del",
|
||||
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
|
||||
"Starts in %startedAt": "Starter om %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
|
||||
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
|
||||
"Starts %startedAt": "Starter om %startedAt",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
|
||||
"Status": "Status",
|
||||
"Status OK": "Status OK",
|
||||
"Theme": "Tema",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "MONITORING",
|
||||
"No Data": "No Data",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Incident On %date": "No Incident On %date",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "Share",
|
||||
"Share this monitor using a link with others": "Share this monitor using a link with others",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Started about %startedAt ago, lasted for about %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Started about %startedAt ago, still ongoing",
|
||||
"Starts in %startedAt": "Starts in %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Starts in %startedAt, will last for about %lastedFor",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
|
||||
"Status": "Status",
|
||||
"Status OK": "Status OK",
|
||||
"Theme": "Theme",
|
||||
@@ -52,4 +52,4 @@
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime",
|
||||
"%status for %duration": "%status for %duration"
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "En surveillance",
|
||||
"No Data": "Pas de données",
|
||||
"No Incidents": "Pas d'incidents",
|
||||
"No Incident On %date": "Pas d'incident le %date",
|
||||
"No Incident in %date": "Pas d'incident le %date",
|
||||
"No Monitor Found": "Aucun moniteur trouvé",
|
||||
"No Updates Yet": "Pas encore de mises à jour",
|
||||
"Ongoing Incidents": "Incidents en cours",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "Partager",
|
||||
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
|
||||
"Starts in %startedAt": "Commence dans %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
|
||||
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
|
||||
"Starts %startedAt": "Commence dans %startedAt",
|
||||
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
|
||||
"Status": "Statut",
|
||||
"Status OK": "Statut OK",
|
||||
"Theme": "Thème",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "निगरानी",
|
||||
"No Data": "कोई डेटा नहीं",
|
||||
"No Incidents": "कोई घटना नहीं",
|
||||
"No Incident On %date": "%date को कोई घटना नहीं",
|
||||
"No Incident in %date": "%date को कोई घटना नहीं",
|
||||
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
|
||||
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
|
||||
"Ongoing Incidents": "चल रही घटनाएँ",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "साझा करें",
|
||||
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
|
||||
"Standard": "मानक",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "लगभग %startedAt पहले शुरू हुआ, लगभग %lastedFor तक चला",
|
||||
"Started about %startedAt ago, still ongoing": "लगभग %startedAt पहले शुरू हुआ, अभी भी जारी है",
|
||||
"Starts in %startedAt": "%startedAt में शुरू होगा",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "%startedAt में शुरू होगा, लगभग %lastedFor तक चलेगा",
|
||||
"Started %startedAt, lasted for %lastedFor": "लगभग %startedAt शुरू हुआ, %lastedFor तक चला",
|
||||
"Started about %startedAt, still ongoing": "लगभग %startedAt शुरू हुआ, अभी भी जारी है",
|
||||
"Starts %startedAt": "%startedAt में शुरू होगा",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
|
||||
"Status": "स्थिति",
|
||||
"Status OK": "स्थिति ठीक है",
|
||||
"Theme": "थीम",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "モニタリング中",
|
||||
"No Data": "データなし",
|
||||
"No Incidents": "インシデントなし",
|
||||
"No Incident On %date": "%date にインシデントなし",
|
||||
"No Incident in %date": "%date にインシデントなし",
|
||||
"No Monitor Found": "モニターが見つかりません",
|
||||
"No Updates Yet": "まだ更新はありません",
|
||||
"Ongoing Incidents": "進行中のインシデント",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "共有",
|
||||
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
|
||||
"Standard": "標準",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
|
||||
"Started about %startedAt ago, still ongoing": "%startedAt 前に開始し、まだ進行中です",
|
||||
"Starts in %startedAt": "%startedAt 後に開始",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
|
||||
"Started about %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
|
||||
"Starts %startedAt": "%startedAt 後に開始",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
|
||||
"Status": "ステータス",
|
||||
"Status OK": "ステータス正常",
|
||||
"Theme": "テーマ",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "감시 중",
|
||||
"No Data": "데이터 없음",
|
||||
"No Incidents": "사건 없음",
|
||||
"No Incident On %date": "%date 에는 아무 사건도 없었음",
|
||||
"No Incident in %date": "%date 에는 아무 사건도 없었음",
|
||||
"No Monitor Found": "모니터링 항목을 찾을 수 없음",
|
||||
"No Updates Yet": "아직 아무 업데이트도 없음",
|
||||
"Ongoing Incidents": "진행중 사건",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "공유",
|
||||
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
|
||||
"Standard": "기본",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
|
||||
"Started about %startedAt ago, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
|
||||
"Starts in %startedAt": "%startedAt 에 시작되었습니다",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
|
||||
"Started about %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
|
||||
"Starts %startedAt": "%startedAt 에 시작되었습니다",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
|
||||
"Status": "상태",
|
||||
"Status OK": "양호",
|
||||
"Theme": "테마",
|
||||
|
||||
@@ -42,5 +42,13 @@
|
||||
{
|
||||
"code": "ko",
|
||||
"name": "한국어"
|
||||
},
|
||||
{
|
||||
"code": "pt-BR",
|
||||
"name": "Português Brasileiro"
|
||||
},
|
||||
{
|
||||
"code": "tr",
|
||||
"name": "Türkçe"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "MONITOREN",
|
||||
"No Data": "Geen gegevens",
|
||||
"No Incidents": "Geen incidenten",
|
||||
"No Incident On %date": "Geen incident op %date",
|
||||
"No Incident in %date": "Geen incident op %date",
|
||||
"No Monitor Found": "Geen monitor gevonden",
|
||||
"No Updates Yet": "Nog geen updates",
|
||||
"Ongoing Incidents": "Lopende incidenten",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "Delen",
|
||||
"Share this monitor using a link with others": "Deel deze monitor met anderen via een link",
|
||||
"Standard": "Standaard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
|
||||
"Starts in %startedAt": "Begint over %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
|
||||
"Started %startedAt, lasted for %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
|
||||
"Starts %startedAt": "Begint over %startedAt",
|
||||
"Starts %startedAt, will last for %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
|
||||
"Status": "Status",
|
||||
"Status OK": "Status OK",
|
||||
"Theme": "Thema",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"14 Days": "14 Dias",
|
||||
"30 Days": "30 Dias",
|
||||
"60 Days": "60 Dias",
|
||||
"7 Days": "7 Dias",
|
||||
"90 Days": "90 Dias",
|
||||
"Availability per Component": "Disponibilidade por Componente",
|
||||
"Back": "Voltar",
|
||||
"Badge": "Emblema",
|
||||
"Badge Copied": "Emblema Copiado",
|
||||
"Code Copied": "Código Copiado",
|
||||
"Copy Code": "Copiar Código",
|
||||
"Copy Link": "Copiar Link",
|
||||
"Dark": "Escuro",
|
||||
"Days": "Dias",
|
||||
"DEGRADED": "FALHA",
|
||||
"DOWN": "INDISPONÍVEL",
|
||||
"Embed": "Incorporar",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
|
||||
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
|
||||
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
|
||||
"IDENTIFIED": "IDENTIFICADO",
|
||||
"Incident Updates": "Atualizações do Incidente",
|
||||
"INVESTIGATING": "INVESTIGANDO",
|
||||
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
|
||||
"Light": "Claro",
|
||||
"Link Copied": "Link Copiado",
|
||||
"LIVE Status": "Status AO VIVO",
|
||||
"Mode": "Modo",
|
||||
"MONITORING": "MONITORANDO",
|
||||
"No Data": "Sem dados",
|
||||
"No Incidents": "Sem Incidentes",
|
||||
"No Incident in %date": "Nenhum incidente em %date",
|
||||
"No Monitor Found": "Nenhum monitor encontrado",
|
||||
"No Updates Yet": "Nenhuma atualização ainda",
|
||||
"Ongoing Incidents": "Incidentes em Andamento",
|
||||
"Pinging": "Animado",
|
||||
"Recent Incidents": "Incidentes Recentes",
|
||||
"RESOLVED": "RESOLVIDO",
|
||||
"Share": "Compartilhar",
|
||||
"Share this monitor using a link with others": "Compartilhar este monitor através do link",
|
||||
"Standard": "Padrão",
|
||||
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
|
||||
"Starts %startedAt": "Inicia em %startedAt",
|
||||
"Starts %startedAt, will last for %lastedFor": "Terá início em %startedAt, com duração de %lastedFor",
|
||||
"Status": "Status",
|
||||
"Status OK": "Status OK",
|
||||
"Theme": "Tema",
|
||||
"Today": "Hoje",
|
||||
"UP": "OPERACIONAL",
|
||||
"Updates": "Atualizações",
|
||||
"Uptime": "Tempo de Atividade",
|
||||
"%status for %duration": "%status por %duration"
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "МОНИТОРИНГ",
|
||||
"No Data": "Нет данных",
|
||||
"No Incidents": "Нет инцидентов",
|
||||
"No Incident On %date": "Нет инцидентов на %date",
|
||||
"No Incident in %date": "Нет инцидентов на %date",
|
||||
"No Monitor Found": "Монитор не найден",
|
||||
"No Updates Yet": "Пока нет обновлений",
|
||||
"Ongoing Incidents": "Текущие инциденты",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "Поделиться",
|
||||
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
|
||||
"Standard": "Стандартная",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Началось около %startedAt назад, все еще продолжается",
|
||||
"Starts in %startedAt": "Начнется через %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
|
||||
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
|
||||
"Starts %startedAt": "Начнется через %startedAt",
|
||||
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
|
||||
"Status": "Статус",
|
||||
"Status OK": "Статус ОК",
|
||||
"Theme": "Тема",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"14 Days": "14 Gün",
|
||||
"30 Days": "30 Gün",
|
||||
"60 Days": "60 Gün",
|
||||
"7 Days": "7 Gün",
|
||||
"90 Days": "90 Gün",
|
||||
"Availability per Component": "Bileşen Kullanılabilirliği",
|
||||
"Back": "Geri",
|
||||
"Badge": "Rozet",
|
||||
"Badge Copied": "Rozet Kopyalandı",
|
||||
"Code Copied": "Kod Kopyalandı",
|
||||
"Copy Code": "Kodu Kopyala",
|
||||
"Copy Link": "Bağlantıyı Kopyala",
|
||||
"Dark": "Koyu",
|
||||
"Days": "Günler",
|
||||
"DEGRADED": "BOZUK",
|
||||
"DOWN": "ÇALIŞMIYOR",
|
||||
"Embed": "Yerleştir",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
|
||||
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
|
||||
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
|
||||
"IDENTIFIED": "TANIMLANDI",
|
||||
"Incident Updates": "Olay Güncellemeleri",
|
||||
"INVESTIGATING": "İNCELENİYOR",
|
||||
"Lasted for about %lastedFor": "%lastedFor sürdü",
|
||||
"Light": "Açık",
|
||||
"Link Copied": "Bağlantı Kopyalandı",
|
||||
"LIVE Status": "CANLI Durum",
|
||||
"Mode": "Mod",
|
||||
"MONITORING": "İZLEME",
|
||||
"No Data": "Veri Yok",
|
||||
"No Incidents": "Olay Yok",
|
||||
"No Incident in %date": "%date Tarihinde Olay Yok",
|
||||
"No Monitor Found": "Servis Bulunamadı",
|
||||
"No Updates Yet": "Henüz Güncelleme Yok",
|
||||
"Ongoing Incidents": "Devam Eden Olaylar",
|
||||
"Pinging": "Pinging",
|
||||
"Recent Incidents": "Son Olaylar",
|
||||
"RESOLVED": "ÇÖZÜLDÜ",
|
||||
"Share": "Paylaş",
|
||||
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
|
||||
"Standard": "Standart",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
|
||||
"Started about %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
|
||||
"Starts %startedAt": "%startedAt başlıyor",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt başlıyor, %lastedFor sürecek",
|
||||
"Status": "Durum",
|
||||
"Status OK": "Durum UYGUN",
|
||||
"Theme": "Tema",
|
||||
"Today": "Bugün",
|
||||
"UP": "ÇALIŞIYOR",
|
||||
"Updates": "Güncellemeler",
|
||||
"Uptime": "Çalışma Süresi",
|
||||
"%status for %duration": "%duration boyunca %status"
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "Đang giám sát",
|
||||
"No Data": "Không có dữ liệu",
|
||||
"No Incidents": "Không có sự cố",
|
||||
"No Incident On %date": "Không có sự cố vào %date",
|
||||
"No Incident in %date": "Không có sự cố vào %date",
|
||||
"No Monitor Found": "Không tìm thấy trình theo dõi",
|
||||
"No Updates Yet": "Chưa có cập nhật",
|
||||
"Ongoing Incidents": "Sự cố đang diễn ra",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "Chia sẻ",
|
||||
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
|
||||
"Standard": "Tiêu chuẩn",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
|
||||
"Starts in %startedAt": "Bắt đầu trong %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
|
||||
"Started %startedAt, lasted for %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
|
||||
"Starts %startedAt": "Bắt đầu trong %startedAt",
|
||||
"Starts %startedAt, will last for %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
|
||||
"Status": "Trạng thái",
|
||||
"Status OK": "Trạng thái OK",
|
||||
"Theme": "Giao diện",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "正在监控",
|
||||
"No Data": "无数据",
|
||||
"No Incidents": "无事件",
|
||||
"No Incident On %date": "%date 无事件",
|
||||
"No Incident in %date": "%date 无事件",
|
||||
"No Monitor Found": "未找到监控",
|
||||
"No Updates Yet": "尚无更新",
|
||||
"Ongoing Incidents": "正在处理的事件",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "分享",
|
||||
"Share this monitor using a link with others": "通过链接与他人分享此监控",
|
||||
"Standard": "标准",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
|
||||
"Starts in %startedAt": "%startedAt 后开始",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
|
||||
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
|
||||
"Starts %startedAt": "%startedAt 后开始",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
|
||||
"Status": "状态",
|
||||
"Status OK": "状态正常",
|
||||
"Theme": "主题",
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
IsValidI18n,
|
||||
IsValidAnalytics,
|
||||
IsValidColors,
|
||||
IsValidJSONString
|
||||
IsValidJSONString,
|
||||
IsValidJSONArray
|
||||
} from "./validators.js";
|
||||
import db from "../db/db.js";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
|
||||
|
||||
const saltRounds = 10;
|
||||
const DUMMY_SECRET = "DUMMY_SECRET";
|
||||
@@ -133,6 +135,11 @@ const siteDataKeys = [
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object"
|
||||
},
|
||||
{
|
||||
key: "monitorSort",
|
||||
isValid: IsValidJSONArray,
|
||||
data_type: "object"
|
||||
},
|
||||
{
|
||||
key: "categories",
|
||||
isValid: IsValidJSONString,
|
||||
@@ -381,6 +388,41 @@ export const HashString = (str) => {
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
export const InterpolateData = (data, start, anchorStatus, e) => {
|
||||
let finalData = [];
|
||||
let status = anchorStatus || "UP";
|
||||
let end = start;
|
||||
if (!!data && data.length > 0) {
|
||||
end = data[data.length - 1].timestamp;
|
||||
}
|
||||
|
||||
if (e) {
|
||||
end = e;
|
||||
}
|
||||
let dataMap = data.reduce((acc, d) => {
|
||||
acc[d.timestamp] = d;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (let i = start; i <= end; i += 60) {
|
||||
let nowData = dataMap[i];
|
||||
if (!!nowData) {
|
||||
status = nowData.status;
|
||||
}
|
||||
|
||||
finalData.push({ timestamp: i, status: status });
|
||||
}
|
||||
return finalData;
|
||||
};
|
||||
|
||||
export const GetLastStatusBefore = async (monitor_tag, timestamp) => {
|
||||
let data = await db.getLastStatusBefore(monitor_tag, timestamp);
|
||||
if (data) {
|
||||
return data.status;
|
||||
}
|
||||
return "UP";
|
||||
};
|
||||
|
||||
export const GetDataGroupByDayAlternative = async (
|
||||
monitor_tag,
|
||||
start,
|
||||
@@ -394,7 +436,10 @@ export const GetDataGroupByDayAlternative = async (
|
||||
|
||||
const offsetSeconds = offsetMinutes * 60;
|
||||
|
||||
const rawData = await db.getDataGroupByDayAlternative(monitor_tag, start, end);
|
||||
let rawData = await db.getDataGroupByDayAlternative(monitor_tag, start, end);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor_tag, start);
|
||||
rawData = InterpolateData(rawData, start, anchorStatus, rawData.length == 0 ? end - 60 : null);
|
||||
|
||||
const groupedData = rawData.reduce((acc, row) => {
|
||||
// Calculate day group considering timezone offset
|
||||
const dayGroup = Math.floor((row.timestamp + offsetSeconds) / 86400);
|
||||
@@ -700,6 +745,9 @@ export const InsertNewAlert = async (data) => {
|
||||
};
|
||||
|
||||
export const CookieConfig = () => {
|
||||
//get base path from env
|
||||
let cookiePath = !!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "/";
|
||||
|
||||
let isSecuredDomain = false;
|
||||
if (!!process.env.ORIGIN) {
|
||||
isSecuredDomain = process.env.ORIGIN.startsWith("https://");
|
||||
@@ -709,13 +757,13 @@ export const CookieConfig = () => {
|
||||
secure: isSecuredDomain,
|
||||
maxAge: 365 * 24 * 60 * 60, // 1 year in seconds
|
||||
httpOnly: true,
|
||||
sameSite: "lax"
|
||||
sameSite: "lax",
|
||||
path: cookiePath
|
||||
};
|
||||
};
|
||||
export const GetLocaleFromCookie = (site, cookies) => {
|
||||
let selectedLang = "en";
|
||||
let selectedLang = site.i18n?.defaultLocale || "en";
|
||||
const localLangCookie = cookies.get("localLang");
|
||||
|
||||
if (!!localLangCookie && site.i18n?.locales.find((l) => l.code === localLangCookie)) {
|
||||
selectedLang = localLangCookie;
|
||||
} else if (site.i18n?.defaultLocale && site.i18n?.locales[site.i18n.defaultLocale]) {
|
||||
@@ -758,3 +806,93 @@ export const IsLoggedInSession = async (cookies) => {
|
||||
user: userDB
|
||||
};
|
||||
};
|
||||
|
||||
export const GetSiteMap = async (cookies) => {
|
||||
let siteMapData = [];
|
||||
let siteURLData = await GetSiteDataByKey("siteURL");
|
||||
let categories = await GetSiteDataByKey("categories");
|
||||
let navs = await GetSiteDataByKey("nav");
|
||||
if (!!siteURLData) {
|
||||
siteMapData.push({
|
||||
url: siteURLData,
|
||||
lastmod: new Date().toISOString(),
|
||||
priority: 1
|
||||
});
|
||||
}
|
||||
|
||||
//get today's date in January-2025 format date-fns
|
||||
const today = format(new Date(), "MMMM-yyyy");
|
||||
//last month
|
||||
const lastMonth = format(subMonths(new Date(), 1), "MMMM-yyyy", { addMonths: -1 });
|
||||
const nextMonth = format(addMonths(new Date(), 1), "MMMM-yyyy", { addMonths: -1 });
|
||||
|
||||
siteMapData.push({
|
||||
url: siteURLData + "/incidents/" + today,
|
||||
lastmod: startOfMonth(new Date()).toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
siteMapData.push({
|
||||
url: siteURLData + "/incidents/" + lastMonth,
|
||||
lastmod: startOfMonth(new Date()).toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
siteMapData.push({
|
||||
url: siteURLData + "/incidents/" + nextMonth,
|
||||
lastmod: startOfMonth(new Date()).toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
|
||||
if (!!categories) {
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
if (categories[i].name !== "Home") {
|
||||
siteMapData.push({
|
||||
url: siteURLData + "?category=" + categories[i].name,
|
||||
lastmod: new Date().toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!!navs) {
|
||||
for (let i = 0; i < navs.length; i++) {
|
||||
if (navs[i].url.startsWith(siteURLData)) {
|
||||
siteMapData.push({
|
||||
url: navs[i].url,
|
||||
lastmod: new Date().toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
} else if (navs[i].url.startsWith("/")) {
|
||||
siteMapData.push({
|
||||
url: siteURLData + navs[i].url,
|
||||
lastmod: new Date().toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
siteMapData.push({
|
||||
url: siteURLData + "?monitor=" + monitors[i].tag,
|
||||
lastmod: new Date(monitors[i].updated_at).toISOString(),
|
||||
priority: 0.8
|
||||
});
|
||||
}
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${siteMapData
|
||||
.map(
|
||||
(page) => `
|
||||
<url>
|
||||
<loc>${page.url}</loc>
|
||||
<lastmod>${page.lastmod}</lastmod>
|
||||
<priority>${page.priority}</priority>
|
||||
</url>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</urlset>`;
|
||||
};
|
||||
|
||||
@@ -37,6 +37,16 @@ export function IsValidJSONString(data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//IsValidJSONArray
|
||||
export function IsValidJSONArray(data) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return Array.isArray(data);
|
||||
}
|
||||
|
||||
export function IsValidNav(nav) {
|
||||
try {
|
||||
nav = JSON.parse(nav);
|
||||
|
||||
@@ -5,7 +5,8 @@ import { UP, DOWN, DEGRADED } from "./constants.js";
|
||||
import {
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
ReplaceAllOccurrences,
|
||||
GetRequiredSecrets
|
||||
GetRequiredSecrets,
|
||||
Wait
|
||||
} from "./tool.js";
|
||||
|
||||
import alerting from "./alerting.js";
|
||||
@@ -28,8 +29,13 @@ const alertingQueue = new Queue({
|
||||
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
|
||||
autostart: true // Automatically start the queue (optional)
|
||||
});
|
||||
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)
|
||||
autostart: true // Automatically start the queue (optional)
|
||||
});
|
||||
|
||||
const defaultEval = `(function (statusCode, responseTime, responseData) {
|
||||
const defaultEval = `(async function (statusCode, responseTime, responseData) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
return {
|
||||
@@ -135,9 +141,9 @@ const pingCall = async (hostsV4, hostsV6) => {
|
||||
type: REALTIME
|
||||
};
|
||||
};
|
||||
const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorEval) => {
|
||||
const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorEval, tag) => {
|
||||
let axiosHeaders = {};
|
||||
axiosHeaders["User-Agent"] = "Kener/2.0.0";
|
||||
axiosHeaders["User-Agent"] = "Kener/3.0.2";
|
||||
axiosHeaders["Accept"] = "*/*";
|
||||
const start = Date.now();
|
||||
//replace all secrets
|
||||
@@ -183,7 +189,7 @@ const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorE
|
||||
statusCode = data.status;
|
||||
resp = data.data;
|
||||
} catch (err) {
|
||||
console.log("Error in apiCall", err.message);
|
||||
console.log(`Error in apiCall ${tag}`, err.message);
|
||||
if (err.message.startsWith("timeout of") && err.message.endsWith("exceeded")) {
|
||||
timeoutError = true;
|
||||
}
|
||||
@@ -207,9 +213,9 @@ const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorE
|
||||
let evalResp = undefined;
|
||||
|
||||
try {
|
||||
evalResp = eval(monitorEval + `(${statusCode}, ${latency}, "${resp}")`);
|
||||
evalResp = await eval(monitorEval + `(${statusCode}, ${latency}, "${resp}")`);
|
||||
} catch (error) {
|
||||
console.log("Error in monitorEval", error.message);
|
||||
console.log(`Error in monitorEval for ${tag}`, error.message);
|
||||
}
|
||||
|
||||
if (evalResp === undefined || evalResp === null) {
|
||||
@@ -330,10 +336,42 @@ const Minuter = async (monitor) => {
|
||||
JSON.stringify(monitor.type_data.headers),
|
||||
monitor.type_data.body,
|
||||
monitor.type_data.timeout,
|
||||
monitor.type_data.eval
|
||||
monitor.type_data.eval,
|
||||
monitor.tag
|
||||
);
|
||||
|
||||
realTimeData[startOfMinute] = apiResponse;
|
||||
if (apiResponse.type === TIMEOUT) {
|
||||
apiQueue.push(async (cb) => {
|
||||
await Wait(500); //wait for 500ms
|
||||
console.log(
|
||||
"Retrying api call for " +
|
||||
monitor.name +
|
||||
" at " +
|
||||
startOfMinute +
|
||||
" due to timeout"
|
||||
);
|
||||
apiCall(
|
||||
envSecrets,
|
||||
monitor.type_data.url,
|
||||
monitor.type_data.method,
|
||||
JSON.stringify(monitor.type_data.headers),
|
||||
monitor.type_data.body,
|
||||
monitor.type_data.timeout,
|
||||
monitor.type_data.eval,
|
||||
monitor.tag
|
||||
).then(async (data) => {
|
||||
await db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: startOfMinute,
|
||||
status: data.status,
|
||||
latency: data.latency,
|
||||
type: data.type
|
||||
});
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (monitor.monitor_type === "PING") {
|
||||
let pingResponse = await pingCall(monitor.type_data.hostsV4, monitor.type_data.hostsV6);
|
||||
realTimeData[startOfMinute] = pingResponse;
|
||||
@@ -395,4 +433,10 @@ alertingQueue.start((err) => {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
apiQueue.start((err) => {
|
||||
if (err) {
|
||||
console.error("Error occurred:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
export { Minuter };
|
||||
|
||||
@@ -56,6 +56,16 @@ class DbImpl {
|
||||
.first();
|
||||
}
|
||||
|
||||
//get the last status before the timestamp given monitor_tag and start timestamp
|
||||
async getLastStatusBefore(monitor_tag, timestamp) {
|
||||
return await this.knex("monitoring_data")
|
||||
.where("monitor_tag", monitor_tag)
|
||||
.where("timestamp", "<", timestamp)
|
||||
.orderBy("timestamp", "desc")
|
||||
.limit(1)
|
||||
.first();
|
||||
}
|
||||
|
||||
async getDataGroupByDayAlternative(monitor_tag, start, end) {
|
||||
// Fetch all raw data
|
||||
//{ timestamp: 1732900380, status: 'UP', latency: 42 }
|
||||
|
||||
@@ -10,13 +10,7 @@ class Notification {
|
||||
constructor(trigger, siteData, monitorData) {
|
||||
let trigger_meta = JSON.parse(trigger.trigger_meta);
|
||||
if (trigger.trigger_type === "webhook") {
|
||||
this.client = new Webhook(
|
||||
trigger_meta.url,
|
||||
trigger_meta.headers,
|
||||
"POST",
|
||||
siteData,
|
||||
monitorData
|
||||
);
|
||||
this.client = new Webhook(trigger_meta, "POST", siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "discord") {
|
||||
this.client = new Discord(trigger_meta.url, siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "slack") {
|
||||
|
||||
@@ -8,14 +8,16 @@ class Webhook {
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
trigger_meta;
|
||||
|
||||
constructor(url, headers, method, siteData, monitorData) {
|
||||
constructor(trigger_meta, method, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener/3.0.0"
|
||||
};
|
||||
|
||||
this.url = url;
|
||||
let headers = trigger_meta.headers;
|
||||
this.trigger_meta = trigger_meta;
|
||||
this.url = trigger_meta.url;
|
||||
this.headers = kenerHeader;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const header = headers[i];
|
||||
@@ -25,19 +27,49 @@ class Webhook {
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
|
||||
let envSecrets = GetRequiredSecrets(`${this.url} ${JSON.stringify(this.headers)}`);
|
||||
let envSecrets = GetRequiredSecrets(
|
||||
`${this.url} ${JSON.stringify(this.headers)} ${JSON.stringify(this.trigger_meta.webhook_body)}`
|
||||
);
|
||||
//replace secrets in url and headers
|
||||
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.url = ReplaceAllOccurrences(this.url, secret.key, secret.value);
|
||||
this.url = ReplaceAllOccurrences(this.url, secret.find, secret.replace);
|
||||
this.headers = JSON.parse(
|
||||
ReplaceAllOccurrences(JSON.stringify(this.headers), secret.find, secret.replace)
|
||||
);
|
||||
}
|
||||
|
||||
if (!!this.trigger_meta.has_webhook_body && !!this.trigger_meta.webhook_body) {
|
||||
envSecrets = GetRequiredSecrets(this.trigger_meta.webhook_body);
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.trigger_meta.webhook_body = ReplaceAllOccurrences(
|
||||
this.trigger_meta.webhook_body,
|
||||
secret.find,
|
||||
secret.replace
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
return data;
|
||||
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
|
||||
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
|
||||
let body = this.trigger_meta.webhook_body;
|
||||
body = ReplaceAllOccurrences(body, "${id}", data.id);
|
||||
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
|
||||
body = ReplaceAllOccurrences(body, "${severity}", data.severity);
|
||||
body = ReplaceAllOccurrences(body, "${status}", data.status);
|
||||
body = ReplaceAllOccurrences(body, "${source}", data.source);
|
||||
body = ReplaceAllOccurrences(body, "${timestamp}", data.timestamp);
|
||||
body = ReplaceAllOccurrences(body, "${description}", data.description);
|
||||
body = ReplaceAllOccurrences(body, "${metric}", data.details.metric);
|
||||
body = ReplaceAllOccurrences(body, "${current_value}", data.details.current_value);
|
||||
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
|
||||
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
|
||||
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
|
||||
return body;
|
||||
}
|
||||
|
||||
type() {
|
||||
@@ -49,7 +81,7 @@ class Webhook {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(this.transformData(data))
|
||||
body: this.transformData(data)
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
|
||||
+51
-47
@@ -9,8 +9,12 @@ import {
|
||||
GetDayStartTimestampUTC
|
||||
} from "$lib/server/tool.js";
|
||||
import { formatDuration, intervalToDuration } from "date-fns";
|
||||
import { fdm } from "$lib/i18n/client";
|
||||
import { GetDataGroupByDayAlternative } from "$lib/server/controllers/controller.js";
|
||||
import { fdm, l, summaryTime } from "$lib/i18n/client";
|
||||
import {
|
||||
GetDataGroupByDayAlternative,
|
||||
InterpolateData,
|
||||
GetLastStatusBefore
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
|
||||
function getSummaryDuration(numOfMinute, selectedLang) {
|
||||
// Convert minutes to milliseconds and create duration object
|
||||
@@ -33,12 +37,12 @@ function getTimezoneOffset(timeZone) {
|
||||
return parseInt(hours) * 60 + parseInt(minutes);
|
||||
}
|
||||
|
||||
function returnStatusClass(val, up, c, barStyle) {
|
||||
function returnStatusClass(val, total, c, barStyle) {
|
||||
// return "api-degraded-10";
|
||||
if (barStyle === undefined || barStyle == "FULL" || up == 0) {
|
||||
if (barStyle === undefined || barStyle == "FULL") {
|
||||
return c;
|
||||
} else if (barStyle == "PARTIAL") {
|
||||
let totalHeight = 24 * 60;
|
||||
let totalHeight = total;
|
||||
let cl = `api-up`;
|
||||
if (val > 0 && val <= 0.1 * totalHeight) {
|
||||
cl = c + "-10";
|
||||
@@ -72,10 +76,9 @@ function getCountOfSimilarStatuesEnd(arr, statusType) {
|
||||
return count;
|
||||
}
|
||||
|
||||
const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
const secondsInDay = 24 * 60 * 60;
|
||||
//get offset from utc in minutes
|
||||
|
||||
const now = GetMinuteStartNowTimestampUTC() + 60;
|
||||
const midnight = BeginningOfDay({ timeZone: localTz });
|
||||
const midnight90DaysAgo = midnight - 90 * 24 * 60 * 60;
|
||||
@@ -84,24 +87,6 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(now) - midnight) / 60);
|
||||
const _90Day = {};
|
||||
let latestTimestamp = 0;
|
||||
let ij = 0;
|
||||
for (let i = midnight90DaysAgo; i < midnightTomorrow; i += secondsInDay) {
|
||||
_90Day[i] = {
|
||||
UP: 0,
|
||||
DEGRADED: 0,
|
||||
DOWN: 0,
|
||||
timestamp: i,
|
||||
cssClass: StatusObj.NO_DATA,
|
||||
textClass: StatusObj.NO_DATA,
|
||||
summaryDuration: 0,
|
||||
summaryStatus: NO_DATA,
|
||||
message: NO_DATA,
|
||||
border: true,
|
||||
ij: ij
|
||||
};
|
||||
ij++;
|
||||
latestTimestamp = i;
|
||||
}
|
||||
|
||||
let dbData = await GetDataGroupByDayAlternative(
|
||||
monitor.tag,
|
||||
@@ -113,18 +98,13 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
let totalDownCount = 0;
|
||||
let totalUpCount = 0;
|
||||
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = "UP";
|
||||
|
||||
let summaryColorClass = "api-nodata";
|
||||
for (let i = 0; i < dbData.length; i++) {
|
||||
let dayData = dbData[i];
|
||||
let ts = dayData.timestamp;
|
||||
let cssClass = StatusObj.UP;
|
||||
let message = "Status OK";
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = "UP";
|
||||
|
||||
latestTimestamp = ts;
|
||||
totalDegradedCount += dayData.DEGRADED;
|
||||
totalDownCount += dayData.DOWN;
|
||||
totalUpCount += dayData.UP;
|
||||
@@ -132,7 +112,7 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
if (dayData.DEGRADED >= monitor.day_degraded_minimum_count) {
|
||||
cssClass = returnStatusClass(
|
||||
dayData.DEGRADED,
|
||||
dayData.UP,
|
||||
dayData.total,
|
||||
StatusObj.DEGRADED,
|
||||
site.barStyle
|
||||
);
|
||||
@@ -140,18 +120,30 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
summaryStatus = "DEGRADED";
|
||||
}
|
||||
if (dayData.DOWN >= monitor.day_down_minimum_count) {
|
||||
cssClass = returnStatusClass(dayData.DOWN, dayData.UP, StatusObj.DOWN, site.barStyle);
|
||||
cssClass = returnStatusClass(
|
||||
dayData.DOWN,
|
||||
dayData.total,
|
||||
StatusObj.DOWN,
|
||||
site.barStyle
|
||||
);
|
||||
|
||||
summaryDuration = getSummaryDuration(dayData.DOWN, selectedLang);
|
||||
summaryStatus = "DOWN";
|
||||
}
|
||||
|
||||
if (!!_90Day[ts]) {
|
||||
_90Day[ts].timestamp = ts;
|
||||
_90Day[ts].cssClass = cssClass;
|
||||
_90Day[ts].summaryDuration = summaryDuration;
|
||||
_90Day[ts].summaryStatus = summaryStatus;
|
||||
_90Day[ts].textClass = cssClass.replace(/-\d+$/, "");
|
||||
}
|
||||
_90Day[ts] = {
|
||||
border: true
|
||||
};
|
||||
|
||||
_90Day[ts].timestamp = ts;
|
||||
_90Day[ts].cssClass = cssClass;
|
||||
|
||||
_90Day[ts].summaryStatus = l(lang, summaryTime(summaryStatus), {
|
||||
status: l(lang, summaryStatus),
|
||||
duration: summaryDuration
|
||||
});
|
||||
|
||||
_90Day[ts].textClass = cssClass.replace(/-\d+$/, "");
|
||||
}
|
||||
let uptime90DayNumerator = totalUpCount + totalDegradedCount;
|
||||
let uptime90DayDenominator = totalUpCount + totalDownCount + totalDegradedCount;
|
||||
@@ -162,13 +154,22 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
}
|
||||
// return _90Day;
|
||||
let uptime90Day = ParseUptime(uptime90DayNumerator, uptime90DayDenominator);
|
||||
if (site.summaryStyle === "CURRENT") {
|
||||
let todayDataDb = await db.getMonitoringData(
|
||||
monitor.tag,
|
||||
latestTimestamp,
|
||||
latestTimestamp + secondsInDay
|
||||
);
|
||||
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = "UP";
|
||||
|
||||
let summaryColorClass = "api-nodata";
|
||||
|
||||
let todayDataDb = await db.getMonitoringData(monitor.tag, midnight, midnight + secondsInDay);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, midnight);
|
||||
todayDataDb = InterpolateData(
|
||||
todayDataDb,
|
||||
midnight,
|
||||
anchorStatus,
|
||||
todayDataDb.length == 0 ? midnight + secondsInDay : null
|
||||
);
|
||||
|
||||
if (site.summaryStyle === "CURRENT") {
|
||||
summaryColorClass = "api-up";
|
||||
|
||||
let lastRow = todayDataDb[todayDataDb.length - 1];
|
||||
@@ -189,6 +190,10 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
summaryStatus = "DOWN";
|
||||
summaryColorClass = "api-down";
|
||||
}
|
||||
summaryStatus = l(lang, summaryTime(summaryStatus), {
|
||||
status: l(lang, summaryStatus),
|
||||
duration: summaryDuration
|
||||
});
|
||||
} else {
|
||||
let lastData = _90Day[latestTimestamp];
|
||||
summaryColorClass = lastData.cssClass.replace(/-\d+$/, "");
|
||||
@@ -198,7 +203,6 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
return {
|
||||
_90Day: _90Day,
|
||||
uptime90Day: uptime90Day,
|
||||
summaryDuration: summaryDuration,
|
||||
summaryStatus: summaryStatus,
|
||||
summaryColorClass: summaryColorClass,
|
||||
barRoundness: site.barRoundness,
|
||||
|
||||
+19
-1
@@ -108,6 +108,17 @@ const BeginningOfDay = (options = {}) => {
|
||||
);
|
||||
return dt.getTime() / 1000;
|
||||
};
|
||||
const BeginningOfMinute = (options = {}) => {
|
||||
const { date = new Date(), timeZone } = options;
|
||||
const parts = Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
hourCycle: "h23",
|
||||
second: "numeric"
|
||||
}).formatToParts(date);
|
||||
const second = parseInt(parts.find((i) => i.type === "second").value);
|
||||
const dt = new Date(1000 * Math.floor((date - second * 1000) / 1000));
|
||||
return dt.getTime() / 1000;
|
||||
};
|
||||
const ValidateIpAddress = function (input) {
|
||||
// Check if input is a valid IPv4 address
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
@@ -276,6 +287,11 @@ function GenerateRandomColor() {
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
return randomColor;
|
||||
}
|
||||
|
||||
//wait for x ms promise
|
||||
function Wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
export {
|
||||
IsValidURL,
|
||||
IsValidHTTPMethod,
|
||||
@@ -299,5 +315,7 @@ export {
|
||||
ReplaceAllOccurrences,
|
||||
GetRequiredSecrets,
|
||||
ValidateMonitorAlerts,
|
||||
GenerateRandomColor
|
||||
GenerateRandomColor,
|
||||
BeginningOfMinute,
|
||||
Wait
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
|
||||
<svelte:window on:pagechange={pageChange} on:rightbar={updateTableOfContents} />
|
||||
<svelte:head>
|
||||
<title>Kener Documentation</title>
|
||||
<link rel="icon" id="kener-app-favicon" href="{base}/logo96.png" />
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
@@ -86,7 +87,7 @@
|
||||
<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">
|
||||
v3.0.0
|
||||
v3.0.7
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,8 @@ export async function load({ params, route, url, parent }) {
|
||||
siteData,
|
||||
monitors[i],
|
||||
parentData.localTz,
|
||||
parentData.selectedLang
|
||||
parentData.selectedLang,
|
||||
parentData.lang
|
||||
);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
|
||||
@@ -141,10 +141,10 @@
|
||||
{/each}
|
||||
{/if}
|
||||
{#if hasConfiguredAnalytics}
|
||||
<script src="https://unpkg.com/analytics/dist/analytics.min.js" defer></script>
|
||||
<script src="https://unpkg.com/analytics/dist/analytics.min.js"></script>
|
||||
{#each data.site.analytics as { id, type, script }}
|
||||
{#if !!id}
|
||||
<script data-type={type} src={script} defer></script>
|
||||
<script data-type={type} src={script}></script>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// @ts-nocheck
|
||||
import { FetchData } from "$lib/server/page";
|
||||
import { GetMonitors, GetIncidentsOpenHome } from "$lib/server/controllers/controller.js";
|
||||
import { SortMonitor } from "$lib/clientTools.js";
|
||||
import moment from "moment";
|
||||
|
||||
export async function load({ parent, url }) {
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
|
||||
const query = url.searchParams;
|
||||
const requiredCategory = query.get("category") || "Home";
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
let pageTitle = siteData.title;
|
||||
let pageDescription = "";
|
||||
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
|
||||
@@ -30,7 +33,8 @@ export async function load({ parent, url }) {
|
||||
siteData,
|
||||
monitors[i],
|
||||
parentData.localTz,
|
||||
parentData.selectedLang
|
||||
parentData.selectedLang,
|
||||
parentData.lang
|
||||
);
|
||||
monitors[i].pageData = data;
|
||||
|
||||
@@ -48,6 +52,19 @@ export async function load({ parent, url }) {
|
||||
//if not home page
|
||||
let isCategoryPage = !!query.get("category") && query.get("category") !== "Home";
|
||||
let isMonitorPage = !!query.get("monitor");
|
||||
if (isMonitorPage && monitorsActive.length > 0) {
|
||||
pageTitle = monitorsActive[0].name + " - " + pageTitle;
|
||||
pageDescription = monitorsActive[0].description;
|
||||
}
|
||||
//if category page
|
||||
if (isCategoryPage) {
|
||||
let allCategories = siteData.categories;
|
||||
let selectedCategory = allCategories.find((category) => category.name === requiredCategory);
|
||||
if (selectedCategory) {
|
||||
pageTitle = selectedCategory.name + " - " + pageTitle;
|
||||
pageDescription = selectedCategory.description;
|
||||
}
|
||||
}
|
||||
if (isCategoryPage || isMonitorPage) {
|
||||
let eligibleTags = monitorsActive.map((monitor) => monitor.tag);
|
||||
//filter incidents that have monitor_tag in monitors
|
||||
@@ -82,11 +99,14 @@ export async function load({ parent, url }) {
|
||||
return incident;
|
||||
});
|
||||
let unresolvedIncidents = allOpenIncidents.filter((incident) => incident.state !== "RESOLVED");
|
||||
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
unresolvedIncidents: allOpenIncidents,
|
||||
categoryName: requiredCategory,
|
||||
isCategoryPage: isCategoryPage,
|
||||
isMonitorPage: isMonitorPage
|
||||
isMonitorPage: isMonitorPage,
|
||||
pageTitle: pageTitle,
|
||||
pageDescription: pageDescription
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@
|
||||
|
||||
if (data.isCategoryPage) {
|
||||
let category = data.site.categories.find((e) => e.name == data.categoryName);
|
||||
data.site.hero.title = category.name;
|
||||
data.site.hero.subtitle = category.description;
|
||||
if (!!category) {
|
||||
data.site.hero.title = category.name;
|
||||
data.site.hero.subtitle = category.description;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -43,6 +45,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.pageTitle}</title>
|
||||
{#if !!data.pageDescription}
|
||||
<meta name="description" content={data.pageDescription} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
<div class="mt-12"></div>
|
||||
{#if data.site.hero && !data.isMonitorPage}
|
||||
<section
|
||||
@@ -183,7 +191,7 @@
|
||||
window.location.href = `?category=${category.name}`;
|
||||
}}
|
||||
>
|
||||
<Card.Root class="hover:bg-secondary">
|
||||
<Card.Root class="mb-4 hover:bg-secondary">
|
||||
<Card.Header class="bounce-right relative w-full cursor-pointer px-4 ">
|
||||
<Card.Title class="w-full ">
|
||||
{category.name}
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
ParseUptime
|
||||
} from "$lib/server/tool.js";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import { GetAllSiteData, GetIncidentsByIDS } from "$lib/server/controllers/controller.js";
|
||||
import {
|
||||
GetAllSiteData,
|
||||
GetIncidentsByIDS,
|
||||
InterpolateData,
|
||||
GetLastStatusBefore
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
@@ -31,6 +36,8 @@ export async function POST({ request }) {
|
||||
}
|
||||
|
||||
let dayData = await db.getMonitoringData(monitor.tag, payload.startTs, end);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
|
||||
dayData = InterpolateData(dayData, payload.startTs, anchorStatus, end);
|
||||
let siteData = await GetAllSiteData();
|
||||
|
||||
let ups = 0;
|
||||
|
||||
@@ -8,18 +8,29 @@ import {
|
||||
ParseUptime
|
||||
} from "$lib/server/tool.js";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import { InterpolateData, GetLastStatusBefore } from "$lib/server/controllers/controller.js";
|
||||
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
const monitor = payload.monitor;
|
||||
const start = payload.startTs;
|
||||
let end = GetMinuteStartNowTimestampUTC();
|
||||
let aggregatedData = await db.getAggregatedMonitoringData(monitor.tag, start, end);
|
||||
let rawData = await db.getMonitoringData(monitor.tag, start, end);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
|
||||
rawData = InterpolateData(rawData, start, anchorStatus, rawData.length == 0 ? end : null);
|
||||
|
||||
let aggregatedData = rawData.reduce(
|
||||
(acc, row) => {
|
||||
acc[row.status] = (acc[row.status] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
UP: 0,
|
||||
DOWN: 0,
|
||||
DEGRADED: 0
|
||||
}
|
||||
);
|
||||
//covert all keys to uppercase
|
||||
aggregatedData = Object.keys(aggregatedData).reduce((acc, key) => {
|
||||
acc[key.toUpperCase()] = aggregatedData[key];
|
||||
return acc;
|
||||
}, {});
|
||||
let ups = Number(aggregatedData.UP);
|
||||
let downs = Number(aggregatedData.DOWN);
|
||||
let degradeds = Number(aggregatedData.DEGRADED);
|
||||
|
||||
@@ -50,6 +50,13 @@
|
||||
let sortedIncidentSmartDates = Object.keys(incidentSmartDates).sort((a, b) => a - b);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang)}
|
||||
{l(data.lang, "Incident Updates")} |
|
||||
{data.site.title}
|
||||
</title>
|
||||
</svelte:head>
|
||||
<div class="mt-12"></div>
|
||||
<section class="mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
|
||||
<Button
|
||||
@@ -98,8 +105,12 @@
|
||||
</div>
|
||||
|
||||
<h1 class=" text-xl font-semibold leading-tight">
|
||||
{l(data.lang, "No Incident On %date", {
|
||||
date: data.thisMonthName.replace("-", ", ")
|
||||
{l(data.lang, "No Incident in %date", {
|
||||
date: f(
|
||||
parse(data.thisMonthName, "MMMM-yyyy", new Date()),
|
||||
"MMMM, yyyy",
|
||||
selectedLang
|
||||
)
|
||||
})}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function POST({ request, cookies }) {
|
||||
let token = await GenerateToken(userDB);
|
||||
let cookieConfig = CookieConfig();
|
||||
cookies.set(cookieConfig.name, token, {
|
||||
path: "/" + base,
|
||||
path: cookieConfig.path,
|
||||
maxAge: cookieConfig.maxAge, // 1 year in seconds
|
||||
httpOnly: cookieConfig.httpOnly,
|
||||
secure: cookieConfig.secure,
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function GET({ request, cookies }) {
|
||||
|
||||
//delete cookie from browser
|
||||
cookies.set(cookieConfig.name, "", {
|
||||
path: "/" + base,
|
||||
path: cookieConfig.path,
|
||||
maxAge: 0,
|
||||
httpOnly: cookieConfig.httpOnly,
|
||||
secure: cookieConfig.secure,
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function POST({ request, cookies }) {
|
||||
let cookieConfig = CookieConfig();
|
||||
//set server side cookie with 1 year expiry
|
||||
cookies.set(cookieConfig.name, token, {
|
||||
path: "/" + base,
|
||||
path: cookieConfig.path,
|
||||
maxAge: cookieConfig.maxAge, // 1 year in seconds
|
||||
httpOnly: cookieConfig.httpOnly,
|
||||
secure: cookieConfig.secure,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
<MonitorsAdd
|
||||
monitorSort={data.siteData?.monitorSort}
|
||||
categories={data.categories}
|
||||
colorDown={data.siteData?.colors?.DOWN || "#ca3038"}
|
||||
colorDegraded={data.siteData?.colors?.DEGRADED || "#e6ca61"}
|
||||
|
||||
Reference in New Issue
Block a user