Compare commits

..

36 Commits

Author SHA1 Message Date
Raj Nandan Sharma 64b94da585 Merge pull request #340 from rajnandan1/release/3.2.2
Release/3.2.2
2025-03-08 22:57:28 +05:30
Raj Nandan Sharma 1609b4fc50 refactor: remove unnecessary future and ongoing incident checks from IncidentNew component 2025-03-08 22:56:57 +05:30
Raj Nandan Sharma dfffffb101 fix: remove database directory writable check from entrypoint script 2025-03-08 22:26:14 +05:30
Raj Nandan Sharma 81250a117a chore: update version to 3.2.2 in package.json and documentation 2025-03-08 22:18:53 +05:30
Raj Nandan Sharma 72bd0241e9 fix: streamline incident creation logic for database compatibility , fixes #325 2025-03-08 22:17:48 +05:30
Raj Nandan Sharma 63c24f147f fix: remove redundant startup command in entrypoint script #338 2025-03-08 21:19:46 +05:30
Raj Nandan Sharma 88fb7df3f5 Enhances incident display with time status
Improves the incident display by adding time status information
such as "Starts in", "Started", and "Will last for".
Also fixes database directory write permissions on startup.

Also fixes #337
2025-03-08 21:17:34 +05:30
Raj Nandan Sharma 320b1a0cc5 Improves webhook and notification handling
- Makes the monitor tags wrap on smaller screens.
- Validates webhook body.
- Adds user agent to webhook.
- Fixes Discord logo URL construction.

Issue #336
2025-03-07 20:09:35 +05:30
Raj Nandan Sharma 08b168ee6e Merge pull request #331 from rajnandan1/feature/hb1
Adds heartbeat monitor and improvements
2025-03-01 21:56:21 +05:30
Raj Nandan Sharma d6a87ac81a docs: update heartbeat monitors description for clarity
refactor: remove unused RandomString import from monitorSheet component

chore: clean up commented-out code in FetchData function
2025-03-01 21:55:37 +05:30
Raj Nandan Sharma 615dba42b8 fix: change default monitor type from HEARTBEAT to NONE 2025-03-01 21:02:12 +05:30
Raj Nandan Sharma 3ffec4f1fe Adds heartbeat monitor and improvements
Implements push-based monitoring via heartbeats.

Fixes data interpolation issues.

Enhances UI and documentation.
2025-03-01 21:00:05 +05:30
Raj Nandan Sharma 60b4b6f207 Merge pull request #327 from rajnandan1/feature/hb
feat: enhance documentation and add donation banner for Kener
2025-02-28 10:00:37 +05:30
Raj Nandan Sharma b9f5eb56c5 feat: enhance documentation and add donation banner for Kener 2025-02-28 09:59:39 +05:30
github-actions 7b9ae73044 Auto-generate README.md with release versions 2025-02-27 05:47:27 +00:00
Raj Nandan Sharma 87f2c33eba Merge pull request #323 from rajnandan1/feature/optimize-1
feat: upgrade to version 3.2.0 with improved monitor evaluation funct…
2025-02-27 10:54:40 +05:30
Raj Nandan Sharma 0a73a8b10a fix: update eval function to use responseRaw instead of responseData 2025-02-27 10:53:23 +05:30
Raj Nandan Sharma af65404fd3 feat: upgrade to version 3.2.0 with improved monitor evaluation functions and enhanced API support 2025-02-27 10:46:59 +05:30
github-actions 398f891dac Auto-generate README.md with release versions 2025-02-27 01:19:17 +00:00
Raj Nandan Sharma 6a2375a774 Merge pull request #322 from rajnandan1/fix/optimize
fix: eval not working for api
2025-02-27 06:30:30 +05:30
Raj Nandan Sharma 43bbcf4015 fix: eval not working for api 2025-02-27 06:29:39 +05:30
github-actions fadb563337 Auto-generate README.md with release versions 2025-02-26 07:02:13 +00:00
Raj Nandan Sharma 1ae54b3906 Improve formatting and add video tutorial section in quick start documentation 2025-02-26 12:20:17 +05:30
Raj Nandan Sharma 1c069e2ee2 Bump version to 3.1.9 and update documentation layout 2025-02-26 12:12:17 +05:30
Raj Nandan Sharma ed1099ac11 Merge pull request #319 from rajnandan1/feature/sql-monitor
Adds SQL monitor functionality as asked in #244
2025-02-26 12:09:39 +05:30
Raj Nandan Sharma 99d3a7e046 Adds SQL monitor functionality as asked in #244
Implements the SQL monitor feature, allowing users to monitor database connections and queries.

Adds UI elements for configuring SQL monitor parameters, including connection string, query, and timeout.

