Compare commits

..

51 Commits

Author SHA1 Message Date
Raj Nandan Sharma 1b4ca67cf0 Merge pull request #231 from rajnandan1/release/3.0.7
feat(triggers): support custom webhook body as requested in #230
2025-01-31 09:10:18 +05:30
Raj Nandan Sharma 53936e19d4 feat(triggers): support custom webhook body as requested in #230 2025-01-31 09:08:26 +05:30
Raj Nandan Sharma b58d00c5a6 feat(triggers): support custom webhook body as requested in #230 2025-01-31 09:03:57 +05:30
Raj Nandan Sharma 41db0c45cd Merge pull request #229 from YunusEmreAlps/feature/locales-tr
feat: add Turkish locale support and update locale files
2025-01-30 19:43:55 +05:30
Yunus Emre Alpu 5361b551eb feat: add Turkish locale support and update locale files 2025-01-30 13:05:08 +03:00
Raj Nandan Sharma eb87647466 Merge pull request #228 from rajnandan1/release/3.0.6
feat: support of promises in eval
2025-01-29 10:09:35 +05:30
Raj Nandan Sharma d9b900f28d feat: support of promises in eval 2025-01-28 23:37:26 +05:30
Raj Nandan Sharma 198bd6c723 Merge pull request #227 from matribeiro15/main
Update pt-BR.json
2025-01-28 22:44:26 +05:30
Mateus Ribeiro 1831e6309f Update pt-BR.json 2025-01-28 12:32:22 -03:00
Raj Nandan Sharma 007f5149d2 Update README.md 2025-01-27 23:07:06 +05:30
Raj Nandan Sharma 1a094e3511 Merge pull request #226 from rajnandan1/release/3.0.5
fix: locale for no incidents as reported in #225
2025-01-27 22:39:19 +05:30
Raj Nandan Sharma db1ddc1292 fix: locale for no incidents as reported in #225 2025-01-27 22:37:54 +05:30
Raj Nandan Sharma 535e3bb36a fix: hi.json fixed 2025-01-26 16:12:08 +05:30
Raj Nandan Sharma 785237e55e Merge pull request #223 from cosmic-jellyfish/feature/Further-Locale-Fixes
fix: localisation strings for further clarity
2025-01-26 16:08:04 +05:30
Raj Nandan Sharma e6bf47a859 docs: deployment docs 2025-01-26 15:13:46 +05:30
Raj Nandan Sharma 93ae1711f1 docs: deployment docs 2025-01-26 15:04:57 +05:30
Raj Nandan Sharma af97812beb docs: railway deploy button added 2025-01-26 13:12:38 +05:30
Cake 78cf22ade9 fix: correct spacing 2025-01-26 08:09:48 +11:00
Cake b4a461d93a fix: localisation strings for further clarity 2025-01-26 08:04:37 +11:00
Raj Nandan Sharma 5f33c02064 Merge pull request #222 from rajnandan1/release/3.0.4
Release/3.0.4
2025-01-25 21:43:05 +05:30
Raj Nandan Sharma 46c1a392b8 refactor: remove docs from docker build 2025-01-25 21:41:11 +05:30
Raj Nandan Sharma c6880c5df6 feat: sitemap and bug fixes 2025-01-25 21:18:42 +05:30
Raj Nandan Sharma 646da94ef9 feat: sitemap and bug fixes 2025-01-25 21:16:17 +05:30
Raj Nandan Sharma 36ede93dce feat: sitemap and bug fixes 2025-01-25 21:12:41 +05:30
Raj Nandan Sharma 9ed35589f7 Merge pull request #220 from rajnandan1/release/3.0.3
fix: load time #219
2025-01-24 21:45:33 +05:30
Raj Nandan Sharma e946dd18e0 fix: load time #219 2025-01-24 21:44:36 +05:30
Raj Nandan Sharma 51cad598c1 fix: load time #219 2025-01-24 21:42:57 +05:30
Raj Nandan Sharma 0595f23c18 Merge pull request #218 from cosmic-jellyfish/feature/ENG-Locale-Fix
English Locale tweaks
2025-01-24 13:04:58 +05:30
Raj Nandan Sharma b88816706b fix: retry await decrease from 4000 to 500ms 2025-01-24 11:42:06 +05:30
Cake 80edaae023 fix: update localisation strings across the board 2025-01-24 17:08:09 +11:00
Raj Nandan Sharma 883c46b064 Merge pull request #214 from rajnandan1/release/3.0.2
release of 3.0.2
2025-01-24 10:48:15 +05:30
Raj Nandan Sharma 4248cc31f2 chore: updated package.json version 2025-01-24 10:47:56 +05:30
Raj Nandan Sharma 5f81c6d61a fix: daily data local date format 2025-01-24 10:45:50 +05:30
Raj Nandan Sharma 00d82cc817 fix: daily data local date format 2025-01-24 10:44:48 +05:30
Raj Nandan Sharma a9edfde162 fix: bug fix for no data 2025-01-24 10:35:22 +05:30
Cake 468d7f445d fix: update localisation strings further 2025-01-24 15:15:13 +11:00
Cake 4cc74deece fix: update localisation strings 2025-01-24 15:11:50 +11:00
Raj Nandan Sharma f9831490af fix: base path for docker build 2025-01-24 09:08:13 +05:30
Raj Nandan Sharma a055a616eb fix: fix logo url as reported in #213 2025-01-23 23:37:39 +05:30
Raj Nandan Sharma 8009a2cbc4 feat: monitor re-arrange as requested in #215 2025-01-23 22:39:13 +05:30
Raj Nandan Sharma 693cc149cf feat: added retry for api timeouts as requested in #208 2025-01-23 10:33:15 +05:30
Raj Nandan Sharma d126fe0bce fix: fix for #212, data interpolation introduced 2025-01-23 09:36:37 +05:30
Raj Nandan Sharma 4cc49cce9e fix: default locale fix as reported in #209 2025-01-22 09:25:56 +05:30
Raj Nandan Sharma f7cb4cd805 fix: cron validation updated as reported in #206 2025-01-21 22:02:39 +05:30
Raj Nandan Sharma a2bd9883d1 chore: remove old docs.md 2025-01-21 09:18:21 +05:30
Raj Nandan Sharma 10a079c304 fix: fixing #193 base path login was having double slash in cookie 2025-01-21 09:12:10 +05:30
Raj Nandan Sharma 65f12f1082 docs: added contributing file 2025-01-21 08:44:58 +05:30
Raj Nandan Sharma c15f855c72 docs: added contributing file 2025-01-21 08:42:41 +05:30
Raj Nandan Sharma 3d38483022 Merge pull request #205 from matribeiro15/main
Add Brazilian Portuguese
2025-01-21 08:31:33 +05:30
Mateus Ribeiro 57dc1e7175 modified: src/lib/i18n/client.js
modified:   src/lib/locales/locales.json
	new file:   src/lib/locales/pt-BR.json
