mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08b168ee6e | |||
| d6a87ac81a | |||
| 615dba42b8 | |||
| 3ffec4f1fe | |||
| 60b4b6f207 | |||
| b9f5eb56c5 | |||
| 7b9ae73044 |
@@ -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.10"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.1.10-blue" alt="Kener latest stable version: 3.1.10" /></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.10)</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.10" target="_blank"><code>3.1.10</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.10) 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.10) 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 <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.10)</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.10-alpine" target="_blank"><code>3.1.10-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.10) 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.10) 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>
|
||||
|
||||
|
||||
+43
-5
@@ -7,6 +7,45 @@ 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>
|
||||
@@ -59,7 +98,7 @@ If you're using custom evaluation functions in your monitors, you'll need to upd
|
||||
|
||||
```javascript
|
||||
// Old format
|
||||
(async function (statusCode, responseTime, responseDataBase64) {
|
||||
;(async function (statusCode, responseTime, responseDataBase64) {
|
||||
const resp = atob(responseDataBase64)
|
||||
// Your logic here
|
||||
})
|
||||
@@ -67,7 +106,7 @@ If you're using custom evaluation functions in your monitors, you'll need to upd
|
||||
|
||||
```javascript
|
||||
// New format
|
||||
(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
;(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
// responseRaw is the direct response - no need to decode
|
||||
// Access cheerio with modules.cheerio
|
||||
// Your logic here
|
||||
@@ -78,7 +117,7 @@ If you're using custom evaluation functions in your monitors, you'll need to upd
|
||||
|
||||
```javascript
|
||||
// Old format
|
||||
(async function (responseDataBase64) {
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
// Your logic here
|
||||
})
|
||||
@@ -86,13 +125,12 @@ If you're using custom evaluation functions in your monitors, you'll need to upd
|
||||
|
||||
```javascript
|
||||
// New format
|
||||
(async function (arrayOfPings) {
|
||||
;(async function (arrayOfPings) {
|
||||
// arrayOfPings is directly available - no need to decode
|
||||
// Your logic here
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## v3.1.8
|
||||
|
||||
<picture>
|
||||
|
||||
+30
-75
@@ -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
|
||||
|
||||
[](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?
|
||||
|
||||
@@ -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">
|
||||
|
||||

|
||||
|
||||
</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.
|
||||
+21
-10
@@ -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)
|
||||
|
||||
+46
-35
@@ -49,41 +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": "SSL Monitor",
|
||||
"link": "/docs/monitors-ssl",
|
||||
"file": "/monitors-ssl.md"
|
||||
},
|
||||
{
|
||||
"title": "SQL Monitor",
|
||||
"link": "/docs/monitors-sql",
|
||||
"file": "/monitors-sql.md"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Setup Triggers",
|
||||
"link": "/docs/triggers",
|
||||
@@ -142,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": [
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"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.",
|
||||
|
||||
+3
-2
@@ -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 {
|
||||
|
||||
+248
-245
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
@@ -354,8 +359,29 @@
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -607,6 +633,7 @@
|
||||
<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>
|
||||
@@ -1129,6 +1156,60 @@
|
||||
</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">
|
||||
|
||||
@@ -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";
|
||||
@@ -104,6 +104,11 @@
|
||||
query: "SELECT 1",
|
||||
timeout: 5000,
|
||||
dbType: "pg" //mysql2
|
||||
},
|
||||
heartbeatConfig: {
|
||||
degradedRemainingMinutes: 1,
|
||||
downRemainingMinutes: 2,
|
||||
secretString: RandomString(32)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -140,6 +145,8 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -105,6 +105,8 @@ const Minuter = async (monitor) => {
|
||||
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);
|
||||
@@ -117,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) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
|
||||
import { MANUAL } from "../constants.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")
|
||||
|
||||
+17
-18
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -27,6 +28,8 @@ class Service {
|
||||
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);
|
||||
|
||||
@@ -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.2.0 </span>
|
||||
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.2.1 </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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Reference in New Issue
Block a user