Validates user inputs for SQL monitor configuration.
2025-02-26 12:06:05 +05:30
Raj Nandan Sharma 2144acf34d Merge pull request #318 from rajnandan1/feature/ssl
Add SSL monitor functionality and related documentation #317
2025-02-26 09:15:46 +05:30
Raj Nandan Sharma 7a8ad8e833 Enhance port validation logic in SSL configuration for improved error handling 2025-02-26 09:14:04 +05:30
Raj Nandan Sharma 3b45f33692 Remove unnecessary assignment of type_data in monitorSheet component 2025-02-26 09:11:26 +05:30
Raj Nandan Sharma b4a2340ec7 Add SSL monitor functionality and related documentation #317 2025-02-26 09:09:20 +05:30
Raj Nandan Sharma 5910e3b930 Merge pull request #315 from rajnandan1/feature/css-cls
Refactor section classes for improved styling and organization in the…
2025-02-24 22:05:54 +05:30
Raj Nandan Sharma fd58beaa69 Refactor section classes for improved clarity and organization in incident page layout 2025-02-24 22:05:16 +05:30
Raj Nandan Sharma 5449e422a5 Refactor section classes for improved styling and organization in the hero and event sections 2025-02-24 22:02:53 +05:30
Raj Nandan Sharma dfb1784b6f Merge pull request #312 from rajnandan1/feature/cheerio
Add cheerio dependency and enhance API call examples with HTML parsing
2025-02-24 07:01:24 +05:30
Raj Nandan Sharma d956c60b48 Add cheerio dependency and enhance API call examples with HTML parsing 2025-02-24 06:56:09 +05:30
github-actions 69e1f2af6e Auto-generate README.md with release versions 2025-02-23 12:41:00 +00:00
67 changed files with 2447 additions and 1098 deletions
+11 -11
View File
@@ -79,7 +79,7 @@ npm run dev
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.7"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.1.7-blue" alt="Kener latest stable version: 3.1.7" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.0"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.0-blue" alt="Kener latest stable version: 3.2.0" /></a>
#### Available Tags
@@ -93,38 +93,38 @@ Official Docker images for **Kener** are available on [Docker Hub](https://hub.d
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
<td>Latest stable release (aka 3.1.7)</td>
<td>Latest stable release (aka 3.2.0)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.7" target="_blank"><code>3.1.7</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.0" target="_blank"><code>3.2.0</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1" target="_blank"><code>3.1</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.1.7) release within that minor version (3.1.x)</td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2" target="_blank"><code>3.2</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.0) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
<td>Major version tag pointing to the latest stable (3.1.7) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.0) release within that major version (3.x.x)</td>
</tr>
<tr>
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 &nbsp;<strong><em>(smallest image size)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
<td>Latest stable release (aka 3.1.7)</td>
<td>Latest stable release (aka 3.2.0)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.7-alpine" target="_blank"><code>3.1.7-alpine</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.0-alpine" target="_blank"><code>3.2.0-alpine</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1-alpine" target="_blank"><code>3.1-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.1.7) release within that minor version (3.1.x)</td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2-alpine" target="_blank"><code>3.2-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.0) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
<td>Major version tag pointing to the latest stable (3.1.7) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.0) release within that major version (3.x.x)</td>
</tr>
</table>
+124
View File
@@ -7,6 +7,130 @@ description: Changelogs for Kener
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
## v3.2.1
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **New Heartbeat Monitors**
- Added support for push-based monitoring via heartbeats
- Monitors systems that send periodic signals to confirm they're running
- Configurable thresholds for degraded and down states
- Secured with secret token authentication
- Accessible via simple HTTP API endpoints
- **Data Interpolation Improvements**
- Fixed data interpolation issues for more accurate uptime calculations
- Improved handling of timestamp boundaries
### Fixes & Improvements
- **UI Enhancements**
- Better mobile responsiveness for theme toggles and controls
- Fixed layout issues with bottom navigation buttons
- Improved timezone selector display
- **Data Processing**
- Fixed edge case issues with uptime calculations
- Improved handling of "No Data" status in summaries
- More accurate time range display across timezones
- **Documentation**
- Added comprehensive documentation for Heartbeat Monitors
- Updated API documentation with new endpoints
## v3.2.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **Improved Monitor Evaluation Functions**
- Replaced unsafe `eval()` with secure `Function()` constructors across all monitor types
- Added support for using modules (like cheerio) directly in evaluation functions
- **Enhanced API Monitors**
- Raw response data is now passed directly to eval functions instead of base64 encoded
- Added `modules` parameter with access to cheerio for HTML parsing
- Updated default evaluation function to use the new parameter structure
- **Improved TCP & Ping Monitors**
- Simplified evaluation functions with direct access to ping/TCP data
- Removed unnecessary base64 encoding/decoding steps
- Better error handling for invalid evaluation functions
- **Documentation Updates**
- Updated all examples and documentation for the new evaluation function signatures
- Added more detailed explanations of input parameters
- Improved examples showing usage with the new parameter structure
### Breaking Changes
- **Monitor Evaluation Functions**
- Custom evaluation functions will need to be updated to the new parameter structure
- API monitors: `(statusCode, responseTime, responseRaw, modules)` instead of `(statusCode, responseTime, responseDataBase64)`
- TCP/Ping monitors: `(arrayOfPings)` instead of `(responseDataBase64)`
### Fixes
- Fixed "cheerio is undefined" errors in API monitor evaluations
- Improved error handling and logging for monitor evaluation failures
- Security enhancements by removing `eval()` usage
### Migration
If you're using custom evaluation functions in your monitors, you'll need to update them to the new format:
#### API Monitors
```javascript
// Old format
;(async function (statusCode, responseTime, responseDataBase64) {
const resp = atob(responseDataBase64)
// Your logic here
})
```
```javascript
// New format
;(async function (statusCode, responseTime, responseRaw, modules) {
// responseRaw is the direct response - no need to decode
// Access cheerio with modules.cheerio
// Your logic here
})
```
#### TCP/Ping Monitors
```javascript
// Old format
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
// Your logic here
})
```
```javascript
// New format
;(async function (arrayOfPings) {
// arrayOfPings is directly available - no need to decode
// Your logic here
})
```
## v3.1.8
<picture>
+30 -75
View File
@@ -1,5 +1,5 @@
---
title: Kener Documentation
title: Kener Documentation | Open Source Status Page
description: Kener is a feature-rich and modern status page system built with SvelteKit and NodeJS. It is open-source and free to use.
---
@@ -36,86 +36,41 @@ description: Kener is a feature-rich and modern status page system built with Sv
</picture>
</div>
<div class="flex gap-2 kener-home-links">
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://kener.ing">Live Demo</a>
</div>
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://kener.ing/docs/quick-start">Quick Start</a>
</div>
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://github.com/rajnandan1/kener">Clone</a>
</div>
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://kener.ing/docs/deployment">Deploy</a>
</div>
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://kener.ing/docs/kener-apis">APIs</a>
</div>
<a href="https://kener.ing" class="flex-1 border rounded-md py-4 px-2 text-center">
Live Demo
</a>
<a href="https://kener.ing/docs/quick-start" class="flex-1 border rounded-md py-4 px-2 text-center">
Quick Start
</a>
<a href="https://github.com/rajnandan1/kener" class="flex-1 border rounded-md py-4 px-2 text-center">
Clone
</a>
<a href="https://kener.ing/docs/deployment" class="flex-1 border rounded-md py-4 px-2 text-center">
Deploy
</a>
<a href="https://kener.ing/docs/kener-apis" class="flex-1 border rounded-md py-4 px-2 text-center">
APIs
</a>
</div>
## What is Kener?
## Forget Boring Status Pages. Hello Kener!
Kener is status page system built with Sveltekit and NodeJS. It does not try to replace the Datadogs and Atlassian of the world. It tries to help some who wants to come up with a status page that looks nice and minimum overhead, in a modern way.
**Kener** isn't just another status page it's a beautifully crafted, lightning-fast monitoring system that makes your services look good even when they're down. Built with modern tech (**SvelteKit** + **NodeJS**), it delivers enterprise-grade reliability monitoring without the enterprise price tag (it's completely free!).
It comes with all the basic asks for a status page. It is open-source and free to use.
### Why Teams Are Switching to Kener
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.
- **📈 Up and running in minutes**, not days from Docker to deployment in less time than it takes to make coffee
- **🎭 Dark mode that actually looks good** because your 3 AM incident responses shouldn't burn your retinas
- **🧩 Ridiculously customizable** make it match your brand or go wild with a unique design
- **⚡ Blazing fast performance** server-rendered for speed that both users and search engines love
- **🌐 Works everywhere** auto-adjusts to visitors' time zones and looks great on any device
- **🔄 Real-time data** with gorgeous visualizations that make your uptime metrics pop
- **🛠️ Modern tooling** without the bloat everything you need, nothing you don't
## Quick Deployment
### Used by Companies That Value Simplicity and Power
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
From indie developers to growing startups, teams are choosing Kener when they need reliability monitoring that doesn't require a dedicated DevOps team to maintain.
## Features
"_My team set up Kener in less than an hour, and our customers love the transparency it provides. The embedded widgets on our pricing page increased conversions by showing our reliability upfront._" A happy user
### Monitoring and Tracking
- Advanced application performance monitoring tools
- Types of Monitors: HTTP, TCP, DNS, ICMP etc
- Real-time network monitor software capabilities
- Polls HTTP endpoint or Push data to monitor using Rest APIs
- Handles Timezones for visitors
- Categorize Monitors into different Sections
- Cron-based scheduling for monitors. Minimum per minute
- Construct complex API Polls - Chain, Secrets etc
- Supports a Default Status for Monitors
- Supports base path for hosting in k8s
- Pre-built docker image for easy deployment
### Customization and Branding
- Customizable status page
- Badge generation for status and uptime of Monitors
- Support for custom domains
- Embed Monitor as an iframe or widget
- Light + Dark Theme
- Internationalization support
- Beautifully Crafted Status Page
### Incident Management
- Incident Management
- Incident Communication
- Comprehensive APIs for Incident Management
### User Experience and Design
- 100% Accessibility Score
- Easy installation and setup
- User-friendly interface
- Responsive design for various devices
- Auto SEO and Social Media ready
- Server Side Rendering
## Technologies used
- [SvelteKit](https://kit.svelte.dev/)
- [shadcn-svelte](https://www.shadcn-svelte.com/)
## Support Me
If you are using Kener and want to support me, you can do so by sponsoring me on GitHub or buying me a coffee.
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
## Ready to Transform Your Status Page?
+1
View File
@@ -77,6 +77,7 @@ Copy and update the translation file in the `locales` folder. The translation fi
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Started %startedAt, will last for %lastedFor more": "Started %startedAt, will last for %lastedFor more",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
+38 -16
View File
@@ -51,7 +51,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
> `{status:"DEGRADED", latency: 200}`.
```javascript
(async function (statusCode, responseTime, responseDataBase64) {
(async function (statusCode, responseTime, responseRaw, modules) {
let statusCodeShort = Math.floor(statusCode/100);
let status = 'DOWN'
if(statusCodeShort >=2 && statusCodeShort <= 3) {
@@ -64,23 +64,19 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
})
```
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
- `responseRaw` **REQUIRED** is the raw response of the API
- `modules` is an object that has [`cheerio`](https://www.npmjs.com/package/cheerio) that you can use to parse HTML. `const $ = modules.cheerio.load(responseRaw)`
```js
let decodedResp = atob(responseDataBase64)
//if the response is a json object
//let jsonResp = JSON.parse(decodedResp)
```
#### Example
### Example 1
The following example shows how to use the eval function to evaluate the response. The function checks if the status code is 2XX then the status is UP, if the status code is 5XX then the status is DOWN. If the response contains the word `Unknown Error` then the status is DOWN. If the response time is greater than 2000 then the status is DEGRADED.
```javascript
;(async function (statusCode, responseTime, responseDataBase64) {
const resp = atob(responseDataBase64) //convert base64 to string
(async function (statusCode, responseTime, responseRaw, modules) {
let status = "DOWN"
@@ -95,7 +91,7 @@ The following example shows how to use the eval function to evaluate the respons
//if the status code is 5XX then the status is DOWN
if (/^[5]\d{2}$/.test(statusCode)) status = "DOWN"
if (resp.includes("Unknown Error")) {
if (responseRaw.includes("Unknown Error")) {
status = "DOWN"
}
@@ -106,11 +102,13 @@ The following example shows how to use the eval function to evaluate the respons
})
```
### Example 2
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)
```js
(async function (statusCode, responseTime, responseRaw, modules) {
let htmlString = responseRaw;
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g)
if (scriptTags && scriptTags.length >= 2) {
// Extract the second last script tag's src attribute
@@ -136,6 +134,30 @@ This next example shows how to call another API withing eval. It is scrapping th
})
```
### Example 3
The next example shows how to use cheerio to parse bitbucket status page and check if all the components are operational. If all the components are operational then the status is UP else it is DOWN.
```js
(async function (statusCode, responseTime, responseDataBase64, modules) {
let html = atob(responseDataBase64)
const $ = modules.cheerio.load(html)
const components = $(".components-section .components-container .component-container")
let status = true
components.each((index, element) => {
const name = $(element).find(".component-name").text().trim()
const statusText = $(element).find(".component-status").text().trim()
if (statusText !== "Operational") {
status = false
}
})
return {
status: status ? "UP" : "DOWN",
latency: responseTime
}
})
```
## Examples
### Website Monitor
+112
View File
@@ -0,0 +1,112 @@
---
title: Heartbeat Monitors | Kener
description: Heartbeat monitors allow you to monitor systems using a "push" based approach
---
# Heartbeat Monitors
Heartbeat monitors allow you to monitor systems using a "push" based approach, where your services send signals (or "heartbeats") to Kener on a regular basis. If Kener doesn't receive a heartbeat within the configured time period, it will mark the system as degraded or down.
This is useful for monitoring services that:
- Run on a schedule
- Cannot be directly accessed for pull-based monitoring
- Need to report their own health status
## How It Works
1. You configure a heartbeat monitor in Kener
2. Kener provides a unique URL endpoint for your service to ping
3. Your service sends HTTP requests to this endpoint at regular intervals
4. If Kener doesn't receive a ping within the configured time window, it will mark the service as degraded or down
## Setting Up a Heartbeat Monitor
To create a heartbeat monitor:
1. Navigate to the Monitors page in the dashboard
2. Click "Add Monitor"
3. Select "Heartbeat" as the monitor type
4. Configure the following settings:
<div class="border rounded-md">
![Heartbeat Monitor Configuration](/documentation/m_hb.png)
</div>
### Configuration Options
| Option | Description |
| ------------------------ | -------------------------------------------------------------------------------------------- |
| Secret String | A unique secret that authenticates your heartbeat requests |
| Degraded After (Minutes) | Number of minutes after which the service is considered degraded if no heartbeat is received |
| Down After (Minutes) | Number of minutes after which the service is considered down if no heartbeat is received |
## Sending Heartbeats
Your service needs to send HTTP requests to the Kener heartbeat URL. Both GET and POST requests are supported.
### Endpoint Format
```
https://<your-kener-instance>/api/heartbeat/<tag>:<secretString>
```
Where:
- `<tag>` is the tag you configured for your monitor
- `<secretString>` is the secret you configured during setup
### Example
If your monitor has:
- Tag: `nightly-backup`
- Secret String: `super-secret-key`
Your service should send requests to:
```
https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
```
You can use any HTTP client to send these requests, such as curl:
```bash
curl https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
```
Or in a scheduled script:
```bash
#!/bin/bash
# Run your task here
./perform-backup.sh
# Send heartbeat to Kener
curl https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
```
## Monitoring State
The monitor status works as follows:
- **UP**: Heartbeats are being received within the configured time windows
- **DEGRADED**: No heartbeat has been received for longer than the "Degraded After" time period
- **DOWN**: No heartbeat has been received for longer than the "Down After" time period
## Security Considerations
- Keep your secret string confidential, as anyone with knowledge of it can send heartbeats
- Consider using HTTPS for your Kener instance to encrypt heartbeat communications
- Rotate the secret string periodically by updating the monitor configuration
## Best Practices
- Set the "Degraded After" time slightly longer than your expected interval between heartbeats
- Set the "Down After" time based on your SLA requirements
- For critical systems, consider sending heartbeats more frequently than required
- Add logging in your service to track successful and failed heartbeat attempts
Heartbeat monitors provide a simple but effective way to ensure your scheduled tasks and services are running properly, without requiring external polling.
+5 -10
View File
@@ -32,8 +32,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
> `{status:"DEGRADED", latency: 200}`.
```javascript
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
@@ -49,12 +48,9 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
})
```
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
- `arrayOfPings` **REQUIRED** is an array of Ping Response Objects as shown below.
```js
let decodedResp = atob(responseDataBase64)
let jsonResp = JSON.parse(decodedResp)
console.log(jsonResp)
```json
/*
[
{
@@ -93,7 +89,7 @@ console.log(jsonResp)
### Understanding the Input
The input to the eval function is a base64 encoded string. You will have to decode it and then parse it to get the array of objects. Each object in the array represents the ping response of a host.
Each object in the array represents the ping response of a host.
- `alive` is a boolean. It is true if the host is alive and false if the host is down.
- `min` is a string. It is the minimum latency of the pings.
@@ -109,8 +105,7 @@ The input to the eval function is a base64 encoded string. You will have to deco
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
```javascript
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
+63
View File
@@ -0,0 +1,63 @@
---
title: SQL Monitors | Kener
description: Monitor a SQL database
---
# SQL Monitors
SQL monitors are used to monitor a SQL database. You can monitor a SQL database by adding the database connection details to the monitor. The SQL monitor will check the database connection and notify you if the database connection is down.
<div class="border rounded-md">
![Monitors SQL](/documentation/m_sql.png)
</div>
## Database Type
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The database type is the type of the database you want to monitor. The database type should be one of the following values:
- mysql
- postgres
Instead of giving the connection string as plain text you can add environment variables to the connection string. The environment variables should be in the format of `$`. The environment variables should be defined in the `.env` file.
Example: `mysql://$MYSQL_USER:$MYSQL_PASSWORD@$MYSQL_HOST:$MYSQL_PORT/$MYSQL_DATABASE`
Make sure to define the environment variables in the `.env` file as shown below
```env
MYSQL_USER=root
MYSQL_PASSWORD=password
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=database
```
## Timeout
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The timeout is the time in milliseconds to wait for the database connection to respond.
## Connection String
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The connection string is the connection string of the database you want to monitor. The connection string should be a valid connection string for the database type you have selected.
## SQL Query
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The SQL query is the query you want to run on the database. The SQL query should be a valid SQL query for the database type you have selected.
+52
View File
@@ -0,0 +1,52 @@
---
title: SSL Monitors | Kener
description: Monitor the SSL certificate of a domain
---
# SSL Monitors
SSL monitors are used to monitor the SSL certificate of a domain. You can monitor the SSL certificate of a domain by adding the domain name to the monitor. The SSL monitor will check the SSL certificate of the domain and notify you if the SSL certificate is about to expire.
<div class="border rounded-md">
![Monitors SSL](/documentation/m_ssl.png)
</div>
## Host
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The host is the domain name of the website you want to monitor. The host should be a valid domain name.
## Port
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The port is the ssl port of the website you want to monitor. The default ssl port is 443.
## Degraded If
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
Set this to the number of hours that still remains for the certificate to expire. If the certificate is about to expire in the given number of hours, the monitor status will be set to degraded. Example if you send the value to be 24, the monitor status will be set to degraded if the certificate is about to expire in 24 hours.
## Down If
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
Set this to the number of hours that still remains for the certificate to expire. If the certificate is about to expire in the given number of hours, the monitor status will be set to down. Example if you send the value to be 12, the monitor status will be set to down if the certificate is about to expire in 12 hours.
<div class="note">
`Degraded If` should be more than `Down If`.
</div>
+18 -23
View File
@@ -5,7 +5,7 @@ description: Learn how to set up and work with TCP monitors in kener.
# TCP Monitors
TCP monitors are used to monitor the livenees of your servers. You can use TCP monitors to monitor the uptime of your servers and get notified when they are down.
TCP monitors are used to monitor the liveness of your servers. You can use TCP monitors to monitor the uptime of your servers and get notified when they are down.
<div class="border rounded-md">
@@ -17,10 +17,10 @@ TCP monitors are used to monitor the livenees of your servers. You can use TCP m
You can add as many hosts as you want to monitor. The host can be an IP(IP4 and IP6) address or a domain name.
- Type: Choose the type of host you want to monitor. It can be either `IP4` or `IP6` or `DOMAIN`.
- Host: Enter the IP address or domain name of the host you want to monitor.
- Port: Enter the port number of the host you want to monitor.
- Timeout: Enter the timeout in milliseconds for each ping request of each host
- Type: Choose the type of host you want to monitor. It can be either `IP4` or `IP6` or `DOMAIN`.
- Host: Enter the IP address or domain name of the host you want to monitor.
- Port: Enter the port number of the host you want to monitor.
- Timeout: Enter the timeout in milliseconds for each ping request of each host
## Eval
@@ -32,8 +32,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
> `{status:"DEGRADED", latency: 200}`.
```javascript
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
@@ -53,12 +52,9 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
})
```
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
- `arrayOfPings` **REQUIRED** is an array of TCP Response Objects as shown below.
```js
let decodedResp = atob(responseDataBase64)
let jsonResp = JSON.parse(decodedResp)
console.log(jsonResp)
```json
/*
[
{
@@ -88,24 +84,23 @@ console.log(jsonResp)
### Understanding the Input
The input to the eval function is a base64 encoded string. You will have to decode it and then parse it to get the array of objects. Each object in the array represents the ping response of a host.
Each object in the array represents the tcp response of a host.
- `host`: The host that was pinged.
- `port`: The port that was pinged. Defaults to 80 if not provided.
- `type`: The type of IP address. Can be `IP4` or `IP6`.
- `status`: The status of the ping. Can be `open` , `error` or `timeout`.
- `open`: The host is reachable.
- `error`: There was an error while pinging the host.
- `timeout`: The host did not respond in time.
- `latency`: The time taken to ping the host. This is in milliseconds.
- `host`: The host that was pinged.
- `port`: The port that was pinged. Defaults to 80 if not provided.
- `type`: The type of IP address. Can be `IP4` or `IP6`.
- `status`: The status of the ping. Can be `open` , `error` or `timeout`.
- `open`: The host is reachable.
- `error`: There was an error while pinging the host.
- `timeout`: The host did not respond in time.
- `latency`: The time taken to ping the host. This is in milliseconds.
### Example
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
```javascript
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
+21 -10
View File
@@ -109,15 +109,15 @@ To monitor the ping, you need to provide the IP address or the domain name. If t
To add a trigger to a monitor make sure you have created a trigger. You can read more about triggers [here](/docs/triggers).
- Click on the 🔔 icon on the top right corner of the monitor.
- Add details for either DOWN or DEGRADED.
- Failure Threshold(Required): The number of consecutive failures before the trigger is activated.
- Success Threshold(Required): The number of consecutive successes before the trigger is deactivated.
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in Github. So make sure you have set up the Github token in the environment variables. The incident will be closed when the monitor is back to UP.
- Severity (Required): The severity of the incident. It can be `Critcal` or `Warning`.
- Custom Message (Required): Add your owner alert message.
- Choose Triggers: Choose the triggers you want to activate the trigger. You can choose multiple triggers.
- It will take upto 1 minute for the trigger to be activated.
- Click on the 🔔 icon on the top right corner of the monitor.
- Add details for either DOWN or DEGRADED.
- Failure Threshold(Required): The number of consecutive failures before the trigger is activated.
- Success Threshold(Required): The number of consecutive successes before the trigger is deactivated.
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in Github. So make sure you have set up the Github token in the environment variables. The incident will be closed when the monitor is back to UP.
- Severity (Required): The severity of the incident. It can be `Critcal` or `Warning`.
- Custom Message (Required): Add your owner alert message.
- Choose Triggers: Choose the triggers you want to activate the trigger. You can choose multiple triggers.
- It will take upto 1 minute for the trigger to be activated.
---
@@ -135,6 +135,17 @@ You can activate the monitor by switching the toggle to on. Activation a monitor
<div class="note info">
Any changes in the a live monitor will take one minute to reflect.
Any changes in the a live monitor will take 2 minutes to reflect.
</div>
## Supported Monitors
- [API Monitors](/docs/monitors-api)
- [DNS Monitors](/docs/monitors-dns)
- [PING Monitors](/docs/monitors-ping)
- [TCP Monitors](/docs/monitors-tcp)
- [SQL Monitors](/docs/monitors-sql)
- [SSL Monitors](/docs/monitors-ssl)
- [HEARTBEAT Monitors](/docs/monitors-heartbeat)
- [GROUP Monitors](/docs/monitors-group)
+18 -12
View File
@@ -9,9 +9,9 @@ Here is a demonstration of how to get started with Kener in seconds
## Requirements
- Node.js Minimum version required is `v18`.
- Git
- sqlite3
- Node.js Minimum version required is `v18`.
- Git
- sqlite3
## Clone the repository
@@ -54,9 +54,9 @@ Status Page Manage Portal will be at [http://localhost:3000/manage/app/site](htt
If this is the first time your are launching kener then you would be redirected to the [set up page](/manage/setup). Fill in the details and click on `Let's Go` button.
- **Name**: Your Name
- **Email**: Your Email
- **Password**: Your Password
- **Name**: Your Name
- **Email**: Your Email
- **Password**: Your Password
Please note that the email should be a valid email address and password should be atleast 8 characters long with uppercase lowercase and numbers.
@@ -64,13 +64,19 @@ Please note that the email should be a valid email address and password should b
Once you have created the user, you can login with the credentials you have provided by going to the [login page](/manage/signin)
## Video Tutorial
You can watch the video tutorial on how to get started with Kener
[![Kener Video Tutorial](https://img.youtube.com/vi/4L9PnWPqMPw/0.jpg)](https://www.youtube.com/watch?v=4L9PnWPqMPw)
## Next Steps
Learn how to configure kener by going through one of the topics
- [Monitors](/docs/monitors): Learn how to set up and work with monitors in kener.
- [Triggers](/docs/triggers): Learn how to set up and work with triggers in kener.
- [Environment Variables](/docs/environment-vars): Learn how to set up and work with environment variables in kener.
- [API](/docs/kener-apis): Learn how to use the API in kener.
- [Databases](/docs/database): Learn how to set up and work with databases in kener.
- [Theme](/docs/theme): Learn how to set up and work with theme in kener.
- [Monitors](/docs/monitors): Learn how to set up and work with monitors in kener.
- [Triggers](/docs/triggers): Learn how to set up and work with triggers in kener.
- [Environment Variables](/docs/environment-vars): Learn how to set up and work with environment variables in kener.
- [API](/docs/kener-apis): Learn how to use the API in kener.
- [Databases](/docs/database): Learn how to set up and work with databases in kener.
- [Theme](/docs/theme): Learn how to set up and work with theme in kener.
+46 -25
View File
@@ -49,31 +49,7 @@
"link": "/docs/monitors",
"file": "/monitors.md"
},
{
"title": "API/Website Monitor",
"link": "/docs/monitors-api",
"file": "/monitors-api.md"
},
{
"title": "Ping Monitor",
"link": "/docs/monitors-ping",
"file": "/monitors-ping.md"
},
{
"title": "TCP Monitor",
"link": "/docs/monitors-tcp",
"file": "/monitors-tcp.md"
},
{
"title": "DNS Monitor",
"link": "/docs/monitors-dns",
"file": "/monitors-dns.md"
},
{
"title": "Group Monitor",
"link": "/docs/monitors-group",
"file": "/monitors-group.md"
},
{
"title": "Setup Triggers",
"link": "/docs/triggers",
@@ -132,6 +108,51 @@
}
]
},
{
"sectionTitle": "Monitors",
"children": [
{
"title": "API/Website Monitor",
"link": "/docs/monitors-api",
"file": "/monitors-api.md"
},
{
"title": "Ping Monitor",
"link": "/docs/monitors-ping",
"file": "/monitors-ping.md"
},
{
"title": "TCP Monitor",
"link": "/docs/monitors-tcp",
"file": "/monitors-tcp.md"
},
{
"title": "DNS Monitor",
"link": "/docs/monitors-dns",
"file": "/monitors-dns.md"
},
{
"title": "Group Monitor",
"link": "/docs/monitors-group",
"file": "/monitors-group.md"
},
{
"title": "SSL Monitor",
"link": "/docs/monitors-ssl",
"file": "/monitors-ssl.md"
},
{
"title": "SQL Monitor",
"link": "/docs/monitors-sql",
"file": "/monitors-sql.md"
},
{
"title": "Heartbeat Monitor",
"link": "/docs/monitors-heartbeat",
"file": "/monitors-heartbeat.md"
}
]
},
{
"sectionTitle": "API Reference",
"children": [
+18 -17
View File
@@ -9,28 +9,29 @@ const databaseType = databaseURLParts[0];
const databasePath = databaseURLParts[1];
const knexOb = {
migrations: {
directory: "./migrations"
},
seeds: {
directory: "./seeds"
}
migrations: {
directory: "./migrations",
},
seeds: {
directory: "./seeds",
},
databaseType,
};
if (databaseType === "sqlite") {
knexOb.client = "better-sqlite3";
knexOb.connection = {
filename: databasePath
};
knexOb.useNullAsDefault = true;
knexOb.client = "better-sqlite3";
knexOb.connection = {
filename: databasePath,
};
knexOb.useNullAsDefault = true;
} else if (databaseType === "postgresql") {
knexOb.client = "pg";
knexOb.connection = databaseURL;
knexOb.client = "pg";
knexOb.connection = databaseURL;
} else if (databaseType === "mysql") {
knexOb.client = "mysql2";
knexOb.connection = databaseURL;
knexOb.client = "mysql2";
knexOb.connection = databaseURL;
} else {
console.error("Invalid database type");
process.exit(1);
console.error("Invalid database type");
process.exit(1);
}
export default knexOb;
+214 -2
View File
@@ -1,12 +1,12 @@
{
"name": "kener",
"version": "3.1.7",
"version": "3.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kener",
"version": "3.1.7",
"version": "3.1.8",
"license": "MIT",
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
@@ -18,6 +18,7 @@
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.5.0",
"bits-ui": "^0.9.9",
"cheerio": "^1.0.0",
"clsx": "^2.0.0",
"croner": "^7.0.5",
"date-fns": "^4.1.0",
@@ -1701,6 +1702,12 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -1930,6 +1937,76 @@
"binary-search": "^1.3.5"
}
},
"node_modules/cheerio": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"encoding-sniffer": "^0.2.0",
"htmlparser2": "^9.1.0",
"parse5": "^7.1.2",
"parse5-htmlparser2-tree-adapter": "^7.0.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^6.19.5",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=18.17"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
"css-what": "^6.1.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cheerio/node_modules/htmlparser2": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.5.0"
}
},
"node_modules/cheerio/node_modules/undici": {
"version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -2248,6 +2325,22 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
"integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling=="
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
@@ -2267,6 +2360,18 @@
"integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==",
"license": "MIT"
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2696,6 +2801,31 @@
"node": ">= 0.8"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/encoding-sniffer/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -5386,6 +5516,18 @@
"set-blocking": "^2.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/number-flow": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/number-flow/-/number-flow-0.4.2.tgz",
@@ -5511,6 +5653,43 @@
"node": ">=4"
}
},
"node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parseley": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
@@ -8313,6 +8492,39 @@
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "kener",
"version": "3.1.8",
"version": "3.2.2",
"private": false,
"license": "MIT",
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
@@ -77,6 +77,7 @@
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.5.0",
"bits-ui": "^0.9.9",
"cheerio": "^1.0.0",
"clsx": "^2.0.0",
"croner": "^7.0.5",
"date-fns": "^4.1.0",
+3 -2
View File
@@ -18,9 +18,10 @@ main {
text-decoration: none;
background-color: var(--bg-background);
}
.kener-home-links > div:hover {
.kener-home-links > a:hover {
transition: all 0.3s;
box-shadow: 0 0 8px 1.5px #3e9a4b;
border-color: #ed702d;
background-color: #ed702d;
}
.accm input:checked ~ div {
+38 -5
View File
@@ -41,7 +41,8 @@
.squares-pattern::after {
content: "";
position: absolute;
background-image: linear-gradient(#444cf7 1px, transparent 1px),
background-image:
linear-gradient(#444cf7 1px, transparent 1px),
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
-webkit-mask-image: linear-gradient(
180deg,
@@ -87,7 +88,8 @@
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-image: linear-gradient(#616fbf 1px, transparent 1px),
background-image:
linear-gradient(#616fbf 1px, transparent 1px),
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
}
@@ -476,7 +478,8 @@ textarea::placeholder {
}
.mesh {
background-image: linear-gradient(
background-image:
linear-gradient(
323deg,
rgba(255, 255, 255, 0.01) 0%,
rgba(255, 255, 255, 0.01) 16.667%,
@@ -607,7 +610,8 @@ textarea::placeholder {
}
.dark .mesh {
background-image: linear-gradient(
background-image:
linear-gradient(
158deg,
rgba(84, 84, 84, 0.03) 0%,
rgba(84, 84, 84, 0.03) 20%,
@@ -821,7 +825,8 @@ textarea::placeholder {
}
.button-77:active:not(:disabled):after {
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
background-image:
linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
linear-gradient(92.83deg, #ff7426 0, #f93a13 100%);
bottom: 4px;
left: 4px;
@@ -872,3 +877,31 @@ textarea::placeholder {
transform: rotate(15deg);
}
}
.dots-animation::after {
content: ".";
animation: dots 1.5s steps(4) infinite;
}
@keyframes dots {
0% {
content: ".";
}
16% {
content: "..";
}
32% {
content: "...";
}
48% {
content: "....";
}
64% {
content: "...";
}
80% {
content: "..";
}
100% {
content: ".";
}
}
+248 -245
View File
@@ -1,288 +1,291 @@
// @ts-nocheck
import { base } from "$app/paths";
function siteDataExtractFromDb(data, obj) {
let requestedObject = { ...obj };
for (const key in requestedObject) {
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
const element = data[key];
if (data[key]) {
requestedObject[key] = data[key];
}
}
}
//remove any keys that are still null or empty
for (const key in requestedObject) {
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
const element = requestedObject[key];
if (element === null || element === "") {
delete requestedObject[key];
}
}
}
return requestedObject;
let requestedObject = { ...obj };
for (const key in requestedObject) {
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
const element = data[key];
if (data[key]) {
requestedObject[key] = data[key];
}
}
}
//remove any keys that are still null or empty
for (const key in requestedObject) {
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
const element = requestedObject[key];
if (element === null || element === "") {
delete requestedObject[key];
}
}
}
return requestedObject;
}
//a function to make an api call to /manage/api/ to store site data
function storeSiteData(data) {
return fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "storeSiteData", data })
});
return fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ action: "storeSiteData", data }),
});
}
const allRecordTypes = {
A: 1,
NS: 2,
MD: 3,
MF: 4,
CNAME: 5,
SOA: 6,
MB: 7,
MG: 8,
MR: 9,
NULL: 10,
WKS: 11,
PTR: 12,
HINFO: 13,
MINFO: 14,
MX: 15,
TXT: 16,
RP: 17,
AFSDB: 18,
X25: 19,
ISDN: 20,
RT: 21,
NSAP: 22,
NSAP_PTR: 23,
SIG: 24,
KEY: 25,
PX: 26,
GPOS: 27,
AAAA: 28,
LOC: 29,
NXT: 30,
EID: 31,
NIMLOC: 32,
SRV: 33,
ATMA: 34,
NAPTR: 35,
KX: 36,
CERT: 37,
A6: 38,
DNAME: 39,
SINK: 40,
OPT: 41,
APL: 42,
DS: 43,
SSHFP: 44,
IPSECKEY: 45,
RRSIG: 46,
NSEC: 47,
DNSKEY: 48,
DHCID: 49,
NSEC3: 50,
NSEC3PARAM: 51,
TLSA: 52,
SMIMEA: 53,
HIP: 55,
NINFO: 56,
RKEY: 57,
TALINK: 58,
CDS: 59,
CDNSKEY: 60,
OPENPGPKEY: 61,
CSYNC: 62,
SPF: 99,
UINFO: 100,
UID: 101,
GID: 102,
UNSPEC: 103,
NID: 104,
L32: 105,
L64: 106,
LP: 107,
EUI48: 108,
EUI64: 109,
TKEY: 249,
TSIG: 250,
IXFR: 251,
AXFR: 252,
MAILB: 253,
MAILA: 254,
ANY: 255
A: 1,
NS: 2,
MD: 3,
MF: 4,
CNAME: 5,
SOA: 6,
MB: 7,
MG: 8,
MR: 9,
NULL: 10,
WKS: 11,
PTR: 12,
HINFO: 13,
MINFO: 14,
MX: 15,
TXT: 16,
RP: 17,
AFSDB: 18,
X25: 19,
ISDN: 20,
RT: 21,
NSAP: 22,
NSAP_PTR: 23,
SIG: 24,
KEY: 25,
PX: 26,
GPOS: 27,
AAAA: 28,
LOC: 29,
NXT: 30,
EID: 31,
NIMLOC: 32,
SRV: 33,
ATMA: 34,
NAPTR: 35,
KX: 36,
CERT: 37,
A6: 38,
DNAME: 39,
SINK: 40,
OPT: 41,
APL: 42,
DS: 43,
SSHFP: 44,
IPSECKEY: 45,
RRSIG: 46,
NSEC: 47,
DNSKEY: 48,
DHCID: 49,
NSEC3: 50,
NSEC3PARAM: 51,
TLSA: 52,
SMIMEA: 53,
HIP: 55,
NINFO: 56,
RKEY: 57,
TALINK: 58,
CDS: 59,
CDNSKEY: 60,
OPENPGPKEY: 61,
CSYNC: 62,
SPF: 99,
UINFO: 100,
UID: 101,
GID: 102,
UNSPEC: 103,
NID: 104,
L32: 105,
L64: 106,
LP: 107,
EUI48: 108,
EUI64: 109,
TKEY: 249,
TSIG: 250,
IXFR: 251,
AXFR: 252,
MAILB: 253,
MAILA: 254,
ANY: 255,
};
const ValidateIpAddress = function (input) {
// Check if input is a valid IPv4 address with an optional port
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IP4";
}
// Check if input is a valid IPv4 address with an optional port
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IP4";
}
// Improved IPv6 regex that better handles compressed notation
const ipv6Regex =
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){0,7}:|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){6}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|(?:[0-9a-fA-F]{1,4}:){1,7}(?::|:[0-9a-fA-F]{1,4})|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$/;
if (ipv6Regex.test(input)) {
return "IP6";
}
// Improved IPv6 regex that better handles compressed notation
const ipv6Regex =
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){0,7}:|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){6}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|(?:[0-9a-fA-F]{1,4}:){1,7}(?::|:[0-9a-fA-F]{1,4})|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$/;
if (ipv6Regex.test(input)) {
return "IP6";
}
// Check if input is a valid domain name with an optional port
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "DOMAIN";
}
// Check if input is a valid domain name with an optional port
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "DOMAIN";
}
// If none of the above conditions match, the input is invalid
return "Invalid";
// If none of the above conditions match, the input is invalid
return "Invalid";
};
function IsValidHost(domain) {
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
}
function IsValidNameServer(nameServer) {
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
}
const IsValidURL = function (url) {
return /^(http|https):\/\/[^ "]+$/.test(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" };
}
// 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+/);
// 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"
};
}
// 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
];
// 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/*,\-]+$/;
// 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];
// 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`
};
}
// 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 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 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 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 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}`
};
}
}
// 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" };
return { isValid: true, message: "Valid cron expression" };
}
function isValidRange(value, min, max) {
const num = Number(value);
return !isNaN(num) && num >= min && num <= 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;
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;
}
//js function to generate 32 character random string
function RandomString(length) {
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = "";
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
export {
siteDataExtractFromDb,
storeSiteData,
allRecordTypes,
ValidateIpAddress,
IsValidHost,
IsValidNameServer,
IsValidURL,
ValidateCronExpression,
SortMonitor
siteDataExtractFromDb,
storeSiteData,
allRecordTypes,
ValidateIpAddress,
IsValidHost,
IsValidNameServer,
IsValidURL,
ValidateCronExpression,
SortMonitor,
RandomString,
};
+74 -36
View File
@@ -1,16 +1,16 @@
<script>
import { formatDistanceToNow, formatDistance } from "date-fns";
import { Settings } from "lucide-svelte";
import { Settings, ArrowRight } from "lucide-svelte";
import * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import { Tooltip } from "bits-ui";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
export let incident;
export let index;
export let lang;
export let selectedLang = "en";
let startTime = new Date(incident.start_date_time * 1000);
let endTime = new Date();
let nowTime = new Date();
@@ -18,13 +18,17 @@
endTime = new Date(incident.end_date_time * 1000);
}
let incidentType = incident.incident_type;
const lastedFor = fd(startTime, endTime, selectedLang);
const startedAt = fdn(startTime, selectedLang);
const lastedFor = fd(startTime, endTime, $page.data.selectedLang);
const remainingTime = fd(nowTime, endTime, $page.data.selectedLang);
const startedAt = fdn(startTime, $page.data.selectedLang);
let isFuture = false;
//is future incident
let incidentTimeStatus = "";
if (nowTime < startTime) {
isFuture = true;
incidentTimeStatus = "YET_TO_START";
} else if (nowTime >= startTime && nowTime <= endTime) {
incidentTimeStatus = "ONGOING";
} else if (nowTime > endTime) {
incidentTimeStatus = "COMPLETED";
}
let accordionValue = "incident-0";
@@ -37,30 +41,36 @@
let incidentDateSummary = "";
let maintenanceBadge = "";
let maintenanceBadgeColor = "";
if (!isFuture && incident.state != "RESOLVED") {
incidentDateSummary = l(lang, "Started %startedAt, still ongoing", {
if (incidentTimeStatus == "YET_TO_START") {
incidentDateSummary = l($page.data.lang, "Starts %startedAt", { startedAt });
if (incidentType === "MAINTENANCE") {
incidentDateSummary = l($page.data.lang, "Starts %startedAt, will last for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
}
} else if (incidentTimeStatus == "ONGOING") {
incidentDateSummary = l($page.data.lang, "Started %startedAt, still ongoing", {
startedAt
});
maintenanceBadge = "Maintenance in Progress";
maintenanceBadgeColor = "text-maintenance-in-progress";
} else if (!isFuture && incident.state == "RESOLVED") {
incidentDateSummary = l(lang, "Started %startedAt, lasted for %lastedFor", {
if (incidentType === "MAINTENANCE") {
incidentDateSummary = l($page.data.lang, "Started %startedAt, will last for %lastedFor more", {
startedAt,
lastedFor: remainingTime
});
maintenanceBadge = "Maintenance in Progress";
maintenanceBadgeColor = "text-maintenance-in-progress";
}
} else if (incidentTimeStatus == "COMPLETED") {
incidentDateSummary = l($page.data.lang, "Started %startedAt, lasted for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Maintenance Completed";
maintenanceBadgeColor = "text-maintenance-completed";
} else if (isFuture && incident.state != "RESOLVED") {
incidentDateSummary = l(lang, "Starts %startedAt", { startedAt });
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
} else if (isFuture && incident.state == "RESOLVED") {
incidentDateSummary = l(lang, "Starts %startedAt, will last for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
}
</script>
@@ -73,11 +83,11 @@
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
<span class="badge-{incident.state}">
{l(lang, incident.state)}
{l($page.data.lang, incident.state)}
</span>
{:else if incidentType == "MAINTENANCE"}
<span class="{maintenanceBadgeColor} ">
{l(lang, maintenanceBadge)}
{l($page.data.lang, maintenanceBadge)}
</span>
{/if}
{#if $page.data.isLoggedIn}
@@ -91,20 +101,48 @@
{/if}
</p>
<p class="scroll-m-20 text-lg font-medium tracking-tight">
<p class="font-medium">
{incident.title}
</p>
{#if !!incidentDateSummary}
<p class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground">
{incidentDateSummary}
</p>
<Tooltip.Root openDelay={100} side="bottom" align="end">
<Tooltip.Trigger
class=" text-ellipsis whitespace-nowrap text-xs font-medium tracking-normal text-muted-foreground"
>
{incidentDateSummary}
</Tooltip.Trigger>
<Tooltip.Content class=" z-20 mt-11" side="bottom" align="end">
<div
class="items-center justify-center rounded border bg-primary px-1.5 py-1 text-xs font-medium text-primary-foreground shadow-popover"
>
{f(
new Date(incident.start_date_time * 1000),
"MMMM do yyyy, h:mm:ss a",
$page.data.selectedLang,
$page.data.localTz
)}
{#if incident.end_date_time}
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
{f(
new Date(incident.end_date_time * 1000),
"MMMM do yyyy, h:mm:ss a",
$page.data.selectedLang,
$page.data.localTz
)}
{:else}
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
<span class="dots-animation inline-block w-6 text-left"></span>
{/if}
</div>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</div>
</Accordion.Trigger>
<Accordion.Content>
<div class="px-4 pt-2">
{#if incident.monitors.length > 0}
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
{#each incident.monitors as monitor}
<div class="tag-affected-text flex gap-x-2 rounded-md bg-secondary px-1 py-1 pr-2">
<div
@@ -123,7 +161,7 @@
</div>
{/if}
<p class="my-3 text-xs font-semibold uppercase text-muted-foreground">
{l(lang, "Updates")}
{l($page.data.lang, "Updates")}
</p>
{#if incident.comments.length > 0}
{#if incidentType == "INCIDENT"}
@@ -133,14 +171,14 @@
<div
class="absolute top-0 w-28 -translate-x-32 rounded border bg-secondary px-1.5 py-1 text-center text-xs font-semibold"
>
{l(lang, comment.state)}
{l($page.data.lang, comment.state)}
</div>
<time class=" mb-1 text-sm font-medium leading-none text-muted-foreground">
{f(
new Date(comment.commented_at * 1000),
"MMMM do yyyy, h:mm:ss a",
selectedLang,
$page.data.selectedLang,
$page.data.localTz
)}
</time>
@@ -159,7 +197,7 @@
{f(
new Date(comment.commented_at * 1000),
"MMMM do yyyy, h:mm:ss a",
selectedLang,
$page.data.selectedLang,
$page.data.localTz
)}
</time>
@@ -173,7 +211,7 @@
{/if}
{:else}
<p class="text-sm font-medium">
{l(lang, "No Updates Yet")}
{l($page.data.lang, "No Updates Yet")}
</p>
{/if}
</div>
+279 -4
View File
@@ -1,6 +1,6 @@
<script>
import { Button } from "$lib/components/ui/button";
import { Plus, X, Loader } from "lucide-svelte";
import { Plus, X, Loader, Clipboard, Check } from "lucide-svelte";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { clickOutsideAction, slide } from "svelte-legos";
@@ -9,6 +9,7 @@
import * as Select from "$lib/components/ui/select";
import { createEventDispatcher } from "svelte";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import {
allRecordTypes,
ValidateIpAddress,
@@ -84,6 +85,10 @@
let invalidFormMessage = "";
function copyToClipboard(t) {
navigator.clipboard.writeText(t);
}
async function isValidEval(ev) {
if (ev.endsWith(";")) {
invalidFormMessage = "Eval should not end with semicolon";
@@ -277,8 +282,106 @@
timeout: parseInt(newMonitor.groupConfig.timeout),
hideMonitors: newMonitor.groupConfig.hideMonitors
});
}
} else if (newMonitor.monitor_type === "SSL") {
//validating host
if (!newMonitor.sslConfig.host) {
invalidFormMessage = "Host is required";
return;
}
if (!IsValidHost(newMonitor.sslConfig.host)) {
invalidFormMessage = "Invalid Host";
return;
}
//validating port
if (
!!!newMonitor.sslConfig.port ||
isNaN(newMonitor.sslConfig.port) ||
newMonitor.sslConfig.port < 1 ||
newMonitor.sslConfig.port > 65535
) {
invalidFormMessage = "Port should be valid";
return;
}
//check if degradedRemainingHours > 0
if (
!!!newMonitor.sslConfig.degradedRemainingHours ||
isNaN(newMonitor.sslConfig.degradedRemainingHours) ||
newMonitor.sslConfig.degradedRemainingHours < 0
) {
invalidFormMessage = "Degraded Remaining Hours should be greater than 0";
return;
}
//check if downRemainingHours > 0
if (
!!!newMonitor.sslConfig.downRemainingHours ||
isNaN(newMonitor.sslConfig.downRemainingHours) ||
newMonitor.sslConfig.downRemainingHours < 0
) {
invalidFormMessage = "Down Remaining Hours should be greater than 0";
return;
}
newMonitor.sslConfig.degradedRemainingHours = Number(newMonitor.sslConfig.degradedRemainingHours);
newMonitor.sslConfig.downRemainingHours = Number(newMonitor.sslConfig.downRemainingHours);
//check if degradedRemainingHours > downRemainingHours
if (newMonitor.sslConfig.degradedRemainingHours <= newMonitor.sslConfig.downRemainingHours) {
invalidFormMessage = "Degraded Remaining Hours should be greater than Down Remaining Hours";
return;
}
newMonitor.type_data = JSON.stringify(newMonitor.sslConfig);
} else if (newMonitor.monitor_type === "SQL") {
//connectionString cannot be empty
if (!!!newMonitor.sqlConfig.connectionString) {
invalidFormMessage = "Connection String is required";
return;
}
//connection string has to start with postgresql or mysql
if (
!newMonitor.sqlConfig.connectionString.startsWith("postgresql://") &&
!newMonitor.sqlConfig.connectionString.startsWith("mysql://")
) {
invalidFormMessage = "Connection string should start with postgresql:// or mysql2://";
return;
}
//timeout should be positive number
if (newMonitor.sqlConfig.timeout < 1) {
invalidFormMessage = "Timeout should be greater than 0";
return;
}
//query cannot be empty
if (!!!newMonitor.sqlConfig.query) {
invalidFormMessage = "SQL Query is required";
return;
}
newMonitor.type_data = JSON.stringify(newMonitor.sqlConfig);
} else if (newMonitor.monitor_type === "HEARTBEAT") {
//newMonitor.heartbeatConfig.degradedRemainingMinutes should be a number and greater than equal to 1
if (
!!!newMonitor.heartbeatConfig.degradedRemainingMinutes ||
isNaN(newMonitor.heartbeatConfig.degradedRemainingMinutes) ||
newMonitor.heartbeatConfig.degradedRemainingMinutes < 1
) {
invalidFormMessage = "Degraded or grace Period should at least be 1 minute";
return;
}
//newMonitor.heartbeatConfig.downRemainingMinutes should be a number and greater than degradedRemainingMinutes
if (
!!!newMonitor.heartbeatConfig.downRemainingMinutes ||
isNaN(newMonitor.heartbeatConfig.downRemainingMinutes) ||
newMonitor.heartbeatConfig.downRemainingMinutes <= newMonitor.heartbeatConfig.degradedRemainingMinutes
) {
invalidFormMessage = "Down or impacted Period should be greater than Degraded or grace Period";
return;
}
newMonitor.type_data = JSON.stringify(newMonitor.heartbeatConfig);
}
formState = "loading";
try {
@@ -302,6 +405,11 @@
}
}
let typeOfLogoUpload = newMonitor.image.startsWith("http") ? "URL" : "FILE";
let databaseTypes = {
pg: "PostgreSQL",
mysql2: "MySQL"
};
</script>
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
@@ -341,7 +449,7 @@
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</div>
@@ -523,6 +631,9 @@
<Select.Item value="DNS" label="DNS" class="text-sm font-medium">DNS</Select.Item>
<Select.Item value="TCP" label="TCP" class="text-sm font-medium">TCP</Select.Item>
<Select.Item value="GROUP" label="GROUP" class="text-sm font-medium">GROUP</Select.Item>
<Select.Item value="SSL" label="SSL" class="text-sm font-medium">SSL</Select.Item>
<Select.Item value="SQL" label="SQL" class="text-sm font-medium">SQL</Select.Item>
<Select.Item value="HEARTBEAT" label="HEARTBEAT" class="text-sm font-medium">HEARTBEAT</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
@@ -896,7 +1007,10 @@
</Label>
<Input bind:value={newMonitor.groupConfig.timeout} class="w-40" id="timeout" />
<p class="my-1 text-xs text-muted-foreground">
Maximum Time in milliseconds it will wait for all the monitors to resolved in that particular timestamp
Maximum Time in milliseconds it will wait for all the monitors to resolved in that particular timestamp.
Read the <a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-group">
docs
</a> to learn more
</p>
</div>
<div class="col-span-6">
@@ -935,6 +1049,167 @@
</ul>
</div>
</div>
{:else if newMonitor.monitor_type == "SSL"}
<div class="mt-4 grid grid-cols-7 gap-2">
<div class="col-span-2">
<Label for="sslHost">Host</Label>
<Input placeholder="example.com" bind:value={newMonitor.sslConfig.host} id="sslHost" />
</div>
<div class="col-span-1">
<Label for="sslHost">Port</Label>
<Input placeholder="443" bind:value={newMonitor.sslConfig.port} id="sslPort" />
</div>
<div class="relative col-span-2">
<Label for="degradedRemainingHours">Degraded If (in hours)</Label>
<Input
class="pl-20"
type="number"
placeholder="x hours"
bind:value={newMonitor.sslConfig.degradedRemainingHours}
id="degradedRemainingHours"
/>
<span class="absolute left-2 top-9 -mt-0.5 text-sm font-medium text-muted-foreground">Expires In</span>
</div>
<div class="relative col-span-2">
<Label for="downRemainingHours">Down If (in hours)</Label>
<Input
class="pl-20"
placeholder="x hours"
type="number"
bind:value={newMonitor.sslConfig.downRemainingHours}
id="downRemainingHours"
/>
<span class="absolute left-2 top-9 -mt-0.5 text-sm font-medium text-muted-foreground">Expires In</span>
</div>
<div class="col-span-7">
<span class="text-xs text-muted-foreground"
>Degraded Remaining Hours should be greater than Down Remaining Hours. Refer to the
<a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-ssl"
>documentation</a
>
</span>
</div>
</div>
{:else if newMonitor.monitor_type == "SQL"}
<div>
<div class="mt-4 flex gap-2">
<div class="w-36">
<Label for="sqlConfigdbType">Database Type</Label>
<Select.Root
portal={null}
onSelectedChange={(e) => {
newMonitor.sqlConfig.dbType = e.value;
}}
selected={{
value: newMonitor.sqlConfig.dbType,
label: databaseTypes[newMonitor.sqlConfig.dbType]
}}
>
<Select.Trigger id="sqlConfigdbType">
<Select.Value bind:value={newMonitor.sqlConfig.dbType} placeholder="DB" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Select DB</Select.Label>
<Select.Item value="pg" label={databaseTypes.pg} class="text-sm font-medium">
{databaseTypes.pg}
</Select.Item>
<Select.Item value="mysql2" label={databaseTypes.mysql2} class="text-sm font-medium">
{databaseTypes.mysql2}
</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="w-36">
<Label for="dbtimeout">Timeout(ms)</Label>
<Input
placeholder="in milliseconds"
type="number"
bind:value={newMonitor.sqlConfig.timeout}
id="dbtimeout"
/>
</div>
</div>
<div class="mt-2 grid grid-cols-1">
<div class="col-span-1">
<Label for="connectionString">Connection String</Label>
<Input
bind:value={newMonitor.sqlConfig.connectionString}
id="connectionString"
placeholder="Enter the connection string"
/>
</div>
</div>
<div class="mt-2 grid grid-cols-1">
<div class="col-span-1">
<Label for="sqlQuery">SQL Query</Label>
<Input bind:value={newMonitor.sqlConfig.query} id="sqlQuery" placeholder="SELECT 1" />
</div>
</div>
<div class="col-span-1">
<span class="text-xs text-muted-foreground"
>Refer to the
<a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-sql">
documentation
</a> for more details
</span>
</div>
</div>
{:else if newMonitor.monitor_type == "HEARTBEAT"}
<div>
<div class="relative mt-4 justify-start gap-x-4">
<p class="truncate rounded-md border bg-popover p-2 pr-8 text-sm font-medium">
Heart beat url: <span class="text-muted-foreground">
{$page.data.siteData.siteURL +
`${base}/api/heartbeat/${newMonitor.tag}:${newMonitor.heartbeatConfig.secretString}`}</span
>
</p>
<Button
class="copybtn absolute right-2 top-2 flex h-6 w-6 justify-center p-1"
variant="ghost"
size="icon"
on:click={() =>
copyToClipboard(
$page.data.siteData.siteURL +
`${base}/api/heartbeat/${newMonitor.tag}:${newMonitor.heartbeatConfig.secretString}`
)}
>
<Check class="check-btn absolute h-4 w-4 text-green-500" />
<Clipboard class="copy-btn absolute h-4 w-4" />
</Button>
</div>
<div class="mt-2 flex flex-row justify-start gap-x-4">
<div>
<Label for="degradedRemainingMinutes">Degraded or grace Period (in minutes)</Label>
<Input
placeholder="in minutes"
class="w-full"
type="number"
bind:value={newMonitor.heartbeatConfig.degradedRemainingMinutes}
id="degradedRemainingMinutes"
/>
</div>
<div>
<Label for="downRemainingMinutes">Down or impacted Period (in minutes)</Label>
<Input
placeholder="in minutes"
type="number"
bind:value={newMonitor.heartbeatConfig.downRemainingMinutes}
id="downRemainingMinutes"
/>
</div>
</div>
<div class="col-span-1">
<span class="text-xs text-muted-foreground"
>Refer to the
<a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-heartbeat">
documentation
</a> for more details
</span>
</div>
</div>
{/if}
</div>
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
+26 -3
View File
@@ -8,7 +8,7 @@
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 { storeSiteData, SortMonitor, RandomString } from "$lib/clientTools.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { dndzone } from "svelte-dnd-action";
import { flip } from "svelte/animate";
@@ -92,7 +92,24 @@
monitors: [],
timeout: 10000,
hideMonitors: false
})
}),
sslConfig: {
host: "",
port: 443,
degradedRemainingHours: 1,
downRemainingHours: 0
},
sqlConfig: {
connectionString: "",
query: "SELECT 1",
timeout: 5000,
dbType: "pg" //mysql2
},
heartbeatConfig: {
degradedRemainingMinutes: 1,
downRemainingMinutes: 2,
secretString: RandomString(32)
}
};
}
@@ -124,6 +141,12 @@
newMonitor.tcpConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "GROUP") {
newMonitor.groupConfig = createGroupConfig(JSON.parse(newMonitor.type_data));
} else if (newMonitor.monitor_type == "SSL") {
newMonitor.sslConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "SQL") {
newMonitor.sqlConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "HEARTBEAT") {
newMonitor.heartbeatConfig = JSON.parse(newMonitor.type_data);
}
showAddMonitor = true;
}
@@ -548,7 +571,7 @@
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</div>
+9 -1
View File
@@ -149,6 +149,14 @@
}
}
}
if (newTrigger.trigger_meta.has_webhook_body) {
if (newTrigger.trigger_meta.webhook_body == "") {
invalidFormMessage = "Webhook Body is required";
formState = "idle";
return;
}
}
}
//newTrigger.name present not empty
if (newTrigger.name == "") {
@@ -423,7 +431,7 @@
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</div>
+16 -10
View File
@@ -53,10 +53,13 @@
}
function getToday(startTs, incidentIDs) {
let endTs = Math.min(startTs + 86400, monitor.pageData.maxDateTodayTimestamp);
axios
.post(`${base}/api/today`, {
monitor: monitor,
localTz: localTz,
endTs: endTs,
startTs: startTs,
incidentIDs: incidentIDs
})
@@ -165,6 +168,7 @@
let dateFetchedFor = "";
let dayUptime = "NA";
let loadingDayData = false;
function dailyDataGetter(e, bar, incidentObj) {
if (embed) {
return;
@@ -268,16 +272,18 @@
{#if rollerLoading}
<Loader class=" mt-0.5 inline h-3.5 w-3.5 animate-spin text-muted-foreground" />
{/if}
<NumberFlow
class="border-r pr-2 text-xs font-semibold"
value={uptimesRollers[rolledAt].value}
format={{
notation: "standard",
minimumFractionDigits: 4,
maximumFractionDigits: 4
}}
suffix="%"
/>
{#if !isNaN(uptimesRollers[rolledAt].value)}
<NumberFlow
class="border-r pr-2 text-xs font-semibold"
value={uptimesRollers[rolledAt].value}
format={{
notation: "standard",
minimumFractionDigits: 4,
maximumFractionDigits: 4
}}
suffix="%"
/>
{/if}
<div class="truncate text-xs font-semibold text-{monitor.pageData.summaryColorClass}">
{monitor.pageData.summaryStatus}
</div>
+18 -16
View File
@@ -40,22 +40,24 @@
</a>
{/each}
</nav>
<DropdownMenu.Root class="">
<DropdownMenu.Trigger class="mr-2 flex md:hidden">
<Button variant="outline" size="sm">
<Menu size={14} />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{#each data.site.nav as navItem}
<DropdownMenu.Group>
<DropdownMenu.Item>
<a rel="external" href={navItem.url}> {navItem.name} </a>
</DropdownMenu.Item>
</DropdownMenu.Group>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
<div class="flex md:hidden">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline" size="sm">
<Menu size={14} />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{#each data.site.nav as navItem}
<DropdownMenu.Group>
<DropdownMenu.Item>
<a rel="external" href={navItem.url}> {navItem.name} </a>
</DropdownMenu.Item>
</DropdownMenu.Group>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/if}
</div>
</div>
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
"Started %startedAt, will last for %lastedFor more": "Begann vor etwa %startedAt und wird noch etwa %lastedFor dauern",
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
"Starts %startedAt": "Beginnt in %startedAt",
"Status OK": "Status OK",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
"Started %startedAt, will last for %lastedFor more": "Startede for cirka %startedAt siden, vil vare i cirka %lastedFor mere",
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
"Starts %startedAt": "Starter om %startedAt",
"Status OK": "Status OK",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Started %startedAt, will last for %lastedFor more": "Started %startedAt, will last for %lastedFor more",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
"Started %startedAt, will last for %lastedFor more": "Commencé il y a environ %startedAt, durera environ %lastedFor de plus",
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
"Starts %startedAt": "Commence dans %startedAt",
"Status OK": "Statut OK",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "मानक",
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
"Started %startedAt, will last for %lastedFor more": "%startedAt शुरू होगा, %lastedFor और चलेगा",
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
"Starts %startedAt": "%startedAt में शुरू होगा",
"Status OK": "स्थिति ठीक है",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "標準",
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
"Started %startedAt, will last for %lastedFor more": "%startedAt 前に開始し、さらに %lastedFor 続きます",
"Starts %startedAt, will last for %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
"Starts %startedAt": "%startedAt 後に開始",
"Status OK": "ステータス正常",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "기본",
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
"Started %startedAt, will last for %lastedFor more": "%startedAt 전에 시작되었으며, %lastedFor 더 지속될 예정입니다",
"Starts %startedAt, will last for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
"Starts %startedAt": "%startedAt 에 시작되었습니다",
"Status OK": "양호",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
"Started %startedAt, will last for %lastedFor more": "Startet %startedAt, vil vare i %lastedFor til",
"Starts %startedAt, will last for %lastedFor": "Starter %startedAt, vil vare i %lastedFor",
"Starts %startedAt": "Starter %startedAt",
"Status OK": "Status OK",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Standaard",
"Started %startedAt, lasted for %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
"Started %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
"Started %startedAt, will last for %lastedFor more": "Begint %startedAt, zal nog ongeveer %lastedFor duren",
"Starts %startedAt, will last for %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
"Starts %startedAt": "Begint over %startedAt",
"Status OK": "Status OK",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Padrão",
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
"Started %startedAt, will last for %lastedFor more": "Iniciará em %startedAt, durará mais %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Terá início em %startedAt, com duração de %lastedFor",
"Starts %startedAt": "Inicia em %startedAt",
"Status OK": "Status OK",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Стандартная",
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
"Started %startedAt, will last for %lastedFor more": "Начнется около %startedAt, продлится еще около %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
"Starts %startedAt": "Начнется через %startedAt",
"Status OK": "Статус ОК",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Standart",
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
"Started %startedAt, will last for %lastedFor more": "%startedAt başladı, %lastedFor daha sürecek",
"Starts %startedAt, will last for %lastedFor": "%startedAt başlıyor, %lastedFor sürecek",
"Starts %startedAt": "%startedAt başlıyor",
"Status OK": "Durum UYGUN",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "Tiêu chuẩn",
"Started %startedAt, lasted for %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
"Started %startedAt, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
"Started %startedAt, will last for %lastedFor more": "Bắt đầu khoảng %startedAt trước, sẽ kéo dài thêm khoảng %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
"Starts %startedAt": "Bắt đầu trong %startedAt",
"Status OK": "Trạng thái OK",
+1
View File
@@ -48,6 +48,7 @@
"Standard": "标准",
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
"Started %startedAt, will last for %lastedFor more": "大约 %startedAt 前开始,将持续约 %lastedFor",
"Starts %startedAt, will last for %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
"Starts %startedAt": "%startedAt 后开始",
"Status OK": "状态正常",
+4
View File
@@ -102,6 +102,8 @@ const TIMEOUT = "timeout";
const ERROR = "error";
const MANUAL = "manual";
const WEBHOOK = "webhook";
const DEFAULT_STATUS = "default_status";
const SIGNAL = "signal";
export {
MONITOR,
@@ -119,4 +121,6 @@ export {
MANUAL,
NO_DATA,
WEBHOOK,
DEFAULT_STATUS,
SIGNAL,
};
+37 -2
View File
@@ -17,7 +17,8 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import crypto from "crypto";
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
import { UP, DOWN, DEGRADED, NO_DATA } from "../constants.js";
import { UP, DOWN, DEGRADED, NO_DATA, REALTIME, SIGNAL } from "../constants.js";
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC } from "../tool.js";
const saltRounds = 10;
const DUMMY_SECRET = "DUMMY_SECRET";
@@ -298,6 +299,40 @@ export const VerifyPassword = async (plainTextPassword, hashedPassword) => {
}
};
export const GetLatestMonitoringData = async (monitor_tag) => {
return await db.getLatestMonitoringData(monitor_tag);
};
export const GetLastHeartbeat = async (monitor_tag) => {
return await db.getLastHeartbeat(monitor_tag);
};
export const RegisterHeartbeat = async (tag, secret) => {
let monitor = await db.getMonitorByTag(tag);
if (!monitor) {
return null;
}
let typeData = monitor.type_data;
if (!typeData) {
return null;
}
try {
let heartbeatConfig = JSON.parse(typeData);
let heartbeatSecret = heartbeatConfig.secretString;
if (heartbeatSecret === secret) {
return await db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: GetMinuteStartNowTimestampUTC(GetNowTimestampUTC()),
status: UP,
latency: 0,
type: SIGNAL,
});
}
} catch (e) {
console.error("Error registering heartbeat:", e);
}
return null;
};
export const GenerateToken = async (data) => {
try {
const token = jwt.sign(data, process.env.KENER_SECRET_KEY || DUMMY_SECRET, {
@@ -467,7 +502,7 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
let rawData = await db.getDataGroupByDayAlternative(monitor_tag, start, end);
let anchorStatus = await GetLastStatusBefore(monitor_tag, start);
rawData = InterpolateData(rawData, start, anchorStatus, end - 60);
rawData = InterpolateData(rawData, start, anchorStatus, end);
const groupedData = rawData.reduce((acc, row) => {
// Calculate day group considering timezone offset
+14 -4
View File
@@ -1,7 +1,7 @@
// @ts-nocheck
import axios from "axios";
import { Ping, ExtractIPv6HostAndPort, TCP } from "./ping.js";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "./constants.js";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL, DEFAULT_STATUS } from "./constants.js";
import Service from "./services/service.js";
import { GetMinuteStartNowTimestampUTC, ReplaceAllOccurrences, GetRequiredSecrets, Wait } from "./tool.js";
@@ -101,6 +101,12 @@ const Minuter = async (monitor) => {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "GROUP") {
realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
} else if (monitor.monitor_type === "SSL") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "SQL") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "HEARTBEAT") {
realTimeData[startOfMinute] = await serviceClient.execute();
}
manualData = await manualIncident(monitor);
@@ -113,17 +119,21 @@ const Minuter = async (monitor) => {
mergedData[startOfMinute] = {
status: monitor.default_status,
latency: 0,
type: "default_status",
type: DEFAULT_STATUS,
};
}
}
for (const timestamp in realTimeData) {
mergedData[timestamp] = realTimeData[timestamp];
if (!!realTimeData[timestamp] && !!realTimeData[timestamp].status) {
mergedData[timestamp] = realTimeData[timestamp];
}
}
for (const timestamp in manualData) {
mergedData[timestamp] = manualData[timestamp];
if (!!manualData[timestamp] && !!manualData[timestamp].status) {
mergedData[timestamp] = manualData[timestamp];
}
}
for (const timestamp in mergedData) {
+44 -16
View File
@@ -1,6 +1,6 @@
// @ts-nocheck
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
import { MANUAL } from "../constants.js";
import { GetMinuteStartNowTimestampUTC, GetDbType } from "../tool.js";
import { MANUAL, SIGNAL } from "../constants.js";
import Knex from "knex";
class DbImpl {
@@ -40,6 +40,15 @@ class DbImpl {
.first();
}
async getLastHeartbeat(monitor_tag) {
return await this.knex("monitoring_data")
.where("monitor_tag", monitor_tag)
.where("type", SIGNAL)
.orderBy("timestamp", "desc")
.limit(1)
.first();
}
//given monitor_tag, start and end timestamp in utc seconds return total degraded, up, down, avg(latency), max(latency), min(latency)
async getAggregatedMonitoringData(monitor_tag, start, end) {
return await this.knex("monitoring_data")
@@ -449,20 +458,39 @@ class DbImpl {
}
async createIncident(data) {
const [incident] = await this.knex("incidents")
.insert({
title: data.title,
start_date_time: data.start_date_time,
end_date_time: data.end_date_time,
status: data.status,
state: data.state,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
incident_type: data.incident_type,
incident_source: data.incident_source,
})
.returning("*");
return incident;
const dbType = GetDbType(); // sqlite, postgresql, mysql
// Common insert data
const insertData = {
title: data.title,
start_date_time: data.start_date_time,
end_date_time: data.end_date_time,
status: data.status,
state: data.state,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
incident_type: data.incident_type,
incident_source: data.incident_source,
};
// PostgreSQL supports returning clause
if (dbType === "postgresql") {
const [incident] = await this.knex("incidents").insert(insertData).returning("*");
return incident;
}
// MySQL and SQLite need different approaches
else {
// Insert and get the ID
const result = await this.knex("incidents").insert(insertData);
// Different handling for MySQL vs SQLite
let id = result[0];
// Fetch the newly created record
const incident = await this.knex("incidents").where("id", id).first();
return incident;
}
}
//get incidents paginated
+89 -83
View File
@@ -1,93 +1,99 @@
// @ts-nocheck
class Discord {
url;
headers;
method;
siteData;
monitorData;
url;
headers;
method;
siteData;
monitorData;
constructor(url, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener"
};
constructor(url, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener",
};
this.url = url;
this.headers = Object.assign(kenerHeader, {});
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
}
this.url = url;
this.headers = Object.assign(kenerHeader, {});
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
}
transformData(data) {
let siteURL = this.siteData.siteURL;
let logo =
this.siteData.siteURL +
(!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "") +
this.siteData.logo;
transformData(data) {
let siteURL = this.siteData.siteURL;
let color = 13250616; //down;
if (data.severity === "warning") {
color = 15125089;
}
if (data.status === "RESOLVED") {
color = 5156244;
}
return {
username: this.siteData.siteName,
avatar_url: logo,
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
embeds: [
{
title: data.alert_name,
description: data.description,
url: data.actions[0].url,
color: color,
fields: [
{
name: "Monitor",
value: data.details.metric,
inline: false
},
{
name: "Alert ID",
value: data.id,
inline: false
},
{
name: "Current Value",
value: data.details.current_value,
inline: true
},
{
name: "Threshold",
value: data.details.threshold,
inline: true
}
],
footer: {
text: "Kener",
icon_url: logo
},
timestamp: data.timestamp
}
]
};
}
//remove / if there at the end of the url
if (siteURL.endsWith("/")) {
siteURL = siteURL.slice(0, -1);
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.transformData(data))
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
if (!this.siteData.logo.startsWith("http"))
this.siteData.logo = `${siteURL}${!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : ""}${this.siteData.logo}`;
let logo = this.siteData.logo;
let color = 13250616; //down;
if (data.severity === "warning") {
color = 15125089;
}
if (data.status === "RESOLVED") {
color = 5156244;
}
return {
username: this.siteData.siteName,
avatar_url: logo,
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
embeds: [
{
title: data.alert_name,
description: data.description,
url: data.actions[0].url,
color: color,
fields: [
{
name: "Monitor",
value: data.details.metric,
inline: false,
},
{
name: "Alert ID",
value: data.id,
inline: false,
},
{
name: "Current Value",
value: data.details.current_value,
inline: true,
},
{
name: "Threshold",
value: data.details.threshold,
inline: true,
},
],
footer: {
text: "Kener",
icon_url: logo,
},
timestamp: data.timestamp,
},
],
};
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.transformData(data)),
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
}
export default Discord;
+77 -79
View File
@@ -3,92 +3,90 @@
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
class Webhook {
url;
headers;
method;
siteData;
monitorData;
trigger_meta;
url;
headers;
method;
siteData;
monitorData;
trigger_meta;
constructor(trigger_meta, method, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener/3.0.0"
};
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];
this.headers[header.key] = header.value;
}
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
constructor(trigger_meta, method, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener/3.2.2",
};
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];
this.headers[header.key] = header.value;
}
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
let envSecrets = GetRequiredSecrets(
`${this.url} ${JSON.stringify(this.headers)} ${JSON.stringify(this.trigger_meta.webhook_body)}`
);
//replace secrets in url and 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.find, secret.replace);
this.headers = JSON.parse(
ReplaceAllOccurrences(JSON.stringify(this.headers), secret.find, secret.replace)
);
}
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
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
);
}
}
}
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) {
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;
}
transformData(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() {
return "webhook";
}
type() {
return "webhook";
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.transformData(data)
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.transformData(data),
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
}
export default Webhook;
+17 -18
View File
@@ -81,19 +81,20 @@ function getCountOfSimilarStatuesEnd(arr, statusType) {
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;
const nowUTC = GetMinuteStartNowTimestampUTC();
const midnightUTC = GetDayStartTimestampUTC(nowUTC);
const midnightTz = BeginningOfDay({ timeZone: localTz });
const midnight90DaysAgoTz = midnightTz - 90 * 24 * 60 * 60;
const NO_DATA = "No Data";
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(now) - midnight) / 60);
const maxDateTodayTimestamp = BeginningOfMinute({ timeZone: localTz });
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(nowUTC) - midnightTz) / 60);
const maxDateTodayTimestampTz = BeginningOfMinute({ timeZone: localTz });
const _90Day = {};
let latestTimestamp = 0;
let dbData = await GetDataGroupByDayAlternative(
monitor.tag,
midnight90DaysAgo,
maxDateTodayTimestamp,
midnight90DaysAgoTz,
maxDateTodayTimestampTz,
offsetInMinutes,
);
let totalDegradedCount = 0;
@@ -110,7 +111,6 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
totalDegradedCount += dayData.DEGRADED;
totalDownCount += dayData.DOWN;
totalUpCount += dayData.UP;
if (dayData.DEGRADED >= monitor.day_degraded_minimum_count) {
cssClass = returnStatusClass(dayData.DEGRADED, dayData.total, StatusObj.DEGRADED, site.barStyle);
summaryDuration = getSummaryDuration(dayData.DEGRADED, selectedLang);
@@ -156,14 +156,9 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
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,
);
let todayDataDb = await db.getMonitoringData(monitor.tag, midnightTz, maxDateTodayTimestampTz);
let anchorStatus = await GetLastStatusBefore(monitor.tag, midnightTz);
todayDataDb = InterpolateData(todayDataDb, midnightTz, anchorStatus, maxDateTodayTimestampTz);
if (site.summaryStyle === "CURRENT") {
summaryColorClass = "api-up";
@@ -180,6 +175,10 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
summaryStatus = "DOWN";
summaryColorClass = "api-down";
}
if (lastRow.status === "NO_DATA") {
summaryStatus = NO_DATA;
summaryColorClass = "api-nodata";
}
summaryStatus = l(lang, summaryTime(summaryStatus), {
status: l(lang, summaryStatus),
duration: summaryDuration,
@@ -196,8 +195,8 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
summaryStatus: summaryStatus,
summaryColorClass: summaryColorClass,
barRoundness: site.barRoundness,
midnight90DaysAgo: midnight90DaysAgo,
maxDateTodayTimestamp: maxDateTodayTimestamp,
midnight90DaysAgo: midnight90DaysAgoTz,
maxDateTodayTimestamp: maxDateTodayTimestampTz,
};
};
export { FetchData };
+13 -5
View File
@@ -2,8 +2,9 @@
import axios from "axios";
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import * as cheerio from "cheerio";
const defaultEval = `(async function (statusCode, responseTime, responseData) {
const defaultEval = `(async function (statusCode, responseTime, responseRaw, modules) {
let statusCodeShort = Math.floor(statusCode/100);
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
return {
@@ -45,7 +46,8 @@ class ApiCall {
let method = this.monitor.type_data.method;
let timeout = this.monitor.type_data.timeout || 5000;
let tag = this.monitor.tag;
let monitorEval = !!this.monitor.type_data.monitorEval ? this.monitor.type_data.monitorEval : defaultEval;
let monitorEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : defaultEval;
for (let i = 0; i < this.envSecrets.length; i++) {
const secret = this.envSecrets[i];
@@ -113,12 +115,18 @@ class ApiCall {
}
}
resp = Buffer.from(resp).toString("base64");
let evalResp = undefined;
let modules = { cheerio };
try {
evalResp = await eval(monitorEval + `(${statusCode}, ${latency}, "${resp}")`);
const evalFunction = new Function(
"statusCode",
"responseTime",
"responseRaw",
"modules",
`return (${monitorEval})(statusCode, responseTime, responseRaw, modules);`,
);
evalResp = await evalFunction(statusCode, latency, resp, modules);
} catch (error) {
console.log(`Error in monitorEval for ${tag}`, error.message);
}
+44
View File
@@ -0,0 +1,44 @@
// @ts-nocheck
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
import { GetLastHeartbeat } from "../controllers/controller.js";
class HeartbeatCall {
monitor;
constructor(monitor) {
this.monitor = monitor;
}
async execute() {
let nowMinute = GetMinuteStartNowTimestampUTC();
let latestData = await GetLastHeartbeat(this.monitor.tag);
if (!latestData) {
return {};
}
let latency = nowMinute - latestData.timestamp;
let downRemainingMinutes = Number(this.monitor.type_data.downRemainingMinutes);
if (latency > downRemainingMinutes * 60) {
return {
status: DOWN,
latency: latency,
type: REALTIME,
};
}
let degradedRemainingMinutes = Number(this.monitor.type_data.degradedRemainingMinutes);
if (latency > degradedRemainingMinutes * 60) {
return {
status: DEGRADED,
latency: latency,
type: REALTIME,
};
}
return {
status: UP,
latency: latency,
type: REALTIME,
};
}
}
export default HeartbeatCall;
+11 -21
View File
@@ -1,18 +1,9 @@
// @ts-nocheck
import axios from "axios";
import { Ping } from "../ping.js";
import {
UP,
DOWN,
DEGRADED,
REALTIME,
TIMEOUT,
ERROR,
MANUAL,
} from "../constants.js";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
const defaultPingEval = `(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
const defaultPingEval = `(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
@@ -36,9 +27,7 @@ class PingCall {
async execute() {
let hosts = this.monitor.type_data.hosts;
let pingEval = !!this.monitor.type_data.pingEval
? this.monitor.type_data.pingEval
: defaultPingEval;
let pingEval = !!this.monitor.type_data.pingEval ? this.monitor.type_data.pingEval : defaultPingEval;
let tag = this.monitor.tag;
if (hosts === undefined) {
console.log(
@@ -54,20 +43,21 @@ class PingCall {
let arrayOfPings = [];
for (let i = 0; i < hosts.length; i++) {
const host = hosts[i];
arrayOfPings.push(
await Ping(host.type, host.host, host.timeout, host.count),
);
arrayOfPings.push(await Ping(host.type, host.host, host.timeout, host.count));
}
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString(
"base64",
);
let evalResp = undefined;
try {
evalResp = await eval(pingEval + `("${respBase64}")`);
const evalFunction = new Function("arrayOfPings", `return (${pingEval})(arrayOfPings);`);
evalResp = await evalFunction(arrayOfPings);
} catch (error) {
console.log(`Error in pingEval for ${tag}`, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
//reduce to get the status
return {
+9
View File
@@ -4,6 +4,9 @@ import PingCall from "./pingCall.js";
import TcpCall from "./tcpCall.js";
import DnsCall from "./dnsCall.js";
import GroupCall from "./groupCall.js";
import SSLCall from "./sslCall.js";
import SqlCall from "./sqlCall.js";
import HeartbeatCall from "./heartbeatCall.js";
class Service {
service;
@@ -21,6 +24,12 @@ class Service {
this.service = null;
} else if (monitor.monitor_type === "GROUP") {
this.service = new GroupCall(monitor);
} else if (monitor.monitor_type === "SSL") {
this.service = new SSLCall(monitor);
} else if (monitor.monitor_type === "SQL") {
this.service = new SqlCall(monitor);
} else if (monitor.monitor_type === "HEARTBEAT") {
this.service = new HeartbeatCall(monitor);
} else {
console.log("Invalid monitor.monitor_type ", monitor.monitor_type);
process.exit(1);
+89
View File
@@ -0,0 +1,89 @@
// @ts-nocheck
import Knex from "knex";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
class SqlCall {
monitor;
envSecrets;
constructor(monitor) {
this.monitor = monitor;
this.envSecrets = GetRequiredSecrets(`${monitor.type_data.connectionString}`);
}
async execute() {
let client = this.monitor.type_data.dbType;
let connection = this.monitor.type_data.connectionString;
for (let i = 0; i < this.envSecrets.length; i++) {
const secret = this.envSecrets[i];
connection = ReplaceAllOccurrences(connection, secret.find, secret.replace);
}
let query = this.monitor.type_data.query;
let timeout = this.monitor.type_data.timeout || 5000;
const startTime = Date.now();
let knexInstance = null;
try {
// Create database configuration
const config = {
client: client,
connection: connection,
pool: { min: 0, max: 1 },
acquireConnectionTimeout: timeout,
};
// Initialize knex
knexInstance = Knex(config);
// Set up a timeout for the query execution
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Query timeout")), timeout);
});
// Execute query
const queryPromise = knexInstance.raw(query);
// Race the promises to handle timeouts
await Promise.race([queryPromise, timeoutPromise]);
// Calculate latency
const latency = Date.now() - startTime;
return {
status: UP,
latency: latency,
type: REALTIME,
};
} catch (error) {
const latency = Date.now() - startTime;
// Handle timeout specifically
if (error.message === "Query timeout") {
return {
status: DOWN,
latency: latency,
type: TIMEOUT,
};
}
// Handle other connection or query errors
return {
status: DOWN,
latency: latency,
type: ERROR,
};
} finally {
// Destroy knex instance to clean up connections
if (knexInstance) {
try {
await knexInstance.destroy();
} catch (destroyError) {
console.error("Error closing database connection:", destroyError);
}
}
}
}
}
export default SqlCall;
+74
View File
@@ -0,0 +1,74 @@
// @ts-nocheck
import tls from "tls";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
const getSSLExpiry = (domain, port = 443) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const socket = tls.connect(port, domain, { servername: domain }, () => {
const { valid_to } = socket.getPeerCertificate();
if (!valid_to) {
reject(new Error("No certificate found."));
return;
}
const latency = Date.now() - startTime;
const timeTillExpiryMs = new Date(valid_to) - new Date();
resolve({
domain,
expiryDate: new Date(valid_to),
timeTillExpiryMs,
latency,
});
socket.end();
});
socket.on("error", (err) => reject(err));
});
};
class SSLCall {
monitor;
constructor(monitor) {
this.monitor = monitor;
}
async execute() {
let domain = this.monitor.type_data.host;
let port = parseInt(this.monitor.type_data.port || "443");
let degradedRemainingHours = Number(this.monitor.type_data.degradedRemainingHours);
let downRemainingHours = Number(this.monitor.type_data.downRemainingHours);
try {
const { timeTillExpiryMs, latency } = await getSSLExpiry(domain, port);
const timeTillExpiryHours = timeTillExpiryMs / 1000 / 60 / 60;
if (timeTillExpiryHours > degradedRemainingHours) {
return {
status: UP,
latency: latency,
type: REALTIME,
};
} else if (timeTillExpiryHours > downRemainingHours) {
return {
status: DEGRADED,
latency: latency,
type: REALTIME,
};
} else {
return {
status: DOWN,
latency: latency,
type: REALTIME,
};
}
} catch (error) {
console.log(`Error checking SSL for ${domain}:`, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
}
}
export default SSLCall;
+8 -4
View File
@@ -3,8 +3,7 @@ import axios from "axios";
import { TCP } from "../ping.js";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
const defaultTcpEval = `(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
const defaultTcpEval = `(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
@@ -51,14 +50,19 @@ class TcpCall {
const host = hosts[i];
arrayOfPings.push(await TCP(host.type, host.host, host.port, host.timeout));
}
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString("base64");
let evalResp = undefined;
try {
evalResp = await eval(tcpEval + `("${respBase64}")`);
const evalFunction = new Function("arrayOfPings", `return (${tcpEval})(arrayOfPings);`);
evalResp = await evalFunction(arrayOfPings);
} catch (error) {
console.log(`Error in tcpEval for ${tag}`, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
//reduce to get the status
return {
+236 -243
View File
@@ -1,325 +1,318 @@
// @ts-nocheck
import { AllRecordTypes } from "./constants.js";
import knexOb from "../../../knexfile.js";
import dotenv from "dotenv";
dotenv.config();
const IsValidURL = function (url) {
return /^(http|https):\/\/[^ "]+$/.test(url);
return /^(http|https):\/\/[^ "]+$/.test(url);
};
const IsStringURLSafe = function (str) {
const regex = /^[A-Za-z0-9\-_.~]+$/;
return regex.test(str);
const regex = /^[A-Za-z0-9\-_.~]+$/;
return regex.test(str);
};
const IsValidHTTPMethod = function (method) {
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
};
//return given timestamp in UTC
const GetNowTimestampUTC = function () {
//use js date instead of moment
const now = new Date();
const timestamp = now.getTime();
return Math.floor(timestamp / 1000);
//use js date instead of moment
const now = new Date();
const timestamp = now.getTime();
return Math.floor(timestamp / 1000);
};
//return given timestamp minute start timestamp in UTC
const GetMinuteStartTimestampUTC = function (timestamp) {
//use js date instead of moment
const now = new Date(timestamp * 1000);
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
//use js date instead of moment
const now = new Date(timestamp * 1000);
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0,
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
};
//return current timestamp minute start timestamp in UTC
const GetMinuteStartNowTimestampUTC = function () {
//use js date instead of moment
const now = new Date();
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
//use js date instead of moment
const now = new Date();
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0,
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
};
//return given timestamp day start timestamp in UTC
const GetDayStartTimestampUTC = function (timestamp) {
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayStart = new Date(
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0)
);
const dayStartTimestamp = dayStart.getTime();
return Math.floor(dayStartTimestamp / 1000);
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayStart = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0));
const dayStartTimestamp = dayStart.getTime();
return Math.floor(dayStartTimestamp / 1000);
};
const GetDayEndTimestampUTC = function (timestamp) {
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayEnd = new Date(
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999)
);
const dayEndTimestamp = dayEnd.getTime();
return Math.floor(dayEndTimestamp / 1000) + 60;
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayEnd = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999));
const dayEndTimestamp = dayEnd.getTime();
return Math.floor(dayEndTimestamp / 1000) + 60;
};
const DurationInMinutes = function (start, end) {
return Math.floor((end - start) / 60);
return Math.floor((end - start) / 60);
};
const GetDayStartWithOffset = function (timeStampInSeconds, offsetInMinutes) {
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
//have to figure out when to add a day
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
//have to figure out when to add a day
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
//if xtm - 330 > 1 day , add a day to xtm - 330
//if xtm - 330 > 1 day , add a day to xtm - 330
if (offsetInMinutes < 0) {
//add one day to dayStartThen
dayStartThen = dayStartThen + 24 * 60 * 60;
}
return dayStartThen + offsetInMinutes * 60;
if (offsetInMinutes < 0) {
//add one day to dayStartThen
dayStartThen = dayStartThen + 24 * 60 * 60;
}
return dayStartThen + offsetInMinutes * 60;
};
const BeginningOfDay = (options = {}) => {
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
hour: "numeric",
minute: "numeric",
second: "numeric"
}).formatToParts(date);
const hour = parseInt(parts.find((i) => i.type === "hour").value);
const minute = parseInt(parts.find((i) => i.type === "minute").value);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(
1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000)
);
return dt.getTime() / 1000;
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
hour: "numeric",
minute: "numeric",
second: "numeric",
}).formatToParts(date);
const hour = parseInt(parts.find((i) => i.type === "hour").value);
const minute = parseInt(parts.find((i) => i.type === "minute").value);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000));
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 { 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}$/;
if (ipv4Regex.test(input)) {
return "IPv4";
}
// Check if input is a valid IPv4 address
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IPv4";
}
// Check if input is a valid IPv6 address
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
if (ipv6Regex.test(input)) {
return "IPv6";
}
// Check if input is a valid IPv6 address
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
if (ipv6Regex.test(input)) {
return "IPv6";
}
// Check if input is a valid domain name
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "Domain Name";
}
// Check if input is a valid domain name
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "Domain Name";
}
// If none of the above conditions match, the input is invalid
return "Invalid";
// If none of the above conditions match, the input is invalid
return "Invalid";
};
function checkIfDuplicateExists(arr) {
return new Set(arr).size !== arr.length;
return new Set(arr).size !== arr.length;
}
function GetWordsStartingWithDollar(text) {
const regex = /\$\w+/g;
const wordsArray = text.match(regex);
return wordsArray || [];
const regex = /\$\w+/g;
const wordsArray = text.match(regex);
return wordsArray || [];
}
const StatusObj = {
UP: "api-up",
DEGRADED: "api-degraded",
DOWN: "api-down",
NO_DATA: "api-nodata"
UP: "api-up",
DEGRADED: "api-degraded",
DOWN: "api-down",
NO_DATA: "api-nodata",
};
// @ts-ignore
const ParseUptime = function (up, all) {
if (all === 0) return String("-");
if (up == 0) return String("0");
if (up == all) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
//return 50% as 50% and not 50.0000%
if (((up / all) * 100) % 10 == 0) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
return String(((up / all) * parseFloat(100)).toFixed(4));
if (all === 0) return String("-");
if (up == 0) return String("0");
if (up == all) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
//return 50% as 50% and not 50.0000%
if (((up / all) * 100) % 10 == 0) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
return String(((up / all) * parseFloat(100)).toFixed(4));
};
const ParsePercentage = function (n) {
if (isNaN(n)) return "-";
if (n == 0) {
return "0";
}
if (n == 100) {
return "100";
}
return n.toFixed(4);
if (isNaN(n)) return "-";
if (n == 0) {
return "0";
}
if (n == 100) {
return "100";
}
return n.toFixed(4);
};
//valid domain name
function IsValidHost(domain) {
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
}
//valid nameserver
function IsValidNameServer(nameServer) {
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
}
//valid dns record type
function IsValidRecordType(recordType) {
return AllRecordTypes.hasOwnProperty(recordType);
return AllRecordTypes.hasOwnProperty(recordType);
}
function ReplaceAllOccurrences(originalString, searchString, replacement) {
const regex = new RegExp(`\\${searchString}`, "g");
const replacedString = originalString.replace(regex, replacement);
return replacedString;
const regex = new RegExp(`\\${searchString}`, "g");
const replacedString = originalString.replace(regex, replacement);
return replacedString;
}
function GetRequiredSecrets(str) {
let envSecrets = [];
const requiredSecrets = GetWordsStartingWithDollar(str).map((x) => x.substr(1));
for (const [key, value] of Object.entries(process.env)) {
if (requiredSecrets.indexOf(key) !== -1) {
envSecrets.push({
find: `$${key}`,
replace: value
});
}
}
return envSecrets;
let envSecrets = [];
const requiredSecrets = GetWordsStartingWithDollar(str).map((x) => x.substr(1));
for (const [key, value] of Object.entries(process.env)) {
if (requiredSecrets.indexOf(key) !== -1) {
envSecrets.push({
find: `$${key}`,
replace: value,
});
}
}
return envSecrets;
}
function ValidateMonitorAlerts(alerts) {
if (!alerts) {
console.log("Alerts object is not provided.");
return false;
}
// if down degraded not present return false
if (!alerts.hasOwnProperty("DOWN") && !alerts.hasOwnProperty("DEGRADED")) {
console.log("Alerts object does not have DOWN or DEGRADED properties.");
return false;
}
let statues = Object.keys(alerts);
//can be either DOWN or DEGRADED
let validKeys = ["DOWN", "DEGRADED"];
for (let i = 0; i < statues.length; i++) {
const keyName = statues[i];
if (!validKeys.includes(keyName)) {
console.log(`Invalid key found in alerts: ${keyName}`);
return false;
}
}
for (const key in alerts) {
if (Object.prototype.hasOwnProperty.call(alerts, key)) {
const element = alerts[key];
const triggers = element.triggers;
//if triggers not present return false
if (!element.hasOwnProperty("triggers")) {
console.log(`Triggers not present for key: ${key}`);
return false;
}
//if length of triggers is 0 return false
if (triggers.length === 0) {
console.log(`Triggers length is 0 for key: ${key}`);
return false;
}
if (
!element.hasOwnProperty("failureThreshold") ||
!element.hasOwnProperty("successThreshold")
) {
console.log(`Thresholds not present for key: ${key}`);
return false;
}
if (
element.hasOwnProperty("failureThreshold") &&
!Number.isInteger(element.failureThreshold)
) {
console.log(`Failure threshold is not an integer for key: ${key}`);
return false;
}
if (
element.hasOwnProperty("successThreshold") &&
!Number.isInteger(element.successThreshold)
) {
console.log(`Success threshold is not an integer for key: ${key}`);
return false;
}
if (element.hasOwnProperty("failureThreshold") && element.failureThreshold <= 0) {
console.log(`Failure threshold is less than or equal to 0 for key: ${key}`);
return false;
}
if (element.hasOwnProperty("successThreshold") && element.successThreshold <= 0) {
console.log(`Success threshold is less than or equal to 0 for key: ${key}`);
return false;
}
}
}
return true;
if (!alerts) {
console.log("Alerts object is not provided.");
return false;
}
// if down degraded not present return false
if (!alerts.hasOwnProperty("DOWN") && !alerts.hasOwnProperty("DEGRADED")) {
console.log("Alerts object does not have DOWN or DEGRADED properties.");
return false;
}
let statues = Object.keys(alerts);
//can be either DOWN or DEGRADED
let validKeys = ["DOWN", "DEGRADED"];
for (let i = 0; i < statues.length; i++) {
const keyName = statues[i];
if (!validKeys.includes(keyName)) {
console.log(`Invalid key found in alerts: ${keyName}`);
return false;
}
}
for (const key in alerts) {
if (Object.prototype.hasOwnProperty.call(alerts, key)) {
const element = alerts[key];
const triggers = element.triggers;
//if triggers not present return false
if (!element.hasOwnProperty("triggers")) {
console.log(`Triggers not present for key: ${key}`);
return false;
}
//if length of triggers is 0 return false
if (triggers.length === 0) {
console.log(`Triggers length is 0 for key: ${key}`);
return false;
}
if (!element.hasOwnProperty("failureThreshold") || !element.hasOwnProperty("successThreshold")) {
console.log(`Thresholds not present for key: ${key}`);
return false;
}
if (element.hasOwnProperty("failureThreshold") && !Number.isInteger(element.failureThreshold)) {
console.log(`Failure threshold is not an integer for key: ${key}`);
return false;
}
if (element.hasOwnProperty("successThreshold") && !Number.isInteger(element.successThreshold)) {
console.log(`Success threshold is not an integer for key: ${key}`);
return false;
}
if (element.hasOwnProperty("failureThreshold") && element.failureThreshold <= 0) {
console.log(`Failure threshold is less than or equal to 0 for key: ${key}`);
return false;
}
if (element.hasOwnProperty("successThreshold") && element.successThreshold <= 0) {
console.log(`Success threshold is less than or equal to 0 for key: ${key}`);
return false;
}
}
}
return true;
}
function GenerateRandomColor() {
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
return randomColor;
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));
return new Promise((resolve) => setTimeout(resolve, ms));
}
function MaskString(str) {
return "*".repeat(str.length - 4) + str.slice(-4);
return "*".repeat(str.length - 4) + str.slice(-4);
}
function GetDbType() {
//sqlite, postgresql, mysql
return knexOb.databaseType;
}
export {
IsValidURL,
IsValidHTTPMethod,
GetMinuteStartTimestampUTC,
GetNowTimestampUTC,
GetDayStartTimestampUTC,
GetMinuteStartNowTimestampUTC,
DurationInMinutes,
GetDayStartWithOffset,
BeginningOfDay,
IsStringURLSafe,
ValidateIpAddress,
checkIfDuplicateExists,
GetWordsStartingWithDollar,
StatusObj,
ParseUptime,
ParsePercentage,
IsValidHost,
IsValidRecordType,
IsValidNameServer,
ReplaceAllOccurrences,
GetRequiredSecrets,
ValidateMonitorAlerts,
GenerateRandomColor,
BeginningOfMinute,
Wait,
MaskString
IsValidURL,
IsValidHTTPMethod,
GetMinuteStartTimestampUTC,
GetNowTimestampUTC,
GetDayStartTimestampUTC,
GetMinuteStartNowTimestampUTC,
DurationInMinutes,
GetDayStartWithOffset,
BeginningOfDay,
IsStringURLSafe,
ValidateIpAddress,
checkIfDuplicateExists,
GetWordsStartingWithDollar,
StatusObj,
ParseUptime,
ParsePercentage,
IsValidHost,
IsValidRecordType,
IsValidNameServer,
ReplaceAllOccurrences,
GetRequiredSecrets,
ValidateMonitorAlerts,
GenerateRandomColor,
BeginningOfMinute,
Wait,
MaskString,
GetDbType,
};
+47 -2
View File
@@ -6,6 +6,7 @@
import Sun from "lucide-svelte/icons/sun";
import Menu from "lucide-svelte/icons/menu";
import X from "lucide-svelte/icons/x";
import Heart from "lucide-svelte/icons/heart";
import { clickOutsideAction, slide } from "svelte-legos";
import Moon from "lucide-svelte/icons/moon";
import { onMount } from "svelte";
@@ -56,6 +57,23 @@
if (window.innerWidth > 768) {
sideBarHidden = false;
}
const donationBanner = document.getElementById("donation-banner");
const closeButton = document.getElementById("close-donation");
// Check if we should show the banner today
const lastShown = localStorage.getItem("kener_donation_banner_closed");
const today = new Date().toDateString();
// Only show the banner if it hasn't been closed today
if (lastShown !== today) {
donationBanner.style.display = "block";
}
// Add close button handler
closeButton.addEventListener("click", () => {
donationBanner.style.display = "none";
localStorage.setItem("kener_donation_banner_closed", today);
});
});
</script>
@@ -92,7 +110,7 @@
<!-- Document Icon - Replace with your own logo -->
<img src="https://kener.ing/logo.png" class="h-8 w-8" alt="" />
<span class="text-xl font-medium">Kener Documentation</span>
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.1.8 </span>
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.2.2 </span>
</a>
</div>
@@ -107,7 +125,6 @@
</a>
<a href="/api-reference" class="text-sm font-medium"> API Reference </a>
<a href="https://github.com/rajnandan1/kener/issues" class="text-sm font-medium"> Report Issue </a>
<a href="https://github.com/sponsors/rajnandan1" class="text-sm font-medium"> Sponsor </a>
</div>
</div>
@@ -168,6 +185,34 @@
<main class="dark relative z-10 min-h-screen pt-16 md:ml-72">
<div class="mx-auto max-w-5xl px-4 py-10 sm:px-6 md:px-8 md:pr-64">
<!-- Content Header -->
<div
id="donation-banner"
style="display: none;"
class="mb-8 rounded-lg bg-gradient-to-r from-blue-900 to-indigo-900 p-4 shadow-lg"
>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="rounded-full bg-amber-500 p-2">
<Heart class="h-5 w-5 text-white" />
</div>
<div>
<h3 class="text-base font-medium text-white">Support Kener Development</h3>
<p class="text-sm text-blue-200">Help us keep this project running and improve it further</p>
</div>
</div>
<div class="flex items-center space-x-3">
<a
href="https://github.com/sponsors/rajnandan1"
class="rounded-md bg-amber-500 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-amber-600"
>
Sponsor
</a>
<button id="close-donation" class="rounded p-1 text-blue-300 hover:bg-blue-800 hover:text-white">
<X class="h-5 w-5" />
</button>
</div>
</div>
</div>
<div
class="prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
+13 -11
View File
@@ -186,7 +186,7 @@
</div>
</footer>
{/if}
<div class="fixed bottom-4 right-4 z-20 rounded-md bg-background">
<div class="fixed bottom-4 right-4 z-20 flex flex-col rounded-md bg-background">
{#if !!data.site.tzToggle && data.site.tzToggle === "YES"}
<div>
<Popover.Root>
@@ -198,8 +198,8 @@
<Popover.Content class="px-0 py-2">
<div class="flex flex-col gap-2">
<div class="flex px-2 pb-1 text-xs font-semibold">
<span class="flex-1"
>{@html l(data.lang, "Timezone set to %tz", {
<span class="flex-1">
{@html l(data.lang, "Timezone set to %tz", {
tz: "<i>" + data.localTz + "</i>"
})}
</span>
@@ -271,17 +271,19 @@
</div>
{/if}
{#if !!data.site.themeToggle && data.site.themeToggle === "YES"}
<Button on:click={toggleMode} variant="ghost" size="icon" class="flex">
<Sun class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
<div>
<Button on:click={toggleMode} variant="ghost" size="icon" class="flex">
<Sun class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</div>
{/if}
</div>
{#if data.isLoggedIn}
<a href="{base}/manage/app/site" rel="external" class="button-77 fixed bottom-8 left-8 z-50" role="button">
Manage
</a>
<div class="fixed bottom-2 left-2 z-50 md:bottom-8 md:left-8">
<a href="{base}/manage/app/site" rel="external" class="button-77" role="button"> Manage </a>
</div>
{/if}
</main>
+12 -14
View File
@@ -63,7 +63,7 @@
</svelte:head>
<div class="mt-12"></div>
{#if data.hero}
<section class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<section class="section-hero mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center">
{#if data.hero.image}
@@ -86,7 +86,7 @@
</section>
{/if}
{#if data.pageType != "home"}
<section class="mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<section class="section-back mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<Button
variant="outline"
class="bounce-left h-8 justify-start pl-1.5"
@@ -107,10 +107,10 @@
{/if}
{#if data.allRecentIncidents.length + data.allRecentMaintenances.length > 0}
<section
class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
class="section-events mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
id=""
>
<div class="grid w-full grid-cols-2 gap-4">
<div class="mb-2 grid w-full grid-cols-2 gap-4">
<div class="col-span-2 flex gap-x-2 text-center md:text-left">
{#if kindFilter == "INCIDENT"}
{#if data.allRecentIncidents.length > 0}
@@ -137,11 +137,6 @@
{/if}
</div>
</div>
</section>
<section
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
id=""
>
<Card.Root class="w-full">
{#if kindFilter == "INCIDENT"}
<Card.Content class=" newincidents w-full overflow-hidden p-0">
@@ -159,7 +154,7 @@
{/if}
{#if data.monitors.length > 0}
<section
class="mx-auto mb-2 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
class="section-legend mx-auto mb-2 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
id=""
>
<div class="grid w-full grid-cols-2 gap-4">
@@ -189,9 +184,9 @@
</div>
</section>
<section
class="z-20 mx-auto mb-8 flex w-full flex-1 flex-col items-start justify-center backdrop-blur-[2px] md:w-[655px]"
class="section-monitors z-20 mx-auto mb-8 flex w-full flex-1 flex-col items-start justify-center backdrop-blur-[2px] md:w-[655px]"
>
<Card.Root>
<Card.Root class="monitor-root">
<Card.Content class="monitors-card p-0">
{#each data.monitors as monitor}
<Monitor
@@ -208,7 +203,7 @@
{/if}
{#if data.site.categories && data.pageType == "home"}
<section
class="relative z-10 mx-auto mb-8 w-full max-w-[890px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
class="section-categories relative z-10 mx-auto mb-8 w-full max-w-[890px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
>
{#each data.site.categories.filter((e) => e.name != "Home") as category}
<a href={`?category=${category.name}`} rel="external">
@@ -235,7 +230,10 @@
{/each}
</section>
{/if}
<section class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent" id="">
<section
class="section-browser-events mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<a
href="{base}/incidents/{format(new Date(), 'MMMM-yyyy')}"
rel="external"
@@ -0,0 +1,39 @@
// @ts-nocheck
// @ts-ignore
import { json } from "@sveltejs/kit";
import { RegisterHeartbeat } from "$lib/server/controllers/controller";
export async function GET({ request, params }) {
let signal = params.signal;
let signalData = signal.split(":");
if (signalData.length != 2) {
return json({ error: "Invalid url" }, { status: 400 });
}
let resp = await RegisterHeartbeat(signalData[0], signalData[1]);
if (!!!resp) {
return json({ error: "Invalid heartbeat url" }, { status: 400 });
}
return json(
{ status: "OK" },
{
status: 200,
},
);
}
export async function POST({ request, params }) {
let signal = params.signal;
let signalData = signal.split(":");
if (signalData.length != 2) {
return json({ error: "Invalid url" }, { status: 400 });
}
let resp = await RegisterHeartbeat(signalData[0], signalData[1]);
if (!!!resp) {
return json({ error: "Invalid heartbeat url" }, { status: 400 });
}
return json(
{ status: "OK" },
{
status: 200,
},
);
}
+4 -5
View File
@@ -17,9 +17,8 @@ export async function POST({ request }) {
const incidentIDs = payload.incidentIDs || [];
let _0Day = {};
const now = GetMinuteStartNowTimestampUTC() + 60;
const start = payload.startTs;
let end = Math.min(payload.startTs + 24 * 60 * 60, now);
let end = payload.endTs;
for (let i = start; i < end; i += 60) {
_0Day[i] = {
@@ -30,9 +29,9 @@ export async function POST({ request }) {
};
}
let dayData = await db.getMonitoringData(monitor.tag, payload.startTs, end);
let dayData = await db.getMonitoringData(monitor.tag, payload.startTs, payload.endTs);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
dayData = InterpolateData(dayData, payload.startTs, anchorStatus, end);
dayData = InterpolateData(dayData, payload.startTs, anchorStatus, payload.endTs);
let siteData = await GetAllSiteData();
let ups = 0;
@@ -60,7 +59,7 @@ export async function POST({ request }) {
ups++;
} else if (status == "DOWN") {
downs++;
} else {
} else if (status == "DEGRADED") {
degradeds++;
}
}
@@ -13,7 +13,7 @@ export async function POST({ request }) {
let rawData = await db.getMonitoringData(monitor.tag, start, end);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
rawData = InterpolateData(rawData, start, anchorStatus, end - 60);
rawData = InterpolateData(rawData, start, anchorStatus, end);
let aggregatedData = rawData.reduce(
(acc, row) => {
@@ -37,7 +37,7 @@ export async function GET({ params, url }) {
let todayDataDb = await db.getMonitoringData(tag, since, now);
let anchorStatus = await GetLastStatusBefore(tag, since);
todayDataDb = InterpolateData(todayDataDb, since, anchorStatus, now - 60);
todayDataDb = InterpolateData(todayDataDb, since, anchorStatus, now);
let calculatedData = AggregateData(todayDataDb);
@@ -69,7 +69,7 @@
<link rel="canonical" href={canonical} />
</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">
<section class="section-back mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<Button
variant="outline"
class="bounce-left h-8 justify-start pl-1.5"
@@ -81,7 +81,7 @@
{l(data.lang, "Back")}
</Button>
</section>
<section class="mx-auto mb-8 flex max-w-[655px] flex-1 flex-col items-start justify-center">
<section class="section-month-banner mx-auto mb-8 flex max-w-[655px] flex-1 flex-col items-start justify-center">
<div class="mesh mx-auto min-w-full max-w-[655px] rounded-md px-4 py-12 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center text-muted">
<h1 class=" text-5xl font-extrabold leading-tight">
@@ -93,7 +93,9 @@
</div>
</div>
<div class="mx-auto mb-2 mt-4 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]">
<div
class="section-month-events mx-auto mb-2 mt-4 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
>
{#if sortedIncidentSmartDates.length == 0}
<div class="mx-auto w-full rounded-md bg-clip-text p-12 text-center">
<div class="mx-auto mb-4 h-[32px] w-[32px] text-primary">
@@ -633,7 +633,6 @@
variant="ghost"
class="h-5 w-5 p-1"
size="icon"
disabled={incident.isAutoCreated}
on:click={(e) => {
showEditModal(incident);
}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+61 -61
View File
@@ -2,67 +2,67 @@ import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
plugins: [
require("@tailwindcss/typography")
// ...
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
}
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
},
fontFamily: {
sans: [...fontFamily.sans]
}
}
}
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
plugins: [
require("@tailwindcss/typography"),
// ...
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: [...fontFamily.sans],
},
},
},
};
export default config;