mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64b94da585 | |||
| 1609b4fc50 | |||
| dfffffb101 | |||
| 81250a117a | |||
| 72bd0241e9 | |||
| 63c24f147f | |||
| 88fb7df3f5 | |||
| 320b1a0cc5 | |||
| 08b168ee6e | |||
| d6a87ac81a | |||
| 615dba42b8 | |||
| 3ffec4f1fe | |||
| 60b4b6f207 | |||
| b9f5eb56c5 | |||
| 7b9ae73044 | |||
| 87f2c33eba | |||
| 0a73a8b10a | |||
| af65404fd3 | |||
| 398f891dac | |||
| 6a2375a774 | |||
| 43bbcf4015 | |||
| fadb563337 | |||
| 1ae54b3906 |
@@ -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.8"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.1.8-blue" alt="Kener latest stable version: 3.1.8" /></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.8)</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.8" target="_blank"><code>3.1.8</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.8) 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.8) 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.8)</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.8-alpine" target="_blank"><code>3.1.8-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.8) 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.8) 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>
|
||||
|
||||
|
||||
@@ -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
@@ -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?
|
||||
|
||||
@@ -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",
|
||||
|
||||
+10
-14
@@ -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) {
|
||||
@@ -66,21 +66,17 @@ 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
|
||||
- `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 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"
|
||||
}
|
||||
|
||||
@@ -111,8 +107,8 @@ The following example shows how to use the eval function to evaluate the respons
|
||||
This next example shows how to call another API withing eval. It is scrapping the second last script tag from the response and checking if the heading is "No recent issues" then the status is UP else it is DOWN.
|
||||
|
||||
```js
|
||||
(async function (statusCode, responseTime, responseDataBase64) {
|
||||
let htmlString = atob(responseDataBase64)
|
||||
(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
|
||||
@@ -143,9 +139,9 @@ This next example shows how to call another API withing eval. It is scrapping th
|
||||
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) {
|
||||
(async function (statusCode, responseTime, responseDataBase64, modules) {
|
||||
let html = atob(responseDataBase64)
|
||||
const $ = cheerio.load(html)
|
||||
const $ = modules.cheerio.load(html)
|
||||
const components = $(".components-section .components-container .component-container")
|
||||
let status = true
|
||||
components.each((index, element) => {
|
||||
|
||||
@@ -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.
|
||||
+5
-10
@@ -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)
|
||||
|
||||
+18
-23
@@ -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
@@ -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
@@ -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
|
||||
|
||||
[](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
-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": [
|
||||
|
||||
+18
-17
@@ -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;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.1.9",
|
||||
"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.",
|
||||
|
||||
+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 {
|
||||
|
||||
+38
-5
@@ -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
@@ -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,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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "स्थिति ठीक है",
|
||||
|
||||
@@ -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": "ステータス正常",
|
||||
|
||||
@@ -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": "양호",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Статус ОК",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "状态正常",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+44
-16
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 {
|
||||
@@ -46,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];
|
||||
@@ -114,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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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.9 </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"
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 |
+61
-61
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user