new doc site

This commit is contained in:
Raj Nandan Sharma
2024-11-11 08:40:44 +05:30
parent bd87aded67
commit 5abfafe2fa
53 changed files with 2030 additions and 229 deletions
View File
+301
View File
@@ -0,0 +1,301 @@
# Customize Site
There is a folder called `src/lib/server/config`. Inside which there is a `site.yaml` file. You can modify this file to have your own branding and do few other things.
## Sample site.yaml
```yaml
title: "Kener"
siteName: "Kener.ing"
logo: "/logo.svg"
favicon: "/logo96.png"
home: "/"
theme: dark
github:
owner: "rajnandan1"
repo: "kener"
incidentSince: 72
metaTags:
description: "Your description"
keywords: "keyword1, keyword2"
nav:
- name: "Documentation"
url: "/docs"
- name: "Github"
iconURL: "/github.svg"
url: "https://github.com/rajnandan1/kener"
siteURL: https://kener.ing
hero:
title: Kener is a Open-Source Status Page System
subtitle: Let your users know what's going on.
footerHTML: |
Made using
<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">
Kener
</a>
an open source status page system built with Svelte and TailwindCSS.
i18n:
defaultLocale: "en"
locales:
en: "English"
hi: "हिन्दी"
zh-CN: "中文"
ja: "日本語"
vi: "Tiếng Việt"
pattern: "squares"
font:
cssSrc: "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
family: '"Lato", sans-serif'
```
---
## title
This translates to
```html
<title>Your Title</title>
```
---
## siteName
This is the name that will be shown in the nav bar, top left corner of the page
## logo
URL of the logo that will be shown in the nav bar. You can also add your logo in the `static` folder. Use the path `/logo.svg` to refer to it if you have added `logo.svg` in the `static` folder. Otherwise, you can use any URL.
## favicon
URL of the favicon that will be shown in the browser tab. You can also add your favicon in the `static` folder. Use the path `/logo96.png` to refer to it if you have added `logo96.png` in the `static` folder. Otherwise, you can use any URL.
## home
This is the location where someone will be taken when they click on the site name in the nav bar
## theme
This is the default theme of the site that will be used when a user lands for the first time. It can be `light` or `dark`. Defaults to `light`. The user still gets the option to change the theme.
As of now there is no option to change the colors of the theme using `site.yaml`. If you still want to change the colors you can do so by modifying the `src/app.postcss` file.
## favicon
It can be set by modifying the `<head>` tag in `src/app.html` file.
Example add a png called `logo.png` file in `static/` and then
```html
...
<link rel="icon" href="/logo.png" />
...
```
---
## github
For incident kener uses github comments. Create an empty [github](https://github.com) repo and add them to `site.yaml`
```yaml
github:
owner: "username"
repo: "repository"
incidentSince: 72
```
### owner
Owner of the github repository. If the repository is `https://github.com/rajnandan1/kener` then the owner is `rajnandan1`
### repo
Repository name of the github repository. If the repository is `https://github.com/rajnandan1/kener` then the repo is `kener`
### incidentSince
`incidentSince` is in hours. It means if an issue is created before X hours then kener would not honor it. What it means is that kener would not show it active incident pages nor it will update the uptime. Default is 48 hours.
---
## metaTags
Meta tags are nothing but html `<meta>`. You can use them for SEO purposes
```yaml
metaTags:
description: "Your description"
keywords: "keyword1, keyword2"
og:image: "https://example.com/og.png"
```
will become
```html
<head>
<meta name="description" content="Your description" />
<meta name="keywords" content="keyword1, keyword2" />
<meta name="og:image" content="https://example.com/og.png" />
</head>
```
---
## siteURL
You can set this to generate SiteMaps
```yaml
siteURL: https://kener.ing
```
Sitemaps urls will be `https://kener.ing/sitemap.xml`
---
## hero
Use hero to add a banner to your kener page
```yaml
hero:
title: Kener is a Open-Source Status Page System
subtitle: Let your users know what's going on.
```
### title
Title of the hero section
### subtitle
Subtitle of the hero section
---
## nav
You can add more links to your navbar.
```yaml
nav:
- name: "Home"
url: "/home"
```
### name
Name of the link
### url
URL of the link
### iconURL
Icon of the link. You can add an icon in the `static` folder and refer to it using the path `/github.svg`
---
## categories
You can define categories for your monitors. Each category can have a description. The monitors can be grouped by categories.
`name=home` will be shown in the home page. Categories are shown in the order they are defined in the yaml file. A dropdown will appear in the nav bar to select the category.
```yaml
categories:
- name: API
description: "Kener provides a simple API for you to use to update your status page."
- name: home
description: "loroem ipsum lorem ipsum"
```
### name
Name of the category
### description
Description of the category
---
## footerHTML
You can add HTML to the footer. You can add links to your social media or anything else.
```yaml
footerHTML: |
Made using <a href="https://kener.ing" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">Kener</a> an open source status page system built with Svelte and TailwindCSS.
```
---
## i18n
You can add translations to your site. By default it is set to `en`. Available translations are present in `locales/` folders in the root directory. You can add more translations by adding a new file in the `locales` folder.
### Enable
Once you have added a new translation file in the `locales` folder, you can enable it by adding the locale code in the `site.yaml` file.
Let us say you have added a `hi.json` file in the `locales` folder. You can enable it by adding the following to the `site.yaml` file.
```yaml
i18n:
defaultLocale: en
locales:
en: English
hi: हिन्दी
```
### defaultLocale
**_defaultLocale_**: The default locale to be used for a user when he or she visits for the first time. It is important to note that the default locale json file should be present in the locales folder.
### locales
**_locales_**: A list of locales that you want to enable. The key is the locale code and the value is the name of the language. The locale code should be the same as the json file name in the locales folder. `en` means `en.json` should be present in the locales folder.
Adding more than one locales will enable a dropdown in the navbar to select the language.
Selected languages are stored in cookies and will be used when the user visits the site again.
There is no auto detection of the language. The user has to manually select the language.
### Variables
There are few variables that you you should not change,
- %hours : This will be replaced by the hours
- %minutes : This will be replaced by the minutes
- %minute : This will be replaced by the minute
- %status : This will be replaced by the status
---
## pattern
You can set the background pattern of the site. It can be `squares`, `dots`, `none`
---
## font
You can set the font of the site. You can use google fonts or any other font.
```yaml
font:
cssSrc: "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
family: '"Lato", sans-serif'
```
### cssSrc
URL of the font css
### family
Font family
+74
View File
@@ -0,0 +1,74 @@
# Deployment
```shell
export NODE_ENV=production
npm i
npm run build
npm run serve
```
It also needs 2 yaml files to work
- site.yaml: Contains information about the site
- monitors.yaml: Contains your monitors and their related specifications
By default these are present in `config/`. However you can use different location either passing them as argument or having the path as enviorment variable
## Add as Environment variables
```shell
export MONITOR_YAML_PATH=/your/path/monitors.yaml
export SITE_YAML_PATH=/your/path/site.yaml
```
## Add as argument to prod.js
```shell
npm run serve -- --monitors /your/path/monitors.yaml --site /your/path/site.yaml
```
## Install using Docker
[Dockerhub](https://hub.docker.com/r/rajnandan1/kener)
```shell
docker.io/rajnandan1/kener:latest
```
[Github Packages](https://github.com/rajnandan1/kener/pkgs/container/kener)
```shell
ghcr.io/rajnandan1/kener:latest
```
You should mount a host directory to persist your configuration and expose the web port. [Environmental variables](https://rajnandan1.github.io/kener-docs/docs/environment-vars) can be passed with `-e` An example `docker run` command:
Make sure you have a `/static` folder inside your config folder
```shell
docker run -d -v /path/on/host/config:/config -p 3000:3000 -e "GH_TOKEN=1234" rajnandan1/kener
```
Or use **Docker Compose** with the example [docker-compose.yaml](https://raw.githubusercontent.com/rajnandan1/kener/main/docker-compose.yml)
## Using PUID and PGID
If you are
- running on a **linux host** (ie unraid) and
- **not** using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_)
then you must set the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid) in the container in order for it to generate files/folders your normal user can interact it.
Run these commands from your terminal
- `id -u` -- prints UID for **PUID**
- `id -g` -- prints GID for **PGID**
Then add to your docker command like so:
```shell
docker run -d ... -e "PUID=1000" -e "PGID=1000" ... rajnandan1/kener
```
or substitute them in [docker-compose.yml](https://raw.githubusercontent.com/rajnandan1/kener/main/docker-compose.yml)
+59
View File
@@ -0,0 +1,59 @@
# Environment Variables
Kener needs some environment variables to be set to run properly. Here are the list of environment variables that you need to set.
All of these are optional but are required for specific features.
## PORT
Defaults to 3000 if not specified
```shell
export PORT=4242
```
## GH_TOKEN
A github token to read issues and create labels. This is required for **incident management**
```shell
export GH_TOKEN=your-github-token
```
## API_TOKEN
To talk to **kener apis** you will need to set up a token. It uses Bearer Authorization
```shell
export API_TOKEN=sometoken
```
## API_IP
While using API you can set this variable to accept request from a **specific IP**
```shell
export API_IP=127.0.0.1
```
## API_IP_REGEX
While using API you can set this variable to accept request from a specific IP that matches the regex. Below example shows an **IPv6 regex**
```shell
export API_IP_REGEX=^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$
```
If you set both API_IP and API_IP_REGEX, API_IP will be given preference
## KENER_BASE_PATH
By default kener runs on `/` but you can change it to `/status` or any other path.
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
- Important: This env variable should be present during both build and run time
- If you are using docker you will have to do your own build and set this env variable during `docker build`
```shell
export KENER_BASE_PATH=/status
```
+42
View File
@@ -0,0 +1,42 @@
# Github Setup
Kener uses github for incident management. Issues created in github using certain tags go to kener as incidents.
## Step 1: Create Github Repository
Create a Github Repository. It can be either public or private. After you have created a repository open `site.yaml` and add them like this
```yaml
github:
owner: "username"
repo: "repository"
```
## Step 2: Create Github Token
You can create either a classic token or personal access token
### Creating Classic Token
- Go to [Tokens](https://github.com/settings/tokens/new)
- Note: kener
- Expiration: No Expiration
- Scopes: write:packages
- Click on generate Token
### Creating Personal Access Token
- Go to [Personal Access Token](https://github.com/settings/personal-access-tokens/new)
- Token Name: kener
- Expiration: Use custom to select a calendar date
- Description: My Kener
- Repository access: Check Only Selected Repositories. Select your github repository
- Repository Permission: Select Issues Read Write
- Click on generate token
## Step 3: Set environment
```shell
export GH_TOKEN=github_pat_11AD3ZA3Y0
```
+87
View File
@@ -0,0 +1,87 @@
# Kener - Status Page System
<p align="center">
<img src="/newbg.png" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
</p>
<p class="flex space-x-2 justify-center">
<a href="https://github.com/rajnandan1/kener/stargazers" >
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/rajnandan1/kener?label=Star%20Repo&
style=social">
</a>
<a href="https://github.com/ivbeg/awesome-status-pages" >
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome status page" />
</a>
<a href="https://hub.docker.com/r/rajnandan1/kener" >
<img src="https://img.shields.io/docker/pulls/rajnandan1/kener" alt="Docker Kener" />
</a>
</p>
#### 👉 Visit a live server [here](https://kener.ing)
#### 👉 Quick Start [here](https://rajnandan1.github.io/kener-docs/docs/quick-start)
Kener: Open-source Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment.
It uses files to store the data. Other adapters are coming soon
## Features
### Monitoring and Tracking
- Real-time monitoring
- 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
- Flexible monitor configuration using YAML. Define your own parsing for monitor being UP/DOWN/DEGRADED
- Construct complex API Polls - Chain, Secrets etc
- Supports a Default Status for Monitors. Example defaultStatus=DOWN if you dont hit API per minute with Status UP
- Supports base path for hosting in k8s
- Pre-built docker image for easy deployment
### Customization and Branding
- Customizable status page using yaml or code
- Badge generation for status and uptime of Monitors
- Support for custom domains
- Embed Monitor as an iframe or widget
- Light + Dark Theme
- Internationalization support
### Incident Management
- Create Incidents using Github Issues - Rich Text
- Or use APIs to create Incidents
### 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
## Technologies used
- [SvelteKit](https://kit.svelte.dev/)
- [shadcn-svelte](https://www.shadcn-svelte.com/)
## Inspired from
- [Upptime](https://upptime.js.org/)
## Roadmap
- [x] Add api to create incident
- [x] Add docker file
- [ ] Add notification
- [ ] Add Mysql adapter
## 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)
+29
View File
@@ -0,0 +1,29 @@
# How it works
Kener has two parts.
- Sveltekit application which is server rendered and is the frontend. It is running on Svelte 4.
- Hooks into the backend to get the data for your monitors
## Folder structure
```shell
├── src (svelte frontend files)
├── static (things put here can be referenced directly example static/logo.png -> /logo.png)
├── src
├──── routes
├────── (docs) (You can delete this, this has routes for documentation)
├──── lib
├────── server
├──────── config (Location for you site.yaml and monitos.yaml)
├──────── data (This is the location where server computate data is stored. Do not touch this)
├── docs (Documentation, you can delete this folder)
```
## Site.yaml
This is the configuration file for your site. This is where you define the name of your site, the look and feel of your site etc
## Monitors.yaml
This is the configuration file for your monitors. This is where you define the monitors you want to show on your site.
+39
View File
@@ -0,0 +1,39 @@
# i18n
You can add translations to your site. By default it is set to `en`. Available translations are present in `locales/` folders in the root directory. You can add more translations by adding a new file in the `locales` folder.
## How to enable a translation
Once you have added a new translation file in the `locales` folder, you can enable it by adding the locale code in the `site.yaml` file.
Let us say you have added a `hi.json` file in the `locales` folder. You can enable it by adding the following to the `site.yaml` file.
```yaml
i18n:
defaultLocale: en
locales:
en: English
hi: हिन्दी
```
> **_defaultLocale:_** The default locale to be used. This will be the language used when a user visits the site for the first time. It is important to note that the default locale json file should be present in the locales folder.
## Variables
There are few variables that you you should not change,
- %hours : This will be replaced by the hours
- %minutes : This will be replaced by the minutes
- %minute : This will be replaced by the minute
- %status : This will be replaced by the status
> **locales:_** A list of locales that you want to enable. The key is the locale code and the value is the name of the language. The locale code should be the same as the json file name in the locales folder. `en` means `en.json` should be present in the locales folder.
Adding more than one locales will enable a dropdown in the navbar to select the language.
Selected languages are stored in cookies and will be used when the user visits the site again.
There is no auto detection of the language. The user has to manually select the language.
+25
View File
@@ -0,0 +1,25 @@
# Incident Management
Kener uses Github to power incident management using labels
## Labels
Kener auto creates labels for your monitors using the `tag` parameter
- `incident`: If an issue is marked as incident it will show up in kener home page
- `incident-down`: If an issue is marked as incident-down and incident kener would make that monitor down
- `incident-degraded`: If an issue is marked as incident-degraded and incident then kener would make the monitor degraded
- `resolved`: Use this tag to mark the incident has RESOLVED
- `identified`: Use this tag to show that the root cause of the incident has been identified
## Creating your first incident
- Go to your github repo of kener
- Go to issues
- Create an issue. Give it a title
- In the body add [start_datetime:1702651340] and [end_datetime:1702651140] and add some description. Time is UTC
- Add `incident`, `incident-down` and the monitor tag. This will make the monitor down for 4 minutes
If you clone the repo it gives you an issue template to create incidents
Here is a [sample incident](https://github.com/rajnandan1/kener/issues/15) for your reference.
+404
View File
@@ -0,0 +1,404 @@
# Kener APIs
Kener also gives APIs to push data and create incident. Before you use kener apis you will have to set an authorization token called `API_TOKEN`. This also has to be set as an environment variable.
```shell
export API_TOKEN=some-token-set-by-you
```
Additonally you can set IP whitelisting by setting another environment token called `API_IP` or `API_IP_REGEX`. If you set both `API_IP` and `API_IP_REGEX`, `API_IP` will be given preference. Read more [here](/docs/environment-vars#api_ip)
---
## Update Status - API
![Static Badge](https://img.shields.io/badge/METHOD-POST-blue?style=flat-square)
The update status API can be used to manually update the state of a monitor from a remote server.
### Request Body
| Parameter | Description |
| ------------------ | ------------------------------------------------------------------------------------ |
| status | `Required` Can be only UP/DOWN/DEGRADED |
| latency | `Required` In Seconds. Leave 0 if not required |
| timestampInSeconds | `Optional` Timestamp in UTC seconds. Defaults to now. Should between 90 Days and now |
| tag | `Required` Monitor Tag set in monitors.yaml |
```shell
curl --request POST \
--url http://your-kener.host/api/status \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"status": "DOWN",
"latency": 1213,
"timestampInSeconds": 1702405860,
"tag": "google-search"
}'
```
### Response
```json
{
"status": 200,
"message": "success at 1702405860"
}
```
This will update the status of the monitor with tag `google-search` to DOWN at UTC 1702405860
---
## Get Status - API
![Static Badge](https://img.shields.io/badge/METHOD-GET-green?style=flat-square)
Use this API to get the status of a monitor.
### Request
Replace `google-search` with your monitor tag in query param
```shell
curl --request GET \
--url 'http://your-kener.host/api/status?tag=google-search' \
--header 'Authorization: Bearer some-token-set-by-you'
```
### Response
```json
{
"status": "UP",
"uptime": "9.0026",
"lastUpdatedAt": 1706447160
}
```
---
## Create an Incident - API
![Static Badge](https://img.shields.io/badge/METHOD-POST-blue?style=flat-square)
Can be use to create an incident from a remote server
### Request Body
| Parameter | Description |
| ------------- | -------------------------------------------------------- |
| startDatetime | `Optional` When did the incident start in UTC second |
| endDatetime | `Optional` When did the incident end in UTC seconds |
| title | `Required` Title of the incident |
| body | `Optional` Body of the incident |
| tags | `Required` Array of String, Monitor Tags of the incident |
| impact | `Optional` Can be only DOWN/DEGRADED |
| isMaintenance | `Optional` Boolean if incident is a maintenance |
| isIdentified | `Optional` Incident identified |
| isResolved | `Optional` Incident resolved |
```shell
curl --request POST \
--url http://your-kener.host/api/incident \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"title": "Outage in Mumbai",
"body": "Login cluster is down in mumbai region",
"tags": ["google-search"],
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}'
```
### Response
```json
{
"createdAt": 1703940450,
"closedAt": null,
"title": "Outage in Mumbai",
"tags": ["google-search"],
"incidentNumber": 12,
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"body": "Login cluster is down in mumbai region",
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}
```
---
## Update an Incident - API
![Static Badge](https://img.shields.io/badge/METHOD-PATCH-yellow?style=flat-square)
Can be use to update an incident from a remote server. It will clear values if not passed
### Request Param
- `incidentNumber`: Number of the incident
### Request Body
| Parameter | Description |
| ------------- | -------------------------------------------------------- |
| startDatetime | `Optional` When did the incident start in UTC second |
| endDatetime | `Optional` When did the incident end in UTC seconds |
| title | `Required` Title of the incident |
| body | `Optional` Body of the incident |
| tags | `Required` Array of String, Monitor Tags of the incident |
| impact | `Optional` Can be only DOWN/DEGRADED |
| isMaintenance | `Optional` Boolean if incident is a maintenance |
| isIdentified | `Optional` Incident identified |
| isResolved | `Optional` Incident resolved |
```shell
curl --request PATCH \
--url http://your-kener.host/api/incident/{incidentNumber} \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"title": "Outage in Mumbai",
"body": "Login cluster is down in mumbai region",
"tags": ["google-search"],
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}'
```
### Response
```json
{
"createdAt": 1703940450,
"closedAt": null,
"title": "Outage in Mumbai",
"tags": ["google-search"],
"incidentNumber": 12,
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"body": "Login cluster is down in mumbai region",
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}
```
---
## Get an Incident - API
![Static Badge](https://img.shields.io/badge/METHOD-GET-green?style=flat-square)
Use `incidentNumber` to fetch an incident
### Request Body
```shell
curl --request GET \
--url http://your-kener.host/api/incident/{incidentNumber} \
--header 'Authorization: Bearer some-token-set-by-you' \
```
### Response
```json
{
"createdAt": 1703940450,
"closedAt": null,
"title": "Outage in Mumbai",
"tags": ["google-search"],
"incidentNumber": 12,
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"body": "Login cluster is down in mumbai region",
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}
```
---
## Add Comment - API
![Static Badge](https://img.shields.io/badge/METHOD-POST-blue?style=flat-square)
Add comments for incident using `incidentNumber`
### Request
```shell
curl --request POST \
--url http://your-kener.host/api/incident/{incidentNumber}/comment \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"body": "comment 1"
}'
```
### Response
```json
{
"commentID": 1873376745,
"body": "comment 1",
"createdAt": 1704123938
}
```
---
## Get Comments - API
![Static Badge](https://img.shields.io/badge/METHOD-GET-green?style=flat-square)
Use this API to fetch all the comments for an incident
### Request
```shell
curl --request GET \
--url http://your-kener.host/api/incident/{incidentNumber}/comment \
--header 'Authorization: Bearer some-token-set-by-you' \
```
### Response
```json
[
{
"commentID": 1873372042,
"body": "comment 1",
"createdAt": 1704123116
},
{
"commentID": 1873372169,
"body": "comment 2",
"createdAt": 1704123139
}
]
```
---
## Update Incident Status - API
![Static Badge](https://img.shields.io/badge/METHOD-POST-blue?style=flat-square)
Use this to API to update the status of an ongoing incident.
### Request Body
| Parameter | Description |
| ------------ | ------------------------------------------------------------ |
| isIdentified | `Optional` Boolean, set it when incident has been identified |
| isResolved | `Optional` Boolean, set it when incident has been resolved |
| endDatetime | `Optional` When did the incident end in UTC seconds |
### Request
```shell
curl --request POST \
--url http://your-kener.host/api/incident/{incidentNumber}/status \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"isIdentified": true,
"isResolved": false
"endDatetime": 1702405920
}'
```
### Response
```json
{
"createdAt": 1703940450,
"closedAt": null,
"title": "Outage in Mumbai",
"tags": ["google-search"],
"incidentNumber": 12,
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"body": "Login cluster is down in mumbai region",
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}
```
---
## Search Incidents - API
![Static Badge](https://img.shields.io/badge/METHOD-POST-blue?style=flat-square)
Use this to API to search incidents.
### Request Body
| Parameter | Description |
| ------------------ | ---------------------------------------------------------------------------------------------- |
| state | `Optional` open or closed. Default is open |
| tags | `Optional` Comma separated monitor tags, example: earth,google-seach |
| page | `Optional` Page number, starts with 1, defaults to 1 |
| per_page | `Optional` Page size, defaults to 10, max is 100 |
| created_after_utc | `Optional` timestamp in UTC seconds when the incident was created after. Example: 1702405920 |
| created_before_utc | `Optional` timestamp in UTC seconds when the incident was created before . Example: 1702405920 |
| title_like | `Optional` search incidents with title |
### Request
Search incidents that are closed and title contains `hello incident`
```shell
curl --request POST \
--url http://your-kener.host/api/incident?state=closed&title_like=Hello%20Incident \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"isIdentified": true,
"isResolved": false
"endDatetime": 1702405920
}'
```
### Response
```json
[
{
"createdAt": 1703940450,
"closedAt": null,
"title": "Outage in Mumbai - Hello Incident",
"tags": ["google-search"],
"incidentNumber": 12,
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"body": "Login cluster is down in mumbai region",
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}
]
```
+202
View File
@@ -0,0 +1,202 @@
# Monitor Examples
Here are some exhaustive examples for monitors
## Simple GET Monitor
Below example will call https://www.google.com/webhp. If the status code is 200 then it will be UP else DOWN.
```yaml
- name: Google Search
tag: "google-search"
api:
method: GET
url: https://www.google.com/webhp
```
## Simple GET Monitor Without Hyperlink
Below example will call https://www.google.com/webhp. If the status code is 200 then it will be UP else DOWN. It will not show the GET hyperlink in the monitor description.
```yaml
- name: Google Search
tag: "google-search"
api:
method: GET
url: https://www.google.com/webhp
hideURLForGet: true
```
## Monitor with HTML description
Below example will call https://www.google.com/webhp. If the status code is 200 then it will be UP else DOWN. It will show the description in HTML format.
```yaml
- name: Google Search
tag: "google-search"
description: "Hello <b>world</b>"
api:
method: GET
url: https://www.google.com/webhp
```
## Monitor with image
google.png is in the static folder
```yaml
- name: Google Search
tag: "google-search"
image: "/google.png"
api:
method: GET
url: https://www.google.com/webhp
```
## Get Monitor 15 Minute
Below example will call https://www.google.com/webhp every 15 minutes. If the status code is 200 then it will be UP else DOWN.
```yaml
- name: Google Search
description: Search the world's information, including webpages, images, videos and more.
tag: "google-search"
cron: "*/15 * * * *"
api:
method: GET
url: https://www.google.com/webhp
```
## POST Monitor With Body
Below example will call https://www.google.com/webhp with body. If the status code is 200 then it will be UP else DOWN.
```yaml
- name: Google Search
description: Google Search
tag: "google-search-post"
api:
method: POST
url: https://www.google.com/webhp
headers:
Content-Type: application/json
body: '{"order_amount":22222.1,"order_currency":"INR"}'
```
## Secrets in Header
You can set ENV variables in your machine and use them in your monitors. Example below has `GH_TOKEN` as an environment variable. It uses process.env.GH_TOKEN.
```
export GH_TOKEN=some.token.for.github
```
> **_NOTE:_** DO NOT forget the `$` sign in your monitor secret, otherwise it will not be picked up.
```yaml
- name: Github Issues
description: Github Issues Fetch
tag: "gh-search-issue"
api:
method: GET
url: https://api.github.com/repos/rajnandan1/kener/issues
headers:
Authorization: Bearer $GH_TOKEN
```
## Secrets in Body
Assuming `ORDER_ID` is present in env
```yaml
- name: Github Issues
description: Github Issues Fetch
tag: "gh-search-issue"
api:
method: POST
url: https://api.github.com/repos/rajnandan1/kener/issues
headers:
Content-Type: application/json
body: '{"order_amount":22222.1,"order_currency":"INR", "order_id": "$ORDER_ID"}'
```
## Eval Body
Read more about [eval](https://kener.ing/docs/monitors#eval)
Below example will call https://api.github.com/repos/rajnandan1/kener/issues. If the status code is 200 then it will be UP else DOWN. It will also check if the response time is greater than 2000ms then it will be DEGRADED.
```yaml
- name: Github Issues
description: Github Issues Fetch
tag: "gh-search-issue"
api:
method: GET
url: https://api.github.com/repos/rajnandan1/kener/issues
eval: |
(function(statusCode, responseTime, responseDataBase64){
const resp = JSON.parse(atob(responseDataBase64));
let status = 'DOWN'
if(statusCode == 200) status = 'UP';
if(Object.keys(resp).length == 0) status = 'DOWN';
if(statusCode == 200 && responseTime > 2000) status = 'DEGRADED';
return {
status: status,
latency: responseTime,
}
})
```
## With defaultStatus UP
Each minute it will set the status as UP
```yaml
- name: Earth
description: Our Planet
tag: "earth"
defaultStatus: UP
```
## With Category
Add this monitor to the API category instead of the default home category
```yaml
- name: Earth
description: Our Planent
tag: "earth"
category: API
```
## Ping Monitor
This will ping the hosts. It will be up if the ping is successful for all the hosts present in the list of ip4 and ip6.
```yaml
- name: Earth
description: Our Planent
tag: "earth"
ping:
hostsV4:
- www.frogment.com
- 52.84.205.24
hostsV6:
- ipv6.google.com
```
If both ping and api monitors are present then API data will overwrite ping data
## Custom Thresholds
The below monitor will show DEGRADED if 3 or more degraded status in a day and DOWN if 2 or more down status in a day. It will also include degraded in downtime calculation.
```yaml
- name: Earth
description: Our blue planet
tag: "earth"
defaultStatus: "UP"
dayDegradedMinimumCount: 3
dayDownMinimumCount: 2
includeDegradedInDowntime: true
```
+91
View File
@@ -0,0 +1,91 @@
# Monitors
Inside `config/` folder there is a file called `monitors.yaml`. We will be adding our monitors here. Please note that your yaml must be valid. It is an array.
## Understanding monitors
Each monitor runs at 1 minute interval by default. Monitor runs in below priorty order.
- defaultStatus Data
- API call Data overrides above data(if specified)
- Pushed Status Data overrides API Data using [Kener Update Statue API](https://rajnandan1.github.io/kener-docs/docs/kener-apis#update-status)
- Manual Incident Data overrides Pushed Status Data
Sample
```yaml
- name: Google Search
description: Search the world's information, including webpages, images, videos and more.
tag: "google-search"
image: "/google.png"
cron: "* * * * *"
defaultStatus: "UP"
api:
timeout: 4000
method: POST
url: https://www.google.com/webhp
headers:
Content-Type: application/json
body: '{"order_amount":1,"order_currency":"INR"}'
eval: |
(function(statusCode, responseTime, responseDataBase64){
const resp = JSON.parse(atob(responseDataBase64));
return {
status: statusCode == 200 ? 'UP':'DOWN',
latency: responseTime,
}
})
```
| name | Required | This will be shown in the UI to your users. Keep it short and unique |
| ------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | Required + Unique | This will be shown in the UI to your users. Keep it short and unique |
| description | Optional | This will be show below your name |
| tag | Required + Unique | This is used to tag incidents created in Github using comments |
| image | Optional | To show a logo before the name |
| cron | Optional | Use cron expression to specify the interval to run the monitors. Defaults to `* * * * *` i.e every minute |
| api.timeout | Optional | timeout for the api in milliseconds. Default is 10000(10 secs) |
| api.method | Optional | HTTP Method |
| api.url | Optional | HTTP URL |
| api.headers | Optional | HTTP headers |
| api.body | Optional | HTTP Body as string |
| api.hideURLForGet | Optional | if the monitor is a GET URL and no headers are specified and the response body content-type is a text/html then kener shows a GET hyperlink in monitor description. To hide that set this as false. Default is true |
| api.eval | Optional | Evaluator written in JS, to parse HTTP response and calculate uptime and latency |
| defaultStatus | Optional | If no API is given this will be the default status. can be UP/DOWN/DEGRADED |
| hidden | Optional | If set to `true` will not show the monitor in the UI |
| category | Optional | Use this to group your monitors. Make sure you have defined category in `site.yaml` and use the `name` attribute here |
| dayDegradedMinimumCount | Optional | Default is 1. It means minimum this number of count for the day to be classified as DEGRADED(Yellow Bar) in 90 day view. Has to be `number` greater than 0 |
| dayDownMinimumCount | Optional | Default is 1. It means minimum this number of count for the day to be classified as DOWN(Red Bar) in 90 day view. Has to be `number` greater than 0 |
| includeDegradedInDowntime | Optional | By deafault uptime percentage is calculated as (UP+DEGRADED/UP+DEGRADED+DOWN). Setting it as `true` will change the calculation to (UP/UP+DEGRADED+DOWN) |
| ping.hostsV4 | Optional | Array of hosts / IP to monitor ping response. Either domain name or IP4 |
| ping.hostsV6 | Optional | Array of hosts / IP to monitor ping response. Either domain name or IP6 |
## eval
This is a anonymous JS function, by default it looks like this.
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and lantecy(number)
> `{status:"DEGRADED", latency: 200}`.
```js
(function (statusCode, responseTime, responseDataBase64) {
let statusCodeShort = Math.floor(statusCode/100);
let status = 'DOWN'
if(statusCodeShort >=2 && statusCodeShort <= 3) {
status = 'UP',
}
return {
status: 'DOWN',
latency: responseTime,
}
})
```
- `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
```js
let decodedResp = atob(responseDataBase64);
//let jsonResp = JSON.parse(decodedResp)
```
+34
View File
@@ -0,0 +1,34 @@
# Quick Start
Please make sure you have [Node](https://nodejs.org/en) installed in your system. Minimum version required is `v16.17.0`.
## Clone the repository
```shell
git clone https://github.com/rajnandan1/kener.git
cd kener
```
## Install Dependencies
```shell
npm install
```
## Configs
- Rename `config/site.example.yaml` -> `config/site.yaml`
- Rename `config/monitors.example.yaml` -> `config/monitors.yaml`
```shell
mv src/lib/server/config/site.example.yaml src/lib/server/config/site.yaml
mv src/lib/server/config/monitors.example.yaml src/lib/server/config/monitors.yaml
```
## Start Kener Development Server
```bash
npm run dev
```
Kener Development Server would be running at PORT 3000. Go to [http://localhost:3000](http://localhost:3000)
+22
View File
@@ -0,0 +1,22 @@
# Kener Showcase
This page is a showcase of how kener is getting used in the wild. If you want to add your site here, please raise a PR and modify this [file](https://github.com/rajnandan1/kener-docs/blob/main/docs/md/docs/showcase.md)
#### [Kener](https://kener.ing)
#### [Cashfree Payments India](https://statuspage.cashfree.com/)
#### [status.orhun.dev](https://status.orhun.dev/)
#### [status.ordinalsbot.com](https://status.ordinalsbot.com/)
#### [status.britsov.com](https://status.britsov.net/)
#### [status.gosu.bar](https://status.gosu.bar/)
#### [stat.imsun.org](https://stat.imsun.org/)
#### [Goomer](https://status.goomer.com.br/)
#### [kennek.io](https://status.kennek.io/)
#### [evelan.io](https://status.evelan.io/)
#### [evelan.io](https://status.evelan.io/)
#### [sveir.xyz](https://status.sveir.xyz/)
#### [cellcast.com](https://status.cellcast.com/)
#### [flytbase.com](https://status.flytbase.com/)
#### [scriptor-artis.fr](https://status.scriptor-artis.fr/)
#### [jiance.f.ozizio.com](http://jiance.f.ozizio.com/)
#### [sshaw.cn](https://s.sshaw.cn/)
#### [donotes.app](https://status.donotes.app)
+163
View File
@@ -0,0 +1,163 @@
# Status Badges
There are three types of badges
Syntax
```md
http://[hostname]/badge/[tag]/status
http://[hostname]/badge/[tag]/dot
http://[hostname]/badge/[tag]/uptime
```
---
## Status
### Badge SVG
Shows the last health check was UP/DOWN/DEGRADED
![Earth Status](https://kener.ing/badge/earth/status)
Example in HTML
```html
<img src="https://kener.ing/badge/earth/status" />
```
Example in MarkDown
```md
![Status Badge](https://kener.ing/badge/[monitor.tag]/status)
```
### Icon SVG
Shows the last health check was UP/DOWN/DEGRADED as SVG dot
#### Standard
![Earth Status](http://localhost:3000/badge/google-search/dot)
```html
<img src="https://kener.ing/badge/earth/dot" />
```
#### Animated
![Earth Status](http://localhost:3000/badge/google-search/dot?animate=ping)
```html
<img src="https://kener.ing/badge/earth/dot?animate=ping" />
```
---
## Uptime
Shows the 90 Day uptime by default. You can `sinceLast` as query param to get uptime since last x seconds.
![Earth Uptime](https://kener.ing/badge/earth/uptime)
### 90 Day Uptime
Example in HTML
```html
<img src="https://kener.ing/badge/earth/uptime" />
```
Example in MarkDown
```md
![Uptime Badge](https://kener.ing/badge/[monitor.tag]/uptime)
```
### 15 Minute Uptime
Example in HTML
```html
<img src="https://kener.ing/badge/earth/uptime?sinceLast=900" />
```
Example in MarkDown
```md
![Uptime Badge](https://kener.ing/badge/[monitor.tag]/uptime?sinceLast=900)
```
---
## Customize Badges
You can set different colors for badges and style.
### With Custom Label Color
![Earth Status](https://kener.ing/badge/earth/status?labelColor=F2BED1)
```md
![Earth Status](https://kener.ing/badge/earth/status?labelColor=F2BED1)
```
### With Custom Value Color
![Earth Status](https://kener.ing/badge/earth/status?color=FFC0D9)
```md
![Earth Status](https://kener.ing/badge/earth/status?color=FFC0D9)
```
### With Both Different Colors
![Earth Status](https://kener.ing/badge/earth/uptime?color=D0BFFF&labelColor=FFF3DA)
```md
![Earth Status](https://kener.ing/badge/earth/uptime?color=D0BFFF&labelColor=FFF3DA)
```
### Style Of the Badge
You can change the style of the badge. Supported Styles are `plastic`, `flat`, `flat-square`, `for-the-badge` or `social`. Default is `flat`
#### plastic
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=plastic)
```md
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=plastic)
```
#### flat
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=flat)
```md
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=flat)
```
#### flat-square
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=flat-square)
```md
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=flat-square)
```
#### for-the-badge
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=for-the-badge)
```md
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=for-the-badge)
```
#### social
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=social)
```md
![Earth Uptime](https://kener.ing/badge/earth/uptime?style=social)
```
+89
View File
@@ -0,0 +1,89 @@
{
"sidebar": [
{
"sectionTitle": "Getting Started",
"children": [
{
"title": "Introduction",
"link": "/docs/home",
"file": "/home.md"
},
{
"title": "Quick Start",
"link": "/docs/quick-start",
"file": "/docs/quick-start.md"
},
{
"title": "How it works",
"link": "/docs/how-it-works",
"file": "/how-it-works.md"
}
]
},
{
"sectionTitle": "Core Concepts",
"children": [
{
"title": "Site Configuration",
"link": "/docs/customize-site",
"file": "/customize-site.md"
},
{
"title": "Monitors",
"link": "/docs/monitors",
"file": "/monitors.md"
},
{
"title": "Monitors Examples",
"link": "/docs/monitor-examples",
"file": "/monitor-examples.md"
},
{
"title": "Incident Management",
"link": "/docs/incident-management",
"file": "/incident-management.md"
},
{
"title": "Localization",
"link": "/docs/i18n",
"file": "/i18n.md"
},
{
"title": "Badges",
"link": "/docs/status-badges",
"file": "/status-badges.md"
}
]
},
{
"sectionTitle": "Deployment",
"children": [
{
"title": "Environment Setup",
"link": "/docs/environment-vars",
"file": "/environment-vars.md"
},
{
"title": "Deployment",
"link": "/docs/deployment",
"file": "/deployment.md"
},
{
"title": "Github Setup",
"link": "/docs/gh-setup",
"file": "/gh-setup.md"
}
]
},
{
"sectionTitle": "API Reference",
"children": [
{
"title": "Kener APIs",
"link": "/docs/kener-apis",
"file": "/kener-apis.md"
}
]
}
]
}
+9
View File
@@ -0,0 +1,9 @@
import { handler } from "./build/handler.js";
import express from "express";
const PORT = process.env.PORT || 3000;
const app = express();
app.use(handler);
app.listen(PORT, () => {
console.log("Kener is running on port " + PORT + "!");
});
+2 -1
View File
@@ -27,7 +27,8 @@
"url": "https://github.com/rajnandan1/kener.git"
},
"scripts": {
"build": "node scripts/check.js && vite build",
"build": "vite build",
"preview": "vite preview",
"serve": "node prod.js",
"dev": "vite dev",
"kener:dev": "cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener node scripts/check.js && concurrently \"cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener node dev.js\" \"cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener vite dev\"",
+38 -30
View File
@@ -52,7 +52,6 @@
let commentsLoading = true;
function getComments() {
state = state == "open" ? "close" : "open";
if (incident.comments.length > 0) return;
if (commentsLoading === false) return;
axios
@@ -65,17 +64,22 @@
// console.log(error);
});
}
$: {
if (state == "open") {
getComments();
}
}
</script>
<div class="incident-div mb-8 grid w-full grid-cols-3 gap-4">
<div class="incident-div relative mb-8 grid w-full grid-cols-3 gap-4">
<div class="col-span-3">
<Card.Root>
<Card.Header>
<Card.Title class="relative">
<Card.Header class="pb-2 pt-3">
<Card.Title class="relative mb-0">
{#if incidentPriority != "" && incidentDuration > 0}
<p class="absolute -top-11 -translate-y-1 leading-10">
<p class="absolute -top-8 -translate-y-1 leading-10">
<Badge
class="-ml-3 bg-card text-sm font-semibold text-[rgba(0,0,0,.6)] text-{StatusObj[
class="-ml-4 bg-card text-xs font-semibold text-[rgba(0,0,0,.6)] text-{StatusObj[
incidentPriority
]} "
>
@@ -86,24 +90,26 @@
</Badge>
</p>
{/if}
{#if variant.includes("monitor")}
<div class="pb-4">
<div class="scroll-m-20 text-2xl font-semibold tracking-tight">
{#if monitor.image}
<img
src={monitor.image}
class="inline h-6 w-6"
alt=""
srcset=""
/>
{/if}
{monitor.name}
</div>
{#if monitor.image}
<img
src={monitor.image}
class="absolute left-0 top-1 inline h-5 w-5"
alt=""
srcset=""
/>
{/if}
<div class="px-8">
<div class="scroll-m-20 text-xl font-medium tracking-tight">
{#if variant.includes("monitor")}
{monitor.name} -
{/if}
{#if variant.includes("title")}
{incident.title}
{/if}
</div>
{/if}
{#if variant.includes("title")}
{incident.title}
{/if}
</div>
{#if incidentState == "open"}
<span
class="absolute -left-[24px] -top-[24px] inline-flex h-[8px] w-[8px] animate-ping rounded-full {blinker} opacity-75"
@@ -113,16 +119,18 @@
<div class="toggle absolute right-4 {state}">
<Button
variant="outline"
class="rounded-full"
class="h-6 w-6 rounded-full"
size="icon"
on:click={getComments}
on:click={() => {
state = state == "open" ? "close" : "open";
}}
>
<ChevronDown class="text-muted-foreground" size={24} />
<ChevronDown class="text-muted-foreground" size={12} />
</Button>
</div>
{/if}
</Card.Title>
<Card.Description>
<Card.Description class="-mt-4 px-8 text-xs ">
{moment(incidentCreatedAt * 1000).format("MMMM Do YYYY, h:mm:ss a")}
<p class="mt-2 leading-8">
@@ -151,7 +159,7 @@
</Card.Description>
</Card.Header>
{#if (variant.includes("body") || variant.includes("comments")) && state == "open"}
<Card.Content>
<Card.Content class="px-14">
{#if variant.includes("body")}
<div
class="prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm"
@@ -160,7 +168,7 @@
</div>
{/if}
{#if variant.includes("comments") && incident.comments?.length > 0}
<div class="ml-4 mt-8">
<div class="ml-4 mt-8 px-4">
<ol class="relative border-s border-secondary">
{#each incident.comments as comment}
<li class="mb-10 ms-4">
@@ -168,7 +176,7 @@
class="absolute -start-1.5 mt-1.5 h-3 w-3 rounded-full border border-secondary bg-secondary"
></div>
<time
class="mb-1 text-sm font-normal leading-none text-muted-foreground"
class="mb-1 text-xs font-normal leading-none text-muted-foreground"
>
{moment(comment.created_at).format(
"MMMM Do YYYY, h:mm:ss a"
+2 -2
View File
@@ -171,7 +171,7 @@
<div class="monitor relative grid w-full grid-cols-12 gap-2 pb-2 md:w-[655px]">
<div class="col-span-12 md:w-[546px]">
<div class="pt-0">
<div class=" scroll-m-20 pr-5 text-xl font-medium tracking-tight">
<div class="scroll-m-20 pr-5 text-xl font-medium tracking-tight">
{#if monitor.image}
<img
src={monitor.image}
@@ -464,7 +464,7 @@
{#each Object.entries(_0Day) as [ts, bar]}
<div
data-index={bar.index}
class="h-[10px] bg-{bar.cssClass} today-sq m-[1px] w-[10px]"
class="bg-{bar.cssClass} today-sq m-[1px] h-[10px] w-[10px]"
></div>
<div class="hiddenx relative">
<div
+1 -14
View File
@@ -33,20 +33,7 @@ if (process.env.API_IP === undefined) {
} else {
console.log(`✅ API_IP is set`);
}
if (process.env.MONITOR_YAML_PATH === undefined) {
console.log(
`❗ MONITOR_YAML_PATH is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up. Defaulting to config/monitors.yaml`
);
} else {
console.log(`✅ MONITOR_YAML_PATH is set`);
}
if (process.env.SITE_YAML_PATH === undefined) {
console.log(
`❗ SITE_YAML_PATH is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up. Defaulting to config/site.yaml`
);
} else {
console.log(`✅ SITE_YAML_PATH is set`);
}
if (process.env.PORT === undefined) {
console.log(`❗ PORT is not set. Defaulting to 3000`);
} else {
+1 -7
View File
@@ -16,13 +16,7 @@ import { API_TIMEOUT } from "$lib/server/constants.js";
import siteDataRaw from "$lib/server/config/site.yaml?raw";
import monitorDataRaw from "$lib/server/config/monitors.yaml?raw";
import {
IsValidURL,
IsValidHTTPMethod,
LoadMonitorsPath,
LoadSitePath,
ValidateIpAddress
} from "$lib/server/tool.js";
import { IsValidURL, IsValidHTTPMethod, ValidateIpAddress } from "$lib/server/tool.js";
import { GetAllGHLabels, CreateGHLabel } from "$lib/server/github.js";
import { Minuter } from "$lib/server/cron-minute.js";
import axios from "axios";
-33
View File
@@ -17,38 +17,7 @@ function generateRandomColor() {
return randomColor;
//random color will be freshly served
}
const LoadMonitorsPath = function () {
const argv = process.argv;
if (!!process.env.MONITOR_YAML_PATH) {
return process.env.MONITOR_YAML_PATH;
}
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--monitors") {
return argv[i + 1];
}
}
return MONITOR;
};
const LoadSitePath = function () {
const argv = process.argv;
if (!!process.env.SITE_YAML_PATH) {
return process.env.SITE_YAML_PATH;
}
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--site") {
return argv[i + 1];
}
}
return SITE;
};
//return given timestamp in UTC
const GetNowTimestampUTC = function () {
//use js date instead of moment
@@ -168,8 +137,6 @@ const ValidateIpAddress = function (input) {
export {
IsValidURL,
IsValidHTTPMethod,
LoadMonitorsPath,
LoadSitePath,
GetMinuteStartTimestampUTC,
GetNowTimestampUTC,
GetDayStartTimestampUTC,
+52
View File
@@ -0,0 +1,52 @@
// @ts-nocheck
import fs from "fs-extra";
import path from "path";
import { marked } from "marked";
function extractMetadataAndContent(fileContent) {
// Regular expression to match metadata section
const metadataRegex = /^---\s*([\s\S]+?)\s*---\s*/;
const match = fileContent.match(metadataRegex);
const result = {
metadata: {},
content: fileContent // Default to entire content if no metadata is found
};
if (match) {
// Extract metadata section and remaining content
const metadataLines = match[1].split("\n").filter((line) => line.trim() !== "");
result.content = fileContent.slice(match[0].length).trim(); // Remaining content after metadata
metadataLines.forEach((line) => {
const [key, value] = line.split(":").map((part) => part.trim());
if (key && value) {
result.metadata[key.toLowerCase()] = value;
}
});
}
return result;
}
export async function load({ params, route, url, cookies, request }) {
const docFile = params.doc;
const docFolderPath = path.resolve("docs");
let docFilePath = docFolderPath + "/home.md";
if (docFile) {
docFilePath = docFolderPath + `/${docFile}.md`;
}
const fileContents = await fs.readFileSync(docFilePath, "utf-8");
const siteStructure = await fs.readFileSync(docFolderPath + "/structure.json", "utf-8");
const { metadata, content } = extractMetadataAndContent(fileContents);
const selectedDoc = docFilePath.replace(docFolderPath, "");
const siteStructureJSON = JSON.parse(siteStructure);
return {
md: marked.parse(content),
title: metadata.title || "Kener Docs",
description: metadata.description || "Kener Docs",
siteStructure: siteStructureJSON
};
}
+177
View File
@@ -0,0 +1,177 @@
<script>
import "../../app.postcss";
import "../../kener.css";
import { Button } from "$lib/components/ui/button";
import Sun from "lucide-svelte/icons/sun";
import Moon from "lucide-svelte/icons/moon";
import { onMount } from "svelte";
import { base } from "$app/paths";
let defaultTheme = "light";
export let data;
let siteStructure = data.siteStructure;
let sidebar = siteStructure.sidebar;
let docFilePath = data.docFilePath;
let tableOfContents = [];
function toggleMode(defaultTheme) {
let classList = document.documentElement.classList;
if (classList.contains("dark")) {
classList.remove("dark");
localStorage.setItem("theme", "light");
} else {
classList.add("dark");
localStorage.setItem("theme", "dark");
}
}
function setTheme() {
let theme = localStorage.getItem("theme") || defaultTheme;
if (theme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
function activateSidebar(selectedDoc) {
for (let i = 0; i < sidebar.length; i++) {
const item = sidebar[i];
for (let j = 0; j < item.children.length; j++) {
const subItem = item.children[j];
if (subItem.file == selectedDoc) {
subItem.active = true;
sidebar[i].children[j].active = true;
} else {
subItem.active = false;
sidebar[i].children[j].active = false;
}
}
}
}
function pageChange(e) {
if (e.detail.docFilePath) {
activateSidebar(e.detail.docFilePath);
}
}
function updateTableOfContents(e) {
if (e.detail.rightbar) {
tableOfContents = e.detail.rightbar;
}
}
onMount(() => {
setTheme();
});
</script>
<svelte:window on:pagechange={pageChange} on:rightbar={updateTableOfContents} />
<svelte:head>
<link rel="icon" id="kener-app-favicon" href="{base}/logo96.png" />
</svelte:head>
<nav class="fixed left-0 right-0 top-0 z-30 h-16 border-b bg-background">
<div class="mx-auto h-full px-4 sm:px-6 lg:px-8">
<div class="flex h-full items-center justify-between">
<!-- Logo/Brand -->
<div class="flex items-center space-x-3">
<a href="/" class="flex items-center space-x-3">
<!-- 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-semibold">Docs</span>
</a>
</div>
<!-- Navigation Links -->
<div class="hidden md:block">
<div class="flex items-center space-x-8">
<a href="/docs" class="text-sm font-medium">Documentation</a>
<a href="/api" class="text-sm font-medium">API</a>
<a href="/examples" class="text-sm font-medium">Examples</a>
<a href="/support" class="text-sm font-medium">Support</a>
</div>
</div>
<!-- Mobile Menu Button -->
<div class="md:hidden">
<button type="button" class="hover: text-muted-foreground">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
</div>
</div>
</nav>
<!-- Sidebar -->
<aside class="fixed bottom-0 left-0 top-16 w-72 overflow-y-auto border-r">
<nav class="p-6">
<!-- Getting Started Section -->
{#each sidebar as item}
<div class="mb-4">
<h3
class="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground"
>
{item.sectionTitle}
</h3>
<div class="">
{#each item.children as child}
<a
href={child.link}
class="group flex items-center rounded-md px-3 py-2 text-sm font-medium hover:underline {!!child.active
? 'bg-muted'
: ''}"
>
{child.title}
</a>
{/each}
</div>
</div>
{/each}
</nav>
</aside>
<!-- Main Content -->
<main class="ml-72 min-h-screen pt-16">
<div class="mx-auto max-w-5xl px-4 py-10 sm:px-6 lg:px-8 lg:pr-64">
<!-- Content Header -->
<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 dark:prose-pre:bg-neutral-900"
>
<slot />
</div>
</div>
</main>
{#if tableOfContents.length > 0}
<div class="fixed bottom-0 right-0 top-16 hidden w-64 overflow-y-auto px-6 py-10 lg:block">
<h4 class="mb-3 text-sm font-semibold uppercase tracking-wider">On this page</h4>
<nav class="space-y-2">
{#each tableOfContents as item}
<a
href="#{item.id}"
class="block overflow-hidden text-ellipsis whitespace-nowrap text-xs text-muted-foreground hover:underline {item.level ==
3
? 'ml-4'
: ''}"
>
{item.text}
</a>
{/each}
</nav>
</div>
{/if}
<div class="fixed bottom-4 right-4">
<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>
@@ -0,0 +1,16 @@
// @ts-nocheck
import path from "path";
export async function load({ params, route, url, cookies, request }) {
const docFile = params.doc;
const docFolderPath = path.resolve("docs");
let docFilePath = docFolderPath + "/home.md";
if (docFile) {
docFilePath = docFolderPath + `/${docFile}.md`;
}
return {
docFilePath: docFilePath.replace(docFolderPath, "")
};
}
+56
View File
@@ -0,0 +1,56 @@
<script>
import { onMount } from "svelte";
import { afterNavigate } from "$app/navigation";
export let data;
let subHeadings = [];
function fillSubHeadings() {
subHeadings = [];
const headings = document.querySelectorAll("#markdown h2, #markdown h3");
headings.forEach((heading) => {
const id = heading.textContent.replace(/[^a-z0-9]/gi, "-").toLowerCase();
heading.id = id;
subHeadings.push({
id,
text: heading.textContent,
level: heading.tagName === "H2" ? 2 : 3
});
});
}
afterNavigate(() => {
document.dispatchEvent(
new CustomEvent("pagechange", {
bubbles: true,
detail: {
docFilePath: data.docFilePath
}
})
);
fillSubHeadings();
document.dispatchEvent(
new CustomEvent("rightbar", {
bubbles: true,
detail: {
rightbar: subHeadings
}
})
);
});
onMount(async () => {});
</script>
<svelte:head>
<title>{data.title}</title>
<meta name="description" content={data.description} />
</svelte:head>
<div id="markdown">
{@html data.md}
</div>
<style>
#markdown {
scroll-behavior: smooth;
}
</style>
@@ -1,6 +1,6 @@
<script>
import "../app.postcss";
import "../kener.css";
import "../../app.postcss";
import "../../kener.css";
import Nav from "$lib/components/nav.svelte";
import { onMount } from "svelte";
import { base } from "$app/paths";
@@ -67,7 +67,11 @@
<svelte:head>
<title>{data.site.title}</title>
<link rel="icon" id="kener-app-favicon" href="{base}/logo96.png" />
{#if data.site.favicon && data.site.favicon[0] == "/"}
<link rel="icon" id="kener-app-favicon" href="{base}{data.site.favicon}" />
{:else if data.site.favicon}
<link rel="icon" id="kener-app-favicon" href={data.site.favicon} />
{/if}
<link href={data.site.font.cssSrc} rel="stylesheet" />
{#each Object.entries(data.site.metaTags) as [key, value]}
<meta name={key} content={value} />
@@ -35,19 +35,19 @@
{/if}
{#if hasActiveIncidents}
<section
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
class="mx-auto mb-4 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<div class="grid w-full grid-cols-2 gap-4">
<div class="col-span-2 text-center md:col-span-1 md:text-left">
<Badge variant="outline">
<Badge variant="outline" class="border-0 pl-0">
{l(data.lang, "root.ongoing_incidents")}
</Badge>
</div>
</div>
</section>
<section
class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
id=""
>
{#each data.openIncidents as incident, i}
@@ -20,7 +20,7 @@
<div class="mt-32"></div>
{#if hasActiveIncidents}
<section
class="mx-auto mb-4 flex w-full max-w-[840px] flex-1 flex-col items-start justify-center bg-transparent"
class="mx-auto mb-4 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<div class="grid w-full grid-cols-2 gap-4">
@@ -32,7 +32,7 @@
</div>
</section>
<section
class="mx-auto mb-8 flex w-full max-w-[840px] flex-1 flex-col items-start justify-center"
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center"
id=""
>
{#each data.openIncidents as incident, i}
@@ -48,12 +48,12 @@
{/if}
{#if data.monitors.length > 0}
<section
class="mx-auto mb-4 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<div class="grid w-full grid-cols-1 justify-end gap-4">
<div class="col-span-1 text-center md:col-span-1 md:text-right">
<Badge variant="outline">
<Badge variant="outline" class="border-0 pr-0">
<span class="bg-api-up mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="mr-3">
@@ -90,7 +90,7 @@
</section>
{:else}
<section
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
class="mx-auto mb-4 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<Card.Root class="mx-auto">
-9
View File
@@ -1,9 +0,0 @@
import axios from "axios";
export async function load({ params, route, url, parent }) {
const { data } = await axios.get(
"https://raw.githubusercontent.com/rajnandan1/kener/main/docs.md"
);
return {
md: data
};
}
-122
View File
@@ -1,122 +0,0 @@
<script>
import { marked } from "marked";
import { onMount } from "svelte";
import * as Card from "$lib/components/ui/card";
import * as Accordion from "$lib/components/ui/accordion";
export let data;
let html = marked.parse(data.md);
let sideBar = [];
function locationHashChanged() {
const allSideAs = document.querySelectorAll(".sidebar-a");
allSideAs.forEach((sideA) => {
sideA.classList.remove("active");
});
document.querySelector(`[href="${window.location.hash}"]`).classList.add("active");
}
onMount(async () => {
const headings = document.querySelectorAll("#markdown h1");
headings.forEach((heading) => {
const id = "h1" + heading.textContent.replace(/[^a-z0-9]/gi, "-").toLowerCase();
heading.id = id;
heading.setAttribute("sider", "sidemenu");
heading.setAttribute("sider-t", "h1");
});
const headings2 = document.querySelectorAll("#markdown h2");
headings2.forEach((heading) => {
const id = "h2" + heading.textContent.replace(/[^a-z0-9]/gi, "-").toLowerCase();
heading.id = id;
heading.setAttribute("sider", "sidemenu");
heading.setAttribute("sider-t", "h2");
});
const sidemenuHeadings = document.querySelectorAll("#markdown [sider='sidemenu']");
//iterate over all headings and create nexted sidebar, if h2 the add to last h1
let lastH1 = null;
let lastH2 = null;
sidemenuHeadings.forEach((heading) => {
if (heading.getAttribute("sider-t") == "h1") {
lastH1 = {
id: heading.id,
text: heading.textContent,
type: heading.getAttribute("sider-t"),
children: []
};
sideBar = [...sideBar, lastH1];
} else if (heading.getAttribute("sider-t") == "h2") {
lastH2 = {
id: heading.id,
text: heading.textContent,
type: heading.getAttribute("sider-t")
};
lastH1.children = [...lastH1.children, lastH2];
}
});
window.onhashchange = locationHashChanged;
});
</script>
<svelte:head>
<title>Kener Documentation</title>
</svelte:head>
<section class="mx-auto mt-8 scroll-smooth rounded-3xl md:container">
<Card.Root>
<Card.Content class="px-1">
<div class="grid grid-cols-5 gap-4">
<div
class="sticky top-0 col-span-5 hidden max-h-screen overflow-y-auto border-r-2 border-secondary pr-1 pt-2 md:col-span-1 md:block"
>
{#each sideBar as item}
<Accordion.Root>
<Accordion.Item value={item.id}>
<Accordion.Trigger class="pl-2 pr-3 text-sm font-semibold">
<a href="#{item.id}" class="sidebar-a">{item.text}</a>
</Accordion.Trigger>
<Accordion.Content>
<!-- <ul class="w-full text-sm font-medium sticky top-0 overflow-y-auto max-h-screen"> -->
<ul class=" pl-6 text-sm font-medium">
{#each item.children as child}
<li class="w-full py-2">
<a href="#{child.id}" class="sidebar-a"
>{child.text}</a
>
</li>
{/each}
</ul>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
{/each}
</div>
<div class="col-span-5 md:col-span-4">
<div class="p-0 pt-6 md:p-10">
<article
id="markdown"
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 dark:prose-pre:bg-neutral-900"
>
{@html html}
</article>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
</section>
<style>
.h1.inactive ~ .h2 {
display: none;
}
#markdown code:not([class^="language-"]) {
background-color: #faf6b2;
border-radius: 4px;
padding: 2px 4px;
font-size: 0.833em;
color: #000;
}
.sidebar-a.active {
text-decoration: underline;
}
</style>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB