Compare commits

..

73 Commits

Author SHA1 Message Date
Raj Nandan Sharma 974826f42d Merge pull request #261 from rajnandan1/release/3.0.11
fix: handle ping migration
2025-02-09 19:49:46 +05:30
Raj Nandan Sharma 4914b029f9 fix: handle ping migration 2025-02-09 19:38:16 +05:30
Raj Nandan Sharma 4e0c6e85da style: added more readme badges 2025-02-09 09:31:01 +05:30
Raj Nandan Sharma 1147808366 Merge pull request #256 from kaffolder7/feature/small-improvements
feat: small improvements
2025-02-09 09:04:13 +05:30
Kyle Affolder 089ee9bc07 update(formatting): made consistent
Aligned `.prettierrc` and `.editorconfig` files to same, best-practice & most-widely-adopted standards.
2025-02-08 13:39:41 -05:00
Raj Nandan Sharma 786c4f8207 fix: fixed cookie getting set in different paths from client side when language was changed^C 2025-02-08 21:37:18 +05:30
Raj Nandan Sharma 13879aefdd Merge pull request #253 from VincentDatrier/main
Add Norwegian (Bokmål) as an available locale
2025-02-08 17:01:07 +05:30
Kyle Affolder 798af326a2 fix: update docs link to online documentation
Default documentation link in main nav won’t work because /docs are not included in built Docker images (to keep image smaller). Instead, changing seed data to point to the docs homepage. :)
2025-02-08 00:13:49 -05:00
Kyle Affolder 8370145f6c update: change fonts API
Switching from Google Fonts to Bunny Fonts CDN. Bunny Fonts is an open-source, privacy-first web font platform. It is fully GDPR compliant (Google is not) and can act as a drop-in replacement for Google Fonts.
2025-02-07 20:14:45 -05:00
Kyle 304945acea Merge branch 'rajnandan1:main' into feature/small-improvements 2025-02-07 19:51:50 -05:00
Vincent Datrier 22ef4d75b6 Trigger GitHub Actions 2025-02-07 14:11:39 +01:00
Vincent Datrier 534e6a0ad3 Updated nb-NO locale with new strings 2025-02-07 13:46:32 +01:00
Vincent Datrier 80ed4fc5fa Merge branch 'main' of github.com:VincentDatrier/kener 2025-02-07 13:42:06 +01:00
Raj Nandan Sharma 445eb02386 chore: optimized seo 2025-02-07 10:24:06 +05:30
Raj Nandan Sharma 53ae0b89b0 chore: optimized seo 2025-02-07 10:16:49 +05:30
Raj Nandan Sharma c7376b4d8f Merge pull request #250 from rajnandan1/release/3.0.10
Release/3.0.10
2025-02-07 09:13:20 +05:30
Raj Nandan Sharma e3e59b7e24 feat: adding tcp monitor, reinstating ping as reported in #243 2025-02-07 09:12:15 +05:30
Raj Nandan Sharma 07f59ac581 feat: adding tcp monitor, reinstating ping as reported in #243 2025-02-07 09:02:28 +05:30
Kyle Affolder 0f0b447137 remove: forks badge from README 2025-02-05 20:47:51 -05:00
Kyle Affolder f7cc28c896 add: badges to README 2025-02-05 20:45:10 -05:00
Kyle Affolder fd790003d1 add: editorconfig to enforce consistent styles 2025-02-05 18:40:10 -05:00
Raj Nandan Sharma 103d64a659 fix: fix bug where incident status is not getting updated when adding comment #245 2025-02-05 22:43:33 +05:30
Raj Nandan Sharma 92c4d35992 fix: fix bug where incident status is not getting updated #246 2025-02-05 22:42:29 +05:30
Raj Nandan Sharma 3b1d95b71b Merge pull request #241 from kaffolder7/feature/suppress-warnings-production-build
Update: Suppress warnings in production build
2025-02-05 09:46:20 +05:30
Raj Nandan Sharma 4fd9bf2bb6 Merge pull request #234 from rajnandan1/release/3.0.9
feat: support SMTP for email trigger
2025-02-05 09:17:39 +05:30
Raj Nandan Sharma ce96b6f55d docs: update i18n 2025-02-05 09:17:11 +05:30
Raj Nandan Sharma 54bbc1dd00 chore: update i18n 2025-02-05 08:59:07 +05:30
Raj Nandan Sharma ffa31bcacc docs: updated roadmap doc 2025-02-05 08:09:49 +05:30
Kyle Affolder 559f5bd257 Update: Suppress warnings in production build
When building for production, various warnings are output which slows down production build.

The following changes were made:
- Suppress unused export properties (unused-export-let).
- Suppress conflicting Svelte resolve warnings (conflicting-svelte-resolve).
- Suppress empty chunk warnings (empty-chunk).
- Suppress unused module imports (module-unused-import).
- Keep other important warnings visible, so we’re still aware of potential issues.