2025-01-21 01:22:01 +00:00
Raj Nandan Sharma ead68c929f docs: embed docs 2025-01-18 23:19:38 +05:30
63 changed files with 1184 additions and 1565 deletions
+1
View File
@@ -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
+48
View File
@@ -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
View File
@@ -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
+36 -10
View File
@@ -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
[![Deploy on Railway](https://railway.com/button.svg)](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
![image](https://badges.pufler.dev/visits/rajnandan1/kener)
## 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
[![Star History Chart](https://api.star-history.com/svg?repos=rajnandan1/kener&type=Date)](https://star-history.com/#rajnandan1/kener&Date)
-1255
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -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
+11 -1
View File
@@ -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 &#x3C;style&#x3E; tags.
</div>
+21 -13
View File
@@ -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.
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
+4 -4
View File
@@ -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
-8
View File
@@ -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
View File
@@ -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
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
## Features
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+14
View File
@@ -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 &#x3C;style&#x3E; tags.
</div>
+40 -14
View File
@@ -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>.
+5
View File
@@ -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
+13 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
};
+4 -4
View File
@@ -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>
+39 -30
View File
@@ -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
+82 -8
View File
@@ -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
+1 -11
View File
@@ -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.";
}
+83 -3
View 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>$&#123;variable&#125;</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">
+8 -12
View File
@@ -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">
+1 -4
View File
@@ -169,10 +169,7 @@
{l(lang, "Embed")}
</h2>
<p class="mb-1 text-xs text-muted-foreground">
{@html l(
lang,
"Embed this moni2tor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app."
)}
{l(lang, "Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.")}
</p>
<div class="mb-4 grid grid-cols-2 gap-2">
<div class="col-span-1">
+4 -13
View File
@@ -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) {
+5 -5
View File
@@ -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",
+5 -5
View File
@@ -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",
+6 -6
View File
@@ -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"
}
}
+5 -5
View File
@@ -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",
+5 -5
View File
@@ -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": "थीम",
+5 -5
View File
@@ -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": "テーマ",
+5 -5
View File
@@ -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": "테마",
+8
View File
@@ -42,5 +42,13 @@
{
"code": "ko",
"name": "한국어"
},
{
"code": "pt-BR",
"name": "Português Brasileiro"
},
{
"code": "tr",
"name": "Türkçe"
}
]
+5 -5
View File
@@ -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",
+55
View File
@@ -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 &#x3C;script&#x3E; or &#x3C;iframe&#x3E; 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"
}
+5 -5
View File
@@ -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": "Тема",
+55
View File
@@ -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 &#x3C;script&#x3E; or &#x3C;iframe&#x3E; 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"
}
+5 -5
View File
@@ -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",
+5 -5
View File
@@ -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": "主题",
+143 -5
View File
@@ -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>`;
};
+10
View File
@@ -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);
+52 -8
View File
@@ -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 };
+10
View File
@@ -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 }
+1 -7
View File
@@ -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") {
+39 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
};
+2 -1
View File
@@ -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]);
+2 -2
View File
@@ -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}
+23 -3
View File
@@ -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
};
}
+11 -3
View File
@@ -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 -1
View File
@@ -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"}