Now, production build should be cleaner and faster! 🚀
2025-02-04 16:03:10 -05:00
Raj Nandan Sharma 977e49e1ce feat: seo fixes 2025-02-04 23:18:04 +05:30
Raj Nandan Sharma 30cb707436 feat: added category filter for view monitor #239 2025-02-04 21:37:00 +05:30
Raj Nandan Sharma ae439633b9 feat: added category filter for view monitor #239 2025-02-04 21:34:47 +05:30
Vincent Datrier 7f33f6ddfd Add Norwegian (Bokmål) as an available locale 2025-02-04 14:48:25 +01:00
Raj Nandan Sharma 8404415a93 feat: eval in ping #236 and port number in ping #211 2025-02-04 10:34:00 +05:30
Raj Nandan Sharma 5ddddf8b5d fix: bugs and documentation update as mentioned in #237 2025-02-03 21:33:07 +05:30
Raj Nandan Sharma 927db19cc6 feat: smtp autofill and docs update 2025-02-03 11:46:25 +05:30
Raj Nandan Sharma eccff16c5f feat: introducing event type maintenance as asked in #224 2025-02-02 23:03:58 +05:30
Raj Nandan Sharma 7be9c62c7c fix: allow longer TLD as reported in #235 2025-02-01 20:48:42 +05:30
Raj Nandan Sharma 76ce14e8b1 feat: support SMTP for email trigger 2025-02-01 16:11:27 +05:30
Raj Nandan Sharma fd0074c0b0 Merge pull request #233 from rajnandan1/release/3.0.8
fix: discord trigger fix #232
2025-01-31 20:22:43 +05:30
Raj Nandan Sharma ad01cdb5ac fix: discord trigger fix #232 2025-01-31 20:19:27 +05:30
Raj Nandan Sharma 1b4ca67cf0 Merge pull request #231 from rajnandan1/release/3.0.7
feat(triggers): support custom webhook body as requested in #230
2025-01-31 09:10:18 +05:30
Raj Nandan Sharma 53936e19d4 feat(triggers): support custom webhook body as requested in #230 2025-01-31 09:08:26 +05:30
Raj Nandan Sharma b58d00c5a6 feat(triggers): support custom webhook body as requested in #230 2025-01-31 09:03:57 +05:30
Raj Nandan Sharma 41db0c45cd Merge pull request #229 from YunusEmreAlps/feature/locales-tr
feat: add Turkish locale support and update locale files
2025-01-30 19:43:55 +05:30
Yunus Emre Alpu 5361b551eb feat: add Turkish locale support and update locale files 2025-01-30 13:05:08 +03:00
Raj Nandan Sharma eb87647466 Merge pull request #228 from rajnandan1/release/3.0.6
feat: support of promises in eval
2025-01-29 10:09:35 +05:30
Raj Nandan Sharma d9b900f28d feat: support of promises in eval 2025-01-28 23:37:26 +05:30
Raj Nandan Sharma 198bd6c723 Merge pull request #227 from matribeiro15/main
Update pt-BR.json
2025-01-28 22:44:26 +05:30
Mateus Ribeiro 1831e6309f Update pt-BR.json 2025-01-28 12:32:22 -03:00
Raj Nandan Sharma 007f5149d2 Update README.md 2025-01-27 23:07:06 +05:30
Raj Nandan Sharma 1a094e3511 Merge pull request #226 from rajnandan1/release/3.0.5
fix: locale for no incidents as reported in #225
2025-01-27 22:39:19 +05:30
Raj Nandan Sharma db1ddc1292 fix: locale for no incidents as reported in #225 2025-01-27 22:37:54 +05:30
Raj Nandan Sharma 535e3bb36a fix: hi.json fixed 2025-01-26 16:12:08 +05:30
Raj Nandan Sharma 785237e55e Merge pull request #223 from cosmic-jellyfish/feature/Further-Locale-Fixes
fix: localisation strings for further clarity
2025-01-26 16:08:04 +05:30
Raj Nandan Sharma e6bf47a859 docs: deployment docs 2025-01-26 15:13:46 +05:30
Raj Nandan Sharma 93ae1711f1 docs: deployment docs 2025-01-26 15:04:57 +05:30
Raj Nandan Sharma af97812beb docs: railway deploy button added 2025-01-26 13:12:38 +05:30
Cake 78cf22ade9 fix: correct spacing 2025-01-26 08:09:48 +11:00
Cake b4a461d93a fix: localisation strings for further clarity 2025-01-26 08:04:37 +11:00
Raj Nandan Sharma 5f33c02064 Merge pull request #222 from rajnandan1/release/3.0.4
Release/3.0.4
2025-01-25 21:43:05 +05:30
Raj Nandan Sharma 46c1a392b8 refactor: remove docs from docker build 2025-01-25 21:41:11 +05:30
Raj Nandan Sharma c6880c5df6 feat: sitemap and bug fixes 2025-01-25 21:18:42 +05:30
Raj Nandan Sharma 646da94ef9 feat: sitemap and bug fixes 2025-01-25 21:16:17 +05:30
Raj Nandan Sharma 36ede93dce feat: sitemap and bug fixes 2025-01-25 21:12:41 +05:30
Raj Nandan Sharma 9ed35589f7 Merge pull request #220 from rajnandan1/release/3.0.3
fix: load time #219
2025-01-24 21:45:33 +05:30
Raj Nandan Sharma e946dd18e0 fix: load time #219 2025-01-24 21:44:36 +05:30
Raj Nandan Sharma 51cad598c1 fix: load time #219 2025-01-24 21:42:57 +05:30
Raj Nandan Sharma 0595f23c18 Merge pull request #218 from cosmic-jellyfish/feature/ENG-Locale-Fix
English Locale tweaks
2025-01-24 13:04:58 +05:30
Raj Nandan Sharma b88816706b fix: retry await decrease from 4000 to 500ms 2025-01-24 11:42:06 +05:30
Cake 80edaae023 fix: update localisation strings across the board 2025-01-24 17:08:09 +11:00
Cake 468d7f445d fix: update localisation strings further 2025-01-24 15:15:13 +11:00
Cake 4cc74deece fix: update localisation strings 2025-01-24 15:11:50 +11:00
85 changed files with 4501 additions and 2669 deletions
+52
View File
@@ -0,0 +1,52 @@
# EditorConfig helps maintain consistent coding styles between editors
root = true
# Default settings for all files (e.g. most common, best-practice standard across all filetypes)
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
# Svelte files
[*.svelte]
indent_style = space
indent_size = 2
trim_trailing_whitespace = false
# JavaScript and TypeScript
[*.{js,ts,tsx,cjs,mjs}]
indent_style = space
indent_size = 2
# JSON files (package.json, config files, etc.) - per JSON (RFC 8259) specification
[*.json,.prettierrc]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = false
# YAML files (e.g., GitHub Actions, Lint configs) - per YAML 1.2 (2009) specification
[*.{yaml,yml}]
indent_style = space
indent_size = 2
# Markdown files
[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false
# Dockerfile
[Dockerfile*]
indent_style = tab
indent_size = 4
insert_final_newline = false
# Ignore binary files
[*.{png,jpg,jpeg,gif,ico,svg,woff,woff2,eot,ttf,otf}]
charset = unset
trim_trailing_whitespace = false
insert_final_newline = false
+6 -11
View File
@@ -17,22 +17,17 @@ Steps to reproduce the behavior:
3. Scroll down to '....'
4. See error
**Version**
Which version of kener you are using.
**Environment**
Which environment you are using or where is it deployed. `docker`, `kubernetes`, `bare-metal`, `development`, `pm2` etc
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+2 -1
View File
@@ -26,4 +26,5 @@ uploads/*
static/uploads/*
!static/uploads/upload.dir
temp.txt
temp.txt
temp.js
+69 -7
View File
@@ -1,9 +1,71 @@
{
"useTabs": true,
"semi": true,
"tabWidth": 4,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"useTabs": false,
"semi": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte",
"useTabs": false,
"semi": false,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 100
}
},
{
"files": ["*.js", "*.ts", "*.tsx", "*.cjs", "*.mjs"],
"options": {
"useTabs": false,
"semi": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}
},
{
"files": ["*.json", ".prettierrc"],
"options": {
"useTabs": false,
"semi": false,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 100
}
},
{
"files": ["*.yaml", "*.yml"],
"options": {
"useTabs": false,
"semi": false,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 80
}
},
{
"files": "*.md",
"options": {
"useTabs": false,
"semi": false,
"tabWidth": 4,
"trailingComma": "none",
"printWidth": 100
}
},
{
"files": "Dockerfile",
"options": {
"useTabs": true,
"tabWidth": 4,
"semi": false,
"trailingComma": "none",
"printWidth": 120
}
}
]
}
+3
View File
@@ -30,6 +30,9 @@ RUN npm install && npm cache clean --force
# Copy the rest of the application code
COPY . .
# remove dir src/routes/(docs)
RUN rm -rf src/routes/\(docs\)
# Ensure /app/uploads and /app/database have rw permissions
RUN mkdir -p /app/uploads /app/database && \
chmod -R 777 /app/uploads /app/database
+42 -10
View File
@@ -1,4 +1,4 @@
# Kener - A Beautiful Status Page System
# Kener - Stunning Status Pages
<p align="center">
<img src="https://kener.ing/newbg.png?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
@@ -8,8 +8,21 @@
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/rajnandan1/kener?label=Star%20Repo&style=social">
<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>
<a href="https://awesome-selfhosted.net/tags/status--uptime-pages.html#kener"><img src="https://awesome.re/mentioned-badge.svg" alt="Awesome self hosted" /></a>
</p>
<p align="center">
<a href="https://github.com/rajnandan1/kener/actions/workflows/publishImage.yml"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/rajnandan1/kener/publishImage.yml" /></a>
<a href="https://github.com/rajnandan1/kener/commit/HEAD"><img src="https://img.shields.io/github/last-commit/rajnandan1/kener/main" alt="" /></a>
<a href="https://github.com/rajnandan1/kener/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/rajnandan1/kener.svg" /></a>
</p>
<p align="center">
<a href="https://www.producthunt.com/posts/kener-2" target="_blank">
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=kener-2&theme=light" alt="Kener on Product Hunt">
</a>
</p>
<p align="center">
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f514/512.webp" type="image/webp">
@@ -25,11 +38,8 @@
</picture>
</p>
#### [👉 Visit a live server](https://kener.ing)
#### [👉 Quick Start](https://kener.ing/docs/quick-start)
#### [👉 Documentation](https://kener.ing/docs/home)
| [🌍 Live Server](https://kener.ing) | [🎉 Quick Start](https://kener.ing/docs/quick-start) | [🗄 Documentation](https://kener.ing/docs/home) |
| ----------------------------------- | ---------------------------------------------------- | ----------------------------------------------- |
## What is Kener?
@@ -41,10 +51,28 @@ It comes with all the basic asks for a status page. It is open-source and free t
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a cheaply available domain.
<div align="left">
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
</div>
## Installation
### Manual
```shell
# Clone the repository
git clone https://github.com/rajnandan1/kener.git
cd kener
npm install
cp .env.example .env
npm run dev
```
### Docker
The latest image is available on DockerHub at [`rajnandan1/kener:latest`](https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest).
Download and use the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
### One Click
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
## Features
Here are some of the features that you get out of the box. Please read the documentation to know how to use them.
@@ -88,6 +116,10 @@ Here are some of the features that you get out of the box. Please read the docum
- Auto SEO and Social Media ready
- Server Side Rendering
<div align="left">
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
</div>
## Technologies used
- [SvelteKit](https://kit.svelte.dev/)
+41
View File
@@ -5,6 +5,47 @@ description: Changelogs for Kener
# Changelogs
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
## v3.0.10
<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
- Added TCP monitors
### Breaking Changes
- Ping monitors will break.
### Fixes
- Bug fixes in the UI
## v3.0.9
<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
- Support of SMTP for email notifications. Read more [here](/docs/triggers/#email-smtp)
- Introduction of event type `MAINTENANCE` for incidents.
- You can write eval function for ping now in monitors. Read more [here](/docs/monitors-ping/#eval)
- Added category filter for monitor management.
### Fixes
- Support longer TLD in `siteURL` example `https://example.network`
- Remove googleapis preconnect and preload
- Fixed wrong action url in webhook.
## v3.0.1
<picture>
+27
View File
@@ -0,0 +1,27 @@
---
title: How to Add Custom Fonts | Kener
description: Kener allows you to add custom fonts to your site. This guide will help you add custom fonts to your Kener site.
---
# Add Custom Fonts
1. Login to your Kener dashboard.
2. Go to the `Theme` page.
3. Scroll down to the `Fonts` section.
4. Remove `Font URL` and `Font Name`
5. In Custom CSS add your font
```css
@font-face {
font-family: "CustomFont";
src:
url("/path-to-font/CustomFont.woff2") format("woff2"),
url("/path-to-font/CustomFont.woff") format("woff"),
url("/path-to-font/CustomFont.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
* {
font-family: "CustomFont", sans-serif;
}
```
+3 -3
View File
@@ -50,7 +50,7 @@ i18n:
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"
cssSrc: "https://fonts.bunny.net/css?family=lato:100,100i,300,300i,400,400i,700,700i,900,900i&display=swap"
family: '"Lato", sans-serif'
analytics:
- id: "G-QsFT"
@@ -315,11 +315,11 @@ You can set the background pattern of the site. It can be `squares`, `dots`, `no
## font
You can set the font of the site. You can use google fonts or any other font.
You can set the font of the site. You can use google fonts, [bunny fonts](https://fonts.bunny.net/) (which is a fully GDPR-compliant, zero-tracking/no-logging CDN font service), 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"
cssSrc: "https://fonts.bunny.net/css?family=lato:100,100i,300,300i,400,400i,700,700i,900,900i&display=swap"
family: '"Lato", sans-serif'
```
+6
View File
@@ -134,3 +134,9 @@ docker build --build-arg KENER_BASE_PATH=/status -t kener .
```bash
docker run -p 3000:3000 --env-file .env -v $(pwd)/database:/app/uploads -v $(pwd)/database:/app/database kener
```
## Railway
You can deploy Kener on [Railway](https://railway.app) with a single click.
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
+18 -9
View File
@@ -33,14 +33,6 @@ Defaults to 3000 if not specified
export PORT=4242
```
## GH_TOKEN
A github token to read issues and create labels. This is required for **incident management**
```bash
export GH_TOKEN=your-github-token
```
## KENER_BASE_PATH
By default kener runs on `/` but you can change it to `/status` or any other path.
@@ -69,7 +61,7 @@ export RESEND_SENDER_EMAIL=Some Name <email@domain.com>
```
<div class=" note danger ">
Please note that the RESEND_API_KEY is required for sending emails. If you do not set this, Kener will not be able to send emails. RESEND_SENDER_EMAIL is a must if you forget your password.
Please note that the RESEND_API_KEY is required for sending emails. If you do not set this, Kener will not be able to send emails. RESEND_SENDER_EMAIL is a must if you forget your password. If you do not want to use resend, you can set SMTP variables.
</div>
## DATABASE_URL
@@ -88,6 +80,23 @@ Set the timezone for the server. Set it to UTC.
export TZ=UTC
```
## SMTP
Kener can also use SMTP to send emails. You can set the SMTP server details in the environment variables.
```bash
export SMTP_HOST=smtp.example.com
export SMTP_PORT=587
export SMTP_USER=username
export SMTP_PASS=password
export SMTP_SECURE=0
export SMTP_FROM_EMAIL=Some Name <user@example.com>
```
<div class=" note danger ">
If you set SMTP variables, Kener will use SMTP to send emails. If you set RESEND_API_KEY, Kener will use resend to send emails. If you do both Kener will use SMTP.
</div>
## Using .env
You can also use a `.env` file to set these variables. Create a `.env` file in the root of the project and add the variables like below
+9 -3
View File
@@ -1,6 +1,6 @@
---
title: Kener - A Sveltekit NodeJS Status Page System
description: Kener is an open-source Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents.
title: Kener Documentation
description: Kener is a feature-rich and modern status page system built with SvelteKit and NodeJS. It is open-source and free to use.
---
# Kener - A Feature Rich & Modern Status Page
@@ -59,13 +59,19 @@ Kener is status page system built with Sveltekit and NodeJS. It does not try to
It comes with all the basic asks for a status page. It is open-source and free to use.
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a cheaply available domain.
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a
cheaply available domain.
## Quick Deployment
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
## Features
### 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
+23 -17
View File
@@ -28,6 +28,7 @@ Copy and update the translation file in the `locales` folder. The translation fi
```json
{
"%status for %duration": "%status for %duration",
"14 Days": "14 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
@@ -35,52 +36,57 @@ Copy and update the translation file in the `locales` folder. The translation fi
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"Back": "Back",
"Badge": "Badge",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Code Copied": "Code Copied",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"Dark": "Dark",
"Days": "Days",
"DEGRADED": "DEGRADED",
"DOWN": "DOWN",
"Embed": "Embed",
"Dark": "Dark",
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
"Incident Updates": "Incident Updates",
"INVESTIGATING": "INVESTIGATING",
"Incident Updates": "Incident Updates",
"LIVE Status": "LIVE Status",
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"LIVE Status": "LIVE Status",
"Mode": "Mode",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
"No Incidents": "No Incidents",
"No Incident On %date": "No Incident On %date",
"No Monitor Found": "No Monitor Found",
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"Recent Incidents": "Recent Incidents",
"RESOLVED": "RESOLVED",
"Share": "Share",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Standard": "Standard",
"Started about %startedAt ago, lasted for about %lastedFor": "Started about %startedAt ago, lasted for about %lastedFor",
"Started about %startedAt ago, still ongoing": "Started about %startedAt ago, still ongoing",
"Starts in %startedAt": "Starts in %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Starts in %startedAt, will last for about %lastedFor",
"Status": "Status",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Theme",
"Today": "Today",
"UP": "UP",
"Upcoming Maintenance": "Upcoming Maintenance",
"Updates": "Updates",
"Uptime": "Uptime",
"%status for %duration": "%status for %duration"
"Uptime": "Uptime"
}
```
+4
View File
@@ -74,3 +74,7 @@ To close an incident, you need to add a message and set the status to `RESOLVED`
## Add Monitors
To add monitors to the incident, you need to add the monitor tag to the incident. This will automatically add the monitor to the incident. You will also get the status of the monitor in the incident. This can be `DEGRADED` or `DOWN`.
## Maintenance
You can also create a maintenance incident. This is similar to an incident but is used to notify users about maintenance activities. Both start and end date and time are required for maintenance incidents.
+33 -7
View File
@@ -45,13 +45,13 @@ The headers are used to define the headers that should be sent with the request.
The eval is used to define the JavaScript code that should be used to evaluate the response. It is optional and has be a valid JavaScript code.
This is a anonymous JS function, by default it looks like this.
This is an anonymous JS function, it should return a **Promise**, that resolves or rejects to `{status, latency}`, by default it looks like this.
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and latency(number)
> `{status:"DEGRADED", latency: 200}`.
```javascript
(function (statusCode, responseTime, responseDataBase64) {
(async function (statusCode, responseTime, responseDataBase64) {
let statusCodeShort = Math.floor(statusCode/100);
let status = 'DOWN'
if(statusCodeShort >=2 && statusCodeShort <= 3) {
@@ -74,16 +74,12 @@ let decodedResp = atob(responseDataBase64);
//let jsonResp = JSON.parse(decodedResp)
```
<div class="note danger">
The eval is validated against (200, 1000, "e30=") which translates to (200, 1000, '{}') within the function. So whatever function you write should be able to handle these values otherwise you won't be able to save the monitor.
</div>
#### Example
The following example shows how to use the eval function to evaluate the response. The function checks if the status code is 2XX then the status is UP, if the status code is 5XX then the status is DOWN. If the response contains the word `Unknown Error` then the status is DOWN. If the response time is greater than 2000 then the status is DEGRADED.
```javascript
(function (statusCode, responseTime, responseDataBase64) {
(async function (statusCode, responseTime, responseDataBase64) {
const resp = atob(responseDataBase64); //convert base64 to string
let status = "DOWN";
@@ -110,6 +106,36 @@ The following example shows how to use the eval function to evaluate the respons
});
```
This next example shows how to call another API withing eval. It is scrapping the second last script tag from the response and checking if the heading is "No recent issues" then the status is UP else it is DOWN.
```javascript
(async function raj(statusCode, responseTime, responseDataBase64) {
let htmlString = atob(responseDataBase64);
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g);
if (scriptTags && scriptTags.length >= 2) {
// Extract the second last script tag's src attribute
const secondLastScript = scriptTags[scriptTags.length - 2];
const srcMatch = secondLastScript.match(/src="([^"]+)"/);
const secondLastScriptSrc = srcMatch ? srcMatch[1] : null;
let jsResp = await fetch(secondLastScriptSrc); //api call
let jsRespText = await jsResp.text();
//check if heading":"No recent issues" exists
let noRecentIssues = jsRespText.indexOf('heading":"No recent issues"');
if (noRecentIssues != -1) {
return {
status: "UP",
latency: responseTime
};
}
}
return {
status: "DOWN",
latency: responseTime
};
});
```
## Examples
### Website Monitor
+117 -7
View File
@@ -13,14 +13,124 @@ Ping monitors are used to monitor livenees of your servers. You can use Ping mon
</div>
## Host V4
## Hosts
You can add as many IP addresses as you want to monitor. The IP address should be a valid IPv4 address. Example of IP4 address is `106.12.43.232`.
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.
## Host V6
- 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.
- Timeout: Enter the timeout in milliseconds for each ping request of each host. For IP6 timeout is not supported.
- Count: The number of pings you want to do for each host. The average latency of all the pings will be used to evaluate the response.
You can add as many IP addresses as you want to monitor. The IP address should be a valid IPv6 address. Example of IP6 address is `2001:0db8:85a3:0000:0000:8a2e:0370:7334`.
## Eval
<p class="note danger">
Please note that atleast one of the Host V4 or Host V6 is required.
<p>
The eval is used to define the JavaScript code that should be used to evaluate the response. It is optional and has be a valid JavaScript code.
This is an anonymous JS function, it should return a **Promise**, that resolves or rejects to `{status, latency}`, by default it looks like this.
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and latency(number)
> `{status:"DEGRADED", latency: 200}`.
```javascript
(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let alive = arrayOfPings.reduce((acc, ping) => {
return acc && ping.alive;
}, true);
return {
status: alive ? "UP" : "DOWN",
latency: latencyTotal / arrayOfPings.length
};
});
```
- `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.
```js
let decodedResp = atob(responseDataBase64);
let jsonResp = JSON.parse(decodedResp);
console.log(jsonResp);
/*
[
{
alive: true,
min: '145.121',
max: '149.740',
avg: '147.430',
latencies: [ 145.121, 149.74 ],
latency: 145.121,
host: '45.91.169.49',
type: 'IP4'
},
{
alive: true,
min: '145.348',
max: '149.143',
avg: '147.245',
latencies: [ 145.348, 149.143 ],
latency: 145.348,
host: 'kener.ing',
type: 'DOMAIN'
},
{
alive: true,
min: '57.696',
max: '58.330',
avg: '58.013',
latencies: [ 57.696, 58.33 ],
latency: 57.696,
host: '2404:6800:4003:c1c::66',
type: 'IP6'
}
]
*/
```
### 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.
- `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.
- `max` is a string. It is the maximum latency of the pings.
- `avg` is a string. It is the average latency of the pings.
- `latencies` is an array of numbers. It is the latency of each ping.
- `latency` is a number. It is the average latency of the pings.
- `host` is a string. It is the host that was pinged.
- `type` is a string. It is the type of the host. It can be either `IP4` or `IP6` or `DOMAIN`.
### 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));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
return acc && ping.alive;
}, true);
let avgLatency = latencyTotal / arrayOfPings.length;
if (areAllOpen && avgLatency > 10) {
return {
status: "DEGRADED",
latency: avgLatency
};
}
return {
status: areAllOpen ? "UP" : "DOWN",
latency: avgLatency
};
});
```
+135
View File
@@ -0,0 +1,135 @@
---
title: TCP Monitors | Kener
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.
<div class="border rounded-md">
![Monitors TCP](/m_tcp.png)
</div>
## Hosts
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
## Eval
The eval is used to define the JavaScript code that should be used to evaluate the response. It is optional and has be a valid JavaScript code.
This is an anonymous JS function, it should return a **Promise**, that resolves or rejects to `{status, latency}`, by default it looks like this.
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and latency(number)
> `{status:"DEGRADED", latency: 200}`.
```javascript
(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let alive = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true;
} else {
return false;
}
}, true);
return {
status: alive ? "UP" : "DOWN",
latency: latencyTotal / arrayOfPings.length
};
});
```
- `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.
```js
let decodedResp = atob(responseDataBase64);
let jsonResp = JSON.parse(decodedResp);
console.log(jsonResp);
/*
[
{
status: 'open',
latency: 31.93,
host: '66.51.120.219',
port: 465,
type: 'IP4'
},
{
status: 'open',
latency: 47.041417,
host: '2404:6800:4003:c1c::66',
port: 80,
type: 'IP6'
},
{
status: 'open',
latency: 82.1865,
host: 'rajnandan.com',
port: 80,
type: 'DOMAIN'
}
]
*/
```
### 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.
- `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));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true;
} else {
return false;
}
}, true);
let avgLatency = latencyTotal / arrayOfPings.length;
if (areAllOpen && avgLatency > 10) {
return {
status: "DEGRADED",
latency: avgLatency
};
}
return {
status: areAllOpen ? "UP" : "DOWN",
latency: avgLatency
};
});
```
+3 -3
View File
@@ -26,13 +26,13 @@ description: Roadmap for Kener
~~Handle uploaded images for docker containers~~
Introduce sitemap.xml
~~Introduce sitemap.xml~~
~~Migrate from moment to date-fns for status page~~ (moment remains in dashboard. Will remove later)
~~Create Admin UI~~ Released in (3.0.0)
☐ Webhook customization
~~Webhook customization~~
☐ Monitor Update Screen
@@ -40,4 +40,4 @@ description: Roadmap for Kener
---
Request a feature or start a discussion [here](https://github.com/rajnandan1/kener/discussions/119)
Request a feature or start a discussion [here](https://github.com/rajnandan1/kener/discussions/new/choose)
+10
View File
@@ -59,6 +59,11 @@
"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",
@@ -140,6 +145,11 @@
{
"sectionTitle": "Help",
"children": [
{
"title": "Fonts",
"link": "/docs/custom-fonts",
"file": "/custom-fonts.md"
},
{
"title": "Changelogs",
"link": "/docs/changelogs",
+1 -1
View File
@@ -81,7 +81,7 @@ You can change the colors of UP/DEGRADED/DOWN states of the monitor. You show ha
## Font
You can change the font of the status page. You can choose from a list of google fonts.
You can change the font of the status page. You can choose from a list of google, or [bunny fonts](https://fonts.bunny.net/) (which is a fully GDPR-compliant, zero-tracking/no-logging CDN font service).
### Font URL
+55 -18
View File
@@ -93,20 +93,24 @@ Body of the webhook will be sent as below:
}
```
| Key | Description |
| --------------------- | ----------------------------------------------------------- |
| id | Unique ID of the alert |
| alert_name | Name of the alert |
| severity | Severity of the alert. Can be `critical`, `warn` |
| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` |
| source | Source of the alert. Can be `Kener` |
| timestamp | Timestamp of the alert |
| description | Description of the alert. This you can customize. See below |
| details | Details of the alert. |
| details.metric | Name of the monitor |
| details.current_value | Current value of the monitor |
| details.threshold | Alert trigger threshold of the monitor |
| actions | Actions to be taken. Link to view the monitor. |
| Key | Description | Variable |
| --------------------- | ----------------------------------------------------------- | ----------------------------- |
| id | Unique ID of the alert | ${id} |
| alert_name | Name of the alert | ${alert_name} |
| severity | Severity of the alert. Can be `critical`, `warn` | ${severity} |
| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` | ${status} |
| source | Source of the alert. Can be `Kener` | ${source} |
| timestamp | Timestamp of the alert | ${timestamp} |
| description | Description of the alert. This you can customize. See below | ${description} |
| details | Details of the alert. | - |
| details.metric | Name of the monitor | ${metric} |
| details.current_value | Current value of the monitor | ${current_value} |
| details.threshold | Alert trigger threshold of the monitor | ${threshold} |
| actions | Actions to be taken. Link to view the monitor. | ${action_text}, ${action_url} |
### Custom Body
You can customize the body of the webhook. You can use the variables mentioned above. If you are not using a json body then please make sure you are using the right content-type by setting custom headers. See examples below.
## Discord
@@ -185,7 +189,7 @@ The slack message when alert is `RESOLVED` will look like this
## Email
Email triggers are used to send an email when a monitor goes down or up.
Email triggers are used to send an email when a monitor goes down or up. Kener supports sending emails via [resend](https://resend.com) or over SMTP.
<div class="border rounded-md">
@@ -193,11 +197,22 @@ Email triggers are used to send an email when a monitor goes down or up.
</div>
<div class="border px-2 rounded-md mt-4">
### Resend
#### Note
To send emails using Resend you just need to set `RESEND_API_KEY` in the environment variables.
Please make sure you have set the `RESEND_API_KEY` in the environment variables.
### SMTP
To send emails using SMTP, please enter
- Host: SMTP server host
- Port: SMTP server port
- User: SMTP server username
- Password: SMTP server password
<div class="note danger">
Since the password will be stored as plain text we encourage to use environment variables for the password. Let us say if you have an environment variable `SMTP_PASSWORD` then you can use it as `$SMTP_PASSWORD`.
</div>
@@ -250,3 +265,25 @@ Click on the ⚙️ to edit the trigger.
### Deactivate Trigger
You can deactivate the trigger by switching the toggle to off. You cannot send message to a deactivated trigger. Any monitor with this trigger will not send any notifications.
---
## Examples
### Telegram
You can use the webhook trigger to send a message to a telegram channel. Enable `Use a custom webhook body`.
Set the URL to `https://api.telegram.org/bot[BOT_TOKEN]/sendMessage`. Replace [BOT_TOKEN] with your bot token.
```json
{
"chat_id": "[CHAT_ID]", // Replace [CHAT_ID] with your chat id
"text": "<b>${alert_name}</b>\n\n<b>Severity:</b> <code>${severity}</code>\n<b>Status:</b> ${status}\n<b>Source:</b> Kener\n<b>Time:</b> ${timestamp}\n\n📌 <b>Details:</b>\n- <b>Metric:</b>${metric}\n- <b>Current Value:</b> <code>${current_value}</code>\n- <b>Threshold:</b> <code>${threshold}</code>\n\n🔍 <a href=\"${action_url}\">${action_text}</a>",
"parse_mode": "HTML"
}
```
If you want to send a message to a group, then replace `[CHAT_ID]` with the group id.
You can also use environment variables to store the bot token and chat id. In that case the URL will be `https://api.telegram.org/bot$BOT_TOKEN/sendMessage`. In the body you can use `"chat_id": "$CHAT_ID"`. Make sure you have set the `BOT_TOKEN` and `CHAT_ID` in the <a href="/docs/environment-vars#secrets">environment variables</a>.
+5
View File
@@ -4,6 +4,7 @@ import dotenv from "dotenv";
dotenv.config();
import express from "express";
import Startup from "./src/lib/server/startup.js";
import { GetSiteMap } from "./src/lib/server/controllers/controller.js";
import fs from "fs-extra";
import knex from "knex";
import knexOb from "./knexfile.js";
@@ -24,6 +25,10 @@ app.use((req, res, next) => {
app.get(base + "/healthcheck", (req, res) => {
res.end("ok");
});
app.get(base + "/sitemap.xml", async (req, res) => {
res.header("Content-Type", "application/xml");
res.send(await GetSiteMap());
});
//part /uploads server static files from static/uploads
//set env variable for upload path
@@ -0,0 +1,11 @@
export function up(knex) {
return knex.schema.alterTable("incidents", function (table) {
table.text("incident_type").defaultTo("INCIDENT");
});
}
export function down(knex) {
return knex.schema.alterTable("incidents", function (table) {
table.dropColumn("incident_type");
});
}
+38 -21
View File
@@ -1,12 +1,12 @@
{
"name": "kener",
"version": "3.0.1",
"version": "3.0.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kener",
"version": "3.0.1",
"version": "3.0.9",
"license": "MIT",
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
@@ -36,6 +36,7 @@
"moment-timezone": "^0.5.43",
"mysql2": "^3.12.0",
"node-cache": "^5.1.2",
"nodemailer": "^6.10.0",
"npm-run-all": "^4.1.5",
"pg": "^8.13.1",
"pg-pool": "^3.7.0",
@@ -2916,9 +2917,9 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
@@ -2940,7 +2941,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -2955,6 +2956,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/cookie": {
@@ -4824,15 +4829,16 @@
}
},
"node_modules/nanoid": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz",
"integrity": "sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
"integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
@@ -4927,6 +4933,15 @@
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
},
"node_modules/nodemailer": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz",
"integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -5332,9 +5347,9 @@
"license": "ISC"
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/path-type": {
@@ -5676,15 +5691,16 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -7533,10 +7549,11 @@
}
},
"node_modules/undici": {
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"version": "5.28.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
"integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
@@ -7623,9 +7640,9 @@
}
},
"node_modules/vite": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz",
"integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==",
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz",
"integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==",
"dev": true,
"license": "MIT",
"dependencies": {
+109 -108
View File
@@ -1,110 +1,111 @@
{
"name": "kener",
"version": "3.0.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.",
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
"keywords": [
"Node.js application",
"Open-source status page",
"Service monitoring tool",
"Real-time incident management",
"Customizable reporting",
"Service outage tracker",
"User-friendly dashboard",
"Incident communication platform",
"Scalable monitoring solution",
"Community-driven software",
"Website status tracker",
"Incident response tool",
"System status monitoring",
"Service reliability management",
"Incident alert system"
],
"repository": {
"type": "git",
"url": "https://github.com/rajnandan1/kener.git"
},
"scripts": {
"build": "vite build",
"preview": "vite preview",
"configure": "node build.js",
"migrate": "npx knex migrate:latest",
"preseed": "npx knex migrate:latest",
"seed": "npx knex seed:run",
"predev": "npm run seed",
"devschedule": "node src/lib/server/startup.js",
"schedule": "node src/lib/server/startup.js",
"development": "vite dev",
"dev": "npm-run-all --parallel devschedule development",
"prettify": "prettier --write .",
"start": "node main.js"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/kit": "^1.27.4",
"@tailwindcss/typography": "^0.5.10",
"@zerodevx/svelte-toast": "^0.9.6",
"autoprefixer": "^10.4.14",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"date-picker-svelte": "^2.15.1",
"postcss": "^8.4.24",
"postcss-load-config": "^4.0.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.5.14",
"svelte": "^4.0.5",
"svelte-awesome-color-picker": "^3.1.4",
"svelte-check": "^3.6.0",
"svelte-dnd-action": "^0.9.55",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.0",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
"analytics": "^0.8.14",
"axios": "^1.6.2",
"badge-maker": "^3.3.1",
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.5.0",
"bits-ui": "^0.9.9",
"clsx": "^2.0.0",
"croner": "^7.0.5",
"date-fns": "^4.1.0",
"dns2": "^2.1.0",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"figlet": "^1.8.0",
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lucide-svelte": "^0.292.0",
"marked": "^11.1.1",
"mode-watcher": "^0.4.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"mysql2": "^3.12.0",
"node-cache": "^5.1.2",
"npm-run-all": "^4.1.5",
"pg": "^8.13.1",
"pg-pool": "^3.7.0",
"ping": "^0.4.4",
"queue": "^7.0.0",
"randomstring": "^1.3.0",
"resend": "^4.0.1",
"svelte-legos": "^0.2.5",
"tailwind-merge": "^2.0.0",
"tailwind-variants": "^0.1.18"
},
"engines": {
"node": ">=20.0.0"
}
"name": "kener",
"version": "3.0.11",
"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.",
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
"keywords": [
"Node.js application",
"Open-source status page",
"Service monitoring tool",
"Real-time incident management",
"Customizable reporting",
"Service outage tracker",
"User-friendly dashboard",
"Incident communication platform",
"Scalable monitoring solution",
"Community-driven software",
"Website status tracker",
"Incident response tool",
"System status monitoring",
"Service reliability management",
"Incident alert system"
],
"repository": {
"type": "git",
"url": "https://github.com/rajnandan1/kener.git"
},
"scripts": {
"build": "vite build",
"preview": "vite preview",
"configure": "node build.js",
"migrate": "npx knex migrate:latest",
"preseed": "npx knex migrate:latest",
"seed": "npx knex seed:run",
"predev": "npm run seed",
"devschedule": "node src/lib/server/startup.js",
"schedule": "node src/lib/server/startup.js",
"development": "vite dev",
"dev": "npm-run-all --parallel devschedule development",
"prettify": "prettier --write .",
"start": "node main.js"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/kit": "^1.27.4",
"@tailwindcss/typography": "^0.5.10",
"@zerodevx/svelte-toast": "^0.9.6",
"autoprefixer": "^10.4.14",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"date-picker-svelte": "^2.15.1",
"postcss": "^8.4.24",
"postcss-load-config": "^4.0.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.5.14",
"svelte": "^4.0.5",
"svelte-awesome-color-picker": "^3.1.4",
"svelte-check": "^3.6.0",
"svelte-dnd-action": "^0.9.55",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.0",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
"analytics": "^0.8.14",
"axios": "^1.6.2",
"badge-maker": "^3.3.1",
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.5.0",
"bits-ui": "^0.9.9",
"clsx": "^2.0.0",
"croner": "^7.0.5",
"date-fns": "^4.1.0",
"dns2": "^2.1.0",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"figlet": "^1.8.0",
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lucide-svelte": "^0.292.0",
"marked": "^11.1.1",
"mode-watcher": "^0.4.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"mysql2": "^3.12.0",
"node-cache": "^5.1.2",
"nodemailer": "^6.10.0",
"npm-run-all": "^4.1.5",
"pg": "^8.13.1",
"pg-pool": "^3.7.0",
"ping": "^0.4.4",
"queue": "^7.0.0",
"randomstring": "^1.3.0",
"resend": "^4.0.1",
"svelte-legos": "^0.2.5",
"tailwind-merge": "^2.0.0",
"tailwind-variants": "^0.1.18"
},
"engines": {
"node": ">=20.0.0"
}
}
-2
View File
@@ -3,8 +3,6 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
%sveltekit.head%
</head>
+16 -2
View File
@@ -35,8 +35,7 @@
background-size: 100%;
height: 100svh;
clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%);
transform: blur(3px);
/* clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%); */
}
.squares-pattern::after {
@@ -835,3 +834,18 @@ textarea::placeholder {
padding-right: 8px;
}
}
.bg-maintenance-in-progress {
background-color: #f9f3c2;
}
.text-maintenance-in-progress-text {
color: #ff6868;
}
.text-upcoming-maintenance {
color: #f0a04b;
}
.text-maintenance-completed {
color: #6499e9;
}
+6 -6
View File
@@ -115,22 +115,22 @@ const allRecordTypes = {
};
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}(:\d{1,5})?$/;
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IPv4";
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}|:))(?::\d{1,5})?$/;
/^(?:(?:[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 "IPv6";
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,}(:\d{1,5})?$/;
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "Domain Name";
return "DOMAIN";
}
// If none of the above conditions match, the input is invalid
+99 -61
View File
@@ -14,7 +14,7 @@
if (incident.end_date_time) {
endTime = new Date(incident.end_date_time * 1000);
}
let incidentType = incident.incident_type;
const lastedFor = fd(startTime, endTime, selectedLang);
const startedAt = fdn(startTime, selectedLang);
@@ -23,6 +23,35 @@
if (nowTime < startTime) {
isFuture = true;
}
let incidentDateSummary = "";
let maintenanceBadge = "";
let maintenanceBadgeColor = "";
if (!isFuture && incident.state != "RESOLVED") {
incidentDateSummary = l(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", {
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>
<div class="newincident relative grid w-full grid-cols-12 gap-2 px-0 py-0 last:border-b-0">
@@ -30,47 +59,31 @@
<Accordion.Root bind:value={index} class="accor">
<Accordion.Item value="incident-0">
<Accordion.Trigger class="px-4 hover:bg-muted hover:no-underline">
<div class="justify-start text-left hover:no-underline">
<p
class="scroll-m-20 text-xs font-semibold leading-5 tracking-normal badge-{incident.state}"
>
{l(lang, incident.state)}
</p>
<div class="w-full text-left hover:no-underline">
{#if incidentType == "INCIDENT"}
<p
class="scroll-m-20 text-xs font-semibold leading-5 tracking-normal badge-{incident.state}"
>
{l(lang, incident.state)}
</p>
{:else if incidentType == "MAINTENANCE"}
<p
class="{maintenanceBadgeColor} scroll-m-20 text-xs font-semibold leading-5 tracking-normal"
>
{l(lang, maintenanceBadge)}
</p>
{/if}
<p class="scroll-m-20 text-lg font-medium tracking-tight">
{incident.title}
</p>
<p
class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground"
>
{#if !isFuture && incident.state != "RESOLVED"}
<span>
{l(lang, "Started about %startedAt ago, still ongoing", {
startedAt
})}
</span>
{:else if !isFuture && incident.state == "RESOLVED"}
<span>
{l(
lang,
"Started about %startedAt ago, lasted for about %lastedFor",
{ startedAt, lastedFor }
)}
</span>
{:else if isFuture && incident.state != "RESOLVED"}
<span>
{l(lang, "Starts in %startedAt", { startedAt })}
</span>
{:else if isFuture && incident.state == "RESOLVED"}
<span>
{l(
lang,
"Starts in %startedAt, will last for about %lastedFor",
{ startedAt, lastedFor }
)}
</span>
{/if}
</p>
{#if !!incidentDateSummary}
<p
class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground"
>
{incidentDateSummary}
</p>
{/if}
</div>
</Accordion.Trigger>
<Accordion.Content>
@@ -100,30 +113,55 @@
{l(lang, "Updates")}
</p>
{#if incident.comments.length > 0}
<ol class="relative mt-2 pl-14">
{#each incident.comments as comment}
<li class="relative border-l pb-4 pl-[4.5rem] last:border-0">
<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"
{#if incidentType == "INCIDENT"}
<ol class="relative mt-2 pl-14">
{#each incident.comments as comment}
<li
class="relative border-l pb-4 pl-[4.5rem] last:border-0"
>
{l(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
)}
</time>
<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)}
</div>
<p class="mb-4 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
<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
)}
</time>
<p class="mb-4 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
{:else if incidentType == "MAINTENANCE"}
<ol class="relative mt-2 pl-0">
{#each incident.comments as comment}
<li class="relative pb-2 last:border-0">
<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
)}
</time>
<p class="mb-2 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
{/if}
{:else}
<p class="text-sm font-medium">
{l(lang, "No Updates Yet")}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -29,7 +29,7 @@
DEGRADED: "#e6ca61"
},
fontJ: {
cssSrc: "https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@0,100..900;1,100..900&display=swap",
cssSrc: "https://fonts.bunny.net/css?family=albert-sans:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i&display=swap",
family: "Albert Sans"
},
colors: "",
@@ -334,7 +334,7 @@
class="mt-2"
type="text"
id="cssSrc"
placeholder="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"
placeholder="https://fonts.bunny.net/css?family=lato:100,100i,300,300i,400,400i,700,700i,900,900i&display=swap"
/>
</div>
</div>
@@ -353,13 +353,16 @@
</div>
</div>
<hr />
<p class="font-medium">Custom CSS</p>
<div class="flex w-full flex-row justify-start gap-2">
<div class="w-full">
<Label for="customCSS" class="text-sm text-muted-foreground">Custom CSS</Label>
<Label for="customCSS" class="text-sm text-muted-foreground">
You can add custom CSS to style your site. Do not include style tags.
</Label>
<textarea
bind:value={themeData.customCSS}
id="customCSS"
class="h-48 w-full rounded-sm border p-2"
class="mt-1 h-48 w-full rounded-sm border p-2"
placeholder=".className&#123;color: red;&#125;"
></textarea>
</div>
+225 -19
View File
@@ -1,13 +1,15 @@
<script>
import { Button } from "$lib/components/ui/button";
import { Plus, X, Loader, Settings, Check } from "lucide-svelte";
import { Plus, X, Loader, Settings, Check, ChevronRight } from "lucide-svelte";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { base } from "$app/paths";
import * as Select from "$lib/components/ui/select";
import * as RadioGroup from "$lib/components/ui/radio-group";
import { createEventDispatcher } from "svelte";
import { onMount } from "svelte";
import { IsValidURL } from "$lib/clientTools.js";
import { clickOutsideAction, slide } from "svelte-legos";
import * as Card from "$lib/components/ui/card";
let status = "ACTIVE";
let formState = "idle";
@@ -26,7 +28,15 @@
url: "",
headers: [],
to: "",
from: data.RESEND_SENDER_EMAIL || ""
from: data.fromEmail,
webhook_body: "",
has_webhook_body: false,
email_type: data.preferredModeEmail,
smtp_host: data.smtp?.smtp_host ? data.smtp.smtp_host : "",
smtp_port: data.smtp?.smtp_port ? data.smtp.smtp_port : "",
smtp_user: data.smtp?.smtp_user ? data.smtp.smtp_user : "",
smtp_pass: data.smtp?.smtp_pass ? data.smtp.smtp_pass : "",
smtp_secure: data.smtp?.smtp_secure ? data.smtp.smtp_secure : ""
}
};
let invalidFormMessage = "";
@@ -41,7 +51,15 @@
url: "",
headers: [],
to: "",
from: data.RESEND_SENDER_EMAIL || ""
from: data.fromEmail,
webhook_body: "",
has_webhook_body: false,
email_type: data.preferredModeEmail,
smtp_host: data.smtp?.smtp_host ? data.smtp.smtp_host : "",
smtp_port: data.smtp?.smtp_port ? data.smtp.smtp_port : "",
smtp_user: data.smtp?.smtp_user ? data.smtp.smtp_user : "",
smtp_pass: data.smtp?.smtp_pass ? data.smtp.smtp_pass : "",
smtp_secure: data.smtp?.smtp_secure ? data.smtp.smtp_secure : ""
}
};
}
@@ -89,10 +107,51 @@
let emValid = validateNameEmailPattern(newTrigger.trigger_meta.from);
if (!emValid.isValid) {
invalidFormMessage = "Invalid Name and Email Address for Sender";
invalidFormMessage =
"Invalid Name and Email Address for Sender. It should be like this: Name <email@example.com>";
formState = "idle";
return;
}
if (newTrigger.trigger_meta.email_type == "smtp") {
if (newTrigger.trigger_meta.smtp_host == "") {
invalidFormMessage = "SMTP Host is required";
formState = "idle";
return;
}
if (newTrigger.trigger_meta.smtp_port == "") {
invalidFormMessage = "SMTP Port is required";
formState = "idle";
return;
}
if (newTrigger.trigger_meta.smtp_user == "") {
invalidFormMessage = "SMTP User is required";
formState = "idle";
return;
}
if (newTrigger.trigger_meta.smtp_pass == "") {
invalidFormMessage = "SMTP Password is required";
formState = "idle";
return;
}
}
}
if (newTrigger.trigger_type == "webhook") {
//headers elements key value should not be empty
if (newTrigger.trigger_meta.headers.length > 0) {
for (let i = 0; i < newTrigger.trigger_meta.headers.length; i++) {
if (newTrigger.trigger_meta.headers[i].key == "") {
invalidFormMessage = "Header Key is required";
formState = "idle";
return;
}
if (newTrigger.trigger_meta.headers[i].value == "") {
invalidFormMessage = "Header Value is required";
formState = "idle";
return;
}
}
}
}
//newTrigger.name present not empty
if (newTrigger.name == "") {
@@ -152,6 +211,9 @@
function showUpdateSheet(m) {
newTrigger = { ...newTrigger, ...m };
newTrigger.trigger_meta = JSON.parse(newTrigger.trigger_meta);
if (newTrigger.trigger_type === "email" && !!!newTrigger.trigger_meta.email_type) {
newTrigger.trigger_meta.email_type = "resend";
}
showAddTrigger = true;
}
@@ -202,6 +264,30 @@
onMount(() => {
loadData();
});
let placeholderWebhookBody = JSON.stringify(
{
id: "${id}",
alert_name: "${alert_name}",
severity: "${severity}",
status: "${status}",
source: "${source}",
timestamp: "${timestamp}",
description: "${description}",
details: {
metric: "${metric}",
current_value: "${current_value}",
threshold: "${threshold}"
},
actions: [
{
text: "${action_text}",
url: "${action_url}"
}
]
},
null,
2
);
</script>
<div class="mt-4 flex justify-between">
@@ -295,8 +381,25 @@
</div>
{#if showAddTrigger}
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
<div class="absolute right-0 top-0 h-screen w-[800px] bg-background shadow-xl">
<div class="absolute top-0 flex h-12 w-full justify-between gap-2 border-b p-3">
<div
transition:slide={{ direction: "right", duration: 200 }}
use:clickOutsideAction
on:clickoutside={(e) => {
showAddTrigger = false;
}}
class="absolute right-0 top-0 h-screen w-[800px] bg-background px-3 shadow-xl"
>
<Button
variant="outline"
size="icon"
class="absolute right-[785px] top-8 z-10 h-8 w-8 rounded-md"
on:click={(e) => {
showAddTrigger = false;
}}
>
<ChevronRight class="h-6 w-6 " />
</Button>
<div class="absolute top-0 flex h-12 w-full justify-between gap-2 border-b p-3 pr-6">
{#if newTrigger.id}
<h2 class="text-lg font-medium">Edit Trigger</h2>
{:else}
@@ -419,7 +522,7 @@
{#if newTrigger.trigger_type == "webhook"}
<div class="mt-4 w-full">
<Label for="url">Add Optional Headers for Webhooks</Label>
<div class="grid grid-cols-6 gap-2">
<div class="mt-2 grid grid-cols-6 gap-2">
{#each newTrigger.trigger_meta.headers as header, index}
<div class="col-span-2">
<Input
@@ -464,10 +567,60 @@
<Plus class="mr-1 h-4 w-4" /> Add Headers
</Button>
</div>
<div>
<label>
<input
on:change={(e) => {
newTrigger.trigger_meta.has_webhook_body =
e.target.checked;
}}
type="checkbox"
checked={newTrigger.trigger_meta.has_webhook_body}
/>
Use a Custom Webhook Body
</label>
{#if !!newTrigger.trigger_meta.has_webhook_body}
<p class="my-2 text-xs text-muted-foreground">
You can use a custom webhook body. The body should be a
valid for the Content-Type header you have set. The default
is JSON. There are <a
target="_blank"
class="text-blue-500"
href="https://kener.ing/docs/triggers#webhook-body"
>variables</a
>
that you can use for the webhook body. To use a variable wrap
it like
<code>$&#123;variable&#125;</code>
</p>
<div class="w-full">
<textarea
bind:value={newTrigger.trigger_meta.webhook_body}
class="mt-2 h-[500px] w-full rounded-sm border p-2"
placeholder={placeholderWebhookBody}
></textarea>
</div>
{/if}
</div>
</div>
{:else if newTrigger.trigger_type == "email"}
<div class="mt-4 w-full">
{#if !!!data.RESEND_API_KEY}
<RadioGroup.Root
class="my-4 flex"
bind:value={newTrigger.trigger_meta.email_type}
>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="resend" id="email-resend" />
<Label for="email-resend" class="cursor-pointer">
Use Resend
</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="smtp" id="email-smtp" />
<Label for="email-smtp" class="cursor-pointer">Use SMTP</Label>
</div>
</RadioGroup.Root>
{#if !!!data.RESEND_API_KEY && newTrigger.trigger_meta.email_type == "resend"}
<div class="rounded-md border bg-card p-2 text-xs">
<p class="text-sm font-semibold">Email Trigger</p>
<p class="text-xs">
@@ -485,6 +638,66 @@
>.
</p>
</div>
{:else if newTrigger.trigger_meta.email_type == "smtp"}
<div class="flex gap-x-2">
<div>
<Label class="text-sm">
Host
<span class="text-red-500">*</span>
</Label>
<Input
class="mt-2"
bind:value={newTrigger.trigger_meta.smtp_host}
placeholder="smtp.example.com"
/>
</div>
<div>
<Label class="text-sm">
Port
<span class="text-red-500">*</span>
</Label>
<Input
class="mt-2"
bind:value={newTrigger.trigger_meta.smtp_port}
placeholder="587"
/>
</div>
<div>
<Label class="text-sm">
User
<span class="text-red-500">*</span>
</Label>
<Input
class="mt-2"
bind:value={newTrigger.trigger_meta.smtp_user}
placeholder="raj@example.com"
/>
</div>
<div>
<Label class="text-sm">
Password
<span class="text-red-500">*</span>
</Label>
<Input
class="mt-2"
bind:value={newTrigger.trigger_meta.smtp_pass}
placeholder="*******"
/>
</div>
</div>
<div class="my-2">
<label class="text-sm">
<input
on:change={(e) => {
newTrigger.trigger_meta.smtp_secure =
e.target.checked;
}}
type="checkbox"
checked={newTrigger.trigger_meta.smtp_secure}
/>
Use Secure Connection
</label>
</div>
{/if}
</div>
<div class="mt-4 w-full">
@@ -512,17 +725,10 @@
{/if}
</div>
</div>
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 gap-2 border-t p-3">
<div class="col-span-1">
<Button
variant="ghost"
class="col-span-1 w-full"
on:click={(e) => {
showAddTrigger = false;
}}>Cancel</Button
>
</div>
<div class="col-span-4 py-2.5">
<div
class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6"
>
<div class="col-span-5 py-2.5">
<p class="text-right text-xs font-medium text-red-500">{invalidFormMessage}</p>
</div>
<div class="col-span-1">
+1 -4
View File
@@ -169,10 +169,7 @@
{l(lang, "Embed")}
</h2>
<p class="mb-1 text-xs text-muted-foreground">
{@html l(
lang,
"Embed this moni2tor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app."
)}
{l(lang, "Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.")}
</p>
<div class="mb-4 grid grid-cols-2 gap-2">
<div class="col-span-1">
+28 -17
View File
@@ -1,9 +1,24 @@
// @ts-nocheck
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR } from "date-fns/locale";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr, nb } from "date-fns/locale";
const locales = { ru, en: enUS, hi, de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko, "pt-BR": ptBR };
const locales = {
ru,
en: enUS,
hi,
de,
"zh-CN": zhCN,
vi,
ja,
nl,
dk: da,
fr,
ko,
"pt-BR": ptBR,
tr,
"nb-NO": nb
};
const f = function (date, formatStr, locale) {
return format(date, formatStr, {
@@ -27,24 +42,20 @@ const fdm = function (duration, locale) {
};
const l = function (sessionLangMap, key, args = {}) {
const keys = key.split(".");
let obj = sessionLangMap;
try {
let obj = sessionLangMap[key];
for (const keyPart of keys) {
obj = obj?.[keyPart];
if (!obj) {
break;
// Replace placeholders in the string using the args object
if (obj && typeof obj === "string") {
obj = obj.replace(/%\w+/g, (placeholder) => {
const argKey = placeholder.slice(1); // Remove the `%` to get the key
return args[argKey] !== undefined ? args[argKey] : placeholder;
});
}
return obj || key;
} catch (e) {
return key;
}
// Replace placeholders in the string using the args object
if (obj && typeof obj === "string") {
obj = obj.replace(/%\w+/g, (placeholder) => {
const argKey = placeholder.slice(1); // Remove the `%` to get the key
return args[argKey] !== undefined ? args[argKey] : placeholder;
});
}
return obj || key;
};
const summaryTime = function (summaryStatus) {
if (summaryStatus == "No Data") {
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%status für %duration",
"14 Days": "14 Tage",
"30 Days": "30 Tage",
"60 Days": "60 Tage",
@@ -6,50 +7,55 @@
"90 Days": "90 Tage",
"Availability per Component": "Verfügbarkeit pro Komponente",
"Back": "Zurück",
"Badge": "Abzeichen",
"Badge Copied": "Abzeichen kopiert",
"Badge": "Abzeichen",
"Browse Events": "Ereignisse durchsuchen",
"Code Copied": "Code kopiert",
"Copy Code": "Code kopieren",
"Copy Link": "Link kopieren",
"Dark": "Dunkel",
"Days": "Tage",
"DEGRADED": "EINGESCHRÄNKT",
"DOWN": "AUSGEFALLEN",
"Embed": "Einbetten",
"Dark": "Dunkel",
"Days": "Tage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
"Embed": "Einbetten",
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
"IDENTIFIED": "IDENTIFIZIERT",
"Incident Updates": "Vorfall-Updates",
"INVESTIGATING": "UNTERSUCHUNG",
"Incident Updates": "Vorfall-Updates",
"LIVE Status": "LIVE-Status",
"Lasted for about %lastedFor": "Dauerte etwa %lastedFor",
"Light": "Hell",
"Link Copied": "Link kopiert",
"LIVE Status": "LIVE-Status",
"Mode": "Modus",
"MAINTENANCE": "WARTUNG",
"MONITORING": "ÜBERWACHUNG",
"Maintenance Completed": "Wartung abgeschlossen",
"Maintenance in Progress": "Wartung läuft",
"Mode": "Modus",
"No Data": "Keine Daten",
"No Incident in %date": "Kein Vorfall am %date",
"No Incidents": "Keine Vorfälle",
"No Incident On %date": "Kein Vorfall am %date",
"No Monitor Found": "Kein Monitor gefunden",
"No Updates Yet": "Noch keine Updates",
"Ongoing Incidents": "Laufende Vorfälle",
"Pinging": "Pingen",
"Recent Incidents": "Kürzliche Vorfälle",
"RESOLVED": "GELÖST",
"Share": "Teilen",
"Recent Incidents": "Kürzliche Vorfälle",
"Recent Maintenances": "Kürzliche Wartungen",
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
"Share": "Teilen",
"Standard": "Standard",
"Started about %startedAt ago, lasted for about %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
"Started about %startedAt ago, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
"Starts in %startedAt": "Beginnt in %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
"Status": "Status",
"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",
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
"Starts %startedAt": "Beginnt in %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Thema",
"Today": "Heute",
"UP": "VERFÜGBAR",
"Upcoming Maintenance": "Bevorstehende Wartung",
"Updates": "Updates",
"Uptime": "Verfügbarkeit",
"%status for %duration": "%status für %duration"
"Uptime": "Verfügbarkeit"
}
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%status i %duration",
"14 Days": "14 Dage",
"30 Days": "30 Dage",
"60 Days": "60 Dage",
@@ -6,50 +7,55 @@
"90 Days": "90 Dage",
"Availability per Component": "Tilgængelighed pr. Komponent",
"Back": "Tilbage",
"Badge": "Mærke",
"Badge Copied": "Mærke Kopieret",
"Badge": "Mærke",
"Browse Events": "Gennemse begivenheder",
"Code Copied": "Kode Kopieret",
"Copy Code": "Kopier Kode",
"Copy Link": "Kopier Link",
"Dark": "Mørk",
"Days": "Dage",
"DEGRADED": "FORRINGET",
"DOWN": "NEDE",
"Embed": "Integrer",
"Dark": "Mørk",
"Days": "Dage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
"Embed": "Integrer",
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
"IDENTIFIED": "IDENTIFICERET",
"Incident Updates": "Hændelsesopdateringer",
"INVESTIGATING": "UNDERSØGER",
"Incident Updates": "Hændelsesopdateringer",
"LIVE Status": "LIVE-status",
"Lasted for about %lastedFor": "Varede i cirka %lastedFor",
"Light": "Lys",
"Link Copied": "Link Kopieret",
"LIVE Status": "LIVE-status",
"Mode": "Tilstand",
"MAINTENANCE": "VEDLIGEHOLDELSE",
"MONITORING": "OVERVÅGNING",
"Maintenance Completed": "Vedligeholdelse afsluttet",
"Maintenance in Progress": "Vedligeholdelse i gang",
"Mode": "Tilstand",
"No Data": "Ingen Data",
"No Incident in %date": "Ingen Hændelse Den %date",
"No Incidents": "Ingen Hændelser",
"No Incident On %date": "Ingen Hændelse Den %date",
"No Monitor Found": "Ingen Monitor Fundet",
"No Updates Yet": "Ingen Opdateringer Endnu",
"Ongoing Incidents": "Igangværende Hændelser",
"Pinging": "Pinger",
"Recent Incidents": "Seneste Hændelser",
"RESOLVED": "LØST",
"Share": "Del",
"Recent Incidents": "Seneste Hændelser",
"Recent Maintenances": "Seneste Vedligeholdelser",
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
"Share": "Del",
"Standard": "Standard",
"Started about %startedAt ago, lasted for about %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
"Started about %startedAt ago, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
"Starts in %startedAt": "Starter om %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
"Status": "Status",
"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",
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
"Starts %startedAt": "Starter om %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Tema",
"Today": "I dag",
"UP": "OPPE",
"Upcoming Maintenance": "Kommende vedligeholdelse",
"Updates": "Opdateringer",
"Uptime": "Oppetid",
"%status for %duration": "%status i %duration"
"Uptime": "Oppetid"
}
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%status for %duration",
"14 Days": "14 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
@@ -6,50 +7,55 @@
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"Back": "Back",
"Badge": "Badge",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Code Copied": "Code Copied",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"Dark": "Dark",
"Days": "Days",
"DEGRADED": "DEGRADED",
"DOWN": "DOWN",
"Embed": "Embed",
"Dark": "Dark",
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
"Incident Updates": "Incident Updates",
"INVESTIGATING": "INVESTIGATING",
"Incident Updates": "Incident Updates",
"LIVE Status": "LIVE Status",
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"LIVE Status": "LIVE Status",
"Mode": "Mode",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
"No Incidents": "No Incidents",
"No Incident On %date": "No Incident On %date",
"No Monitor Found": "No Monitor Found",
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"Recent Incidents": "Recent Incidents",
"RESOLVED": "RESOLVED",
"Share": "Share",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Standard": "Standard",
"Started about %startedAt ago, lasted for about %lastedFor": "Started about %startedAt ago, lasted for about %lastedFor",
"Started about %startedAt ago, still ongoing": "Started about %startedAt ago, still ongoing",
"Starts in %startedAt": "Starts in %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Starts in %startedAt, will last for about %lastedFor",
"Status": "Status",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Theme",
"Today": "Today",
"UP": "UP",
"Upcoming Maintenance": "Upcoming Maintenance",
"Updates": "Updates",
"Uptime": "Uptime",
"%status for %duration": "%status for %duration"
"Uptime": "Uptime"
}
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%status pendant %duration",
"14 Days": "14 jours",
"30 Days": "30 jours",
"60 Days": "60 jours",
@@ -6,50 +7,55 @@
"90 Days": "90 jours",
"Availability per Component": "Disponibilité par composant",
"Back": "Retour",
"Badge": "Badge",
"Badge Copied": "Badge copié",
"Badge": "Badge",
"Browse Events": "Parcourir les événements",
"Code Copied": "Code copié",
"Copy Code": "Copier le code",
"Copy Link": "Copier le lien",
"Dark": "Sombre",
"Days": "Jours",
"DEGRADED": "Dégradé",
"DOWN": "Hors ligne",
"Embed": "Intégrer",
"Dark": "Sombre",
"Days": "Jours",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
"Embed": "Intégrer",
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
"IDENTIFIED": "Identifié",
"Incident Updates": "Mises à jour des incidents",
"INVESTIGATING": "En cours d'investigation",
"Incident Updates": "Mises à jour des incidents",
"LIVE Status": "Statut LIVE",
"Lasted for about %lastedFor": "A duré environ %lastedFor",
"Light": "Clair",
"Link Copied": "Lien copié",
"LIVE Status": "Statut LIVE",
"Mode": "Mode",
"MAINTENANCE": "Maintenance",
"MONITORING": "En surveillance",
"Maintenance Completed": "Maintenance terminée",
"Maintenance in Progress": "Maintenance en cours",
"Mode": "Mode",
"No Data": "Pas de données",
"No Incident in %date": "Pas d'incident le %date",
"No Incidents": "Pas d'incidents",
"No Incident On %date": "Pas d'incident le %date",
"No Monitor Found": "Aucun moniteur trouvé",
"No Updates Yet": "Pas encore de mises à jour",
"Ongoing Incidents": "Incidents en cours",
"Pinging": "Ping en cours",
"Recent Incidents": "Incidents récents",
"RESOLVED": "Résolu",
"Share": "Partager",
"Recent Incidents": "Incidents récents",
"Recent Maintenances": "Maintenances récentes",
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
"Share": "Partager",
"Standard": "Standard",
"Started about %startedAt ago, lasted for about %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
"Started about %startedAt ago, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
"Starts in %startedAt": "Commence dans %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
"Status": "Statut",
"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",
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
"Starts %startedAt": "Commence dans %startedAt",
"Status OK": "Statut OK",
"Status": "Statut",
"Theme": "Thème",
"Today": "Aujourd'hui",
"UP": "En ligne",
"Upcoming Maintenance": "Maintenance à venir",
"Updates": "Mises à jour",
"Uptime": "Temps de fonctionnement",
"%status for %duration": "%status pendant %duration"
"Uptime": "Temps de fonctionnement"
}
+24 -18
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%duration के लिए %status",
"14 Days": "14 दिन",
"30 Days": "30 दिन",
"60 Days": "60 दिन",
@@ -6,50 +7,55 @@
"90 Days": "90 दिन",
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
"Back": "वापस",
"Badge": "बैज",
"Badge Copied": "बैज कॉपी किया गया",
"Badge": "बैज",
"Browse Events": "ईवेंट ब्राउज़ करें",
"Code Copied": "कोड कॉपी किया गया",
"Copy Code": "कोड कॉपी करें",
"Copy Link": "लिंक कॉपी करें",
"Dark": "डार्क",
"Days": "दिन",
"DEGRADED": "क्षीण",
"DOWN": "डाउन",
"Embed": "एम्बेड",
"Dark": "डार्क",
"Days": "दिन",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
"Embed": "एम्बेड",
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
"IDENTIFIED": "पहचाना गया",
"Incident Updates": "घटना अपडेट",
"INVESTIGATING": "जांच चल रही है",
"Lasted for about %lastedFor": "लगभग %lastedFor तक चला",
"Incident Updates": "घटना अपडेट",
"LIVE Status": "लाइव स्टेटस",
"Lasted for about %lastedFor": "%lastedFor तक चला",
"Light": "लाइट",
"Link Copied": "लिंक कॉपी किया गया",
"LIVE Status": "लाइव स्टेटस",
"Mode": "मोड",
"MAINTENANCE": "मेंटेनेंस",
"MONITORING": "निगरानी",
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
"Maintenance in Progress": "मेंटेनेंस जारी है",
"Mode": "मोड",
"No Data": "कोई डेटा नहीं",
"No Incident in %date": "%date को कोई घटना नहीं",
"No Incidents": "कोई घटना नहीं",
"No Incident On %date": "%date को कोई घटना नहीं",
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
"Ongoing Incidents": "चल रही घटनाएँ",
"Pinging": "पिंगिंग",
"Recent Incidents": "हाल की घटनाएँ",
"RESOLVED": "सुलझाया गया",
"Share": "साझा करें",
"Recent Incidents": "हाल की घटनाएँ",
"Recent Maintenances": "हाल की मेंटेनेंस",
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
"Share": "साझा करें",
"Standard": "मानक",
"Started about %startedAt ago, lasted for about %lastedFor": "लगभग %startedAt पहले शुरू हुआ, लगभग %lastedFor तक चला",
"Started about %startedAt ago, still ongoing": "लगभग %startedAt पहले शुरू हुआ, अभी भी जारी है",
"Starts in %startedAt": "%startedAt में शुरू होगा",
"Starts in %startedAt, will last for about %lastedFor": "%startedAt में शुरू होगा, लगभग %lastedFor तक चलेगा",
"Status": "स्थिति",
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
"Starts %startedAt": "%startedAt में शुरू होगा",
"Status OK": "स्थिति ठीक है",
"Status": "स्थिति",
"Theme": "थीम",
"Today": "आज",
"UP": "अप",
"Upcoming Maintenance": "आगामी मेंटेनेंस",
"Updates": "अपडेट्स",
"Uptime": "अपटाइम",
"%status for %duration": "%duration के लिए %status"
"Uptime": "अपटाइम"
}
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%duration の間 %status",
"14 Days": "14日間",
"30 Days": "30日間",
"60 Days": "60日間",
@@ -6,50 +7,55 @@
"90 Days": "90日間",
"Availability per Component": "コンポーネントごとの可用性",
"Back": "戻る",
"Badge": "バッジ",
"Badge Copied": "バッジがコピーされました",
"Badge": "バッジ",
"Browse Events": "イベントを閲覧",
"Code Copied": "コードがコピーされました",
"Copy Code": "コードをコピー",
"Copy Link": "リンクをコピー",
"Dark": "ダーク",
"Days": "日間",
"DEGRADED": "劣化",
"DOWN": "ダウン",
"Embed": "埋め込み",
"Dark": "ダーク",
"Days": "日間",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "このモニターをアプリに <script> または <iframe> を使用して埋め込む。",
"Embed": "埋め込み",
"Get SVG badge for this monitor": "このモニターのSVGバッジを取得",
"Get a LIVE Status for this monitor": "このモニターのライブステータスを取得",
"IDENTIFIED": "識別済み",
"Incident Updates": "インシデント更新",
"INVESTIGATING": "調査中",
"Incident Updates": "インシデント更新",
"LIVE Status": "ライブステータス",
"Lasted for about %lastedFor": "約 %lastedFor 続きました",
"Light": "ライト",
"Link Copied": "リンクがコピーされました",
"LIVE Status": "ライブステータス",
"Mode": "モード",
"MAINTENANCE": "メンテナンス",
"MONITORING": "モニタリング中",
"Maintenance Completed": "メンテナンス完了",
"Maintenance in Progress": "メンテナンス中",
"Mode": "モード",
"No Data": "データなし",
"No Incident in %date": "%date にインシデントなし",
"No Incidents": "インシデントなし",
"No Incident On %date": "%date にインシデントなし",
"No Monitor Found": "モニターが見つかりません",
"No Updates Yet": "まだ更新はありません",
"Ongoing Incidents": "進行中のインシデント",
"Pinging": "ピング中",
"Recent Incidents": "最近のインシデント",
"RESOLVED": "解決済み",
"Share": "共有",
"Recent Incidents": "最近のインシデント",
"Recent Maintenances": "最近のメンテナンス",
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
"Share": "共有",
"Standard": "標準",
"Started about %startedAt ago, lasted for about %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
"Started about %startedAt ago, still ongoing": "%startedAt 前に開始し、まだ進行中です",
"Starts in %startedAt": "%startedAt 後に開始",
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
"Status": "ステータス",
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
"Starts %startedAt, will last for %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
"Starts %startedAt": "%startedAt 後に開始",
"Status OK": "ステータス正常",
"Status": "ステータス",
"Theme": "テーマ",
"Today": "今日",
"UP": "稼働中",
"Upcoming Maintenance": "今後のメンテナンス",
"Updates": "更新",
"Uptime": "稼働時間",
"%status for %duration": "%duration の間 %status"
"Uptime": "稼働時間"
}
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%duration 동안 %status 상태였음",
"14 Days": "14 일",
"30 Days": "30 ",
"60 Days": "60 일",
@@ -6,50 +7,55 @@
"90 Days": "90 일",
"Availability per Component": "항목당 가용성",
"Back": "뒤로",
"Badge": "배지",
"Badge Copied": "배지 복사됨",
"Badge": "배지",
"Browse Events": "이벤트 찾아보기",
"Code Copied": "소스코드 복사됨",
"Copy Code": "소스코드 복사",
"Copy Link": "링크 복사",
"Dark": "다크",
"Days": "일",
"DEGRADED": "성능 저하됨",
"DOWN": "중단",
"Embed": "임베드",
"Dark": "다크",
"Days": "일",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "<script> 나 <iframe> 태그를 사용하여 내 앱에 이 모니터링을 임베딩 해보세요.",
"Embed": "임베드",
"Get SVG badge for this monitor": "이 모니터링 항목에 대한 SVG 배지를 구해보세요.",
"Get a LIVE Status for this monitor": "이 모니터링 항목에 대한 실시간 상태를 구해보세요.",
"IDENTIFIED": "확인됨",
"Incident Updates": "사건 보고서",
"INVESTIGATING": "조사 중",
"Incident Updates": "사건 보고서",
"LIVE Status": "실시간 상태",
"Lasted for about %lastedFor": "%lastedFor 동안 지속",
"Light": "라이트",
"Link Copied": "링크 복사됨",
"LIVE Status": "실시간 상태",
"Mode": "모드",
"MAINTENANCE": "유지보수",
"MONITORING": "감시 중",
"Maintenance Completed": "유지보수 완료",
"Maintenance in Progress": "유지보수 진행 중",
"Mode": "모드",
"No Data": "데이터 없음",
"No Incident in %date": "%date 에는 아무 사건도 없었음",
"No Incidents": "사건 없음",
"No Incident On %date": "%date 에는 아무 사건도 없었음",
"No Monitor Found": "모니터링 항목을 찾을 수 없음",
"No Updates Yet": "아직 아무 업데이트도 없음",
"Ongoing Incidents": "진행중 사건",
"Pinging": "핑 요청",
"Recent Incidents": "최근 사건",
"RESOLVED": "해결됨",
"Share": "공유",
"Recent Incidents": "최근 사건",
"Recent Maintenances": "최근 유지보수",
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
"Share": "공유",
"Standard": "기본",
"Started about %startedAt ago, lasted for about %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
"Started about %startedAt ago, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
"Starts in %startedAt": "%startedAt 에 시작되었니다",
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
"Status": "상태",
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
"Starts %startedAt, will last for %lastedFor": "%startedAt 에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
"Starts %startedAt": "%startedAt 에 시작되었니다",
"Status OK": "양호",
"Status": "상태",
"Theme": "테마",
"Today": "오늘",
"UP": "작동 중",
"Upcoming Maintenance": "예정된 유지보수",
"Updates": "업데이트",
"Uptime": "업타임",
"%status for %duration": "%duration 동안 %status 상태였음"
"Uptime": "업타임"
}
+8
View File
@@ -46,5 +46,13 @@
{
"code": "pt-BR",
"name": "Português Brasileiro"
},
{
"code": "tr",
"name": "Türkçe"
},
{
"code": "nb-NO",
"name": "Norsk Bokmål"
}
]
+61
View File
@@ -0,0 +1,61 @@
{
"%status for %duration": "%status i %duration",
"14 Days": "14 dager",
"30 Days": "30 dager",
"60 Days": "60 dager",
"7 Days": "7 dager",
"90 Days": "90 dager",
"Availability per Component": "Tilgjengelighet per komponent",
"Back": "Tilbake",
"Badge Copied": "Merke kopiert",
"Badge": "Merke",
"Browse Events": "Bla gjennom hendelser",
"Code Copied": "Kode kopiert",
"Copy Code": "Kopier kode",
"Copy Link": "Kopier lenke",
"DEGRADED": "NEDSATT",
"DOWN": "NEDE",
"Dark": "Mørk",
"Days": "Dager",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Bygg inn denne monitoren ved å bruke <script> eller <iframe> i appen din.",
"Embed": "Bygg inn",
"Get SVG badge for this monitor": "Hent SVG-merke for denne monitoren",
"Get a LIVE Status for this monitor": "Få en LIVE-status for denne monitoren",
"IDENTIFIED": "IDENTIFISERT",
"INVESTIGATING": "UNDERSØKER",
"Incident Updates": "Hendelsesoppdateringer",
"LIVE Status": "LIVE-status",
"Lasted for about %lastedFor": "Varte i omtrent %lastedFor",
"Light": "Lys",
"Link Copied": "Lenke kopiert",
"MAINTENANCE": "VEDLIKEHOLD",
"MONITORING": "OVERVÅKER",
"Maintenance Completed": "Vedlikehold fullført",
"Maintenance in Progress": "Vedlikehold pågår",
"Mode": "Modus",
"No Data": "Ingen data",
"No Incident in %date": "Ingen hendelser på %date",
"No Incidents": "Ingen hendelser",
"No Monitor Found": "Ingen monitor funnet",
"No Updates Yet": "Ingen oppdateringer ennå",
"Ongoing Incidents": "Pågående hendelser",
"Pinging": "Pinger",
"RESOLVED": "LØST",
"Recent Incidents": "Nylige hendelser",
"Recent Maintenances": "Nylige vedlikehold",
"Share this monitor using a link with others": "Del denne monitoren med andre via lenke",
"Share": "Del",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
"Starts %startedAt, will last for %lastedFor": "Starter %startedAt, vil vare i %lastedFor",
"Starts %startedAt": "Starter %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Tema",
"Today": "I dag",
"UP": "OPPE",
"Upcoming Maintenance": "Kommende vedlikehold",
"Updates": "Oppdateringer",
"Uptime": "Oppetid"
}
+24 -18
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%status voor %duration",
"14 Days": "14 dagen",
"30 Days": "30 dagen",
"60 Days": "60 dagen",
@@ -6,50 +7,55 @@
"90 Days": "90 dagen",
"Availability per Component": "Beschikbaarheid per component",
"Back": "Terug",
"Badge": "Badge",
"Badge Copied": "Badge gekopieerd",
"Badge": "Badge",
"Browse Events": "Evenementen bekijken",
"Code Copied": "Code gekopieerd",
"Copy Code": "Kopieer code",
"Copy Link": "Kopieer link",
"Dark": "Donker",
"Days": "Dagen",
"DEGRADED": "BEPERKT",
"DOWN": "NIET BEREIKBAAR",
"Embed": "Insluiten",
"Dark": "Donker",
"Days": "Dagen",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Sluit deze monitor in met behulp van <script> of <iframe> in je app.",
"Embed": "Insluiten",
"Get SVG badge for this monitor": "SVG-badge ophalen voor deze monitor",
"Get a LIVE Status for this monitor": "LIVE-status ophalen voor deze monitor",
"IDENTIFIED": "GEIDENTIFICEERD",
"Incident Updates": "Incident updates",
"INVESTIGATING": "ONDERZOEK BEZIG",
"Incident Updates": "Incident updates",
"LIVE Status": "LIVE status",
"Lasted for about %lastedFor": "Duurde ongeveer %lastedFor",
"Light": "Licht",
"Link Copied": "Link gekopieerd",
"LIVE Status": "LIVE status",
"Mode": "Modus",
"MAINTENANCE": "ONDERHOUD",
"MONITORING": "MONITOREN",
"Maintenance Completed": "Onderhoud voltooid",
"Maintenance in Progress": "Onderhoud bezig",
"Mode": "Modus",
"No Data": "Geen gegevens",
"No Incident in %date": "Geen incident op %date",
"No Incidents": "Geen incidenten",
"No Incident On %date": "Geen incident op %date",
"No Monitor Found": "Geen monitor gevonden",
"No Updates Yet": "Nog geen updates",
"Ongoing Incidents": "Lopende incidenten",
"Pinging": "Pingen",
"Recent Incidents": "Recente incidenten",
"RESOLVED": "OPGELOST",
"Share": "Delen",
"Recent Incidents": "Recente incidenten",
"Recent Maintenances": "Recente onderhoudsbeurten",
"Share this monitor using a link with others": "Deel deze monitor met anderen via een link",
"Share": "Delen",
"Standard": "Standaard",
"Started about %startedAt ago, lasted for about %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
"Started about %startedAt ago, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
"Starts in %startedAt": "Begint over %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
"Status": "Status",
"Started %startedAt, lasted for %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
"Started %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
"Starts %startedAt, will last for %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
"Starts %startedAt": "Begint over %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Thema",
"Today": "Vandaag",
"UP": "BEREIKBAAR",
"Upcoming Maintenance": "Aankomend onderhoud",
"Updates": "Updates",
"Uptime": "Uptime",
"%status for %duration": "%status voor %duration"
}
"Uptime": "Uptime"
}
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%status por %duration",
"14 Days": "14 Dias",
"30 Days": "30 Dias",
"60 Days": "60 Dias",
@@ -6,50 +7,55 @@
"90 Days": "90 Dias",
"Availability per Component": "Disponibilidade por Componente",
"Back": "Voltar",
"Badge": "Emblema",
"Badge Copied": "Emblema Copiado",
"Badge": "Emblema",
"Browse Events": "Navegar Eventos",
"Code Copied": "Código Copiado",
"Copy Code": "Copiar Código",
"Copy Link": "Copiar Link",
"Dark": "Escuro",
"Days": "Dias",
"DEGRADED": "FALHA",
"DOWN": "INDISPONÍVEL",
"Embed": "Incorporar",
"Dark": "Escuro",
"Days": "Dias",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
"Embed": "Incorporar",
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
"IDENTIFIED": "IDENTIFICADO",
"Incident Updates": "Atualizações do Incidente",
"INVESTIGATING": "INVESTIGANDO",
"Incident Updates": "Atualizações do Incidente",
"LIVE Status": "Status AO VIVO",
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
"Light": "Claro",
"Link Copied": "Link Copiado",
"LIVE Status": "Status AO VIVO",
"Mode": "Modo",
"MAINTENANCE": "MANUTENÇÃO",
"MONITORING": "MONITORANDO",
"Maintenance Completed": "Manutenção Concluída",
"Maintenance in Progress": "Manutenção em Progresso",
"Mode": "Modo",
"No Data": "Sem dados",
"No Incident in %date": "Nenhum incidente em %date",
"No Incidents": "Sem Incidentes",
"No Incident On %date": "Nenhum incidente em %date",
"No Monitor Found": "Nenhum monitor encontrado",
"No Updates Yet": "Nenhuma atualização ainda",
"Ongoing Incidents": "Incidentes em Andamento",
"Pinging": "Animado",
"Recent Incidents": "Incidentes Recentes",
"RESOLVED": "RESOLVIDO",
"Share": "Compartilhar",
"Recent Incidents": "Incidentes Recentes",
"Recent Maintenances": "Manutenções Recentes",
"Share this monitor using a link with others": "Compartilhar este monitor através do link",
"Share": "Compartilhar",
"Standard": "Padrão",
"Started about %startedAt ago, lasted for about %lastedFor": "Iniciado há cerca de %startedAt e durou aproximadamente %lastedFor",
"Started about %startedAt ago, still ongoing": "Iniciado há cerca de %startedAt, ainda em andamento",
"Starts in %startedAt": "Inicia em %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Inícia em %startedAt, com duração de aproximadamente %lastedFor",
"Status": "Status",
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
"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",
"Status": "Status",
"Theme": "Tema",
"Today": "Hoje",
"UP": "OPERACIONAL",
"Upcoming Maintenance": "Manutenção Agendada",
"Updates": "Atualizações",
"Uptime": "Tempo de Atividade",
"%status for %duration": "%status por %duration"
"Uptime": "Tempo de Atividade"
}
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%status в течение %duration",
"14 Days": "14 Дней",
"30 Days": "30 Дней",
"60 Days": "60 Дней",
@@ -6,50 +7,55 @@
"90 Days": "90 Дней",
"Availability per Component": "Доступность по компонентам",
"Back": "Назад",
"Badge": "Значок",
"Badge Copied": "Значок скопирован",
"Badge": "Значок",
"Browse Events": "Просмотр событий",
"Code Copied": "Код скопирован",
"Copy Code": "Копировать код",
"Copy Link": "Копировать ссылку",
"Dark": "Темная",
"Days": "Дней",
"DEGRADED": "УХУДШЕНО",
"DOWN": "НЕДОСТУПНО",
"Embed": "Встроить",
"Dark": "Темная",
"Days": "Дней",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Встройте этот монитор используя <script> или <iframe> в ваше приложение.",
"Embed": "Встроить",
"Get SVG badge for this monitor": "Получить SVG значок для этого монитора",
"Get a LIVE Status for this monitor": "Получить ЖИВОЙ статус для этого монитора",
"IDENTIFIED": "ВЫЯВЛЕНО",
"Incident Updates": "Обновления инцидента",
"INVESTIGATING": "ИССЛЕДУЕТСЯ",
"Incident Updates": "Обновления инцидента",
"LIVE Status": "ЖИВОЙ статус",
"Lasted for about %lastedFor": "Длилось около %lastedFor",
"Light": "Светлая",
"Link Copied": "Ссылка скопирована",
"LIVE Status": "ЖИВОЙ статус",
"Mode": "Режим",
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
"MONITORING": "МОНИТОРИНГ",
"Maintenance Completed": "Техническое обслуживание завершено",
"Maintenance in Progress": "Техническое обслуживание",
"Mode": "Режим",
"No Data": "Нет данных",
"No Incident in %date": "Нет инцидентов на %date",
"No Incidents": "Нет инцидентов",
"No Incident On %date": "Нет инцидентов на %date",
"No Monitor Found": "Монитор не найден",
"No Updates Yet": "Пока нет обновлений",
"Ongoing Incidents": "Текущие инциденты",
"Pinging": "Пингуется",
"Recent Incidents": "Недавние инциденты",
"RESOLVED": "РЕШЕНО",
"Share": "Поделиться",
"Recent Incidents": "Недавние инциденты",
"Recent Maintenances": "Последние обслуживания",
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
"Share": "Поделиться",
"Standard": "Стандартная",
"Started about %startedAt ago, lasted for about %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
"Started about %startedAt ago, still ongoing": "Началось около %startedAt назад, все еще продолжается",
"Starts in %startedAt": "Начнется через %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
"Status": "Статус",
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
"Starts %startedAt": "Начнется через %startedAt",
"Status OK": "Статус ОК",
"Status": "Статус",
"Theme": "Тема",
"Today": "Сегодня",
"UP": "РАБОТАЕТ",
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
"Updates": "Обновления",
"Uptime": "Время работы",
"%status for %duration": "%status в течение %duration"
"Uptime": "Время работы"
}
+61
View File
@@ -0,0 +1,61 @@
{
"%status for %duration": "%duration boyunca %status",
"14 Days": "14 Gün",
"30 Days": "30 Gün",
"60 Days": "60 Gün",
"7 Days": "7 Gün",
"90 Days": "90 Gün",
"Availability per Component": "Bileşen Kullanılabilirliği",
"Back": "Geri",
"Badge Copied": "Rozet Kopyalandı",
"Badge": "Rozet",
"Browse Events": "Etkinliklere Göz At",
"Code Copied": "Kod Kopyalandı",
"Copy Code": "Kodu Kopyala",
"Copy Link": "Bağlantıyı Kopyala",
"DEGRADED": "BOZUK",
"DOWN": "ÇALIŞMIYOR",
"Dark": "Koyu",
"Days": "Günler",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
"Embed": "Yerleştir",
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
"IDENTIFIED": "TANIMLANDI",
"INVESTIGATING": "İNCELENİYOR",
"Incident Updates": "Olay Güncellemeleri",
"LIVE Status": "CANLI Durum",
"Lasted for about %lastedFor": "%lastedFor sürdü",
"Light": "Açık",
"Link Copied": "Bağlantı Kopyalandı",
"MAINTENANCE": "BAKIM",
"MONITORING": "İZLEME",
"Maintenance Completed": "Bakım Tamamlandı",
"Maintenance in Progress": "Bakım Devam Ediyor",
"Mode": "Mod",
"No Data": "Veri Yok",
"No Incident in %date": "%date Tarihinde Olay Yok",
"No Incidents": "Olay Yok",
"No Monitor Found": "Servis Bulunamadı",
"No Updates Yet": "Henüz Güncelleme Yok",
"Ongoing Incidents": "Devam Eden Olaylar",
"Pinging": "Pinging",
"RESOLVED": "ÇÖZÜLDÜ",
"Recent Incidents": "Son Olaylar",
"Recent Maintenances": "Son Bakımlar",
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
"Share": "Paylaş",
"Standard": "Standart",
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
"Starts %startedAt, will last for %lastedFor": "%startedAt başlıyor, %lastedFor sürecek",
"Starts %startedAt": "%startedAt başlıyor",
"Status OK": "Durum UYGUN",
"Status": "Durum",
"Theme": "Tema",
"Today": "Bugün",
"UP": "ÇALIŞIYOR",
"Upcoming Maintenance": "Yaklaşan Bakım",
"Updates": "Güncellemeler",
"Uptime": "Çalışma Süresi"
}
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%status trong %duration",
"14 Days": "14 ngày",
"30 Days": "30 ngày",
"60 Days": "60 ngày",
@@ -6,50 +7,55 @@
"90 Days": "90 ngày",
"Availability per Component": "Tính khả dụng theo thành phần",
"Back": "Quay lại",
"Badge": "Huy hiệu",
"Badge Copied": "Huy hiệu đã được sao chép",
"Badge": "Huy hiệu",
"Browse Events": "Duyệt sự kiện",
"Code Copied": "Mã đã được sao chép",
"Copy Code": "Sao chép mã",
"Copy Link": "Sao chép liên kết",
"Dark": "Tối",
"Days": "Ngày",
"DEGRADED": "Suy giảm",
"DOWN": "Ngừng hoạt động",
"Embed": "Nhúng",
"Dark": "Tối",
"Days": "Ngày",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Nhúng trình theo dõi này vào ứng dụng của bạn bằng <script> hoặc <iframe>.",
"Embed": "Nhúng",
"Get SVG badge for this monitor": "Lấy huy hiệu SVG cho trình theo dõi này",
"Get a LIVE Status for this monitor": "Lấy trạng thái TRỰC TIẾP cho trình theo dõi này",
"IDENTIFIED": "Đã xác định",
"Incident Updates": "Cập nhật sự cố",
"INVESTIGATING": "Đang điều tra",
"Incident Updates": "Cập nhật sự cố",
"LIVE Status": "Trạng thái TRỰC TIẾP",
"Lasted for about %lastedFor": "Kéo dài khoảng %lastedFor",
"Light": "Sáng",
"Link Copied": "Liên kết đã được sao chép",
"LIVE Status": "Trạng thái TRỰC TIẾP",
"Mode": "Chế độ",
"MAINTENANCE": "Bảo trì",
"MONITORING": "Đang giám sát",
"Maintenance Completed": "Bảo trì đã hoàn thành",
"Maintenance in Progress": "Bảo trì đang diễn ra",
"Mode": "Chế độ",
"No Data": "Không có dữ liệu",
"No Incident in %date": "Không có sự cố vào %date",
"No Incidents": "Không có sự cố",
"No Incident On %date": "Không có sự cố vào %date",
"No Monitor Found": "Không tìm thấy trình theo dõi",
"No Updates Yet": "Chưa có cập nhật",
"Ongoing Incidents": "Sự cố đang diễn ra",
"Pinging": "Đang kiểm tra",
"Recent Incidents": "Sự cố gần đây",
"RESOLVED": "Đã giải quyết",
"Share": "Chia sẻ",
"Recent Incidents": "Sự cố gần đây",
"Recent Maintenances": "Bảo trì gần đây",
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
"Share": "Chia sẻ",
"Standard": "Tiêu chuẩn",
"Started about %startedAt ago, lasted for about %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
"Started about %startedAt ago, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
"Starts in %startedAt": "Bắt đầu trong %startedAt",
"Starts in %startedAt, will last for about %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
"Status": "Trạng thái",
"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",
"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",
"Status": "Trạng thái",
"Theme": "Giao diện",
"Today": "Hôm nay",
"UP": "Hoạt động",
"Upcoming Maintenance": "Bảo trì sắp tới",
"Updates": "Cập nhật",
"Uptime": "Thời gian hoạt động",
"%status for %duration": "%status trong %duration"
"Uptime": "Thời gian hoạt động"
}
+23 -17
View File
@@ -1,4 +1,5 @@
{
"%status for %duration": "%duration 内的 %status",
"14 Days": "14天",
"30 Days": "30天",
"60 Days": "60天",
@@ -6,50 +7,55 @@
"90 Days": "90天",
"Availability per Component": "每个组件的可用性",
"Back": "返回",
"Badge": "徽章",
"Badge Copied": "徽章已复制",
"Badge": "徽章",
"Browse Events": "浏览事件",
"Code Copied": "代码已复制",
"Copy Code": "复制代码",
"Copy Link": "复制链接",
"Dark": "深色",
"Days": "天",
"DEGRADED": "性能下降",
"DOWN": "不可用",
"Embed": "嵌入",
"Dark": "深色",
"Days": "天",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "使用 <script> 或 <iframe> 将此监控嵌入您的应用。",
"Embed": "嵌入",
"Get SVG badge for this monitor": "获取此监控的SVG徽章",
"Get a LIVE Status for this monitor": "获取此监控的实时状态",
"IDENTIFIED": "已识别",
"Incident Updates": "事件更新",
"INVESTIGATING": "正在调查",
"Incident Updates": "事件更新",
"LIVE Status": "实时状态",
"Lasted for about %lastedFor": "持续了约 %lastedFor",
"Light": "浅色",
"Link Copied": "链接已复制",
"LIVE Status": "实时状态",
"Mode": "模式",
"MAINTENANCE": "维护",
"MONITORING": "正在监控",
"Maintenance Completed": "维护已完成",
"Maintenance in Progress": "维护进行中",
"Mode": "模式",
"No Data": "无数据",
"No Incident in %date": "%date 无事件",
"No Incidents": "无事件",
"No Incident On %date": "%date 无事件",
"No Monitor Found": "未找到监控",
"No Updates Yet": "尚无更新",
"Ongoing Incidents": "正在处理的事件",
"Pinging": "正在测试",
"Recent Incidents": "最近的事件",
"RESOLVED": "已解决",
"Share": "分享",
"Recent Incidents": "最近的事件",
"Recent Maintenances": "最近的维护",
"Share this monitor using a link with others": "通过链接与他人分享此监控",
"Share": "分享",
"Standard": "标准",
"Started about %startedAt ago, lasted for about %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
"Started about %startedAt ago, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
"Starts in %startedAt": "%startedAt 后开始",
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
"Status": "状态",
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
"Starts %startedAt, will last for %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
"Starts %startedAt": "%startedAt 后开始",
"Status OK": "状态正常",
"Status": "状态",
"Theme": "主题",
"Today": "今天",
"UP": "可用",
"Upcoming Maintenance": "即将进行的维护",
"Updates": "更新",
"Uptime": "正常运行时间",
"%status for %duration": "%duration 内的 %status"
"Uptime": "正常运行时间"
}
+1 -1
View File
@@ -33,7 +33,7 @@ async function createJSONCommonAlert(monitor, config, alert, severity) {
let actions = [
{
text: "View Monitor",
url: siteURL + "/monitor-" + monitor.tag
url: siteURL + "?monitor=" + monitor.tag
}
];
return {
+129 -5
View File
@@ -16,6 +16,7 @@ import db from "../db/db.js";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import crypto from "crypto";
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
const saltRounds = 10;
const DUMMY_SECRET = "DUMMY_SECRET";
@@ -398,8 +399,13 @@ export const InterpolateData = (data, start, anchorStatus, e) => {
if (e) {
end = e;
}
let dataMap = data.reduce((acc, d) => {
acc[d.timestamp] = d;
return acc;
}, {});
for (let i = start; i <= end; i += 60) {
let nowData = data.find((d) => d.timestamp === i);
let nowData = dataMap[i];
if (!!nowData) {
status = nowData.status;
}
@@ -484,11 +490,17 @@ export const CreateIncident = async (data) => {
start_date_time: data.start_date_time,
status: !!data.status ? data.status : "OPEN",
end_date_time: !!data.end_date_time ? data.end_date_time : null,
state: !!data.state ? data.state : "INVESTIGATING"
state: !!data.state ? data.state : "INVESTIGATING",
incident_type: !!data.incident_type ? data.incident_type : "INCIDENT"
};
//incident_type == INCIDENT delete endDateTime
if (incident.incident_type === "INCIDENT") {
incident.end_date_time = null;
}
//if endDateTime is provided and it is less than startDateTime, throw error
if (incident.end_date_time && incident.end_date_time < incident.start_date_time) {
if (!!incident.end_date_time && incident.end_date_time < incident.start_date_time) {
throw new Error("End date time cannot be less than start date time");
}
@@ -632,9 +644,9 @@ export const AddIncidentComment = async (incident_id, comment, state, commented_
}
let c = await db.insertIncidentComment(incident_id, comment, state, commented_at);
let incidentType = incidentExists.incident_type;
//update incident state
if (c) {
if (c && incidentType === "INCIDENT") {
let incidentUpdate = {
state: state
};
@@ -800,3 +812,115 @@ export const IsLoggedInSession = async (cookies) => {
user: userDB
};
};
export const GetSMTPFromENV = () => {
//if variables are not return null
if (
!!!process.env.SMTP_HOST ||
!!!process.env.SMTP_PORT ||
!!!process.env.SMTP_USER ||
!!!process.env.SMTP_FROM_EMAIL ||
!!!process.env.SMTP_PASS
) {
return null;
}
return {
smtp_host: process.env.SMTP_HOST,
smtp_port: process.env.SMTP_PORT,
smtp_user: process.env.SMTP_USER,
smtp_from_email: process.env.SMTP_FROM_EMAIL,
smtp_pass: process.env.SMTP_PASS,
smtp_secure: !!process.env.SMTP_SECURE
};
};
export const GetSiteMap = async (cookies) => {
let siteMapData = [];
let siteURLData = await GetSiteDataByKey("siteURL");
let categories = await GetSiteDataByKey("categories");
let navs = await GetSiteDataByKey("nav");
if (!!siteURLData) {
siteMapData.push({
url: siteURLData,
lastmod: new Date().toISOString(),
priority: 1
});
}
//get today's date in January-2025 format date-fns
const today = format(new Date(), "MMMM-yyyy");
//last month
const lastMonth = format(subMonths(new Date(), 1), "MMMM-yyyy", { addMonths: -1 });
const nextMonth = format(addMonths(new Date(), 1), "MMMM-yyyy", { addMonths: -1 });
siteMapData.push({
url: siteURLData + "/incidents/" + today,
lastmod: startOfMonth(new Date()).toISOString(),
priority: 0.9
});
siteMapData.push({
url: siteURLData + "/incidents/" + lastMonth,
lastmod: startOfMonth(new Date()).toISOString(),
priority: 0.9
});
siteMapData.push({
url: siteURLData + "/incidents/" + nextMonth,
lastmod: startOfMonth(new Date()).toISOString(),
priority: 0.9
});
if (!!categories) {
for (let i = 0; i < categories.length; i++) {
if (categories[i].name !== "Home") {
siteMapData.push({
url: siteURLData + "?category=" + categories[i].name,
lastmod: new Date().toISOString(),
priority: 0.9
});
}
}
}
if (!!navs) {
for (let i = 0; i < navs.length; i++) {
if (navs[i].url.startsWith(siteURLData)) {
siteMapData.push({
url: navs[i].url,
lastmod: new Date().toISOString(),
priority: 0.9
});
} else if (navs[i].url.startsWith("/")) {
siteMapData.push({
url: siteURLData + navs[i].url,
lastmod: new Date().toISOString(),
priority: 0.9
});
}
}
}
let monitors = await GetMonitors({ status: "ACTIVE" });
for (let i = 0; i < monitors.length; i++) {
siteMapData.push({
url: siteURLData + "?monitor=" + monitors[i].tag,
lastmod: new Date(monitors[i].updated_at).toISOString(),
priority: 0.8
});
}
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${siteMapData
.map(
(page) => `
<url>
<loc>${page.url}</loc>
<lastmod>${page.lastmod}</lastmod>
<priority>${page.priority}</priority>
</url>
`
)
.join("")}
</urlset>`;
};
+1 -1
View File
@@ -3,7 +3,7 @@
export function IsValidURL(url) {
const regex =
/^(https?:\/\/)?((localhost|[\da-z.-]+\.[a-z]{2,6})(:[0-9]{1,5})?)?(\/[\w .-]*)*\/?$/i;
/^(https?:\/\/)?((localhost|[\da-z.-]+\.[a-z]{2,10})(:[0-9]{1,5})?)?(\/[\w .-]*)*\/?$/i;
return regex.test(url);
}
+446 -359
View File
@@ -1,12 +1,12 @@
// @ts-nocheck
import axios from "axios";
import ping from "ping";
import { Ping, ExtractIPv6HostAndPort, TCP } from "./ping.js";
import { UP, DOWN, DEGRADED } from "./constants.js";
import {
GetMinuteStartNowTimestampUTC,
ReplaceAllOccurrences,
GetRequiredSecrets,
Wait
GetMinuteStartNowTimestampUTC,
ReplaceAllOccurrences,
GetRequiredSecrets,
Wait,
} from "./tool.js";
import alerting from "./alerting.js";
@@ -25,17 +25,17 @@ const ERROR = "error";
const MANUAL = "manual";
const alertingQueue = new Queue({
concurrency: 10, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true // Automatically start the queue (optional)
concurrency: 10, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true, // Automatically start the queue (optional)
});
const apiQueue = new Queue({
concurrency: 10, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true // Automatically start the queue (optional)
concurrency: 10, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true, // Automatically start the queue (optional)
});
const defaultEval = `(function (statusCode, responseTime, responseData) {
const defaultEval = `(async function (statusCode, responseTime, responseData) {
let statusCodeShort = Math.floor(statusCode/100);
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
return {
@@ -49,392 +49,479 @@ const defaultEval = `(function (statusCode, responseTime, responseData) {
}
})`;
const defaultPingEval = `(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let alive = arrayOfPings.reduce((acc, ping) => {
return acc && ping.alive;
}, true);
return {
status: alive ? 'UP' : 'DOWN',
latency: latencyTotal / arrayOfPings.length,
}
})`;
const defaultTcpEval = `(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let alive = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true;
} else {
return false;
}
}, true);
return {
status: alive ? 'UP' : 'DOWN',
latency: latencyTotal / arrayOfPings.length,
}
})`;
async function manualIncident(monitor) {
let startTs = GetMinuteStartNowTimestampUTC();
let impactArr = await db.getIncidentsByMonitorTagRealtime(monitor.tag, startTs);
let startTs = GetMinuteStartNowTimestampUTC();
let incidentArr = await db.getIncidentsByMonitorTagRealtime(
monitor.tag,
startTs,
);
let maintenanceArr = await db.getMaintenanceByMonitorTagRealtime(
monitor.tag,
startTs,
);
let impact = "";
if (impactArr.length == 0) {
return {};
}
let impactArr = incidentArr.concat(maintenanceArr);
for (let i = 0; i < impactArr.length; i++) {
const element = impactArr[i];
let impact = "";
if (impactArr.length == 0) {
return {};
}
let autoIncidents = await db.getActiveAlertIncident(
monitor.tag,
element.monitor_impact,
element.id
);
for (let i = 0; i < impactArr.length; i++) {
const element = impactArr[i];
if (!!autoIncidents) {
continue;
}
let autoIncidents = await db.getActiveAlertIncident(
monitor.tag,
element.monitor_impact,
element.id,
);
if (element.monitor_impact === "DOWN") {
impact = "DOWN";
break;
}
if (element.monitor_impact === "DEGRADED") {
impact = "DEGRADED";
}
}
if (!!autoIncidents) {
continue;
}
if (impact === "") {
return {};
}
if (element.monitor_impact === "DOWN") {
impact = "DOWN";
break;
}
if (element.monitor_impact === "DEGRADED") {
impact = "DEGRADED";
}
}
let manualData = {
[startTs]: {
status: impact,
latency: 0,
type: MANUAL
}
};
return manualData;
if (impact === "") {
return {};
}
let manualData = {
[startTs]: {
status: impact,
latency: 0,
type: MANUAL,
},
};
return manualData;
}
const pingCall = async (hostsV4, hostsV6) => {
if (hostsV4 === undefined) hostsV4 = [];
if (hostsV6 === undefined) hostsV6 = [];
let alive = true;
let latencyTotal = 0;
let countHosts = hostsV4.length + hostsV6.length;
const tcpCall = async (hosts, tcpEval, tag) => {
let arrayOfPings = [];
for (let i = 0; i < hosts.length; i++) {
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");
for (let i = 0; i < hostsV4.length; i++) {
const host = hostsV4[i].trim();
try {
let res = await ping.promise.probe(host);
alive = alive && res.alive;
latencyTotal += res.time;
if (!res.alive) {
throw new Error(JSON.stringify(res));
}
} catch (error) {
alive = alive && false;
latencyTotal += 30;
console.log(`Error in pingCall IP4 for ${host}`, error);
}
}
let evalResp = undefined;
for (let i = 0; i < hostsV6.length; i++) {
const host = hostsV6[i].trim();
try {
let res = await ping.promise.probe(host, {
v6: true,
timeout: false
});
alive = alive && res.alive;
if (!res.alive) {
throw new Error(JSON.stringify(res));
}
latencyTotal += res.time;
} catch (error) {
alive = alive && false;
latencyTotal += 30;
console.log(`Error in pingCall IP6 for ${host}`, error);
}
}
return {
status: alive ? UP : DOWN,
latency: parseInt(latencyTotal / countHosts),
type: REALTIME
};
try {
evalResp = await eval(tcpEval + `("${respBase64}")`);
} catch (error) {
console.log(`Error in tcpEval for ${tag}`, error.message);
}
//reduce to get the status
return {
status: evalResp.status,
latency: evalResp.latency,
type: REALTIME,
};
};
const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorEval) => {
let axiosHeaders = {};
axiosHeaders["User-Agent"] = "Kener/3.0.2";
axiosHeaders["Accept"] = "*/*";
const start = Date.now();
//replace all secrets
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
if (!!body) {
body = ReplaceAllOccurrences(body, secret.find, secret.replace);
}
if (!!url) {
url = ReplaceAllOccurrences(url, secret.find, secret.replace);
}
if (!!headers) {
headers = ReplaceAllOccurrences(headers, secret.find, secret.replace);
}
}
if (!!headers) {
headers = JSON.parse(headers);
headers = headers.reduce((acc, header) => {
acc[header.key] = header.value;
return acc;
}, {});
axiosHeaders = { ...axiosHeaders, ...headers };
}
const pingCall = async (hosts, pingEval, tag) => {
if (hosts === undefined) {
console.log(
"Hosts is undefined. The ping monitor has changed in version 3.0.10. Please update your monitor with tag",
tag,
);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
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),
);
}
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString("base64");
const options = {
method: method,
headers: headers,
timeout: timeout,
transformResponse: (r) => r
};
if (!!headers) {
options.headers = headers;
}
if (!!body) {
options.data = body;
}
let statusCode = 500;
let latency = 0;
let resp = "";
let timeoutError = false;
try {
let data = await axios(url, options);
statusCode = data.status;
resp = data.data;
} catch (err) {
console.log(`Error in apiCall ${url}`, err.message);
if (err.message.startsWith("timeout of") && err.message.endsWith("exceeded")) {
timeoutError = true;
}
if (err.response !== undefined && err.response.status !== undefined) {
statusCode = err.response.status;
}
if (err.response !== undefined && err.response.data !== undefined) {
resp = err.response.data;
} else {
resp = JSON.stringify(resp);
}
} finally {
const end = Date.now();
latency = end - start;
if (resp === undefined || resp === null) {
resp = "";
}
}
resp = Buffer.from(resp).toString("base64");
let evalResp = undefined;
let evalResp = undefined;
try {
evalResp = await eval(pingEval + `("${respBase64}")`);
} catch (error) {
console.log(`Error in pingEval for ${tag}`, error.message);
}
//reduce to get the status
return {
status: evalResp.status,
latency: evalResp.latency,
type: REALTIME,
};
};
const apiCall = async (
envSecrets,
url,
method,
headers,
body,
timeout,
monitorEval,
tag,
) => {
let axiosHeaders = {};
axiosHeaders["User-Agent"] = "Kener/3.0.2";
axiosHeaders["Accept"] = "*/*";
const start = Date.now();
//replace all secrets
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
if (!!body) {
body = ReplaceAllOccurrences(body, secret.find, secret.replace);
}
if (!!url) {
url = ReplaceAllOccurrences(url, secret.find, secret.replace);
}
if (!!headers) {
headers = ReplaceAllOccurrences(headers, secret.find, secret.replace);
}
}
if (!!headers) {
headers = JSON.parse(headers);
headers = headers.reduce((acc, header) => {
acc[header.key] = header.value;
return acc;
}, {});
axiosHeaders = { ...axiosHeaders, ...headers };
}
try {
evalResp = eval(monitorEval + `(${statusCode}, ${latency}, "${resp}")`);
} catch (error) {
console.log("Error in monitorEval", error.message);
}
const options = {
method: method,
headers: headers,
timeout: timeout,
transformResponse: (r) => r,
};
if (!!headers) {
options.headers = headers;
}
if (!!body) {
options.data = body;
}
let statusCode = 500;
let latency = 0;
let resp = "";
let timeoutError = false;
try {
let data = await axios(url, options);
statusCode = data.status;
resp = data.data;
} catch (err) {
console.log(`Error in apiCall ${tag}`, err.message);
if (
err.message.startsWith("timeout of") &&
err.message.endsWith("exceeded")
) {
timeoutError = true;
}
if (err.response !== undefined && err.response.status !== undefined) {
statusCode = err.response.status;
}
if (err.response !== undefined && err.response.data !== undefined) {
resp = err.response.data;
} else {
resp = JSON.stringify(resp);
}
} finally {
const end = Date.now();
latency = end - start;
if (resp === undefined || resp === null) {
resp = "";
}
}
resp = Buffer.from(resp).toString("base64");
if (evalResp === undefined || evalResp === null) {
evalResp = {
status: DOWN,
latency: latency,
type: ERROR
};
} else if (
evalResp.status === undefined ||
evalResp.status === null ||
[UP, DOWN, DEGRADED].indexOf(evalResp.status) === -1
) {
evalResp = {
status: DOWN,
latency: latency,
type: ERROR
};
} else {
evalResp.type = REALTIME;
}
let evalResp = undefined;
let toWrite = {
status: DOWN,
latency: latency,
type: ERROR
};
if (evalResp.status !== undefined && evalResp.status !== null) {
toWrite.status = evalResp.status;
}
if (evalResp.latency !== undefined && evalResp.latency !== null) {
toWrite.latency = evalResp.latency;
}
if (evalResp.type !== undefined && evalResp.type !== null) {
toWrite.type = evalResp.type;
}
if (timeoutError) {
toWrite.type = TIMEOUT;
}
try {
evalResp = await eval(
monitorEval + `(${statusCode}, ${latency}, "${resp}")`,
);
} catch (error) {
console.log(`Error in monitorEval for ${tag}`, error.message);
}
return toWrite;
if (evalResp === undefined || evalResp === null) {
evalResp = {
status: DOWN,
latency: latency,
type: ERROR,
};
} else if (
evalResp.status === undefined ||
evalResp.status === null ||
[UP, DOWN, DEGRADED].indexOf(evalResp.status) === -1
) {
evalResp = {
status: DOWN,
latency: latency,
type: ERROR,
};
} else {
evalResp.type = REALTIME;
}
let toWrite = {
status: DOWN,
latency: latency,
type: ERROR,
};
if (evalResp.status !== undefined && evalResp.status !== null) {
toWrite.status = evalResp.status;
}
if (evalResp.latency !== undefined && evalResp.latency !== null) {
toWrite.latency = evalResp.latency;
}
if (evalResp.type !== undefined && evalResp.type !== null) {
toWrite.type = evalResp.type;
}
if (timeoutError) {
toWrite.type = TIMEOUT;
}
return toWrite;
};
async function dsnChecker(dnsResolver, host, recordType, matchType, values) {
try {
let queryStartTime = Date.now();
let dnsRes = await dnsResolver.getRecord(host, recordType);
let latency = Date.now() - queryStartTime;
try {
let queryStartTime = Date.now();
let dnsRes = await dnsResolver.getRecord(host, recordType);
let latency = Date.now() - queryStartTime;
if (dnsRes[recordType] === undefined) {
return {
status: DOWN,
latency: latency,
type: REALTIME
};
}
let data = dnsRes[recordType];
let dnsData = data.map((d) => d.data);
if (matchType === "ALL") {
for (let i = 0; i < values.length; i++) {
if (dnsData.indexOf(values[i].trim()) === -1) {
return {
status: DOWN,
latency: latency,
type: REALTIME
};
}
}
return {
status: UP,
latency: latency,
type: REALTIME
};
} else if (matchType === "ANY") {
for (let i = 0; i < values.length; i++) {
if (dnsData.indexOf(values[i].trim()) !== -1) {
return {
status: UP,
latency: latency,
type: REALTIME
};
}
}
return {
status: DOWN,
latency: latency,
type: REALTIME
};
}
} catch (error) {
console.log("Error in dnsChecker", error);
return {
status: DOWN,
latency: 0,
type: REALTIME
};
}
if (dnsRes[recordType] === undefined) {
return {
status: DOWN,
latency: latency,
type: REALTIME,
};
}
let data = dnsRes[recordType];
let dnsData = data.map((d) => d.data);
if (matchType === "ALL") {
for (let i = 0; i < values.length; i++) {
if (dnsData.indexOf(values[i].trim()) === -1) {
return {
status: DOWN,
latency: latency,
type: REALTIME,
};
}
}
return {
status: UP,
latency: latency,
type: REALTIME,
};
} else if (matchType === "ANY") {
for (let i = 0; i < values.length; i++) {
if (dnsData.indexOf(values[i].trim()) !== -1) {
return {
status: UP,
latency: latency,
type: REALTIME,
};
}
}
return {
status: DOWN,
latency: latency,
type: REALTIME,
};
}
} catch (error) {
console.log("Error in dnsChecker", error);
return {
status: DOWN,
latency: 0,
type: REALTIME,
};
}
}
const Minuter = async (monitor) => {
let realTimeData = {};
let manualData = {};
let realTimeData = {};
let manualData = {};
const startOfMinute = GetMinuteStartNowTimestampUTC();
if (monitor.monitor_type === "API") {
let envSecrets = GetRequiredSecrets(
`${monitor.type_data.url} ${monitor.type_data.body} ${JSON.stringify(monitor.type_data.headers)}`
);
const startOfMinute = GetMinuteStartNowTimestampUTC();
if (monitor.monitor_type === "API") {
let envSecrets = GetRequiredSecrets(
`${monitor.type_data.url} ${monitor.type_data.body} ${JSON.stringify(monitor.type_data.headers)}`,
);
if (monitor.type_data.eval === "") {
monitor.type_data.eval = defaultEval;
}
if (monitor.type_data.eval === "") {
monitor.type_data.eval = defaultEval;
}
let apiResponse = await apiCall(
envSecrets,
monitor.type_data.url,
monitor.type_data.method,
JSON.stringify(monitor.type_data.headers),
monitor.type_data.body,
monitor.type_data.timeout,
monitor.type_data.eval
);
let apiResponse = await apiCall(
envSecrets,
monitor.type_data.url,
monitor.type_data.method,
JSON.stringify(monitor.type_data.headers),
monitor.type_data.body,
monitor.type_data.timeout,
monitor.type_data.eval,
monitor.tag,
);
realTimeData[startOfMinute] = apiResponse;
if (apiResponse.type === TIMEOUT) {
apiQueue.push(async (cb) => {
await Wait(4000);
console.log(
"Retrying api call for " +
monitor.name +
" at " +
startOfMinute +
" due to timeout"
);
apiCall(
envSecrets,
monitor.type_data.url,
monitor.type_data.method,
JSON.stringify(monitor.type_data.headers),
monitor.type_data.body,
monitor.type_data.timeout,
monitor.type_data.eval
).then(async (data) => {
await db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: startOfMinute,
status: data.status,
latency: data.latency,
type: data.type
});
cb();
});
});
}
} else if (monitor.monitor_type === "PING") {
let pingResponse = await pingCall(monitor.type_data.hostsV4, monitor.type_data.hostsV6);
realTimeData[startOfMinute] = pingResponse;
} else if (monitor.monitor_type === "DNS") {
const dnsResolver = new DNSResolver(monitor.type_data.nameServer);
let dnsResponse = await dsnChecker(
dnsResolver,
monitor.type_data.host,
monitor.type_data.lookupRecord,
monitor.type_data.matchType,
monitor.type_data.values
);
realTimeData[startOfMinute] = dnsResponse;
}
realTimeData[startOfMinute] = apiResponse;
if (apiResponse.type === TIMEOUT) {
apiQueue.push(async (cb) => {
await Wait(500); //wait for 500ms
console.log(
"Retrying api call for " +
monitor.name +
" at " +
startOfMinute +
" due to timeout",
);
apiCall(
envSecrets,
monitor.type_data.url,
monitor.type_data.method,
JSON.stringify(monitor.type_data.headers),
monitor.type_data.body,
monitor.type_data.timeout,
monitor.type_data.eval,
monitor.tag,
).then(async (data) => {
await db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: startOfMinute,
status: data.status,
latency: data.latency,
type: data.type,
});
cb();
});
});
}
} else if (monitor.monitor_type === "PING") {
if (!!!monitor.type_data.pingEval) {
monitor.type_data.pingEval = defaultPingEval;
}
let pingResponse = await pingCall(
monitor.type_data.hosts,
monitor.type_data.pingEval,
monitor.tag,
);
realTimeData[startOfMinute] = pingResponse;
} else if (monitor.monitor_type === "TCP") {
if (!!!monitor.type_data.tcpEval) {
monitor.type_data.tcpEval = defaultTcpEval;
}
let pingResponse = await tcpCall(
monitor.type_data.hosts,
monitor.type_data.tcpEval,
monitor.tag,
);
realTimeData[startOfMinute] = pingResponse;
} else if (monitor.monitor_type === "DNS") {
const dnsResolver = new DNSResolver(monitor.type_data.nameServer);
let dnsResponse = await dsnChecker(
dnsResolver,
monitor.type_data.host,
monitor.type_data.lookupRecord,
monitor.type_data.matchType,
monitor.type_data.values,
);
realTimeData[startOfMinute] = dnsResponse;
}
manualData = await manualIncident(monitor);
//merge noData, apiData, webhookData, dayData
let mergedData = {};
manualData = await manualIncident(monitor);
//merge noData, apiData, webhookData, dayData
let mergedData = {};
if (monitor.default_status !== undefined && monitor.default_status !== null) {
if ([UP, DOWN, DEGRADED].indexOf(monitor.default_status) !== -1) {
mergedData[startOfMinute] = {
status: monitor.default_status,
latency: 0,
type: "default_status"
};
}
}
if (monitor.default_status !== undefined && monitor.default_status !== null) {
if ([UP, DOWN, DEGRADED].indexOf(monitor.default_status) !== -1) {
mergedData[startOfMinute] = {
status: monitor.default_status,
latency: 0,
type: "default_status",
};
}
}
for (const timestamp in realTimeData) {
mergedData[timestamp] = realTimeData[timestamp];
}
for (const timestamp in realTimeData) {
mergedData[timestamp] = realTimeData[timestamp];
}
for (const timestamp in manualData) {
mergedData[timestamp] = manualData[timestamp];
}
for (const timestamp in manualData) {
mergedData[timestamp] = manualData[timestamp];
}
for (const timestamp in mergedData) {
const element = mergedData[timestamp];
db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: parseInt(timestamp),
status: element.status,
latency: element.latency,
type: element.type
});
}
alertingQueue.push(async (cb) => {
setTimeout(async () => {
await alerting(monitor);
cb();
}, 1042);
});
for (const timestamp in mergedData) {
const element = mergedData[timestamp];
db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: parseInt(timestamp),
status: element.status,
latency: element.latency,
type: element.type,
});
}
alertingQueue.push(async (cb) => {
setTimeout(async () => {
await alerting(monitor);
cb();
}, 1042);
});
};
alertingQueue.start((err) => {
if (err) {
console.error("Error occurred:", err);
process.exit(1);
}
if (err) {
console.error("Error occurred:", err);
process.exit(1);
}
});
apiQueue.start((err) => {
if (err) {
console.error("Error occurred:", err);
process.exit(1);
}
if (err) {
console.error("Error occurred:", err);
process.exit(1);
}
});
export { Minuter };
+30 -25
View File
@@ -262,7 +262,11 @@ class DbImpl {
//get monitors given status
async getMonitors(data) {
return await this.knex("monitors").where("status", data.status).orderBy("id", "desc");
let query = this.knex("monitors").where("status", data.status);
if (data.category_name && data.category_name !== "All Categories") {
query = query.andWhere("category_name", data.category_name);
}
return await query.orderBy("id", "desc");
}
//get monitor by tag
@@ -394,7 +398,8 @@ class DbImpl {
status: data.status,
state: data.state,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now()
updated_at: this.knex.fn.now(),
incident_type: data.incident_type
});
}
@@ -504,7 +509,8 @@ class DbImpl {
"created_at",
"updated_at",
"status",
"state"
"state",
"incident_type"
)
.where("id", id)
.first();
@@ -529,6 +535,7 @@ class DbImpl {
"i.updated_at as updated_at",
"i.status as status",
"i.state as state",
"i.incident_type as incident_type",
"im.monitor_impact"
)
.innerJoin("incident_monitors as im", "i.id", "im.incident_id")
@@ -549,11 +556,30 @@ class DbImpl {
)
.innerJoin("incident_monitors as im", "i.id", "im.incident_id")
.where("im.monitor_tag", monitor_tag)
.andWhere("i.start_date_time", "<", timestamp)
.andWhere("i.start_date_time", "<=", timestamp)
.andWhere("i.status", "OPEN")
.andWhere("i.incident_type", "INCIDENT")
.andWhere("i.state", "!=", "RESOLVED");
}
//get maintenance incidents by monitor tag
async getMaintenanceByMonitorTagRealtime(monitor_tag, timestamp) {
return await this.knex("incidents as i")
.select(
"i.id as id",
"i.start_date_time as start_date_time",
"i.end_date_time as end_date_time",
"im.monitor_impact"
)
.innerJoin("incident_monitors as im", "i.id", "im.incident_id")
.where("im.monitor_tag", monitor_tag)
.andWhere("i.start_date_time", "<=", timestamp)
.andWhere("i.end_date_time", ">=", timestamp)
.andWhere("i.status", "OPEN")
.andWhere("i.incident_type", "MAINTENANCE")
.andWhere("i.state", "=", "RESOLVED");
}
//given array of ids get incidents
async getIncidentsByIds(ids) {
return await this.knex("incidents").whereIn("id", ids).andWhere("status", "OPEN");
@@ -572,27 +598,6 @@ class DbImpl {
.merge({ monitor_impact, updated_at: this.knex.fn.now() });
}
//return monitor tags with incidents using join incidents & incident_monitors
async getIncidentCompleteByIncidentID(incident_id) {
return await this.knex("incidents as i")
.select(
"i.id as incident_id",
"i.title as incident_title",
"i.description as incident_description",
"i.start_date_time as incident_start_date_time",
"i.end_date_time as incident_end_date_time",
"i.created_at as incident_created_at",
"i.updated_at as incident_updated_at",
"i.status as incident_status",
"i.identified as incident_identified",
"i.maintenance as incident_maintenance",
"im.monitor_tag",
"im.monitor_impact"
)
.innerJoin("incident_monitors as im", "i.id", "im.incident_id")
.where("i.id", incident_id);
}
//insert incident_comment
async insertIncidentComment(incident_id, comment, state, commented_at) {
return await this.knex("incident_comments").insert({
+2 -6
View File
@@ -10,10 +10,6 @@ const seedSiteData = {
key: "description",
value: "Kener: Open-source modern looking 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."
},
{
key: "keywords",
value: "Node.js status page, Incident management tool, Service monitoring, Service outage tracking, Real-time status updates, GitHub integration for incidents, Open-source status page, Node.js monitoring application, Service reliability, User-friendly incident management, Collaborative incident resolution, Seamless outage communication, Service disruption tracker, Real-time incident alerts, Node.js status reporting"
},
{
key: "og:description",
value: "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."
@@ -39,7 +35,7 @@ const seedSiteData = {
}
],
nav: [
{ name: "Documentation", url: "/docs/home", iconURL: "" },
{ name: "Documentation", url: "https://kener.ing/docs/home", iconURL: "" },
{ name: "Github", iconURL: "", url: "https://github.com/rajnandan1/kener" },
{ name: "Login", iconURL: "", url: "/manage/signin" }
],
@@ -85,7 +81,7 @@ const seedSiteData = {
DEGRADED: "#e6ca61"
},
font: {
cssSrc: "https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@0,100..900;1,100..900&display=swap",
cssSrc: "https://fonts.bunny.net/css?family=albert-sans:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i&display=swap",
family: "Albert Sans"
},
categories: [{ name: "Home", description: "Monitors for Home Page" }],
-31
View File
@@ -78,34 +78,3 @@ class DNSResolver {
}
export default DNSResolver;
// const resolver = new DNSResolver();
// const domain = process.argv[2] || "google.com";
// const recordType = process.argv[3] || "A";
// resolver.getRecord(domain, recordType).then(
// function (records) {
// Object.entries(records).forEach(([type, records]) => {
// if (records.length === 0) return []; // Skip empty records
// return records;
// console.log(">>>>>>---- dns:167 ", records);
// console.log(`\n${type} Records:`);
// records.forEach((record) => {
// console.log("----------------------------------------");
// console.log(`Type: ${type}`);
// console.log(`Name: ${record.name}`);
// console.log(`TTL: ${record.ttl}`);
// // Format the output based on record type
// if (type === "MX") {
// console.log(`Priority: ${record.data.priority}`);
// console.log(`Exchange: ${record.data.exchange}`);
// } else {
// console.log(`Data: ${record.data}`);
// // console.log(">>>>>>---- dns-resolver:120 ", record);
// }
// });
// });
// },
// function (err) {
// console.error(err);
// }
// );
+4 -1
View File
@@ -21,7 +21,10 @@ class Discord {
transformData(data) {
let siteURL = this.siteData.siteURL;
let logo = this.siteData.logo;
let logo =
this.siteData.siteURL +
(!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "") +
this.siteData.logo;
let color = 13250616; //down;
if (data.severity === "warning") {
+42 -6
View File
@@ -1,5 +1,8 @@
// @ts-nocheck
import { Resend } from "resend";
import nodemailer from "nodemailer";
import getSMTPTransport from "./smtps.js";
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
class Email {
to;
@@ -7,12 +10,24 @@ class Email {
method;
siteData;
monitorData;
meta;
constructor(meta, siteData, monitorData) {
this.to = meta.to;
this.from = meta.from;
this.siteData = siteData;
this.monitorData = monitorData;
let metaString = JSON.stringify(meta);
let envSecrets = GetRequiredSecrets(`${JSON.stringify(meta)}`);
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
metaString = ReplaceAllOccurrences(metaString, secret.find, secret.replace);
}
this.meta = JSON.parse(metaString);
}
transformData(data) {
@@ -179,13 +194,34 @@ class Email {
}
async send(data) {
const resend = new Resend(process.env.RESEND_API_KEY);
let emailBody = this.transformData(data); // object containing email data (to, subject, text, html, etc)
if (!this.meta.email_type || this.meta.email_type === "resend") {
const resend = new Resend(process.env.RESEND_API_KEY);
try {
return await resend.emails.send(emailBody);
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
if (this.meta.email_type === "smtp") {
// Configure the SMTP transporter using environment variables
const transporter = getSMTPTransport(this.meta);
try {
return await resend.emails.send(this.transformData(data));
} catch (error) {
console.error("Error sending webhook", error);
return error;
const mailOptions = {
from: emailBody.from, // sender address
to: Array.isArray(emailBody.to) ? emailBody.to.join(",") : emailBody.to, // recipient address(es)
subject: emailBody.subject, // email subject
text: emailBody.text, // plain text body
html: emailBody.html // HTML body (if any)
};
try {
return await transporter.sendMail(mailOptions);
} catch (error) {
console.error("Error sending email via SMTP", error);
return error;
}
}
}
}
+1 -7
View File
@@ -10,13 +10,7 @@ class Notification {
constructor(trigger, siteData, monitorData) {
let trigger_meta = JSON.parse(trigger.trigger_meta);
if (trigger.trigger_type === "webhook") {
this.client = new Webhook(
trigger_meta.url,
trigger_meta.headers,
"POST",
siteData,
monitorData
);
this.client = new Webhook(trigger_meta, "POST", siteData, monitorData);
} else if (trigger.trigger_type === "discord") {
this.client = new Discord(trigger_meta.url, siteData, monitorData);
} else if (trigger.trigger_type === "slack") {
+26
View File
@@ -0,0 +1,26 @@
// @ts-nocheck
import nodemailer from "nodemailer";
import { HashString } from "../controllers/controller.js";
const transports = {};
export default function getSMTPTransport(meta) {
//convert meta to string and generate has id
let transportId = "smtp_" + HashString(JSON.stringify(meta));
if (!!transports[transportId]) {
return transports[transportId];
}
const transporter = nodemailer.createTransport({
host: meta.smtp_host,
port: Number(meta.smtp_port) || 587,
secure: meta.smtp_secure, // true for 465, false for other ports
auth: {
user: meta.smtp_user,
pass: meta.smtp_pass
}
});
transports[transportId] = transporter;
return transporter;
}
+39 -7
View File
@@ -8,14 +8,16 @@ class Webhook {
method;
siteData;
monitorData;
trigger_meta;
constructor(url, headers, method, siteData, monitorData) {
constructor(trigger_meta, method, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener/3.0.0"
};
this.url = url;
let headers = trigger_meta.headers;
this.trigger_meta = trigger_meta;
this.url = trigger_meta.url;
this.headers = kenerHeader;
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
@@ -25,19 +27,49 @@ class Webhook {
this.siteData = siteData;
this.monitorData = monitorData;
let envSecrets = GetRequiredSecrets(`${this.url} ${JSON.stringify(this.headers)}`);
let envSecrets = GetRequiredSecrets(
`${this.url} ${JSON.stringify(this.headers)} ${JSON.stringify(this.trigger_meta.webhook_body)}`
);
//replace secrets in url and headers
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.url = ReplaceAllOccurrences(this.url, secret.key, secret.value);
this.url = ReplaceAllOccurrences(this.url, secret.find, secret.replace);
this.headers = JSON.parse(
ReplaceAllOccurrences(JSON.stringify(this.headers), secret.find, secret.replace)
);
}
if (!!this.trigger_meta.has_webhook_body && !!this.trigger_meta.webhook_body) {
envSecrets = GetRequiredSecrets(this.trigger_meta.webhook_body);
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.trigger_meta.webhook_body = ReplaceAllOccurrences(
this.trigger_meta.webhook_body,
secret.find,
secret.replace
);
}
}
}
transformData(data) {
return data;
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
let body = this.trigger_meta.webhook_body;
body = ReplaceAllOccurrences(body, "${id}", data.id);
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
body = ReplaceAllOccurrences(body, "${severity}", data.severity);
body = ReplaceAllOccurrences(body, "${status}", data.status);
body = ReplaceAllOccurrences(body, "${source}", data.source);
body = ReplaceAllOccurrences(body, "${timestamp}", data.timestamp);
body = ReplaceAllOccurrences(body, "${description}", data.description);
body = ReplaceAllOccurrences(body, "${metric}", data.details.metric);
body = ReplaceAllOccurrences(body, "${current_value}", data.details.current_value);
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
return body;
}
type() {
@@ -49,7 +81,7 @@ class Webhook {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.transformData(data))
body: this.transformData(data)
});
return response;
} catch (error) {
-1
View File
@@ -137,7 +137,6 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
_90Day[ts].timestamp = ts;
_90Day[ts].cssClass = cssClass;
_90Day[ts].summaryStatus = l(lang, summaryTime(summaryStatus), {
status: l(lang, summaryStatus),
duration: summaryDuration
+87
View File
@@ -0,0 +1,87 @@
// @ts-nocheck
import net from "net"; // Use import instead of require
import ping from "ping";
const TCP = function (type, host, port, timeout) {
port = parseInt(port, 10);
return new Promise((resolve) => {
const socket = new net.Socket();
const start = process.hrtime.bigint(); // High-precision timestamp
let resolved = false;
const onFinish = (status) => {
if (!resolved) {
resolved = true;
const end = process.hrtime.bigint();
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
socket.destroy();
resolve({ status, latency, host, port, type });
}
};
socket.setTimeout(timeout);
socket.once("connect", () => onFinish("open"));
socket.once("timeout", () => onFinish("timeout"));
socket.once("error", () => onFinish("error"));
// Check if it's an IPv6 address (contains ':')
const options = {
host,
port,
family: type === "IP6" ? 6 : 4
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
socket.connect(options);
});
};
const Ping = async function (type, host, timeout, count) {
let output = {
alive: false,
min: null,
max: null,
avg: null,
latencies: [],
latency: null,
host: host,
type: type
};
try {
let res = await ping.promise.probe(host, {
v6: type === "IP6",
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
min_reply: count
});
output.alive = res.alive;
output.min = res.min;
output.max = res.max;
output.avg = res.avg;
output.latencies = res.times;
output.latency = res.time;
} catch (error) {
console.log(`Error in pingCall IP4 for ${host}`, error);
}
return output;
};
/**
* @param {string} input
*/
function ExtractIPv6HostAndPort(input) {
const parts = input.split(":"); // Split by colons
// If there's a valid port at the end, extract it
const lastPart = parts[parts.length - 1];
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
// Reconstruct the IPv6 address
const host = parts.join(":");
// Ensure it's a valid IPv6 format
if (host.includes(":")) {
return { host, port }; // Port may be null if not present
}
return null; // Return null if the format is incorrect
}
export { TCP, ExtractIPv6HostAndPort, Ping };
+5 -1
View File
@@ -292,6 +292,9 @@ function GenerateRandomColor() {
function Wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function MaskString(str) {
return "*".repeat(str.length - 4) + str.slice(-4);
}
export {
IsValidURL,
IsValidHTTPMethod,
@@ -317,5 +320,6 @@ export {
ValidateMonitorAlerts,
GenerateRandomColor,
BeginningOfMinute,
Wait
Wait,
MaskString
};
+166 -176
View File
@@ -1,195 +1,185 @@
<script>
import "../../app.postcss";
import "../../kener.css";
import "../../docs.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 = [];
import "../../app.postcss"
import "../../kener.css"
import "../../docs.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 setTheme() {
document.documentElement.classList.add("dark");
}
function setTheme() {
document.documentElement.classList.add("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 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;
}
}
function pageChange(e) {
if (e.detail.docFilePath) {
activateSidebar(e.detail.docFilePath)
}
}
function updateTableOfContents(e) {
if (e.detail.rightbar) {
tableOfContents = e.detail.rightbar
}
}
onMount(() => {
setTheme();
});
onMount(() => {
setTheme()
})
</script>
<svelte:window on:pagechange={pageChange} on:rightbar={updateTableOfContents} />
<svelte:head>
<link rel="icon" id="kener-app-favicon" href="{base}/logo96.png" />
<link rel="icon" id="kener-app-favicon" href="{base}/logo96.png" />
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q3MLRXCBFT"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q3MLRXCBFT"></script>
<script>
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments)
}
gtag("js", new Date())
gtag("config", "G-Q3MLRXCBFT");
</script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/atom-one-dark.min.css"
/>
gtag("config", "G-Q3MLRXCBFT")
</script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/atom-one-dark.min.css"
/>
<!-- Highlight.js JS -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"
></script>
<!-- Highlight.js JS -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"
></script>
</svelte:head>
<div class="dark">
<nav class="z-2 fixed left-0 right-0 top-0 z-30 h-16 bg-card">
<div class="mx-auto h-full border-b bg-card 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-medium">Kener Documentation</span>
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium">
v3.0.2
</span>
</a>
</div>
<nav class="z-2 fixed left-0 right-0 top-0 z-30 h-16 bg-card">
<div class="mx-auto h-full border-b bg-card 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-medium">Kener Documentation</span>
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.0.11 </span>
</a>
</div>
<!-- Navigation Links -->
<div class="hidden md:block">
<div class="flex items-center space-x-8">
<a href="https://github.com/rajnandan1/kener" class="text-sm font-medium">
<img
alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/rajnandan1/kener?label=Star%20Repo&style=social"
/>
</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>
<!-- Navigation Links -->
<div class="hidden md:block">
<div class="flex items-center space-x-8">
<a href="https://github.com/rajnandan1/kener" class="text-sm font-medium">
<img
alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/rajnandan1/kener?label=Star%20Repo&style=social"
/>
</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>
<!-- 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>
<!-- 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="z-2 fixed bottom-0 left-0 top-16 w-72 overflow-y-auto">
<nav class="border-r bg-card 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.startsWith("/") ? base + child.link : child.link}
class="sidebar-item group flex items-center rounded-md px-3 py-2 text-sm font-medium {!!child.active
? 'active'
: ''}"
>
{child.title}
</a>
{/each}
</div>
</div>
{/each}
</nav>
</aside>
<!-- Sidebar -->
<aside class="z-2 fixed bottom-0 left-0 top-16 w-72 overflow-y-auto">
<nav class="border-r bg-card 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.startsWith("/") ? base + child.link : child.link}
class="sidebar-item group flex items-center rounded-md px-3 py-2 text-sm font-medium {!!child.active
? 'active'
: ''}"
>
{child.title}
</a>
{/each}
</div>
</div>
{/each}
</nav>
</aside>
<!-- Main Content -->
<main class="z-2 dark relative 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 prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
<slot />
</div>
</div>
</main>
{#if tableOfContents.length > 0}
<div
class="blurry-bg 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}
<!-- Main Content -->
<main class="z-2 dark relative 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 prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
<slot />
</div>
</div>
</main>
{#if tableOfContents.length > 0}
<div
class="blurry-bg 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>
+8
View File
@@ -0,0 +1,8 @@
// @ts-nocheck
import { redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
export async function load({ parent, url }) {
throw redirect(302, base + "/docs/home");
}
@@ -49,6 +49,7 @@
<svelte:head>
<title>{data.title}</title>
<meta name="description" content={data.description} />
<link rel="canonical" href="https://kener.ing/docs{data.docFilePath.split('.md')[0]}" />
</svelte:head>
<div id="markdown">
+12 -12
View File
@@ -43,7 +43,7 @@
* @param {string} locale
*/
function setLanguage(locale) {
document.cookie = `localLang=${locale};max-age=${60 * 60 * 24 * 365 * 30}`;
document.cookie = `localLang=${locale};max-age=${60 * 60 * 24 * 365 * 30};path=${base ? "base" : "/"};`;
if (locale === defaultLocaleKey) return;
defaultLocaleValue = allLocales[locale];
analyticsEvent("language_change", {
@@ -58,7 +58,12 @@
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (localTz != data.localTz) {
if (data.isBot === false) {
document.cookie = "localTz=" + localTz + ";max-age=" + 60 * 60 * 24 * 365 * 30;
document.cookie =
"localTz=" +
localTz +
";max-age=" +
60 * 60 * 24 * 365 * 30 +
`;path=${base ? "base" : "/"};`;
location.reload();
}
}
@@ -128,18 +133,13 @@
<svelte:window on:analyticsEvent={captureAnalytics} />
<svelte:head>
<title>{data.site.title}</title>
{#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 data.site.favicon}
<link rel="icon" id="kener-app-favicon" href={base + data.site.favicon} />
{/if}
<link href={data.site.font.cssSrc} rel="stylesheet" />
{#if data.site.metaTags}
{#each data.site.metaTags as metaTag}
<meta name={metaTag.key} content={metaTag.value} />
{/each}
{#if !!data.site.font.cssSrc}
<link href={data.site.font.cssSrc} rel="stylesheet" />
{/if}
{#if hasConfiguredAnalytics}
<script src="https://unpkg.com/analytics/dist/analytics.min.js"></script>
{#each data.site.analytics as { id, type, script }}
+39 -4
View File
@@ -3,14 +3,24 @@ import { FetchData } from "$lib/server/page";
import { GetMonitors, GetIncidentsOpenHome } from "$lib/server/controllers/controller.js";
import { SortMonitor } from "$lib/clientTools.js";
import moment from "moment";
function removeTags(str) {
if (str === null || str === "") return false;
else str = str.toString();
// Regular expression to identify HTML tags in
// the input string. Replacing the identified
// HTML tag with a null string.
return str.replace(/(<([^>]+)>)/gi, "");
}
export async function load({ parent, url }) {
let monitors = await GetMonitors({ status: "ACTIVE" });
const query = url.searchParams;
const requiredCategory = query.get("category") || "Home";
const parentData = await parent();
const siteData = parentData.site;
let pageTitle = siteData.title;
let canonical = siteData.siteURL;
let pageDescription = siteData.metaTags.find((tag) => tag.key === "description").value;
monitors = SortMonitor(siteData.monitorSort, monitors);
const monitorsActive = [];
for (let i = 0; i < monitors.length; i++) {
@@ -51,6 +61,21 @@ export async function load({ parent, url }) {
//if not home page
let isCategoryPage = !!query.get("category") && query.get("category") !== "Home";
let isMonitorPage = !!query.get("monitor");
if (isMonitorPage && monitorsActive.length > 0) {
pageTitle = monitorsActive[0].name + " - " + pageTitle;
pageDescription = monitorsActive[0].description;
canonical = canonical + "?monitor=" + monitorsActive[0].tag;
}
//if category page
if (isCategoryPage) {
let allCategories = siteData.categories;
let selectedCategory = allCategories.find((category) => category.name === requiredCategory);
if (selectedCategory) {
pageTitle = selectedCategory.name + " - " + pageTitle;
pageDescription = selectedCategory.description;
canonical = canonical + "?category=" + requiredCategory;
}
}
if (isCategoryPage || isMonitorPage) {
let eligibleTags = monitorsActive.map((monitor) => monitor.tag);
//filter incidents that have monitor_tag in monitors
@@ -84,12 +109,22 @@ export async function load({ parent, url }) {
});
return incident;
});
let unresolvedIncidents = allOpenIncidents.filter((incident) => incident.state !== "RESOLVED");
let allRecentIncidents = allOpenIncidents.filter(
(incident) => incident.incident_type == "INCIDENT"
);
let allRecentMaintenances = allOpenIncidents.filter(
(incident) => incident.incident_type == "MAINTENANCE"
);
return {
monitors: monitorsActive,
unresolvedIncidents: allOpenIncidents,
allRecentIncidents,
allRecentMaintenances,
categoryName: requiredCategory,
isCategoryPage: isCategoryPage,
isMonitorPage: isMonitorPage
isMonitorPage: isMonitorPage,
pageTitle: pageTitle,
pageDescription: removeTags(pageDescription),
canonical
};
}
+113 -23
View File
@@ -11,6 +11,7 @@
import { onMount } from "svelte";
import ShareMenu from "$lib/components/shareMenu.svelte";
import { scale } from "svelte/transition";
import { format } from "date-fns";
export let data;
let shareMenusToggle = false;
@@ -34,34 +35,74 @@
if (data.isCategoryPage) {
let category = data.site.categories.find((e) => e.name == data.categoryName);
data.site.hero.title = category.name;
data.site.hero.subtitle = category.description;
if (!!category) {
data.site.hero.title = category.name;
data.site.hero.subtitle = category.description;
}
} else if (data.isMonitorPage) {
let monitor = data.monitors[0];
if (!!monitor) {
data.site.hero.title = monitor.name;
data.site.hero.subtitle = monitor.description;
data.site.hero.image = monitor.image;
}
}
onMount(() => {
pageLoaded = true;
});
let kindFilter = "INCIDENT";
function kindOfIncidents(kind) {
kindFilter = kind;
}
if (data.allRecentIncidents.length == 0) {
kindOfIncidents("MAINTENANCE");
}
</script>
<svelte:head>
<title>{data.pageTitle}</title>
{#if !!data.pageDescription}
<meta name="description" content={data.pageDescription} />
{/if}
{#if !!data.canonical}
<link rel="canonical" href={data.canonical} />
{/if}
{#if data.site.metaTags}
{#each data.site.metaTags as metaTag}
{#if metaTag.key != "description"}
<meta name={metaTag.key} content={metaTag.value} />
{/if}
{/each}
{/if}
</svelte:head>
<div class="mt-12"></div>
{#if data.site.hero && !data.isMonitorPage}
{#if data.site.hero}
<section
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center"
>
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center">
{#if data.site.hero.image}
<img src={data.site.hero.image} class="m-auto h-16 w-16" alt="" srcset="" />
<img
src={base + data.site.hero.image}
class="m-auto mb-2 h-14 w-14"
alt=""
srcset=""
/>
{/if}
{#if data.site.hero.title}
<h1
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-tight text-transparent"
>
{data.site.hero.title}
{@html data.site.hero.title}
</h1>
{/if}
{#if data.site.hero.subtitle}
<p class="mx-auto mt-4 max-w-xl sm:text-xl">{data.site.hero.subtitle}</p>
<h2 class="mx-auto mt-4 max-w-xl sm:text-xl">
{@html data.site.hero.subtitle}
</h2>
{/if}
</div>
</div>
@@ -88,16 +129,50 @@
</Button>
</section>
{/if}
{#if data.unresolvedIncidents.length > 0}
{#if data.allRecentIncidents.length + data.allRecentMaintenances.length > 0}
<section
class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
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" class="border-0 pl-0">
{l(data.lang, "Ongoing Incidents")}
</Badge>
<div class="col-span-2 flex gap-x-2 text-center md:text-left">
{#if kindFilter == "INCIDENT"}
{#if data.allRecentIncidents.length > 0}
<Button
class="h-8 text-sm "
on:click={() => kindOfIncidents("INCIDENT")}
>
{l(data.lang, "Recent Incidents")}
</Button>
{/if}
{#if data.allRecentMaintenances.length > 0}
<Button
variant="secondary"
class=" h-8 text-sm"
on:click={() => kindOfIncidents("MAINTENANCE")}
>
{l(data.lang, "Recent Maintenances")}
</Button>
{/if}
{:else}
{#if data.allRecentIncidents.length > 0}
<Button
variant="secondary"
class="h-8 text-sm "
on:click={() => kindOfIncidents("INCIDENT")}
>
{l(data.lang, "Recent Incidents")}
</Button>
{/if}
{#if data.allRecentMaintenances.length > 0}
<Button
class="h-8 text-sm"
on:click={() => kindOfIncidents("MAINTENANCE")}
>
{l(data.lang, "Recent Maintenances")}
</Button>
{/if}
{/if}
</div>
</div>
</section>
@@ -106,8 +181,19 @@
id=""
>
<Card.Root class="w-full">
<Card.Content class=" newincidents w-full overflow-hidden p-0">
{#each data.unresolvedIncidents as incident, index}
{#if kindFilter == "INCIDENT"}
<Card.Content class=" newincidents w-full overflow-hidden p-0">
{#each data.allRecentIncidents as incident, index}
<Incident
{incident}
lang={data.lang}
index="incident-{index}"
selectedLang={data.selectedLang}
/>
{/each}
</Card.Content>
{:else if kindFilter == "MAINTENANCE"}
{#each data.allRecentMaintenances as incident, index}
<Incident
{incident}
lang={data.lang}
@@ -115,7 +201,7 @@
selectedLang={data.selectedLang}
/>
{/each}
</Card.Content>
{/if}
</Card.Root>
</section>
{/if}
@@ -178,12 +264,14 @@
class="relative z-10 mx-auto mb-8 w-full max-w-[890px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
>
{#each data.site.categories.filter((e) => e.name != "Home") as category}
<div
on:click={() => {
<a
href={`?category=${category.name}`}
on:click={(e) => {
e.preventDefault();
window.location.href = `?category=${category.name}`;
}}
>
<Card.Root class="hover:bg-secondary">
<Card.Root class="mb-4 hover:bg-secondary">
<Card.Header class="bounce-right relative w-full cursor-pointer px-4 ">
<Card.Title class="w-full ">
{category.name}
@@ -202,7 +290,7 @@
</Card.Description>
</Card.Header>
</Card.Root>
</div>
</a>
{/each}
</section>
{/if}
@@ -210,21 +298,23 @@
class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<div
on:click={() => {
window.location.href = `${base}/incidents`;
<a
href="{base}/incidents/{format(new Date(), 'MMMM-yyyy')}"
on:click={(e) => {
e.preventDefault();
window.location.href = `${base}/incidents/${format(new Date(), "MMMM-yyyy")}`;
}}
class="bounce-right grid w-full cursor-pointer grid-cols-2 justify-between gap-4 rounded-md border bg-card px-4 py-2 text-sm font-medium hover:bg-secondary"
>
<div class="col-span-1 text-left">
{l(data.lang, "Recent Incidents")}
{l(data.lang, "Browse Events")}
</div>
<div class="text-right">
<span class="arrow float-right mt-0.5">
<ArrowRight class="h-4 w-4 text-muted-foreground hover:text-primary" />
</span>
</div>
</div>
</a>
</section>
{#if shareMenusToggle}
<div
@@ -21,7 +21,7 @@ import { f } from "$lib/i18n/client";
export async function load({ parent, url, params, cookies }) {
const parentData = await parent();
const siteData = parentData.site;
let monitors = await GetMonitors({ status: "ACTIVE" });
const query = url.searchParams;
let todayStart = BeginningOfDay({ timeZone: parentData.localTz });
@@ -60,6 +60,7 @@ export async function load({ parent, url, params, cookies }) {
incidents: incidents,
nextMonthName: f(addMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en"),
prevMonthName: f(subMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en"),
thisMonthName: f(parse(month, "MMMM-yyyy", new Date()), "MMMM-yyyy", "en")
thisMonthName: f(parse(month, "MMMM-yyyy", new Date()), "MMMM-yyyy", "en"),
canonical: siteData.siteURL + "/incidents/" + month
};
}
@@ -48,8 +48,25 @@
//sort the incidentSmartDates ascending
let sortedIncidentSmartDates = Object.keys(incidentSmartDates).sort((a, b) => a - b);
let canonical = data.canonical;
</script>
<svelte:head>
<title>
{f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang)}
{l(data.lang, "Incident Updates")} |
{data.site.title}
</title>
<meta
name="description"
content="{l(data.lang, 'Incident Updates')} | {l(data.lang, 'Recent Incidents')} | {l(
data.lang,
'Recent Maintenances'
)} {f(parse(data.thisMonthName, 'MMMM-yyyy', new Date()), 'MMMM, yyyy', selectedLang)} |
{data.site.title}"
/>
<link rel="canonical" href={canonical} />
</svelte:head>
<div class="mt-12"></div>
<section class="mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<Button
@@ -98,8 +115,12 @@
</div>
<h1 class=" text-xl font-semibold leading-tight">
{l(data.lang, "No Incident On %date", {
date: data.thisMonthName.replace("-", ", ")
{l(data.lang, "No Incident in %date", {
date: f(
parse(data.thisMonthName, "MMMM-yyyy", new Date()),
"MMMM, yyyy",
selectedLang
)
})}
</h1>
</div>
@@ -9,7 +9,6 @@
<ModeWatcher />
<svelte:head>
<title>Setup Kener</title>
<meta name="description" content="Set up Kener" />
<meta name="robots" content="noindex" />
<link rel="icon" href="{base}/logo96.png" />
@@ -1,6 +1,7 @@
import dotenv from "dotenv";
dotenv.config();
import db from "$lib/server/db/db.js";
import { GetSMTPFromENV } from "$lib/server/controllers/controller.js";
export async function load({ params, route, url, parent }) {
//read query parameters
@@ -14,6 +15,7 @@ export async function load({ params, route, url, parent }) {
isSecretSet: !!process.env.KENER_SECRET_KEY,
isResendSet: !!process.env.RESEND_API_KEY && !!process.env.RESEND_SENDER_EMAIL,
isSiteURLSet: !!siteURL.value,
isSMTPSet: !!GetSMTPFromENV(),
view,
token,
email
@@ -9,11 +9,14 @@
email: ""
};
export let data;
if (data.isSecretSet === false || data.isResendSet === false) {
let isResendSet = data.isResendSet && data.isSecretSet;
if (!isResendSet && !data.isSMTPSet) {
data.view = "error";
data.error =
"Environment variables(RESEND_API_KEY, RESEND_SENDER_EMAIL) are not set. Read the documentation to set them. https://kener.ing/docs/environment-vars";
data.error = `<p>Environment variables are not set. Read the documentation to set them. https://kener.ing/docs/environment-vars</p>
<br/>
<p>Either Set RESEND_API_KEY, RESEND_SENDER_EMAIL <br>or<br> Set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM_EMAIL</p>
<br/>
<p>If both are set then SMTP will take priority</p>`;
}
if (data.isSiteURLSet === false) {
@@ -24,7 +27,7 @@
</script>
<svelte:head>
<title>Forgot password Kener</title>
<title>Forgot Password Kener</title>
</svelte:head>
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
@@ -37,7 +40,7 @@
{#if data.view == "error"}
<Alert.Root variant="destructive" class="my-4">
<Alert.Title>Error</Alert.Title>
<Alert.Description>{data.error}</Alert.Description>
<Alert.Description>{@html data.error}</Alert.Description>
</Alert.Root>
{/if}
{#if data.view == "forgot"}
@@ -3,11 +3,13 @@ import { json, redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import db from "$lib/server/db/db.js";
import { Resend } from "resend";
import getSMTPTransport from "$lib/server/notification/smtps.js";
import {
HashPassword,
GenerateSalt,
GenerateToken,
VerifyToken
VerifyToken,
GetSMTPFromENV
} from "$lib/server/controllers/controller.js";
export async function POST({ request, cookies }) {
@@ -48,15 +50,40 @@ export async function POST({ request, cookies }) {
</body>
</html>
`;
let emailText = `
Click on the link below to reset your password:
${link}
This link will expire in 1 hour.
If you did not request a password reset, please ignore this email.
`;
let mail = {
from: senderEmail,
to: [email],
subject: subject,
text: emailText,
html: message
};
const resend = new Resend(resendKey);
await resend.emails.send(mail);
let smtpData = GetSMTPFromENV();
if (!!smtpData) {
const transporter = getSMTPTransport(smtpData);
const mailOptions = {
from: smtpData.smtp_from_email,
to: email,
subject: mail.subject,
html: mail.html,
text: mail.text
};
try {
await transporter.sendMail(mailOptions);
} catch (error) {
console.error("Error sending email via SMTP", error);
return;
}
} else {
const resend = new Resend(resendKey);
await resend.emails.send(mail);
}
throw redirect(302, base + "/manage/forgot?view=sent&email=" + email);
}
@@ -2,11 +2,9 @@
import { GetAllSiteData, VerifyToken } from "$lib/server/controllers/controller.js";
import { redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import { MaskString } from "$lib/server/tool.js";
import db from "$lib/server/db/db.js";
//write a function to mask a string, just have last 4 characters visible
function maskString(str) {
return "*".repeat(str.length - 4) + str.slice(-4);
}
export async function load({ params, route, url, cookies, request }) {
let siteData = await GetAllSiteData();
@@ -33,14 +31,10 @@ export async function load({ params, route, url, cookies, request }) {
throw redirect(302, base + "/manage/signin");
}
//
return {
siteData,
GH_TOKEN: !!process.env.GH_TOKEN ? maskString(process.env.GH_TOKEN) : "",
KENER_SECRET_KEY: !!process.env.KENER_SECRET_KEY
? maskString(process.env.KENER_SECRET_KEY)
: "",
RESEND_API_KEY: !!process.env.RESEND_API_KEY ? maskString(process.env.RESEND_API_KEY) : "",
RESEND_SENDER_EMAIL: process.env.RESEND_SENDER_EMAIL
? MaskString(process.env.KENER_SECRET_KEY)
: ""
};
}
@@ -45,7 +45,7 @@
id: "/(manage)/manage/(app)/app/alerts"
},
{
name: "Incidents",
name: "Events",
url: `${base}/manage/app/incidents`,
id: "/(manage)/manage/(app)/app/incidents"
},
@@ -5,7 +5,7 @@
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import * as Alert from "$lib/components/ui/alert";
import * as RadioGroup from "$lib/components/ui/radio-group";
import moment from "moment";
import { DateInput } from "date-picker-svelte";
import { clickOutsideAction, slide } from "svelte-legos";
@@ -81,7 +81,6 @@
}
return i;
});
totalPages = Math.ceil(resp.total.count / limit);
} catch (error) {
alert("Error: " + error);
@@ -102,9 +101,12 @@
endDatetime: null,
startDatetime: null,
start_date_time: null,
endDatetime: null,
ent_date_time: null,
status: "OPEN",
state: "INVESTIGATING",
firstComment: ""
firstComment: "",
incident_type: "INCIDENT"
};
}
@@ -120,7 +122,8 @@
end_date_time: newIncident.endDatetime,
status: newIncident.status,
state: newIncident.state,
id: newIncident.id
id: newIncident.id,
incident_type: newIncident.incident_type
};
//convert data.start_date_time to timestamp
if (!!!toPost.start_date_time) {
@@ -128,12 +131,14 @@
return;
}
toPost.start_date_time = parseInt(new Date(toPost.start_date_time).getTime() / 1000);
if (!!!toPost.end_date_time) {
delete toPost.end_date_time;
} else {
if (!!toPost.end_date_time) {
toPost.end_date_time = parseInt(new Date(toPost.end_date_time).getTime() / 1000);
}
if (toPost.incident_type == "MAINTENANCE") {
toPost.state = "RESOLVED";
}
formStateCreate = "loading";
try {
let data = await fetch(base + "/manage/app/api/", {
@@ -155,7 +160,7 @@
if (!!!newIncident.id) {
newComment.comment = newIncident.firstComment;
newComment.id = 0;
newComment.state = newIncident.state;
newComment.state = toPost.state;
newComment.commented_at = newIncident.startDatetime;
await addNewComment(resp.incident_id);
@@ -403,8 +408,10 @@
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="mx-2">
{#if loadingData}
<Loader class="ml-2 mt-2 inline h-6 w-6 animate-spin" />
<Loader class="float-right ml-2 mt-2 inline h-6 w-6 animate-spin" />
{/if}
</div>
<Button
@@ -414,7 +421,7 @@
}}
>
<Plus class="mr-2 inline h-6 w-6" />
New Incident
New Event
</Button>
</div>
@@ -563,9 +570,17 @@
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
<span class="badge-{incident.state} rounded px-1.5 py-1"
>{incident.state}</span
>
{#if incident.incident_type == "MAINTENANCE"}
<span class="badge-MAINTENANCE rounded px-1.5 py-1">
MAINTENANCE
</span>
{:else}
<span
class="badge-{incident.state} rounded px-1.5 py-1"
>
{incident.state}
</span>
{/if}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
<div class="flex gap-x-1.5">
@@ -639,21 +654,53 @@
<div class="rounded-md border p-4">
<div>
{#if newIncident.id}
<h2 class="text-lg font-medium">Edit Incident</h2>
<h2 class="text-lg font-medium">Edit Event</h2>
{:else}
<h2 class="text-lg font-medium">Add New Incident</h2>
<h2 class="text-lg font-medium">Add New Event</h2>
{/if}
</div>
<p class="mt-4 text-sm font-medium">Event Type</p>
<div class="mt-2 flex gap-4">
<RadioGroup.Root
class=" flex gap-x-2 {!!newIncident.id ? 'opacity-70' : ''}"
bind:value={newIncident.incident_type}
disabled={!!newIncident.id}
>
<Label
for="type-INCIDENT"
class="flex cursor-pointer items-center space-x-2 rounded-md border {newIncident.incident_type ==
'INCIDENT'
? 'bg-secondary shadow-md'
: ''} p-3"
>
<RadioGroup.Item value="INCIDENT" id="type-INCIDENT" />
<span>Incident</span>
</Label>
<Label
for="type-MAINTENANCE"
class="flex cursor-pointer items-center space-x-2 rounded-md border p-3 {newIncident.incident_type ==
'MAINTENANCE'
? 'bg-secondary shadow-md'
: ''}"
>
<RadioGroup.Item value="MAINTENANCE" id="type-MAINTENANCE" />
<span> Maintenance </span>
</Label>
</RadioGroup.Root>
</div>
<div class="mt-4 flex flex-row gap-4">
<div class="w-full">
<Label class="text-sm">
Incident Title
<span class="capitalize">{newIncident.incident_type}</span>
Title
<span class="text-red-500">*</span>
<span
class="float-right mt-2 text-xs font-semibold badge-{newIncident.state}"
>
{newIncident.state}
</span>
{#if newIncident.incident_type == "INCIDENT"}
<span
class="float-right mt-2 text-xs font-semibold badge-{newIncident.state}"
>
{newIncident.state}
</span>
{/if}
</Label>
<Input
class="mt-2"
@@ -667,7 +714,8 @@
<div class="mt-4 flex flex-row gap-4">
<div class="w-full">
<Label class="text-sm">
Incident Summary
<span class="capitalize">{newIncident.incident_type}</span>
Summary
<span class="text-red-500">*</span>
</Label>
<Input
@@ -678,20 +726,37 @@
</div>
</div>
{/if}
<div class="mt-4 flex gap-4">
<div class="col-span-1">
<Label class="mb-2 text-sm" for="start_date_time">
Incident Start Date Time
<span class="capitalize">{newIncident.incident_type}</span> Start
Date Time
<span class="text-red-500">*</span>
</Label>
<DateInput
bind:value={newIncident.startDatetime}
id="start_date_time"
timePrecision="minute"
disabled={!!newIncident.id}
class="mt-2 text-sm"
/>
</div>
{#if newIncident.incident_type == "MAINTENANCE"}
<div class="col-span-1">
<Label class="mb-2 text-sm" for="start_date_time">
<span class="capitalize">{newIncident.incident_type}</span> End
Date Time
<span class="text-red-500">*</span>
</Label>
<DateInput
bind:value={newIncident.endDatetime}
id="end_date_time"
timePrecision="minute"
class="mt-2 text-sm"
min={newIncident.startDatetime}
/>
</div>
{/if}
</div>
<div class="mt-4 grid h-16 w-full grid-cols-6 gap-2 border-t pt-4">
@@ -708,7 +773,9 @@
newIncident.title.trim().length == 0 ||
!!!newIncident.startDatetime ||
(!!!newIncident.id &&
newIncident.firstComment.trim().length == 0)}
newIncident.firstComment.trim().length == 0) ||
(!!!newIncident.endDatetime &&
newIncident.incident_type == "MAINTENANCE")}
>
Save
{#if formStateCreate === "loading"}
@@ -730,42 +797,44 @@
addNewComment();
}}
>
<div
class="bg-hover state-{newComment.state} mt-2 grid grid-cols-4 overflow-hidden rounded-md border text-xs font-medium"
>
{#if currentIncident.incident_type == "INCIDENT"}
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("INVESTIGATING");
}}
class="bg-hover state-{newComment.state} mt-2 grid grid-cols-4 overflow-hidden rounded-md border text-xs font-medium"
>
INVESTIGATING
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("INVESTIGATING");
}}
>
INVESTIGATING
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("IDENTIFIED");
}}
>
IDENTIFIED
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("MONITORING");
}}
>
MONITORING
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("RESOLVED");
}}
>
RESOLVED
</div>
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("IDENTIFIED");
}}
>
IDENTIFIED
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("MONITORING");
}}
>
MONITORING
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("RESOLVED");
}}
>
RESOLVED
</div>
</div>
{/if}
<div class="mt-4 flex w-full gap-4">
<div class="text-sm font-medium leading-7">Time Stamp</div>
<DateInput
@@ -840,9 +909,11 @@
<div
class="text-xs font-semibold text-muted-foreground"
>
<span class="badge-{comment.state}">
{comment.state}
</span>
{#if currentIncident.incident_type == "INCIDENT"}
<span class="badge-{comment.state}">
{comment.state}
</span>
{/if}
{moment(comment.commented_at * 1000).format(
"YYYY-MM-DD HH:mm:ss"
)}
@@ -0,0 +1,26 @@
// @ts-nocheck
import { GetSMTPFromENV } from "$lib/server/controllers/controller.js";
import { MaskString } from "$lib/server/tool.js";
export async function load({ parent }) {
let preferredModeEmail = "resend";
let fromEmail = "";
let isResendKeySet = !!process.env.RESEND_API_KEY;
let isResendSenderEmailSet = !!process.env.RESEND_SENDER_EMAIL;
if (isResendKeySet && isResendSenderEmailSet) {
fromEmail = process.env.RESEND_SENDER_EMAIL;
}
let smtp = GetSMTPFromENV();
if (!!smtp) {
preferredModeEmail = "smtp";
fromEmail = smtp.smtp_from_email;
smtp.smtp_pass = "$SMTP_PASS";
}
return {
fromEmail,
preferredModeEmail,
RESEND_API_KEY: isResendKeySet ? MaskString(process.env.RESEND_API_KEY) : "",
smtp
};
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 84 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+25 -1
View File
@@ -2,6 +2,8 @@ import { vitePreprocess } from "@sveltejs/kit/vite";
import adapter from "@sveltejs/adapter-node";
const basePath = !!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "";
const VITE_BUILD_ENV = process.env.VITE_BUILD_ENV || "development"; // Default to "development"
const isProduction = VITE_BUILD_ENV === "production";
/** @type {import('@sveltejs/kit').Config} */
const config = {
@@ -12,7 +14,29 @@ const config = {
}
},
preprocess: [vitePreprocess({})]
preprocess: [vitePreprocess({})],
compilerOptions: {
dev: !isProduction, // Disable dev mode in production
enableSourcemap: !isProduction // Disable sourcemaps in production
},
onwarn: (warning, handler) => {
// Suppress specific warnings in production
const ignoredWarnings = [
"a11y-", // Accessibility warnings
"unused-export-let", // Suppresses "unused export property" warnings
"empty-chunk", // Suppresses empty chunk warnings
"module-unused-import", // Suppresses unused imports like "default" from auto-animate
"conflicting-svelte-resolve" // Suppresses conflicting resolve warnings
];
if (isProduction && ignoredWarnings.some((w) => warning.code && warning.code.startsWith(w))) {
return; // Ignore these warnings in production builds
}
handler(warning); // Otherwise, show the warning
}
};
export default config;
+28 -3
View File
@@ -7,8 +7,33 @@ dotenv.config();
const PORT = Number(process.env.PORT) || 3000;
const base = process.env.KENER_BASE_PATH || "";
export default defineConfig({
plugins: [sveltekit()],
const VITE_BUILD_ENV = process.env.VITE_BUILD_ENV || "development"; // Default to "development"
const isProduction = VITE_BUILD_ENV === "production";
export default defineConfig(({ mode }) => ({
plugins: [
sveltekit({
compilerOptions: {
dev: mode === "development"
},
onwarn: (warning, handler) => {
// Suppress specific warnings in production
const ignoredWarnings = [
"a11y-", // Accessibility warnings
"unused-export-let", // Suppresses "unused export property" warnings
"empty-chunk", // Suppresses empty chunk warnings
"module-unused-import", // Suppresses unused imports like "default" from auto-animate
"conflicting-svelte-resolve" // Suppresses conflicting resolve warnings
];
if (isProduction && ignoredWarnings.some((w) => warning.code && warning.code.startsWith(w))) {
return; // Ignore these warnings in production builds
}
handler(warning);
}
})
],
server: {
port: PORT,
watch: {
@@ -16,4 +41,4 @@ export default defineConfig({
}
},
assetsInclude: ["**/*.yaml"]
});
}));