mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
526 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca4f7040e8 | |||
| 1cfa861d7e | |||
| 007baefe80 | |||
| 7d77f4d996 | |||
| 3d5a62c085 | |||
| 24ffef997b | |||
| 7d5275152c | |||
| 93167c2358 | |||
| 734d3ad280 | |||
| 753668dfa5 | |||
| 351817338d | |||
| 7c12218489 | |||
| 7c71298227 | |||
| 167db6d7e6 | |||
| 74834f3559 | |||
| e13ca5c639 | |||
| a51806d62b | |||
| 57238dfb72 | |||
| a2be642af0 | |||
| aec2d23c2f | |||
| 11a11162da | |||
| 12a7be497e | |||
| 9b69da66a5 | |||
| ee831f96bb | |||
| 5542790145 | |||
| 3b1091a34f | |||
| eebe6541c3 | |||
| e308089cfa | |||
| 5132d6894e | |||
| 006d1e1fa0 | |||
| 4c6dc24355 | |||
| acf227a947 | |||
| 3b05ca73e5 | |||
| 236d7ea585 | |||
| 87408d22c4 | |||
| ea6754775d | |||
| 3210f4d406 | |||
| 16ac97a9c9 | |||
| 8857a51dde | |||
| fc2abe7f16 | |||
| f04a930e13 | |||
| 1de1e2d6ba | |||
| 0208e4baf9 | |||
| 21af2a6d02 | |||
| 2fbe301a51 | |||
| 6d5382982c | |||
| a20b4ee570 | |||
| 909ff9e070 | |||
| 7d3e265ece | |||
| 78d8f311c0 | |||
| c52ba95fd0 | |||
| 9462a0331a | |||
| 4e3d1838c0 | |||
| aad7b5c6ea | |||
| abec24d65b | |||
| e0c3d1864c | |||
| bd25c6fe05 | |||
| 4ec5e14431 | |||
| c9b8c5bec3 | |||
| 223883adf7 | |||
| 83c2d7ff79 | |||
| cf33f5a2f6 | |||
| 92b4596924 | |||
| ce9b748570 | |||
| 87a9069ab4 | |||
| 3e6a76c8c3 | |||
| 3509955374 | |||
| 225711f95d | |||
| 5436647cd3 | |||
| fbd9da3b33 | |||
| edb6705ac8 | |||
| 382a2a5bcc | |||
| b5967607d2 | |||
| 03bbb6c3b6 | |||
| 6054be0ff1 | |||
| ff7355bcc2 | |||
| 23a9124e03 | |||
| ac41807cdd | |||
| d95bb13e6e | |||
| b9d58ba3c1 | |||
| a648b29f31 | |||
| e702feb04f | |||
| ce83355e58 | |||
| 683beed3b7 | |||
| c9095036eb | |||
| 29ec935d67 | |||
| 339015a093 | |||
| b8b5b4170f | |||
| 8a104e4480 | |||
| 89e1a5e88a | |||
| 8e5746d068 | |||
| 1df7c6560a | |||
| 0ff4546379 | |||
| ab036c968d | |||
| 625ece2e7b | |||
| 41842cc1b9 | |||
| e63992a7a5 | |||
| 7ae5d8f1b1 | |||
| 0a34f2138a | |||
| 38b326fe72 | |||
| 8785c609a9 | |||
| 1a67560137 | |||
| 6c7534b9b6 | |||
| ae6c2c985f | |||
| 940331b87f | |||
| e6b5600a47 | |||
| ce2a6ab756 | |||
| 6af91af639 | |||
| e1ff156b91 | |||
| b56d20ac35 | |||
| 9ee7a7b861 | |||
| f73ffce3f0 | |||
| 0997967787 | |||
| 3476ec2b29 | |||
| e0cf568aa9 | |||
| c12d7f8345 | |||
| 9ba115d5ab | |||
| 3756bbfd06 | |||
| 9096bcc641 | |||
| c4d04854e5 | |||
| d95fc0005b | |||
| 2b9093184a | |||
| 831376054b | |||
| 885ee0b6d6 | |||
| 511d0f26dd | |||
| 5263698625 | |||
| 7859f9564e | |||
| 149456682d | |||
| 8c040997da | |||
| 868fed0176 | |||
| c7f44a48da | |||
| 6300bff4e7 | |||
| 3e6b1ffdbb | |||
| 408fd5318b | |||
| cfe419f6df | |||
| 5403726099 | |||
| 057c14909f | |||
| 37b18c06bb | |||
| 3ad1eb9a03 | |||
| a1064771f8 | |||
| 463ee286bf | |||
| a7ae6e72ef | |||
| 36c67f60dd | |||
| bb718b04a6 | |||
| b6ea060054 | |||
| 5dba54c048 | |||
| 415276163f | |||
| 017b9e2e84 | |||
| 5740991ee1 | |||
| 06008bd719 | |||
| b53cd485e5 | |||
| 7c1fd39158 | |||
| f7888f1ba4 | |||
| ece5ac37ad | |||
| a51183fb23 | |||
| 7875192fb0 | |||
| eb128ad431 | |||
| ba61ed87ab | |||
| 8b5348081d | |||
| bb7e69ace5 | |||
| 3b8bb60423 | |||
| 93339ce9df | |||
| d30d99d8c2 | |||
| 3468df16e1 | |||
| ccdeff98dc | |||
| 7e182b1df2 | |||
| ff652ad435 | |||
| 1fdafa7966 | |||
| 1f5e683af5 | |||
| db8c881571 | |||
| 14f49c5b3b | |||
| 52286f26a5 | |||
| 45e7567cb6 | |||
| c0edb59884 | |||
| 1e86c429ca | |||
| fbaebdcbcf | |||
| 3f9716b0a4 | |||
| 57d32197cf | |||
| 9e6a3f26c2 | |||
| 23314beb15 | |||
| 64b94da585 | |||
| 1609b4fc50 | |||
| dfffffb101 | |||
| 81250a117a | |||
| 72bd0241e9 | |||
| 63c24f147f | |||
| 88fb7df3f5 | |||
| 320b1a0cc5 | |||
| 08b168ee6e | |||
| d6a87ac81a | |||
| 615dba42b8 | |||
| 3ffec4f1fe | |||
| 1288b463f1 | |||
| 1aed6d0703 | |||
| 69f1e69140 | |||
| ab4ecd29c6 | |||
| 1d76c8a3d3 | |||
| b1c2cedbac | |||
| 60b4b6f207 | |||
| b9f5eb56c5 | |||
| 7b9ae73044 | |||
| 87f2c33eba | |||
| 0a73a8b10a | |||
| af65404fd3 | |||
| 398f891dac | |||
| 6a2375a774 | |||
| 43bbcf4015 | |||
| fadb563337 | |||
| 1ae54b3906 | |||
| 1c069e2ee2 | |||
| ed1099ac11 | |||
| 99d3a7e046 | |||
| 2144acf34d | |||
| 7a8ad8e833 | |||
| 3b45f33692 | |||
| b4a2340ec7 | |||
| 5910e3b930 | |||
| fd58beaa69 | |||
| 5449e422a5 | |||
| dfb1784b6f | |||
| d956c60b48 | |||
| 69e1f2af6e | |||
| 7cc2f5ed4b | |||
| ce2b17a5c9 | |||
| 0fcc60bf65 | |||
| 45ac25055b | |||
| d3e201f2e4 | |||
| 693735dc2e | |||
| a4fa85dd79 | |||
| 29f70860ef | |||
| ae882f6afc | |||
| 57941f67ce | |||
| ab356d516b | |||
| d11b3f1a99 | |||
| 83ba08dd6b | |||
| 34cc00a1f6 | |||
| 7b86429056 | |||
| e67c1b1539 | |||
| e501f8e05d | |||
| 9e40d89317 | |||
| 5d475ab23f | |||
| 01cb39e18e | |||
| 0d0ef25970 | |||
| 6c766e2001 | |||
| 1c73166120 | |||
| fc5dbc85dc | |||
| 622327cdf6 | |||
| b0a4cd5c42 | |||
| 8c95c94472 | |||
| 7dafb2eddc | |||
| 7c9f3eb87f | |||
| ab34dd81f8 | |||
| 4e5fb26be1 | |||
| 6fc80bc0fc | |||
| 292667ac29 | |||
| 8c03058f8d | |||
| 25e73d097c | |||
| 1970cd8891 | |||
| 8ae9b2dec1 | |||
| 1662608984 | |||
| 42693983f1 | |||
| f43048d783 | |||
| b5ec332b4a | |||
| dab1c44b18 | |||
| d9c0bff780 | |||
| beceace2c2 | |||
| 5373636704 | |||
| e8d04eccf6 | |||
| d978c82263 | |||
| 883a458ed3 | |||
| 9009d9df99 | |||
| 81c0fa1243 | |||
| 01221616ae | |||
| be75a0f5e9 | |||
| 1b40839490 | |||
| 80f5780602 | |||
| dcf1817cd6 | |||
| ae251803ad | |||
| f6d9627ceb | |||
| 0cbc6b7b56 | |||
| f0af2d14e4 | |||
| b797039144 | |||
| 86e29e5065 | |||
| fddef22e8e | |||
| ad7551b7af | |||
| a8be878a87 | |||
| 2b1849f1a3 | |||
| 25a6590324 | |||
| fa251dc07f | |||
| 4de93e7e95 | |||
| a5e5f33dc8 | |||
| f09eb18bcc | |||
| 5c65e35f24 | |||
| 8cbe859971 | |||
| 67f38db847 | |||
| 28a72a3592 | |||
| dae334694d | |||
| e57fe3a046 | |||
| 54670c7845 | |||
| 3dbcef1500 | |||
| 24bbad05cb | |||
| cacbda3164 | |||
| da9b1a3a64 | |||
| 394b911eca | |||
| 220778e84f | |||
| c6e343da5d | |||
| 268e5b7478 | |||
| b9ccd16a02 | |||
| 302ad0e8a6 | |||
| e052f435db | |||
| caa1330f39 | |||
| 870d19f566 | |||
| a28f787833 | |||
| 335a51b589 | |||
| 72f9471486 | |||
| f0cd101af5 | |||
| 52876bd77e | |||
| aabe1926bd | |||
| e785227064 | |||
| 0cb88eec3b | |||
| 8cd2914f73 | |||
| f70e2ee8eb | |||
| c60a21ef32 | |||
| 77a57ee609 | |||
| fc15f0e083 | |||
| 6115beece3 | |||
| 54434fbe78 | |||
| 83755bdd25 | |||
| 08f901c5f0 | |||
| 06910fbd4d | |||
| 154e7dd185 | |||
| 8bde3226bf | |||
| e5565145b5 | |||
| 37a667daff | |||
| eda98bacfc | |||
| ee1ee52e13 | |||
| 4060094404 | |||
| d03bf41ad1 | |||
| ba2fe24629 | |||
| 547116090a | |||
| 974826f42d | |||
| 4914b029f9 | |||
| ff864fbaab | |||
| f583ba4938 | |||
| d552f541ac | |||
| d46d02e37a | |||
| 4e0c6e85da | |||
| 1147808366 | |||
| a7c27a60e0 | |||
| 974976bd90 | |||
| 9bbe665984 | |||
| d0ea8551b6 | |||
| 089ee9bc07 | |||
| 786c4f8207 | |||
| 13879aefdd | |||
| d03a8fd7a4 | |||
| c00aae5566 | |||
| e923f4d650 | |||
| 654c07a364 | |||
| 820eeb0aac | |||
| 2a338baa26 | |||
| ec7351272f | |||
| 13dec43ef3 | |||
| 43673e3349 | |||
| 1e77253a63 | |||
| 798af326a2 | |||
| d349a7591e | |||
| 8370145f6c | |||
| 304945acea | |||
| 1dd05fee50 | |||
| 374eda4103 | |||
| 52a8cca3e8 | |||
| 22ef4d75b6 | |||
| 534e6a0ad3 | |||
| 80ed4fc5fa | |||
| 445eb02386 | |||
| 53ae0b89b0 | |||
| c7376b4d8f | |||
| e3e59b7e24 | |||
| 07f59ac581 | |||
| 7176c3d4f4 | |||
| 13366284c6 | |||
| 0f0b447137 | |||
| f7cc28c896 | |||
| fd790003d1 | |||
| fdad329148 | |||
| 73bf5f3fbe | |||
| 103d64a659 | |||
| 92c4d35992 | |||
| 3b1d95b71b | |||
| 4fd9bf2bb6 | |||
| ce96b6f55d | |||
| 54bbc1dd00 | |||
| ffa31bcacc | |||
| 559f5bd257 | |||
| 977e49e1ce | |||
| 30cb707436 | |||
| ae439633b9 | |||
| 7f33f6ddfd | |||
| 8404415a93 | |||
| 5ddddf8b5d | |||
| 927db19cc6 | |||
| eccff16c5f | |||
| 7be9c62c7c | |||
| 76ce14e8b1 | |||
| fd0074c0b0 | |||
| ad01cdb5ac | |||
| 1b4ca67cf0 | |||
| 53936e19d4 | |||
| b58d00c5a6 | |||
| 41db0c45cd | |||
| 5361b551eb | |||
| eb87647466 | |||
| d9b900f28d | |||
| 198bd6c723 | |||
| 1831e6309f | |||
| 007f5149d2 | |||
| 1a094e3511 | |||
| db1ddc1292 | |||
| 535e3bb36a | |||
| 785237e55e | |||
| e6bf47a859 | |||
| 93ae1711f1 | |||
| af97812beb | |||
| 78cf22ade9 | |||
| b4a461d93a | |||
| 5f33c02064 | |||
| 46c1a392b8 | |||
| c6880c5df6 | |||
| 646da94ef9 | |||
| 36ede93dce | |||
| 9ed35589f7 | |||
| e946dd18e0 | |||
| 51cad598c1 | |||
| 0595f23c18 | |||
| b88816706b | |||
| 80edaae023 | |||
| 883c46b064 | |||
| 4248cc31f2 | |||
| 5f81c6d61a | |||
| 00d82cc817 | |||
| a9edfde162 | |||
| 468d7f445d | |||
| 4cc74deece | |||
| f9831490af | |||
| a055a616eb | |||
| 8009a2cbc4 | |||
| 693cc149cf | |||
| d126fe0bce | |||
| 4cc49cce9e | |||
| f7cb4cd805 | |||
| a2bd9883d1 | |||
| 10a079c304 | |||
| 65f12f1082 | |||
| c15f855c72 | |||
| 3d38483022 | |||
| 57dc1e7175 | |||
| ead68c929f | |||
| d54b346259 | |||
| a05de8269c | |||
| 130b70c6af | |||
| 7247a54c08 | |||
| 28de7ebff8 | |||
| e2c9d4dc2d | |||
| 4866382180 | |||
| 1b1507db1a | |||
| 0ea7d687a7 | |||
| 502e1539ca | |||
| f3910fea1b | |||
| ee9b48fb79 | |||
| 142686ef92 | |||
| 2aa187984e | |||
| c20d4d71c3 | |||
| 4f4d1e1b39 | |||
| f38b590f70 | |||
| c2caf42fd3 | |||
| 8c279c8c10 | |||
| 3480c78360 | |||
| 79ce708f60 | |||
| a4526eb4b5 | |||
| 866a133917 | |||
| adce978244 | |||
| dd3c26b29a | |||
| 25ad42ba9b | |||
| 3285b7e472 | |||
| c7da55480a | |||
| 35e81b5782 | |||
| 769a9526e7 | |||
| 5470153f4f | |||
| 9d6ad87275 | |||
| ac6261d9ac | |||
| 48effe077e | |||
| c656bf8612 | |||
| 9daf7c7156 | |||
| 4bd3bfae9d | |||
| 727964949c | |||
| 21e1fc1aca | |||
| eeba2ef2d4 | |||
| b3e738ce03 | |||
| 46848c7b19 | |||
| 5f3d48597f | |||
| 0efc3e5999 | |||
| 0c0ce89317 | |||
| 4a47061f76 | |||
| 9b3ff9b362 | |||
| 8315fede7d | |||
| d6d0568f67 | |||
| ff0119db9b | |||
| 37d776c315 | |||
| f53d4abd8b | |||
| f827bd4aeb | |||
| f28c4e96c5 | |||
| cf81b11c0b | |||
| 17dc752902 | |||
| bc9faf9456 | |||
| e5b615267c | |||
| 70e9086646 | |||
| 0cc30bc67e | |||
| 73792e7ce6 | |||
| 32f873d9c2 | |||
| 6566bc5f8f | |||
| cc93114eab | |||
| 6d5d949f5c | |||
| f1be4a4db0 | |||
| 8b9f576b30 | |||
| 0735f959ef |
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
dist
|
||||
build
|
||||
.env
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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
|
||||
|
||||
# CSS & PostCSS files
|
||||
[*.{css,postcss}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Markdown files
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
print_width = 180
|
||||
|
||||
# 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
|
||||
+26
-7
@@ -1,7 +1,26 @@
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
GH_TOKEN=your_github_token
|
||||
API_TOKEN=your_api_token
|
||||
API_IP=""
|
||||
API_IP_REGEX=""
|
||||
KENER_BASE_PATH=""
|
||||
TZ=Etc/UTC
|
||||
KENER_SECRET_KEY=please_change_me_to_something_secure
|
||||
|
||||
# For SQLite database...
|
||||
DATABASE_URL=sqlite://./database/kener.sqlite.db
|
||||
|
||||
# For PostgreSQL database...
|
||||
# DATABASE_URL=postgresql://db_user:db_password@localhost:5432/kener_db
|
||||
# POSTGRES_PASSWORD=some_super_random_secure_password
|
||||
|
||||
# For MySQL database...
|
||||
# DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/kener_db
|
||||
# MYSQL_PASSWORD=some_super_random_secure_password
|
||||
|
||||
KENER_BASE_PATH=""
|
||||
ORIGIN=http://localhost:3000
|
||||
|
||||
RESEND_API_KEY=""
|
||||
RESEND_SENDER_EMAIL=Accounts <accounts@resend.dev>
|
||||
|
||||
# Likely no need to change...
|
||||
# NODE_ENV=production # already defined in container
|
||||
# PORT=3000 # default port Kener service is exposed upon
|
||||
|
||||
# Add the below variable if you would like to ‘white-label’ the product (aka. remove some of the attributions scattered throughout the app)
|
||||
# WHITE_LABEL=true
|
||||
@@ -0,0 +1,48 @@
|
||||
# Contributing to Kener
|
||||
|
||||
Thank you for considering contributing to our project! Here are some guidelines to help you get started.
|
||||
|
||||
---
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. Fork the repository and clone it locally.
|
||||
2. Create a new branch for your feature or bug fix:
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
3. Make your changes and commit them:
|
||||
```bash
|
||||
git commit -m 'Describe your changes'
|
||||
```
|
||||
4. Push your changes to your fork:
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
5. Create a pull request to the `main` branch.
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
2. Create a `.env` file in the root of the project and add the following:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
2. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
3. Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is available in the `docs` folder. You can view it by going to [http://localhost:3000/docs/home](http://localhost:3000/docs/home) in your browser.
|
||||
|
||||
## Where to Start
|
||||
|
||||
1. Check out the [roadmap items](https://kener.ing/docs/roadmap/)
|
||||
2. Add language support by following the [i18n guide](https://kener.ing/docs/i18n/)
|
||||
@@ -17,22 +17,20 @@ 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
|
||||
|
||||
**Database**
|
||||
Which database you are using. `sqlite`, `mysql`, `postgres`
|
||||
|
||||
**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.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
name: Create Incident Template
|
||||
about: Create Incident Template
|
||||
title: Title of Incident
|
||||
labels: incident
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Your Incident Description goes here. Markdown Supported
|
||||
|
||||
[start_datetime:utcSeconds]
|
||||
[end_datetime:utcSeconds]
|
||||
@@ -0,0 +1,47 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Track base image versions via .env.build
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
file-patterns:
|
||||
- ".env.build"
|
||||
- "node:*" # Ensures Node.js images are correctly detected
|
||||
|
||||
# Monitor OS package versions in Dockerfile (Debian/Alpine)
|
||||
- package-ecosystem: "gitsubmodule" # Alternative method to track OS packages in Dockerfile
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "os-packages"
|
||||
commit-message:
|
||||
prefix: "os"
|
||||
include: "scope"
|
||||
|
||||
# Monitor Node.js dependencies from package.json
|
||||
# TODO: Uncomment below if we want to begin letting Dependabot monitor & open PRs for Node.js project dependencies
|
||||
# - package-ecosystem: "npm"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# labels:
|
||||
# - "dependencies"
|
||||
# - "npm"
|
||||
# commit-message:
|
||||
# prefix: "npm"
|
||||
# include: "scope"
|
||||
|
||||
# Monitor GitHub Actions dependencies
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
commit-message:
|
||||
prefix: "actions"
|
||||
include: "scope"
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Generate README
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'README.template.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'README.template.md'
|
||||
workflow_dispatch: # Allows for manual execution
|
||||
workflow_run: # Triggers this workflow to run when it recognizes 'publish-images' workflow has ran and successfully completed
|
||||
workflows: ["Publish Docker Image to Registries"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: write # Explicitly allow pushing changes
|
||||
|
||||
jobs:
|
||||
generate-readme:
|
||||
name: Generate README from template
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0 # Fetch full history, including tags
|
||||
persist-credentials: false # We'll manually authenticate
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "github-actions"
|
||||
git config --global user.email "github-actions@github.com"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install mustache dotenv
|
||||
|
||||
- name: Extract Major and Major-Minor Versions
|
||||
run: |
|
||||
VERSION="${{ vars.BUILD_VERSION }}"
|
||||
|
||||
# Check if VERSION is empty and set a fallback value
|
||||
if [ -z "$VERSION" ]; then
|
||||
# Fetch the latest release using Git
|
||||
VERSION=$(git tag -l --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || echo "3.1.0")
|
||||
fi
|
||||
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
|
||||
|
||||
echo "Full version: $VERSION"
|
||||
echo "Major version: $MAJOR"
|
||||
echo "Major-Minor version: $MAJOR_MINOR"
|
||||
|
||||
# Export all as environment variables
|
||||
echo "LATEST_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "LATEST_MAJOR_VERSION=$MAJOR" >> $GITHUB_ENV
|
||||
echo "LATEST_MAJOR_MINOR_VERSION=$MAJOR_MINOR" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate README.md
|
||||
env:
|
||||
BUILD_FULL_VERSION: ${{ env.LATEST_VERSION }} # e.g., 1.2.3
|
||||
BUILD_MAJOR_VERSION: ${{ env.LATEST_MAJOR_VERSION}} # e.g., 1
|
||||
BUILD_MAJOR_MINOR_VERSION: ${{ env.LATEST_MAJOR_MINOR_VERSION }} # e.g., 1.2
|
||||
run: node scripts/generate-readme.js
|
||||
|
||||
- name: Commit and Push Changes
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
git add README.md
|
||||
git commit -m "Auto-generate README.md with release versions" || echo "No changes to commit"
|
||||
git push https://x-access-token:${{ secrets.GH_PAT }}@github.com/${{ github.repository }}.git HEAD:main
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Prevent Direct README Changes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "README.md"
|
||||
|
||||
jobs:
|
||||
check-readme:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Detect direct README changes
|
||||
run: |
|
||||
if git diff --name-only origin/main | grep -q "README.md"; then
|
||||
echo "❌ Direct modifications to README.md are not allowed!"
|
||||
echo "Please update README.template.md instead."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,156 @@
|
||||
name: Publish Docker Image to Registries
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published # Runs only when a GitHub Release is published
|
||||
workflow_dispatch: # Allows for manual execution
|
||||
|
||||
env:
|
||||
ALPINE_VERSION: "23-alpine"
|
||||
DEBIAN_VERSION: "23-slim"
|
||||
# Registry URLs
|
||||
DOCKERHUB_REGISTRY: docker.io
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
# Docker Hub image name (using Docker Hub username)
|
||||
DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
|
||||
# GitHub image name (formatted as `account/repo`)
|
||||
GITHUB_IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
check-lockfile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '23' # Adjust as needed
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check for lock-file changes
|
||||
run: |
|
||||
git diff --exit-code package-lock.json || (
|
||||
echo "🫥 package-lock.json is outdated or missing. Please run 'npm install' and commit the updated lockfile."
|
||||
exit 1
|
||||
)
|
||||
|
||||
build-and-push-to-registries:
|
||||
needs: check-lockfile # Runs only after `check-lockfile` completes successfully
|
||||
name: Push Docker images to Docker Hub and GitHub Container Registry
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [alpine, debian]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
packages: write
|
||||
# This is used to complete the identity challenge with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
# Install the cosign tool (except on PR)
|
||||
# https://github.com/sigstore/cosign-installer
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@v3.8.0
|
||||
with:
|
||||
cosign-release: 'v2.2.4'
|
||||
|
||||
# Set up BuildKit Docker container builder to be able to build multi-platform images and export cache
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
# Log in to Docker Hub (except on PR)
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# Log in to GitHub Container Registry (except on PR)
|
||||
- name: Log in to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Combined metadata extraction for both registries
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5.6.1
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_IMAGE_NAME }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }}
|
||||
tags: |
|
||||
# Debian Variant Tags
|
||||
type=semver,pattern={{version}},enable=${{ matrix.variant == 'debian' }}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ matrix.variant == 'debian' }}
|
||||
type=semver,pattern={{major}},enable=${{ matrix.variant == 'debian' }}
|
||||
type=raw,value=latest,enable=${{ matrix.variant == 'debian' }}
|
||||
|
||||
# Alpine Variant Tags
|
||||
type=semver,pattern={{version}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
type=semver,pattern={{major}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
type=raw,value=alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
# Build and push Docker image with Buildx to both registries (don't push on PR)
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
VARIANT=${{ matrix.variant }}
|
||||
ALPINE_VERSION=${{ env.ALPINE_VERSION }}
|
||||
DEBIAN_VERSION=${{ env.DEBIAN_VERSION }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# Sign the resulting Docker image digests
|
||||
- name: Sign the published Docker images
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
env:
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
run: |
|
||||
echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
||||
|
||||
# For use in other workflows (e.g. 'generate-readme', etc.)
|
||||
- name: Save Build Version to Repository Variable
|
||||
if: matrix.variant == 'debian' && github.run_attempt == 1
|
||||
run: |
|
||||
# VERSION="${{ steps.meta.outputs.version }}"
|
||||
VERSION=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "")
|
||||
|
||||
# Check if VERSION is empty and set a fallback value
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(git tag -l --sort=-version:refname | grep -E '^(v)?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || echo "3.1.0")
|
||||
fi
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
echo "Setting BUILD_VERSION to $VERSION"
|
||||
gh variable set BUILD_VERSION --body "$VERSION"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
name: Publish Docker image to Dockerhub and GHCR
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "*.*.*"
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'docs/**'
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ vars.DOCKERHUB_IMAGE_NAME != '' || vars.GHCR_IMAGE_NAME != '' }}
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Log in to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_IMAGE_NAME != ''
|
||||
}}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ github.event_name != 'pull_request' && vars.GHCR_IMAGE_NAME != '' }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: |
|
||||
${{ vars.DOCKERHUB_IMAGE_NAME }}
|
||||
${{ vars.GHCR_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ endsWith(github.ref, 'main') }}
|
||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'main') }}
|
||||
type=semver,pattern={{version}}
|
||||
flavor: |
|
||||
latest=false
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' && !env.ACT}}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
+9
-1
@@ -9,6 +9,7 @@ config/server.yaml
|
||||
/src/lib/.kener
|
||||
/package
|
||||
.env
|
||||
.vscode
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
@@ -20,4 +21,11 @@ config/static/*
|
||||
db/*
|
||||
!db/.kener
|
||||
database/*
|
||||
!database/.kener
|
||||
!database/.kener
|
||||
uploads/*
|
||||
!uploads/upload.dir
|
||||
|
||||
static/uploads/*
|
||||
!static/uploads/upload.dir
|
||||
temp.txt
|
||||
temp.js
|
||||
+69
-7
@@ -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": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.ts", "*.tsx", "*.cjs", "*.mjs"],
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.json", ".prettierrc"],
|
||||
"options": {
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": 180
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "Dockerfile",
|
||||
"options": {
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 120
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+127
-59
@@ -1,78 +1,146 @@
|
||||
FROM lsiobase/alpine:3.18 as base
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ENV TZ=Etc/UTC
|
||||
# Global build arguments (defined default values in case `.env.build` isn't loaded)
|
||||
ARG ALPINE_VERSION_TAG=23.7.0-alpine3.21
|
||||
ARG DEBIAN_VERSION_TAG=23.7.0-bookworm-slim
|
||||
ARG VARIANT=debian
|
||||
|
||||
RUN \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache \
|
||||
nodejs \
|
||||
npm \
|
||||
python3 \
|
||||
make \
|
||||
gcc \
|
||||
g++ \
|
||||
sqlite \
|
||||
sqlite-dev \
|
||||
libc6-compat && \
|
||||
echo "**** cleanup ****" && \
|
||||
rm -rf \
|
||||
/root/.cache \
|
||||
/tmp/*
|
||||
#==========================================================#
|
||||
# STAGE 1: BUILD STAGE #
|
||||
#==========================================================#
|
||||
|
||||
# set OS timezone specified by docker ENV
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
FROM node:${DEBIAN_VERSION_TAG} AS builder-debian
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential=12.9 \
|
||||
python3=3.11.2-1+b1 \
|
||||
sqlite3=3.40.1-2+deb12u1 \
|
||||
libsqlite3-dev=3.40.1-2+deb12u1 \
|
||||
make=4.3-4.1 \
|
||||
node-gyp=9.3.0-2 \
|
||||
g++=4:12.2.0-3 \
|
||||
tzdata \
|
||||
iputils-ping=3:20221126-1+deb12u1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ARG data_dir=/config
|
||||
# VOLUME $data_dir
|
||||
# ENV CONFIG_DIR=$data_dir
|
||||
FROM node:${ALPINE_VERSION_TAG} AS builder-alpine
|
||||
RUN apk add --no-cache --update \
|
||||
build-base=0.5-r3 \
|
||||
python3 \
|
||||
py3-pip \
|
||||
make=4.4.1-r2 \
|
||||
g++=14.2.0-r4 \
|
||||
sqlite \
|
||||
sqlite-dev \
|
||||
tzdata \
|
||||
iputils
|
||||
|
||||
COPY docker/root/ /
|
||||
FROM builder-${VARIANT} AS builder
|
||||
|
||||
# build requires devDependencies which are not used by production deploy
|
||||
# so build in a stage so we can copy results to clean "deploy" stage later
|
||||
FROM base as build
|
||||
# Set environment variables
|
||||
ENV NPM_CONFIG_LOGLEVEL=error \
|
||||
VITE_BUILD_ENV=production
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=abc:abc . /app
|
||||
# Copy package files for dependency installation
|
||||
COPY package*.json ./
|
||||
|
||||
RUN \
|
||||
npm install node-gyp -g && \
|
||||
npm_config_build_from_source=true npm install better-sqlite3 && \
|
||||
npm install \
|
||||
&& chown -R root:root node_modules \
|
||||
&& npm run build
|
||||
# Install all dependencies, including `devDependencies` (cache enabled for faster builds)
|
||||
# TODO: Possibly add `--no-audit` flag to `npm ci` to prevent `npm` from running a security audit on installed packages. (By default, `npm install` performs an audit to check for vulnerabilities in dependencies, which can slow down installation. Adding this flag would skip the audit, thus making `npm install` significantly faster for the CI/CD pipeline.)
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci --no-fund && \
|
||||
npm cache clean --force
|
||||
|
||||
FROM base as app
|
||||
# Copy application source code
|
||||
COPY . .
|
||||
|
||||
# copy package, required libs (npm,nodejs) results of build, prod entrypoint, and examples to be used to populate config dir
|
||||
# to clean, new stage
|
||||
COPY --chown=abc:abc package*.json ./
|
||||
COPY --from=base /usr/local/bin /usr/local/bin
|
||||
COPY --from=base /usr/local/lib /usr/local/lib
|
||||
# TODO: Reevaluate permissions (possibly reduce?)...
|
||||
# Remove docs directory and ensure required directories exist
|
||||
RUN rm -rf src/routes/\(docs\) \
|
||||
static/documentation \
|
||||
static/fonts/lato/full && \
|
||||
mkdir -p uploads database && \
|
||||
chmod -R 750 uploads database
|
||||
|
||||
COPY --chown=abc:abc static /app/static
|
||||
COPY --chown=abc:abc database /app/database
|
||||
COPY --chown=abc:abc build.js /app/build.js
|
||||
COPY --chown=abc:abc sitemap.js /app/sitemap.js
|
||||
COPY --chown=abc:abc openapi.json /app/openapi.json
|
||||
COPY --chown=abc:abc src/lib/server /app/src/lib/server
|
||||
# Build the application and remove `devDependencies`
|
||||
RUN npm run build && \
|
||||
npm prune --omit=dev
|
||||
|
||||
COPY --from=build --chown=abc:abc /app/build /app/build
|
||||
COPY --from=build --chown=abc:abc /app/main.js /app/main.js
|
||||
#==========================================================#
|
||||
# STAGE 2: PRODUCTION/FINAL STAGE #
|
||||
#==========================================================#
|
||||
|
||||
ENV NODE_ENV=production
|
||||
FROM node:${DEBIAN_VERSION_TAG} AS final-debian
|
||||
# TODO: Consider adding `--no-install-recommends`, but will need testing (may further help reduce final build size)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
iputils-ping=3:20221126-1+deb12u1 \
|
||||
sqlite3=3.40.1-2+deb12u1 \
|
||||
tzdata \
|
||||
# TODO: Is it ok to change to `curl` here so that we don't have to maintain `wget` version mismatch between Debian architectures? (`curl` is only used for the container healthcheck and because there is an Alpine variant (best!) we probably don't care if the Debian image ends up building bigger due to `curl`.)
|
||||
curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# install prod dependencies and clean cache
|
||||
RUN npm install --omit=dev \
|
||||
&& npm cache clean --force \
|
||||
&& chown -R abc:abc node_modules
|
||||
FROM node:${ALPINE_VERSION_TAG} AS final-alpine
|
||||
RUN apk add --no-cache --update \
|
||||
iputils \
|
||||
sqlite \
|
||||
tzdata
|
||||
|
||||
ARG webPort=3000
|
||||
ENV PORT=$webPort
|
||||
FROM final-${VARIANT} AS final
|
||||
|
||||
ARG PORT=3000 \
|
||||
USERNAME=node
|
||||
|
||||
# Set environment variables
|
||||
ENV HEALTHCHECK_PORT=$PORT \
|
||||
HEALTHCHECK_PATH= \
|
||||
NODE_ENV=production \
|
||||
NPM_CONFIG_LOGLEVEL=error \
|
||||
PORT=$PORT \
|
||||
TZ=Etc/UTC
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files build artifacts, and necessary files from builder stage
|
||||
COPY --chown=node:node --from=builder /app/src/lib/ ./src/lib/
|
||||
COPY --chown=node:node --from=builder /app/build ./build
|
||||
COPY --chown=node:node --from=builder /app/uploads ./uploads
|
||||
COPY --chown=node:node --from=builder /app/database ./database
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/templates ./build/server/templates
|
||||
# TODO: Consider changing from copying `node_modules` to instead letting `npm ci --omit=dev` handle production dependencies. Right now, copying `node_modules` is leading to a smaller image, whereas letting `npm ci` handle the install in final image is slightly faster, but leads to larger image size. IMO, having a slightly longer build time (e.g. ~10 sec.) is better in the end to have a smaller image.
|
||||
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
|
||||
COPY --chown=node:node --from=builder /app/migrations ./migrations
|
||||
COPY --chown=node:node --from=builder /app/seeds ./seeds
|
||||
COPY --chown=node:node --from=builder /app/static ./static
|
||||
COPY --chown=node:node --from=builder /app/entrypoint.sh ./entrypoint.sh
|
||||
COPY --chown=node:node --from=builder /app/knexfile.js ./knexfile.js
|
||||
COPY --chown=node:node --from=builder /app/main.js ./main.js
|
||||
COPY --chown=node:node --from=builder /app/openapi.json ./openapi.json
|
||||
COPY --chown=node:node --from=builder /app/openapi.yaml ./openapi.yaml
|
||||
COPY --chown=node:node --from=builder /app/package.json ./package.json
|
||||
|
||||
# Set container timezone and make entrypoint script executable
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
|
||||
chmod +x ./entrypoint.sh
|
||||
# TODO: To improve security, consider dropping unnecessary capabilities instead of granting image all network capabilities of host. (Maybe `setcap cap_net_raw+p /usr/bin/ping`, etc.) Could also drop all and then grant only the capabilities that are explicitly needed. Some examples are commented out below...
|
||||
# setcap cap_net_bind_service=+ep /usr/local/bin/node
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/ping
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/ping6
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/tracepath
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/clockdiff
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE $PORT
|
||||
|
||||
# leave entrypoint blank!
|
||||
# uses LSIO s6-init entrypoint with scripts
|
||||
# that populate CONFIG_DIR with static dir, monitor/site.yaml when dir is empty
|
||||
# and chown's all files so they are owned by proper user based on PUID/GUID env
|
||||
# Add a healthcheck to the container; `wget` vs. `curl` depending on base image. Using this approach because `wget` does not actually maintain versioning across architectures, so we cannot pin a `wget` version (in above `final-debian` base, `apt-get install`) between differing architectures (e.g. arm64, amd64)
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD sh -c 'if [ -f "/etc/alpine-release" ]; then wget --quiet --spider http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; else curl --silent --head --fail http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; fi'
|
||||
|
||||
# TODO: Revisit letting user define $PUID & $PGID overrides (e.g. `addgroup -g $PGID newgroup && adduser -D -G newgroup -u $PUID node`) as well as potentially ensure no root user exists. (Make sure no processes are running as root, first!)
|
||||
# Use a non-root user (recommended for security)
|
||||
USER $USERNAME
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["node", "main"]
|
||||
@@ -1,16 +1,33 @@
|
||||
# Kener - A Sveltekit NodeJS Status Page System
|
||||
# Kener - Stunning Status Pages
|
||||
|
||||
<p align="center">
|
||||
<img src="https://kener.ing/newbg.png" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
|
||||
<img src="https://kener.ing/newbg.png?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
<img alt="GitHub Repo Updated" src="https://badges.pufler.dev/updated/rajnandan1/kener">
|
||||
<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://hub.docker.com/r/rajnandan1/kener"><img src="https://img.shields.io/docker/pulls/rajnandan1/kener" alt="Docker Kener" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/latest?logo=docker&logoColor=white&label=debian" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/alpine?logo=docker&logoColor=white&label=alpine" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rajnandan1/kener/actions/workflows/publish-images.yml"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/rajnandan1/kener/publish-images.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">
|
||||
@@ -26,90 +43,175 @@
|
||||
</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?
|
||||
|
||||
Kener: Open-source sveltekit status page system, crafted with lot of thought so that it looks modern.
|
||||
**Kener** is a sleek and lightweight status page system built with **SvelteKit** and **NodeJS**. It’s not here to replace heavyweights like Datadog or Atlassian but rather to offer a simple, modern, and hassle-free way to set up a great-looking status page with minimal effort.
|
||||
|
||||
It does not aim to replace the Datadogs of the world. It simply tries to help someone come with a status page for the world.
|
||||
Designed with **ease of use** and **customization in mind**, Kener provides all the essential features you’d expect from a status page—without unnecessary complexity.
|
||||
|
||||
### Why Kener?
|
||||
|
||||
✅ Minimal overhead – Set up quickly with a clean, modern UI<br>
|
||||
✅ Customizable – Easily tailor it to match your brand<br>
|
||||
✅ Open-source & free – Because great tools should be accessible to everyone
|
||||
|
||||
### What's in a Name?
|
||||
|
||||
“Kener” is inspired by the Assamese word _“Kene”_, meaning _“how’s it going?”_. The _‘.ing’_ was added because, well… that domain was available. 😄
|
||||
|
||||
## 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
|
||||
|
||||
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
|
||||
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.17"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.17-blue" alt="Kener latest stable version: 3.2.17" /></a>
|
||||
|
||||
#### Available Tags
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Image Tag</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" colspan="2" style="color:#A81D33;text-align:left;">Debian 12 <small>(Bookwork Slim)</small> w/ Node.js v23.7.0 <strong><em>(default)</em></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
|
||||
<td>Latest stable release (aka 3.2.17)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.17" target="_blank"><code>3.2.17</code></a></td>
|
||||
<td>Specific release version</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2" target="_blank"><code>3.2</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.17) release within that minor version (3.2.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.17) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 <strong><em>(smallest image size)</em></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
|
||||
<td>Latest stable release (aka 3.2.17)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.17-alpine" target="_blank"><code>3.2.17-alpine</code></a></td>
|
||||
<td>Specific release version</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2-alpine" target="_blank"><code>3.2-alpine</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.17) release within that minor version (3.2.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.17) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### Usage
|
||||
|
||||
Pull the latest stable version:
|
||||
|
||||
```sh
|
||||
docker pull rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
Or use the smaller, Alpine-based variant:
|
||||
|
||||
```sh
|
||||
docker pull rajnandan1/kener:alpine
|
||||
```
|
||||
|
||||
For a production setup, refer to the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
|
||||
This keeps things clean, structured, and easy to read while preserving all the details.
|
||||
|
||||
### One Click
|
||||
|
||||
[](https://railway.com/template/spSvic?referralCode=1Pn7vs)
|
||||
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing is added to make cooler.
|
||||
<div align="left">
|
||||
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
Here are some of the features that you get out of the box. Please read the documentation to know how to use them.
|
||||
|
||||
### Monitoring and Tracking
|
||||
### 📊 Monitoring and Tracking
|
||||
|
||||
- Real-time monitoring
|
||||
- Polls HTTP endpoint or Push data to monitor using Rest APIs
|
||||
- Handles Timezones for visitors
|
||||
- Categorize Monitors into different Sections
|
||||
- Cron-based scheduling for monitors. Minimum per minute
|
||||
- Flexible monitor configuration using YAML. Define your own parsing for monitor being UP/DOWN/DEGRADED
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors. Example defaultStatus=DOWN if you don't hit API per minute with Status UP
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
- Supports webhooks/discord/slack for notifications
|
||||
- Advanced **application performance monitoring** tools
|
||||
- **Real-time network monitoring** capabilities
|
||||
- Supports **polling HTTP endpoints** or **pushing data** via REST APIs
|
||||
- **Timezone auto-adjustment** for visitors
|
||||
- Organize monitors into **custom sections**
|
||||
- **Cron-based scheduling** (minimum: **every minute**)
|
||||
- **Create complex API polls** (chaining, secrets, etc.)
|
||||
- Set a **default status** for monitors
|
||||
- Supports **base path hosting in Kubernetes (k8s)**
|
||||
- **Pre-built Docker images** for easy deployment
|
||||
|
||||
### Customization and Branding
|
||||
### 🎨 Customization and Branding
|
||||
|
||||
- Customizable status page using yaml or code
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
- Fully **customizable status page**
|
||||
- **Badge generation** for status and uptime tracking
|
||||
- Support for **custom domains**
|
||||
- Embed monitors as **iframes or widgets**
|
||||
- **Light & Dark Mode**
|
||||
- **Internationalization (i18n) support**
|
||||
- **Sleek, beautifully crafted UI**
|
||||
|
||||
### Incident Management
|
||||
### 🚨 Incident Management
|
||||
|
||||
- Create Incidents using Github Issues - Rich Text
|
||||
- Or use APIs to create Incidents
|
||||
- **Incident tracking & communication** tools
|
||||
- **Comprehensive APIs** for incident management
|
||||
|
||||
### User Experience and Design
|
||||
### 🧑💻 User Experience and Design
|
||||
|
||||
- 100% Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
- **Accessible & user-friendly interface**
|
||||
- **Quick & easy installation**
|
||||
- **Responsive design** for all devices
|
||||
- **Auto SEO & Social Media ready**
|
||||
- **Server-Side Rendering (SSR) for better performance**
|
||||
|
||||
## Technologies used
|
||||
<div align="left">
|
||||
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
|
||||
</div>
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Inspired from
|
||||
|
||||
- [Upptime](https://upptime.js.org/)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Support Me
|
||||
|
||||
If you are using Kener and want to support me, you can do so by sponsoring me on GitHub or buying me a coffee.
|
||||
If you’re enjoying Kener and want to support its development, consider sponsoring me on GitHub or treating me to a coffee. Your support helps keep the project growing! 🚀
|
||||
|
||||
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
|
||||
|
||||
[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
☕ [Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
|
||||

|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to contribute to Kener, please read the [Contribution Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#rajnandan1/kener&Date)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
# 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">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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://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://hub.docker.com/r/rajnandan1/kener"><img src="https://img.shields.io/docker/pulls/rajnandan1/kener" alt="Docker Kener" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/latest?logo=docker&logoColor=white&label=debian" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/alpine?logo=docker&logoColor=white&label=alpine" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/rajnandan1/kener/actions/workflows/publish-images.yml"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/rajnandan1/kener/publish-images.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">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f514/512.gif" alt="🔔" width="32" height="32">
|
||||
</picture>
|
||||
<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>
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f6a7/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f6a7/512.gif" alt="🚧" width="32" height="32">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
| [🌍 Live Server](https://kener.ing) | [🎉 Quick Start](https://kener.ing/docs/quick-start) | [🗄 Documentation](https://kener.ing/docs/home) |
|
||||
| ----------------------------------- | ---------------------------------------------------- | ----------------------------------------------- |
|
||||
|
||||
## What is Kener?
|
||||
|
||||
**Kener** is a sleek and lightweight status page system built with **SvelteKit** and **NodeJS**. It’s not here to replace heavyweights like Datadog or Atlassian but rather to offer a simple, modern, and hassle-free way to set up a great-looking status page with minimal effort.
|
||||
|
||||
Designed with **ease of use** and **customization in mind**, Kener provides all the essential features you’d expect from a status page—without unnecessary complexity.
|
||||
|
||||
### Why Kener?
|
||||
|
||||
✅ Minimal overhead – Set up quickly with a clean, modern UI<br>
|
||||
✅ Customizable – Easily tailor it to match your brand<br>
|
||||
✅ Open-source & free – Because great tools should be accessible to everyone
|
||||
|
||||
### What's in a Name?
|
||||
|
||||
“Kener” is inspired by the Assamese word _“Kene”_, meaning _“how’s it going?”_. The _‘.ing’_ was added because, well… that domain was available. 😄
|
||||
|
||||
## 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
|
||||
|
||||
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
|
||||
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_full_version}}"><img src="https://img.shields.io/badge/Latest_Stable_Release-{{kener_full_version}}-blue" alt="Kener latest stable version: {{kener_full_version}}" /></a>
|
||||
|
||||
#### Available Tags
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Image Tag</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" colspan="2" style="color:#A81D33;text-align:left;">Debian 12 <small>(Bookwork Slim)</small> w/ Node.js v23.7.0 <strong><em>(default)</em></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
|
||||
<td>Latest stable release (aka {{kener_full_version}})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_full_version}}" target="_blank"><code>{{kener_full_version}}</code></a></td>
|
||||
<td>Specific release version</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_minor_version}}" target="_blank"><code>{{kener_major_minor_version}}</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch ({{kener_full_version}}) release within that minor version ({{kener_major_minor_version}}.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_version}}" target="_blank"><code>{{kener_major_version}}</code></a></td>
|
||||
<td>Major version tag pointing to the latest stable ({{kener_full_version}}) release within that major version ({{kener_major_version}}.x.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 <strong><em>(smallest image size)</em></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
|
||||
<td>Latest stable release (aka {{kener_full_version}})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_full_version}}-alpine" target="_blank"><code>{{kener_full_version}}-alpine</code></a></td>
|
||||
<td>Specific release version</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_minor_version}}-alpine" target="_blank"><code>{{kener_major_minor_version}}-alpine</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch ({{kener_full_version}}) release within that minor version ({{kener_major_minor_version}}.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_version}}-alpine" target="_blank"><code>{{kener_major_version}}-alpine</code></a></td>
|
||||
<td>Major version tag pointing to the latest stable ({{kener_full_version}}) release within that major version ({{kener_major_version}}.x.x)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### Usage
|
||||
|
||||
Pull the latest stable version:
|
||||
|
||||
```sh
|
||||
docker pull rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
Or use the smaller, Alpine-based variant:
|
||||
|
||||
```sh
|
||||
docker pull rajnandan1/kener:alpine
|
||||
```
|
||||
|
||||
For a production setup, refer to the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
|
||||
This keeps things clean, structured, and easy to read while preserving all the details.
|
||||
|
||||
### One Click
|
||||
|
||||
[](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.
|
||||
|
||||
### 📊 Monitoring and Tracking
|
||||
|
||||
- Advanced **application performance monitoring** tools
|
||||
- **Real-time network monitoring** capabilities
|
||||
- Supports **polling HTTP endpoints** or **pushing data** via REST APIs
|
||||
- **Timezone auto-adjustment** for visitors
|
||||
- Organize monitors into **custom sections**
|
||||
- **Cron-based scheduling** (minimum: **every minute**)
|
||||
- **Create complex API polls** (chaining, secrets, etc.)
|
||||
- Set a **default status** for monitors
|
||||
- Supports **base path hosting in Kubernetes (k8s)**
|
||||
- **Pre-built Docker images** for easy deployment
|
||||
|
||||
### 🎨 Customization and Branding
|
||||
|
||||
- Fully **customizable status page**
|
||||
- **Badge generation** for status and uptime tracking
|
||||
- Support for **custom domains**
|
||||
- Embed monitors as **iframes or widgets**
|
||||
- **Light & Dark Mode**
|
||||
- **Internationalization (i18n) support**
|
||||
- **Sleek, beautifully crafted UI**
|
||||
|
||||
### 🚨 Incident Management
|
||||
|
||||
- **Incident tracking & communication** tools
|
||||
- **Comprehensive APIs** for incident management
|
||||
|
||||
### 🧑💻 User Experience and Design
|
||||
|
||||
- **Accessible & user-friendly interface**
|
||||
- **Quick & easy installation**
|
||||
- **Responsive design** for all devices
|
||||
- **Auto SEO & Social Media ready**
|
||||
- **Server-Side Rendering (SSR) for better performance**
|
||||
|
||||
<div align="left">
|
||||
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
|
||||
</div>
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Support Me
|
||||
|
||||
If you’re enjoying Kener and want to support its development, consider sponsoring me on GitHub or treating me to a coffee. Your support helps keep the project growing! 🚀
|
||||
|
||||
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
|
||||
|
||||
☕ [Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
|
||||

|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to contribute to Kener, please read the [Contribution Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#rajnandan1/kener&Date)
|
||||
@@ -1,460 +0,0 @@
|
||||
import yaml from "js-yaml";
|
||||
import fs from "fs-extra";
|
||||
import axios from "axios";
|
||||
import {
|
||||
IsValidURL,
|
||||
checkIfDuplicateExists,
|
||||
IsValidHTTPMethod,
|
||||
ValidateIpAddress,
|
||||
IsValidHost,
|
||||
IsValidRecordType,
|
||||
IsValidNameServer
|
||||
} from "./src/lib/server/tool.js";
|
||||
import { API_TIMEOUT, AnalyticsProviders } from "./src/lib/server/constants.js";
|
||||
import { GetAllGHLabels, CreateGHLabel } from "./src/lib/server/github.js";
|
||||
|
||||
const configPathFolder = "./config";
|
||||
const databaseFolder = process.argv[2] || "./database";
|
||||
const defaultEval = `(function (statusCode, responseTime, responseData) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
return {
|
||||
status: 'UP',
|
||||
latency: responseTime,
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})`;
|
||||
|
||||
function validateServerFile(server) {
|
||||
//if empty return true
|
||||
if (Object.keys(server).length === 0) {
|
||||
return true;
|
||||
}
|
||||
//server.triggers is present then it should be an array
|
||||
if (server.triggers !== undefined && !Array.isArray(server.triggers)) {
|
||||
console.log("triggers should be an array");
|
||||
return false;
|
||||
}
|
||||
///each trigger should have a name, type, and url
|
||||
if (server.triggers !== undefined) {
|
||||
for (let i = 0; i < server.triggers.length; i++) {
|
||||
const trigger = server.triggers[i];
|
||||
if (
|
||||
trigger.name === undefined ||
|
||||
trigger.type === undefined ||
|
||||
trigger.url === undefined
|
||||
) {
|
||||
console.log("trigger should have name, type, and url");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
//if database is present then it should be an object, and they key can be either postgres or sqlite
|
||||
if (server.database !== undefined && typeof server.database !== "object") {
|
||||
console.log("database should be an object");
|
||||
return false;
|
||||
}
|
||||
if (server.database !== undefined) {
|
||||
let dbtype = Object.keys(server.database);
|
||||
if (dbtype.length !== 1) {
|
||||
console.log("database should have only one key");
|
||||
return false;
|
||||
}
|
||||
if (dbtype[0] !== "postgres" && dbtype[0] !== "sqlite") {
|
||||
console.log("database should be either postgres or sqlite");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function Build() {
|
||||
console.log("ℹ️ Building Kener...");
|
||||
let site = {};
|
||||
let server = {};
|
||||
let monitors = [];
|
||||
try {
|
||||
site = yaml.load(fs.readFileSync(configPathFolder + "/site.yaml", "utf8"));
|
||||
monitors = yaml.load(fs.readFileSync(configPathFolder + "/monitors.yaml", "utf8"));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
server = yaml.load(fs.readFileSync(configPathFolder + "/server.yaml", "utf8"));
|
||||
if (!validateServerFile(server)) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("server.yaml not found");
|
||||
server = {};
|
||||
}
|
||||
|
||||
if (
|
||||
site.github === undefined ||
|
||||
site.github.owner === undefined ||
|
||||
site.github.repo === undefined
|
||||
) {
|
||||
console.log("github owner and repo are required");
|
||||
site.hasGithub = false;
|
||||
// process.exit(1);
|
||||
} else {
|
||||
site.hasGithub = true;
|
||||
}
|
||||
|
||||
if (site.hasGithub && !!!site.github.incidentSince) {
|
||||
site.github.incidentSince = 720;
|
||||
}
|
||||
if (site.hasGithub && !!!site.github.apiURL) {
|
||||
site.github.apiURL = "https://api.github.com";
|
||||
}
|
||||
|
||||
const FOLDER_DB = databaseFolder;
|
||||
const FOLDER_SITE = FOLDER_DB + "/site.json";
|
||||
const FOLDER_MONITOR = FOLDER_DB + "/monitors.json";
|
||||
const FOLDER_SERVER = FOLDER_DB + "/server.json";
|
||||
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
|
||||
let name = monitor.name;
|
||||
let tag = monitor.tag;
|
||||
let hasAPI = monitor.api !== undefined && monitor.api !== null;
|
||||
let hasPing = monitor.ping !== undefined && monitor.ping !== null;
|
||||
let hasDNS = monitor.dns !== undefined && monitor.dns !== null;
|
||||
let folderName = name.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
monitors[i].folderName = folderName;
|
||||
|
||||
if (!name || !tag) {
|
||||
console.log("name, tag are required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
monitor.dayDegradedMinimumCount &&
|
||||
(isNaN(monitor.dayDegradedMinimumCount) || monitor.dayDegradedMinimumCount < 1)
|
||||
) {
|
||||
console.log("dayDegradedMinimumCount is not a number or it is less than 1");
|
||||
process.exit(1);
|
||||
} else if (monitor.dayDegradedMinimumCount === undefined) {
|
||||
monitors[i].dayDegradedMinimumCount = 1;
|
||||
}
|
||||
|
||||
if (
|
||||
monitor.dayDownMinimumCount &&
|
||||
(isNaN(monitor.dayDownMinimumCount) || monitor.dayDownMinimumCount < 1)
|
||||
) {
|
||||
console.log("dayDownMinimumCount is not a number or it is less than 1");
|
||||
process.exit(1);
|
||||
} else if (monitor.dayDownMinimumCount === undefined) {
|
||||
monitors[i].dayDownMinimumCount = 1;
|
||||
}
|
||||
|
||||
if (
|
||||
monitor.includeDegradedInDowntime === undefined ||
|
||||
monitor.includeDegradedInDowntime !== true
|
||||
) {
|
||||
monitors[i].includeDegradedInDowntime = false;
|
||||
}
|
||||
if (hasPing) {
|
||||
let hostsV4 = monitor.ping.hostsV4;
|
||||
let hostsV6 = monitor.ping.hostsV6;
|
||||
let hasV4 = false;
|
||||
let hasV6 = false;
|
||||
if (hostsV4 && Array.isArray(hostsV4) && hostsV4.length > 0) {
|
||||
hostsV4.forEach((host) => {
|
||||
if (ValidateIpAddress(host) == "Invalid") {
|
||||
console.log(`hostsV4 ${host} is not valid`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
hasV4 = true;
|
||||
}
|
||||
if (hostsV6 && Array.isArray(hostsV6) && hostsV6.length > 0) {
|
||||
hostsV6.forEach((host) => {
|
||||
if (ValidateIpAddress(host) == "Invalid") {
|
||||
console.log(`hostsV6 ${host} is not valid`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
hasV6 = true;
|
||||
}
|
||||
|
||||
if (!hasV4 && !hasV6) {
|
||||
console.log("hostsV4 or hostsV6 is required");
|
||||
process.exit(1);
|
||||
}
|
||||
monitors[i].hasPing = true;
|
||||
}
|
||||
if (hasDNS) {
|
||||
let dnsData = monitor.dns;
|
||||
let domain = dnsData.host;
|
||||
//check if domain is valid
|
||||
if (!!!domain || !IsValidHost(domain)) {
|
||||
console.log("domain is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let recordType = dnsData.lookupRecord;
|
||||
//check if recordType is valid
|
||||
if (!!!recordType || !IsValidRecordType(recordType)) {
|
||||
console.log("recordType is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let nameServer = dnsData.nameServer;
|
||||
//check if nameserver is valid
|
||||
if (!!nameServer && !IsValidNameServer(nameServer)) {
|
||||
console.log("nameServer is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// matchType: "ANY" # ANY, ALL
|
||||
let matchType = dnsData.matchType;
|
||||
if (!!!matchType || (matchType !== "ANY" && matchType !== "ALL")) {
|
||||
console.log("matchType is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
//values array of string at least one
|
||||
let values = dnsData.values;
|
||||
if (!!!values || !Array.isArray(values) || values.length === 0) {
|
||||
console.log("values is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
monitors[i].hasDNS = true;
|
||||
}
|
||||
if (hasAPI) {
|
||||
let url = monitor.api.url;
|
||||
let method = monitor.api.method;
|
||||
let headers = monitor.api.headers;
|
||||
let evaluator = monitor.api.eval;
|
||||
let body = monitor.api.body;
|
||||
let timeout = monitor.api.timeout;
|
||||
let hideURLForGet = !!monitor.api.hideURLForGet;
|
||||
//url
|
||||
if (!!url) {
|
||||
if (!IsValidURL(url)) {
|
||||
console.log("url is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (!!method) {
|
||||
if (!IsValidHTTPMethod(method)) {
|
||||
console.log("method is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
method = method.toUpperCase();
|
||||
} else {
|
||||
method = "GET";
|
||||
}
|
||||
monitors[i].api.method = method;
|
||||
//headers
|
||||
if (headers === undefined || headers === null) {
|
||||
monitors[i].api.headers = undefined;
|
||||
} else {
|
||||
//check if headers is a valid json
|
||||
try {
|
||||
JSON.parse(JSON.stringify(headers));
|
||||
} catch (error) {
|
||||
console.log("headers are not valid. Quitting");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
//eval
|
||||
if (evaluator === undefined || evaluator === null) {
|
||||
monitors[i].api.eval = defaultEval;
|
||||
} else {
|
||||
let evalResp = eval(evaluator + `(200, 1000, "e30=")`);
|
||||
|
||||
if (
|
||||
evalResp === undefined ||
|
||||
evalResp === null ||
|
||||
evalResp.status === undefined ||
|
||||
evalResp.status === null ||
|
||||
evalResp.latency === undefined ||
|
||||
evalResp.latency === null
|
||||
) {
|
||||
console.log("eval is not valid ");
|
||||
process.exit(1);
|
||||
}
|
||||
monitors[i].api.eval = evaluator;
|
||||
}
|
||||
//body
|
||||
if (body === undefined || body === null) {
|
||||
monitors[i].api.body = undefined;
|
||||
} else {
|
||||
//check if body is a valid string
|
||||
if (typeof body !== "string") {
|
||||
console.log("body is not valid should be a string");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
//timeout
|
||||
if (timeout === undefined || timeout === null) {
|
||||
monitors[i].api.timeout = API_TIMEOUT;
|
||||
} else {
|
||||
//check if timeout is a valid number
|
||||
if (isNaN(timeout) || timeout < 0) {
|
||||
console.log("timeout is not valid ");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
//add a description to the monitor if it is website using api.url and method = GET and headers == undefined
|
||||
//call the it to see if received content-type is text/html
|
||||
//if yes, append to description
|
||||
if (
|
||||
!hideURLForGet &&
|
||||
(headers === undefined || headers === null) &&
|
||||
url !== undefined &&
|
||||
method === "GET"
|
||||
) {
|
||||
try {
|
||||
const response = await axios({
|
||||
method: "GET",
|
||||
url: url,
|
||||
timeout: API_TIMEOUT
|
||||
});
|
||||
if (response.headers["content-type"].includes("text/html")) {
|
||||
let link = `<a href="${url}" class="font-medium underline underline-offset-4" target="_blank">${url}</a>`;
|
||||
if (monitors[i].description === undefined) {
|
||||
monitors[i].description = link;
|
||||
} else {
|
||||
monitors[i].description = monitors[i].description?.trim() + " " + link;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`error while fetching ${url}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
monitors[i].hasAPI = hasAPI;
|
||||
}
|
||||
|
||||
if (site.siteName === undefined) {
|
||||
site.siteName = site.title;
|
||||
}
|
||||
if (site.theme === undefined) {
|
||||
site.theme = "system";
|
||||
}
|
||||
if (site.themeToggle === undefined) {
|
||||
site.themeToggle = true;
|
||||
}
|
||||
if (site.barStyle === undefined) {
|
||||
site.barStyle = "FULL";
|
||||
}
|
||||
if (site.barRoundness === undefined) {
|
||||
site.barRoundness = "ROUNDED";
|
||||
} else {
|
||||
site.barRoundness = site.barRoundness.toLowerCase();
|
||||
}
|
||||
if (site.summaryStyle === undefined) {
|
||||
site.summaryStyle = "DAY";
|
||||
}
|
||||
site.colors = {
|
||||
UP: site.colors?.UP || "#4ead94",
|
||||
DOWN: site.colors?.DOWN || "#ca3038",
|
||||
DEGRADED: site.colors?.DEGRADED || "#e6ca61"
|
||||
};
|
||||
if (!!site.analytics) {
|
||||
const providers = {};
|
||||
|
||||
for (let i = 0; i < site.analytics.length; i++) {
|
||||
const element = site.analytics[i];
|
||||
if (!!AnalyticsProviders[element.type]) {
|
||||
if (providers[element.type] === undefined) {
|
||||
providers[element.type] = {};
|
||||
providers[element.type].measurementIds = [];
|
||||
providers[element.type].script = AnalyticsProviders[element.type];
|
||||
}
|
||||
providers[element.type].measurementIds.push(element.id);
|
||||
}
|
||||
}
|
||||
site.analytics = providers;
|
||||
}
|
||||
if (!!!site.font || !!!site.font.cssSrc || !!!site.font.family) {
|
||||
site.font = {
|
||||
cssSrc: "https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@0,100..900;1,100..900&display=swap",
|
||||
family: "Albert Sans"
|
||||
};
|
||||
}
|
||||
if (checkIfDuplicateExists(monitors.map((monitor) => monitor.folderName)) === true) {
|
||||
console.log("duplicate monitor detected");
|
||||
process.exit(1);
|
||||
}
|
||||
if (checkIfDuplicateExists(monitors.map((monitor) => monitor.tag)) === true) {
|
||||
console.log("duplicate tag detected");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.ensureFileSync(FOLDER_MONITOR);
|
||||
fs.ensureFileSync(FOLDER_SITE);
|
||||
fs.ensureFileSync(FOLDER_SERVER);
|
||||
try {
|
||||
fs.writeFileSync(FOLDER_MONITOR, JSON.stringify(monitors, null, 4));
|
||||
fs.writeFileSync(FOLDER_SITE, JSON.stringify(site, null, 4));
|
||||
fs.writeFileSync(FOLDER_SERVER, JSON.stringify(server, null, 4));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("✅ Kener built successfully");
|
||||
|
||||
if (site.hasGithub) {
|
||||
const ghLabels = await GetAllGHLabels(site);
|
||||
const tagsAndDescription = monitors.map((monitor) => {
|
||||
return { tag: monitor.tag, description: monitor.name };
|
||||
});
|
||||
//add incident label if does not exist
|
||||
|
||||
if (ghLabels.indexOf("incident") === -1) {
|
||||
await CreateGHLabel(site, "incident", "Status of the site");
|
||||
}
|
||||
if (ghLabels.indexOf("resolved") === -1) {
|
||||
await CreateGHLabel(site, "resolved", "Incident is resolved", "65dba6");
|
||||
}
|
||||
if (ghLabels.indexOf("identified") === -1) {
|
||||
await CreateGHLabel(site, "identified", "Incident is Identified", "EBE3D5");
|
||||
}
|
||||
if (ghLabels.indexOf("manual") === -1) {
|
||||
await CreateGHLabel(site, "manual", "Manually Created Incident", "6499E9");
|
||||
}
|
||||
if (ghLabels.indexOf("auto") === -1) {
|
||||
await CreateGHLabel(site, "auto", "Automatically Created Incident", "D6C0B3");
|
||||
}
|
||||
if (ghLabels.indexOf("investigating") === -1) {
|
||||
await CreateGHLabel(site, "investigating", "Incident is investigated", "D4E2D4");
|
||||
}
|
||||
if (ghLabels.indexOf("incident-degraded") === -1) {
|
||||
await CreateGHLabel(
|
||||
site,
|
||||
"incident-degraded",
|
||||
"Status is degraded of the site",
|
||||
"f5ba60"
|
||||
);
|
||||
}
|
||||
if (ghLabels.indexOf("incident-down") === -1) {
|
||||
await CreateGHLabel(site, "incident-down", "Status is down of the site", "ea3462");
|
||||
}
|
||||
//add tags if does not exist
|
||||
for (let i = 0; i < tagsAndDescription.length; i++) {
|
||||
const tag = tagsAndDescription[i].tag;
|
||||
const description = tagsAndDescription[i].description;
|
||||
if (ghLabels.indexOf(tag) === -1) {
|
||||
await CreateGHLabel(site, tag, description);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ Github labels created successfully");
|
||||
}
|
||||
}
|
||||
|
||||
Build();
|
||||
@@ -1,48 +0,0 @@
|
||||
- name: OkBookmarks
|
||||
description: A free bookmark manager that lets you save and search your bookmarks in the cloud.
|
||||
tag: "okbookmarks"
|
||||
image: "https://okbookmarks.com/assets/img/extension_icon128.png"
|
||||
api:
|
||||
method: GET
|
||||
url: https://okbookmarks.com
|
||||
- name: Earth
|
||||
description: Our blue planet
|
||||
tag: "earth"
|
||||
defaultStatus: "UP"
|
||||
image: "/earth.png"
|
||||
- name: Frogment
|
||||
description: A free openAPI spec editor and linter that breaks down your spec into fragments to make editing easier and more intuitive. Visit https://www.frogment.com
|
||||
tag: "frogment"
|
||||
image: "/frogment.png"
|
||||
api:
|
||||
method: GET
|
||||
url: https://www.frogment.com
|
||||
alerts:
|
||||
DOWN:
|
||||
failureThreshold: 5
|
||||
successThreshold: 2
|
||||
createIncident: false
|
||||
description: "Write a description here please"
|
||||
triggers:
|
||||
- MyWebhook
|
||||
- Discord Test
|
||||
- name: CNAME Lookup
|
||||
description: Monitor example showing how to lookup CNAME record for a domain. The site www.rajnandan.com is hosted on GitHub Pages.
|
||||
tag: "cname-rajnandan"
|
||||
image: "https://www.rajnandan.com/assets/images/me.jpg"
|
||||
defaultStatus: "UP"
|
||||
dns:
|
||||
host: "www.rajnandan.com"
|
||||
lookupRecord: "CNAME"
|
||||
nameServer: "8.8.8.8"
|
||||
matchType: "ANY" # ANY, ALL
|
||||
values:
|
||||
- "rajnandan1.github.io"
|
||||
- name: "Frogment APP Ping"
|
||||
description: "Ping www.frogment.app"
|
||||
image: https://www.frogment.app/icons/Square107x107Logo.png
|
||||
tag: "pingFrogmentApp"
|
||||
defaultStatus: "UP"
|
||||
ping:
|
||||
hostsV4:
|
||||
- "www.frogment.app"
|
||||
@@ -1,13 +0,0 @@
|
||||
triggers:
|
||||
- name: MyWebhook
|
||||
type: webhook
|
||||
url: https://kener.requestcatcher.com/test
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: Bearer SomeToken
|
||||
- name: Discord Test
|
||||
type: discord
|
||||
url: https://discord.com/api/webhooks/1310641119767302164/XJvq4MO2lz5yp9XRCJgfc4dbUfcQdHsttFUKTFJx4y_Oo1jNkUXf-CS3RSnamnNNv4Lx
|
||||
database:
|
||||
sqlite:
|
||||
dbName: kener.db
|
||||
@@ -1,58 +0,0 @@
|
||||
title: "Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
siteName: "Kener.ing"
|
||||
home: "/"
|
||||
logo: "/logo.png"
|
||||
siteURL: "https://kener.ing"
|
||||
favicon: "/logo96.png"
|
||||
github:
|
||||
owner: "rajnandan1"
|
||||
repo: "kener"
|
||||
incidentSince: 720
|
||||
metaTags:
|
||||
description: "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."
|
||||
keywords: "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"
|
||||
og:description: "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."
|
||||
og:image: "https://kener.ing/ss.png"
|
||||
og:title: "Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
og:type: "website"
|
||||
og:site_name: "Kener"
|
||||
twitter:card: "summary_large_image"
|
||||
twitter:site: "@_rajnandan_"
|
||||
twitter:creator: "@_rajnandan_"
|
||||
twitter:image: "https://kener.ing/ss.png"
|
||||
twitter:title: "Kener: Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
twitter:description: "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."
|
||||
nav:
|
||||
- name: "Documentation"
|
||||
url: "/docs/home"
|
||||
- name: "Github"
|
||||
iconURL: "/github.svg"
|
||||
url: "https://github.com/rajnandan1/kener"
|
||||
- name: "Buy me a coffee"
|
||||
iconURL: "/buymeacoffee.svg"
|
||||
url: "https://buymeacoffee.com/rajnandan1"
|
||||
hero:
|
||||
title: Kener is an Modern Open-Source Status Page System
|
||||
subtitle: Let your users know what's going on.
|
||||
footerHTML: |
|
||||
Made using
|
||||
<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">
|
||||
Kener
|
||||
</a>
|
||||
an open source status page system built with Svelte and TailwindCSS.
|
||||
i18n:
|
||||
defaultLocale: "en"
|
||||
locales:
|
||||
en: "English"
|
||||
hi: "हिन्दी"
|
||||
zh-CN: "中文"
|
||||
ja: "日本語"
|
||||
vi: "Tiếng Việt"
|
||||
pattern: "squares"
|
||||
analytics:
|
||||
- id: "G-Q3MLRXCBFT"
|
||||
type: "GA"
|
||||
barRoundness: SHARP
|
||||
summaryStyle: CURRENT
|
||||
barStyle: PARTIAL
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
|
||||
let maxWait = 5000;
|
||||
let interval = 1000;
|
||||
let waitTime = 0;
|
||||
let serverDataPath = path.join(process.cwd(), "database", "server.json");
|
||||
let siteDataPath = path.join(process.cwd(), "database", "site.json");
|
||||
let monitorsDataPath = path.join(process.cwd(), "database", "monitors.json");
|
||||
|
||||
function allFilesExist() {
|
||||
return (
|
||||
fs.existsSync(serverDataPath) &&
|
||||
fs.existsSync(siteDataPath) &&
|
||||
fs.existsSync(monitorsDataPath)
|
||||
);
|
||||
}
|
||||
|
||||
//use setTimeout to create a delay promise
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
let requiredFilesExist = allFilesExist();
|
||||
|
||||
//create anonymous function to call the init function
|
||||
(async function init() {
|
||||
while (!requiredFilesExist && waitTime < maxWait) {
|
||||
await delay(1000);
|
||||
requiredFilesExist = allFilesExist();
|
||||
|
||||
waitTime += interval;
|
||||
}
|
||||
if (!requiredFilesExist) {
|
||||
console.error("Error loading site data");
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("✅ All files exist. Starting Frontend server...");
|
||||
}
|
||||
})();
|
||||
+52
-18
@@ -1,27 +1,61 @@
|
||||
version: '3.7'
|
||||
# Docker Compose Configuration
|
||||
# Description: This file sets up a multi-container environment for Kener (https://github.com/rajnandan1/kener).
|
||||
# Last Updated: 2025-02-08
|
||||
# Docker Compose Version: 3.8
|
||||
# Notes: Ensure that you specify a random value for the `KENER_SECRET_KEY` environment variable before running `docker-compose up -d`.
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
kener:
|
||||
image: rajnandan1/kener:latest
|
||||
image: rajnandan1/kener:latest # Change to 'rajnandan1/kener:alpine' for an even smaller image! 😁🚀
|
||||
container_name: kener
|
||||
#env_file: .env #uncomment this, if you are using .env file
|
||||
# env_file: custom.env # Uncomment this if you are needing to export environment variables from a custom environment file. By default, Docker will import any variables that exist in `.env`
|
||||
environment:
|
||||
- TZ=Etc/UTC
|
||||
#- GH_TOKEN=
|
||||
#- API_TOKEN=
|
||||
#- API_IP=
|
||||
#- API_IP_REGEX=
|
||||
#- KENER_BASE_PATH=
|
||||
TZ: Etc/UTC
|
||||
KENER_SECRET_KEY: replace_me_with_a_random_string # Keep private!! - best to define in `.env` file or through Docker Secret
|
||||
# DATABASE_URL: custom_db_url # By default, a SQLite database is used - you may override the database url/type here
|
||||
# RESEND_API_KEY:
|
||||
# RESEND_SENDER_EMAIL:
|
||||
|
||||
# If running on a LINUX HOST and not podman rootless these MUST BE SET
|
||||
# run "id $user" from command line and replace numbers below with output from command
|
||||
#- PUID=1000 # gid
|
||||
#- PGID=1000 # uid
|
||||
### You most likely will NOT need to change anything below this line. Be sure you know what you're doing!! (https://kener.ing/docs/deployment/#docker-environment-variables)
|
||||
|
||||
### Most likely DO NOT need to change anything below this ###
|
||||
|
||||
#- PORT=3000 Port app listens on IN CONTAINER
|
||||
# PORT: 3000 # Port that app listens on in the container
|
||||
# KENER_BASE_PATH: # By default, Kener runs at `/`. You may change this to be, e.g. `/status`, etc. Do NOT add a trailing slash!! (more info here: https://kener.ing/docs/deployment/#docker-environment-variables)
|
||||
# ORIGIN: http://localhost:3000
|
||||
# NODE_ENV: production # This is already set to "production" by default within the container
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
volumes:
|
||||
- './database:/app/database:rw'
|
||||
- './config:/app/config'
|
||||
- data:/app/database # We suggest using a Docker named volume, which is more performant for databases
|
||||
- $(pwd)/uploads:/app/uploads
|
||||
# read_only: true # Uncommenting this fortifies security by marking the container's filesystem as read-only (aka no data can be written to the container's filesystem except for explicitly defined writable volumes and bind mounts, an exception has already been defined for `/database` and `/uploads`)
|
||||
restart: unless-stopped
|
||||
# depends_on: # <-- Uncomment if you would like to use PostgreSQL or MySQL
|
||||
# - postgres # ...instead of SQLite
|
||||
# - mysql #
|
||||
|
||||
# Only use below section if you would like to utilize PostgreSQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=postgresql://db_user:db_password@localhost:5432/kener_db`)
|
||||
postgres:
|
||||
image: postgres:alpine
|
||||
container_name: postgres
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
|
||||
POSTGRES_DB: kener_db
|
||||
restart: unless-stopped
|
||||
|
||||
# Only use below section if you would like to utilize MySQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=mysql://db_user:db_password@localhost:3306/kener_db`)
|
||||
mysql:
|
||||
image: mariadb:11
|
||||
container_name: mysql
|
||||
environment:
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
|
||||
MYSQL_DATABASE: kener_db
|
||||
MYSQL_RANDOM_ROOT_PASSWORD: true
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
data:
|
||||
name: kener_db
|
||||
|
||||
@@ -5,10 +5,5 @@ export NODE_ENV=production
|
||||
cd /app || exit
|
||||
|
||||
# Run build first
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/build.js && (
|
||||
# Run startup and main in parallel
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/src/lib/server/startup.js &
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/main.js &
|
||||
# Wait for both processes
|
||||
wait
|
||||
)
|
||||
exec \
|
||||
s6-setuidgid abc /usr/bin/node $NODE_ARGS /app/main.js
|
||||
+20
-20
@@ -64,24 +64,24 @@ Body of the webhook will be sent as below:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mockoon-9",
|
||||
"alert_name": "Mockoon DOWN",
|
||||
"severity": "critical",
|
||||
"status": "TRIGGERED",
|
||||
"source": "Kener",
|
||||
"timestamp": "2024-11-27T04:55:00.369Z",
|
||||
"description": "🚨 **Service Alert**: Check the details below",
|
||||
"details": {
|
||||
"metric": "Mockoon",
|
||||
"current_value": 1,
|
||||
"threshold": 1
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"text": "View Monitor",
|
||||
"url": "https://kener.ing/monitor-mockoon"
|
||||
}
|
||||
]
|
||||
"id": "mockoon-9",
|
||||
"alert_name": "Mockoon DOWN",
|
||||
"severity": "critical",
|
||||
"status": "TRIGGERED",
|
||||
"source": "Kener",
|
||||
"timestamp": "2024-11-27T04:55:00.369Z",
|
||||
"description": "🚨 **Service Alert**: Check the details below",
|
||||
"details": {
|
||||
"metric": "Mockoon",
|
||||
"current_value": 1,
|
||||
"threshold": 1
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"text": "View Monitor",
|
||||
"url": "https://kener.ing/monitor-mockoon"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -108,7 +108,7 @@ The discord message when alert is `TRIGGERED` will look like this
|
||||
|
||||
The discord message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||

|
||||
|
||||
### Slack
|
||||
|
||||
@@ -118,7 +118,7 @@ The slack message when alert is `TRIGGERED` will look like this
|
||||
|
||||
The slack message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||

|
||||
|
||||
### Add Alerts to Monitors
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: View Alerts | Kener
|
||||
description: Learn how to view alerts in kener.
|
||||
---
|
||||
|
||||
# View Alerts
|
||||
|
||||
Alerts are used to notify you when a monitor goes down or up. You can view alerts in the Kener dashboard.
|
||||
|
||||
## ID
|
||||
|
||||
The unique ID of the alert.
|
||||
|
||||
## Type
|
||||
|
||||
The type of status that the alert is for. Example DOWN, DEGRADED
|
||||
|
||||
## Monitor
|
||||
|
||||
The monitor for which the alert is triggered.
|
||||
|
||||
## Status
|
||||
|
||||
The status of the alert. TRIGGERED, RESOLVED
|
||||
|
||||
## Checks
|
||||
|
||||
The number of checks that have been done.
|
||||
|
||||
## Incident
|
||||
|
||||
The incident that the alert is part of.
|
||||
|
||||
## Created At
|
||||
|
||||
The time when the alert was created.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: API Keys | Kener
|
||||
description: Learn how to set up and work with API keys in kener.
|
||||
---
|
||||
|
||||
# API Keys
|
||||
|
||||
API keys can be used to call the [APIs of kener](/docs/kener-apis). You can create multiple API keys and use them to call the APIs.
|
||||
|
||||
API keys once generated cannot be viewed again so please keep them safe and secure.
|
||||
|
||||
## Create API Key
|
||||
|
||||
1. To create an API key click on the `Create API Key` button in the API Keys page.
|
||||
2. Add a name for the key so that it is easy for you to remember
|
||||
3. Click on Create.
|
||||
@@ -20,7 +20,7 @@ Let us add a category to our monitors.
|
||||
- name: Earth
|
||||
description: Our blue planet
|
||||
tag: "earth"
|
||||
defaultStatus: "UP"
|
||||
default_status: "UP"
|
||||
image: "/earth.png"
|
||||
category: "Hello"
|
||||
- name: Frogment
|
||||
|
||||
+85
-35
@@ -5,62 +5,112 @@ description: Changelogs for Kener
|
||||
|
||||
# Changelogs
|
||||
|
||||
## v2.0.0
|
||||
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
|
||||
|
||||
## v3.2.16
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **Hidden Categories:** You can now create hidden categories that will not show up for users that are not logged in. All the monitors will also not show up. Some one however can directly get to see them if they visit the monitor link directly
|
||||
- **Configure data rage:** Now you can configure the data range in home page between 1 and 180 days. You can set different ranges for mobile and desktop devices.
|
||||
|
||||
## v3.2.14
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **Gamedig Monitor Functionality:** Introduced a new monitor type using Gamedig to monitor over 320+ games and services. Includes options for host, port, timeout, game/service selection, and documentation updates. ([#389](https://github.com/rajnandan1/kener/pull/389) by ToxykAuBleu)
|
||||
- **Event Subscription:** Added functionality for event subscriptions, involving new database tables (`subscribers`, `subscriptions`, `subscription_triggers`), UI enhancements, a comprehensive game list, and package version update (3.2.13 to 3.2.14). ([#385](https://github.com/rajnandan1/kener/pull/385) by rajnandan1)
|
||||
- **CRUD API for Monitors:** Implemented API endpoints for creating, reading, updating, and deleting monitors (`GET /api/monitor`, `POST /api/monitor`, `GET /api/monitor/[monitor_id]`, `PUT /api/monitor/[monitor_id]`, `DELETE /api/monitor/[monitor_id]`). Includes OpenAPI spec updates. ([#384](https://github.com/rajnandan1/kener/pull/384) by jensvandenreyt)
|
||||
- **Modify Monitor Data:** Added the ability for users to modify historical monitoring data via a new UI option and backend API, useful for correcting past statuses (e.g., false positives). ([#380](https://github.com/rajnandan1/kener/pull/380) by mcorbin)
|
||||
- **Site Status Banner:** Now you can choose to show a site status banner in the main home page. The setting has to be turned on in the Home section of the kener portal.
|
||||
- **Upcoming Maintenance**: Dedicated Page for upcoming maintenance events.
|
||||
- **Event Page**: Dedicated page for each event (incident/maintenance)
|
||||
- **Admin Portal UI Update**: Revamped kener management portal for better accessibility.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **SMTP Secure Variable Fix:** Corrected the evaluation of the `SMTP_SECURE` environment variable to properly handle values like '0' or empty strings, preventing SSL errors with STARTTLS. ([#392](https://github.com/rajnandan1/kener/pull/392) by myned)
|
||||
- **Group Monitor Fix:** Fix group monitor using queues
|
||||
|
||||
### 🌍 Internationalization
|
||||
|
||||
- **Polish Translation:** Added Polish language support (`pl.json`) and integrated it into the application's localization framework. ([#386](https://github.com/rajnandan1/kener/pull/386) by lolen)
|
||||
|
||||
## v3.2.5
|
||||
|
||||
<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>
|
||||
|
||||
Here are the changes in this release
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for sqlite3 and removed dependency on file system
|
||||
- Added support for postgres database. Read more [here](/docs/database)
|
||||
- Added support for alerting. Read more [here](/docs/alerting)
|
||||
- Added color customization. Read more [here](/docs/customize-site#color)
|
||||
- Added three new customizations for home page. Read more [here](/docs/customize-site#barstyle)
|
||||
- `barStyle`
|
||||
- `barRoundness`
|
||||
- `summaryStyle`
|
||||
- Added code editing for all JavaScript and JSON fields for trigger custom body and monitor `evals`.
|
||||
- Syntax highlighting and error checking for monitor evaluation functions
|
||||
- Added support for custom Discord and Slack body templates in triggers
|
||||
- Improved variable templating for all notification types
|
||||
- Added comprehensive user management with 3 roles: admin, editor, and member. Read more [here](/docs/rbac)
|
||||
- Self-service profile management for all users
|
||||
- User activation/deactivation controls for admins
|
||||
- Added dedicated Badges management page in the admin dashboard
|
||||
- Site-wide status badges to show overall service health
|
||||
- Version information automatically pulled from package.json
|
||||
|
||||
### Migration
|
||||
### Improvements
|
||||
|
||||
Kener will automatically migrate your data from file system to sqlite3. If you are using a custom domain, you need to update the `site.yaml` file with the new `siteURL` field. Read more [here](/docs/customize-site#siteURL)
|
||||
- Improved build time by migrating lucid-svelte to individual components
|
||||
|
||||
## v0.0.16
|
||||
### Github Issues Resolved
|
||||
|
||||
- Feature: Enhancement of Webhook Custom Body Functionality [#238](https://github.com/rajnandan1/kener/issues/238)
|
||||
- Feature: Add LDAP for authentication [#210](https://github.com/rajnandan1/kener/issues/210)
|
||||
- the event created by tigger can't update affects. [#309](https://github.com/rajnandan1/kener/issues/309)
|
||||
- Multiple account or RBAC [#334](https://github.com/rajnandan1/kener/issues/334)
|
||||
- The event that has been edited isn't clickable after you save it [#350](https://github.com/rajnandan1/kener/issues/350)
|
||||
- /app/package.json ERROR on fresh install [#346](https://github.com/rajnandan1/kener/issues/346)
|
||||
- Site stops when monitor processing throws an error [#345](https://github.com/rajnandan1/kener/issues/345)
|
||||
- Support for Self Signed Certificates [#351](https://github.com/rajnandan1/kener/issues/351)
|
||||
- Wrong month is showing up after clicking on the 'Browse Events' link [#349](https://github.com/rajnandan1/kener/issues/349)
|
||||
|
||||
### Migration Notes
|
||||
|
||||
This update is fully backward compatible with existing installations. User accounts created before this update will retain admin privileges by default.
|
||||
|
||||
## v3.2.1
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
Here are the changes in this release
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for `hideURLForGet` in monitors. Read more [here](/docs/monitors)
|
||||
- New SVG badges for LIVE status. Read more [here](/docs/status-badges#live)
|
||||
- `[Breaking Change]` Removed dependency on Environment variable `PUBLIC_KENER_FOLDER`. Read more [here](#v0-0-16-migration)
|
||||
- Simplified build and deploy process
|
||||
- Added support for fonts. Read more [here](/docs/customize-site#font)
|
||||
- Added support for home page pattern. Read more [here](/docs/customize-site#pattern)
|
||||
- Added support for adding your analytics provider. Read more [here](/docs/site-analytics)
|
||||
- New Documentation Site
|
||||
- Addes support for `sqaures` pattern in home page. Read more [here](/docs/customize-site#pattern)
|
||||
- Redesigned the UI for better consistency
|
||||
- Embed now supports background color using a parameter `bgc`. Read more [here](/docs/embed#javascript-parameters)
|
||||
- Now title in `site.yaml` is `<title>` and `siteName` is actually the name of the site. Read more [here](/docs/customize-site#siteName)
|
||||
- **New Heartbeat Monitors**
|
||||
|
||||
### Migration
|
||||
- Added support for push-based monitoring via heartbeats
|
||||
- Monitors systems that send periodic signals to confirm they're running
|
||||
- Configurable thresholds for degraded and down states
|
||||
- Secured with secret token authentication
|
||||
- Accessible via simple HTTP API endpoints
|
||||
|
||||
#### Source
|
||||
- **Data Interpolation Improvements**
|
||||
- Fixed data interpolation issues for more accurate uptime calculations
|
||||
- Improved handling of timestamp boundaries
|
||||
|
||||
- Move data from `PUBLIC_KENER_FOLDER` to `/database` file.
|
||||
- Move `site.yaml` to `/config` folder
|
||||
- Move `monitors.yaml` to `/config` folder
|
||||
### Fixes & Improvements
|
||||
|
||||
#### Docker
|
||||
- **UI Enhancements**
|
||||
|
||||
- Use `-v $(pwd)/database:/app/database` and `-v $(pwd)/config:/app/config` in your docker run command
|
||||
- Better mobile responsiveness for theme toggles and controls
|
||||
- Fixed layout issues with bottom navigation buttons
|
||||
- Improved timezone selector display
|
||||
|
||||
- **Data Processing**
|
||||
|
||||
- Fixed edge case issues with uptime calculations
|
||||
- Improved handling of "No Data" status in summaries
|
||||
- More accurate time range display across timezones
|
||||
|
||||
- **Documentation**
|
||||
- Added comprehensive documentation for Heartbeat Monitors
|
||||
- Updated API documentation with new endpoints
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Concepts | Kener
|
||||
description: Concepts for Kener
|
||||
---
|
||||
|
||||
# Concepts
|
||||
|
||||
Here are the different concepts that Kener uses to make your life easier.
|
||||
|
||||
## Monitors
|
||||
|
||||
Monitors are the heart of Kener. They are the ones that keep an eye on your services and let you know when something goes wrong. Monitors can be of different types like HTTP, DNS, Ping etc. They can be configured to check your services at different intervals and alert you when something goes wrong.
|
||||
|
||||
### HTTP Monitor
|
||||
|
||||
HTTP Monitor is used to check the status of a website or any http api. It can be configured to check the status of the website at different intervals.
|
||||
|
||||
It can also be configured to check for a specific status code or a specific response.
|
||||
|
||||
You can also add secrets to the HTTP request using environment variables.
|
||||
|
||||
### DNS Monitor
|
||||
|
||||
DNS Monitor is used to check the status of a DNS server. It can be configured to check the status of the DNS server at different intervals.
|
||||
|
||||
### Ping Monitor
|
||||
|
||||
Ping Monitor is used to check the status of a server. It can be configured to check the status of the server at different intervals.
|
||||
|
||||
### Data Interpolation
|
||||
|
||||
Kener interpolates the data for missing intervals. For example, suppose if you have a cron monitor of 5 minutes and it has run twice at 10AM and 10:05AM and the monitor is `DOWN` at 10AM. Kener will interpolate the data for 10:01AM, 10:02AM, 10:03AM and 10:04AM as `DOWN`. Similarly, if the monitor is UP at 10:05AM, Kener will interpolate the data for 10:06AM, 10:07AM, 10:08AM and 10:09AM as UP. Similarly for missing data points it will consider the previous data point. If there is no previous data point, it will consider it as `UP`.
|
||||
|
||||
---
|
||||
|
||||
## Triggers
|
||||
|
||||
Triggers are the ones that are responsible for sending alerts when something goes wrong. They can be configured to send alerts to different channels like Email, Slack, Discord, Webhook etc.
|
||||
|
||||
You can set multiple triggers of the same type.
|
||||
|
||||
### Email
|
||||
|
||||
Email Trigger is used to send an email when something goes wrong. It can be configured to send an email to different email addresses.
|
||||
|
||||
Kener uses [resend.com](https://resend.com) to send emails. Please make sure to sign up in [resend.com](https://resend.com) and get the API key. You will need to set the API key in the environment variable `RESEND_API_KEY`. Learn more about environment variables [here](/docs/environment-vars).
|
||||
|
||||
### Slack
|
||||
|
||||
Slack Trigger is used to send a message to a slack channel when something goes wrong. It can be configured to send a message to different slack channels. Needs slack URL
|
||||
|
||||
### Discord
|
||||
|
||||
Discord Trigger is used to send a message to a discord channel when something goes wrong. It can be configured to send a message to different discord channels. Needs discord URL
|
||||
|
||||
### Webhook
|
||||
|
||||
Webhook Trigger is used to send a message to a webhook when something goes wrong. It can be configured to send a message to different webhooks. Needs webhook URL. Method will be POST.
|
||||
|
||||
Body of the webhook will be sent as below:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mockoon-9",
|
||||
"alert_name": "Mockoon DOWN",
|
||||
"severity": "critical",
|
||||
"status": "TRIGGERED",
|
||||
"source": "Kener",
|
||||
"timestamp": "2024-11-27T04:55:00.369Z",
|
||||
"description": "🚨 **Service Alert**: Check the details below",
|
||||
"details": {
|
||||
"metric": "Mockoon",
|
||||
"current_value": 1,
|
||||
"threshold": 1
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"text": "View Monitor",
|
||||
"url": "https://kener.ing/monitor-mockoon"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 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 hreshold of the monitor |
|
||||
| actions | Actions to be taken. Link to view the monitor. |
|
||||
|
||||
---
|
||||
|
||||
## Incidents
|
||||
|
||||
Incidents are the ones that are created when something goes wrong. They can be created using Manage portal or using APIs.
|
||||
|
||||
There can be two types of incidents - `DOWN` and `DEGRADED`. Incident can have a title, description, severity and status. Status can be `INVESTIGATING`, `IDENTIFIED`, `MONITORING` and `RESOLVED`.
|
||||
|
||||
To learn more about incidents, click [here](/docs/incident-management).
|
||||
|
||||
### Incident Updates
|
||||
|
||||
Incident updates are done using comment. Each comment has a status and a timestamp. The status of the latest added comment becomes the current status of the incident.
|
||||
|
||||
### Incident Monitors
|
||||
|
||||
Monitors can be added as affected components for an incident. Depending on the severity of the incident the status of the monitor also changes.
|
||||
@@ -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;
|
||||
}
|
||||
```
|
||||
@@ -15,6 +15,10 @@ Add your custom JS to `static/` file. And in the `src/app.html` file, add the fo
|
||||
|
||||
## Adding Custom CSS
|
||||
|
||||
Three are two ways you can add custom CSS to your Kener instance.
|
||||
|
||||
### CSS file
|
||||
|
||||
Add your custom CSS to `static/` file. And in the `src/app.html` file, add the following line:
|
||||
|
||||
```html
|
||||
@@ -22,3 +26,60 @@ Add your custom CSS to `static/` file. And in the `src/app.html` file, add the f
|
||||
```
|
||||
|
||||
Do not forget to add the base path if you are using a subpath. For example, if you are using a subpath `/kener`, then the path should be `/kener/your-custom-js-file.js`.
|
||||
|
||||
### Inline CSS
|
||||
|
||||
To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS there.
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
<div class="note danger">
|
||||
Do not include <style> tags.
|
||||
</div>
|
||||
|
||||
## Grid Layout
|
||||
|
||||
To change from a column layout to a grid layout in your Kener instance, you can use the following CSS:
|
||||
|
||||
```css
|
||||
@media (min-width: 1330px) {
|
||||
.section-monitors,
|
||||
.section-categories,
|
||||
.section-hero,
|
||||
.section-back,
|
||||
.section-events,
|
||||
.section-legend,
|
||||
.section-categories,
|
||||
.section-browser-events {
|
||||
width: 1330px !important;
|
||||
max-width: 1330px !important;
|
||||
}
|
||||
|
||||
.section-monitors .monitor-root {
|
||||
background-color: transparent;
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.section-monitors .monitor-root .monitors-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.section-monitors .monitor-root .monitors-card .monitor {
|
||||
grid-column: span 1;
|
||||
border-radius: 0.375rem;
|
||||
background-color: hsl(var(--card) / var(--tw-bg-opacity, 1));
|
||||
border-width: 1px;
|
||||
}
|
||||
.section-monitors .monitor-root .monitors-card .monitor:last-child {
|
||||
border-bottom: inherit;
|
||||
border-bottom-width: 1px !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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'
|
||||
```
|
||||
|
||||
|
||||
+27
-28
@@ -3,29 +3,28 @@ title: Database Config - Server.yaml - Kener
|
||||
description: Add database configuration to your kener server.yaml
|
||||
---
|
||||
|
||||
# Database Config
|
||||
# Databases
|
||||
|
||||
Use the `config/server.yaml` file to configure the database settings.
|
||||
Kener uses relational database to store its data.
|
||||
|
||||
## Supported Database
|
||||
|
||||
- Sqlite (default)
|
||||
- Postgres
|
||||
- Mysql
|
||||
|
||||
We are adding more database support in the future.
|
||||
We will be adding more database support in the future.
|
||||
|
||||
## Sqlite
|
||||
|
||||
Sqlite is the default database for Kener. You don't need to do anything to use it. The database file will be created in the `database` folder.
|
||||
Sqlite is the default database for Kener. You don't need to do anything to use it. The database file will be created in the `database` folder which is present in the root of the project.
|
||||
|
||||
The name of the default database file is `kener.db`. The path will be `database/kener.db`.
|
||||
The name of the default database file is `kener.db`. The path will be `database/kener.sqlite.db`.
|
||||
|
||||
You can change the database file name by changing the `database` key in the `server.yaml` file.
|
||||
You can change the database file name by changing the environment variable `DATABASE_URL`. Prefix the connection string with `sqlite://` and add the path to the database file.
|
||||
|
||||
```yaml
|
||||
database:
|
||||
sqlite:
|
||||
dbName: awesomeKener.db
|
||||
```bash
|
||||
export DATABASE_URL=sqlite://./database/awesomeKener.db
|
||||
```
|
||||
|
||||
In this case, the database file will be created in the `database` folder with the name `awesomeKener.db`.
|
||||
@@ -34,26 +33,26 @@ Make sure the `database` folder is writable by the Kener process.
|
||||
|
||||
## Postgres
|
||||
|
||||
To use Postgres, you need to provide the connection details in the `server.yaml` file.
|
||||
To use Postgres, you need to update the connection string details in the `DATABASE_URL` environment variable. The connection string has to with `postgresql`
|
||||
|
||||
```yaml
|
||||
database:
|
||||
postgres:
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: kener
|
||||
password: kener
|
||||
database: kener
|
||||
```bash
|
||||
export DATABASE_URL=postgresql://myuser:mypassword@your.host:5432/kenerdb
|
||||
```
|
||||
|
||||
Or if you want to use environment variables, you can do that as well. Make sure the environment variables are set before starting the Kener process. The environment variables should be `PG_HOST`, `PG_PORT`, `PG_USER`, `PG_PASSWORD`, and `PG_DB`.
|
||||
## Mysql
|
||||
|
||||
```yaml
|
||||
database:
|
||||
postgres:
|
||||
host: $PG_HOST
|
||||
port: $PG_PORT
|
||||
user: $PG_USER
|
||||
password: $PG_PASSWORD
|
||||
database: $PG_DB
|
||||
To use Mysql, you need to update the connection string details in the `DATABASE_URL` environment variable. The connection string has to with `mysql`
|
||||
|
||||
```bash
|
||||
export DATABASE_URL=mysql://root:password@your.host:3306/kenerdb
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
To run migrations to latest, run
|
||||
|
||||
```bash
|
||||
npm run migrate
|
||||
```
|
||||
|
||||
When kener runs it will always migrate up to the latest version
|
||||
|
||||
+98
-45
@@ -11,29 +11,25 @@ Kener can be deployed in multiple ways. You can use the pre-built docker image o
|
||||
|
||||
Make sure you have the following installed:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/)
|
||||
- [Node.js 20 or above](https://nodejs.org/en/download/)
|
||||
- [npm](https://www.npmjs.com/get-npm)
|
||||
- Make sure `./database` and `./config` directories are present in the root directory
|
||||
- [config/site.yaml](/docs/customize-site) Contains information about the site
|
||||
- [config/monitors.yaml](/docs/monitors) Contains your monitors and their related specifications
|
||||
- [Sqlite3](https://www.sqlite.org/download.html)
|
||||
- Make sure `./database` directory is present in the root directory
|
||||
- [Set up Environment Variables](/docs/environment-vars). You can use a `.env` file or pass them as arguments. Be sure to add `NODE_ENV=production` for production deployment
|
||||
|
||||
## NPM
|
||||
|
||||
```shell
|
||||
```bash
|
||||
npm i
|
||||
npm run build
|
||||
npm run configure
|
||||
npm run prod
|
||||
npm run build #build the frontend
|
||||
npm run start
|
||||
```
|
||||
|
||||
## PM2
|
||||
|
||||
```shell
|
||||
```bash
|
||||
npm i
|
||||
npm run build #build the frontend
|
||||
npm run configure #build the backend
|
||||
pm2 start src/lib/server/startup.js
|
||||
pm2 start main.js
|
||||
```
|
||||
|
||||
@@ -41,40 +37,59 @@ pm2 start main.js
|
||||
|
||||
[Dockerhub](https://hub.docker.com/r/rajnandan1/kener)
|
||||
|
||||
```shell
|
||||
```bash
|
||||
docker.io/rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
[Github Packages](https://github.com/rajnandan1/kener/pkgs/container/kener)
|
||||
|
||||
```shell
|
||||
```bash
|
||||
ghcr.io/rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
You should mount two host directories to persist your configuration and database. [Environmental variables](/docs/environment-vars) can be passed with `-e` An example `docker run` command:
|
||||
### Volumes
|
||||
|
||||
Make sure `./database` and `./config` directories are present in the root directory
|
||||
- `$(pwd)/uploads` You should mount one host directories to persist your image uploads.
|
||||
- `$(pwd)/database` If you are using sqlite, you should mount one host directory to persist your database.
|
||||
|
||||
```shell
|
||||
mkdir database
|
||||
mkdir config
|
||||
curl -o config/site.yaml https://raw.githubusercontent.com/rajnandan1/kener/refs/heads/main/config/site.example.yaml
|
||||
curl -o config/monitors.yaml https://raw.githubusercontent.com/rajnandan1/kener/refs/heads/main/config/monitors.example.yaml
|
||||
curl -o config/server.yaml https://raw.githubusercontent.com/rajnandan1/kener/refs/heads/main/config/server.example.yaml
|
||||
### Environment Variables
|
||||
|
||||
[Environment variables](/docs/environment-vars) can be passed with `-e` An example `docker run` command:
|
||||
|
||||
Make sure `./database` and `./uploads` directories are present in the root directory.
|
||||
|
||||
### Examples
|
||||
|
||||
This example is for sqlite. You can also use postgres. Read more about it [here](/docs/environment-vars#database-url)
|
||||
|
||||
#### sqlite
|
||||
|
||||
```bash
|
||||
mkdir database uploads
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-v $(pwd)/database:/app/database \
|
||||
-v $(pwd)/config:/app/config \
|
||||
-v $(pwd)/uploads:/app/uploads \
|
||||
-p 3000:3000 \
|
||||
-e "GH_TOKEN=1234" \
|
||||
-e "KENER_SECRET_KEY=somesecretkey" \
|
||||
-e "ORIGIN=http://localhost:3000" \
|
||||
rajnandan1/kener
|
||||
```
|
||||
|
||||
#### .env
|
||||
|
||||
You can also use a .env file
|
||||
|
||||
```shell
|
||||
```bash
|
||||
mkdir database uploads
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-v $(pwd)/database:/app/database \
|
||||
-v $(pwd)/config:/app/config \
|
||||
-v $(pwd)/uploads:/app/uploads \
|
||||
--env-file .env \
|
||||
-p 3000:3000 \
|
||||
rajnandan1/kener
|
||||
@@ -82,28 +97,66 @@ docker run \
|
||||
|
||||
Or use **Docker Compose** with the example [docker-compose.yaml](https://raw.githubusercontent.com/rajnandan1/kener/main/docker-compose.yml)
|
||||
|
||||
## Using PUID and PGID
|
||||
#### Postgres
|
||||
|
||||
If you are
|
||||
|
||||
- running on a **linux host** (ie unraid) and
|
||||
- **not** using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_)
|
||||
|
||||
then you must set the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid) in the container in order for it to generate files/folders your normal user can interact it.
|
||||
|
||||
Run these commands from your terminal
|
||||
|
||||
- `id -u` -- prints UID for **PUID**
|
||||
- `id -g` -- prints GID for **PGID**
|
||||
|
||||
Then add to your docker command like so:
|
||||
|
||||
```shell
|
||||
docker run -d ... -e "PUID=1000" -e "PGID=1000" ... rajnandan1/kener
|
||||
```bash
|
||||
mkdir uploads
|
||||
```
|
||||
|
||||
or substitute them in [docker-compose.yml](https://raw.githubusercontent.com/rajnandan1/kener/main/docker-compose.yml)
|
||||
Database folder is not required for postgres.
|
||||
|
||||
## Base path
|
||||
```bash
|
||||
docker run \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/uploads:/app/uploads \
|
||||
-e "KENER_SECRET_KEY=somesecretkey" \
|
||||
-e "DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase" \
|
||||
-e "ORIGIN=http://localhost:3000" \
|
||||
rajnandan1/kener
|
||||
```
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path. Read more about it [here](/docs/environment-vars/#kener-base-path)
|
||||
#### MySQL
|
||||
|
||||
```bash
|
||||
mkdir uploads
|
||||
```
|
||||
|
||||
Database folder is not required for mysql.
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/uploads:/app/uploads \
|
||||
-e "KENER_SECRET_KEY=somesecretkey" \
|
||||
-e "DATABASE_URL=mysql://root:password@mysql-container-2.orb.local:3306/kener-2" \
|
||||
-e "ORIGIN=http://localhost:3000" \
|
||||
rajnandan1/kener
|
||||
```
|
||||
|
||||
#### Base path
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path. Read more about it [here](/docs/environment-vars/#kener-base-path).
|
||||
|
||||
<div class="note info">
|
||||
|
||||
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
|
||||
- Important: This env variable should be present during both build and run time
|
||||
- Important: You will have to build the frontend with the same base path
|
||||
|
||||
</div>
|
||||
|
||||
Let us say you are running kener on a subpath `/status`. You can set the base path like this:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
[](https://railway.com/template/spSvic?referralCode=1Pn7vs)
|
||||
|
||||
+14
-31
@@ -12,35 +12,31 @@ There are two ways to embed your monitor in your website
|
||||
You can embed your monitor in your website using javascript. We recommend using this method as it takes care of the height of the embedded monitor.
|
||||
|
||||
```html
|
||||
<script
|
||||
async
|
||||
src="http://[hostname]/embed-[tag]/js?theme=light&monitor=http://[hostname]/embed-[tag]"
|
||||
></script>
|
||||
<script async src="http://[hostname]/embed/monitor-[tag]/js?theme=light&monitor=http://[hostname]/embed/monitor-[tag]"></script>
|
||||
```
|
||||
|
||||
Here is an example
|
||||
|
||||
```html
|
||||
<script
|
||||
async
|
||||
src="https://kener.ing/embed-okbookmarks/js?theme=light&monitor=https://kener.ing/embed-okbookmarks"
|
||||
></script>
|
||||
<script async src="https://kener.ing/embed/monitor-earth/js?theme=light&monitor=https://kener.ing/embed/monitor-earth"></script>
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `monitor`: The monitor url
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `monitor`: The monitor url
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
- `tz`: Timezone. Example `Asia/Calcutta`, `Asia/Tokyo`
|
||||
|
||||
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
|
||||
### Demo
|
||||
|
||||
<div class="border mx-auto rounded-sm w-585px">
|
||||
<script async src="/embed-okbookmarks/js?theme=dark&monitor=/embed-okbookmarks"></script>
|
||||
<script async src="/embed/monitor-earth/js?theme=dark&monitor=/embed/monitor-earth"></script>
|
||||
</div>
|
||||
|
||||
## Iframe
|
||||
@@ -48,27 +44,13 @@ Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
This is the simplest way to embed your monitor in your website. You can use the following code to embed your monitor in your website.
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="http://[hostname]/embed-[tag]?theme=light"
|
||||
width="100%"
|
||||
height="200"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allowpaymentrequest
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
<iframe src="http://[hostname]/embed/monitor-[tag]?theme=light" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
|
||||
```
|
||||
|
||||
Here is an example
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://kener.ing/embed-okbookmarks?theme=light"
|
||||
width="100%"
|
||||
height="200"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allowpaymentrequest
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
<iframe src="https://kener.ing/embed/monitor-earth?theme=light" width="100%" height="100" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
|
||||
```
|
||||
|
||||
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
@@ -77,11 +59,12 @@ Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
|
||||
You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
|
||||
### Demo
|
||||
|
||||
<div class="border mx-auto rounded-sm w-585px">
|
||||
<iframe src="/embed-okbookmarks?theme=dark" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
|
||||
<iframe src="/embed/monitor-earth?theme=dark" width="100%" height="100" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
|
||||
</div>
|
||||
|
||||
+102
-56
@@ -9,91 +9,137 @@ Kener needs some environment variables to be set to run properly. Here are the l
|
||||
|
||||
All of these are optional but are required for specific features.
|
||||
|
||||
## KENER_SECRET_KEY [Required]
|
||||
|
||||
Please set a strong secret key for Kener to use for encrypting the data. This is required to run Kener.
|
||||
|
||||
```bash
|
||||
export KENER_SECRET_KEY=a-strong-secret-key
|
||||
```
|
||||
|
||||
## ORIGIN [Required]
|
||||
|
||||
Set this to the origin of your website(protocl + hostname + port if there). This is required for CORS.
|
||||
|
||||
```bash
|
||||
export ORIGIN=http://localhost:3000
|
||||
```
|
||||
|
||||
If your host is at let us say `https://example.com`, then set the ORIGIN to `https://example.com`. This is required for CORS.
|
||||
If you are using a reverse proxy like nginx, make sure to set the `ORIGIN` to the domain name of your website.
|
||||
|
||||
```bash
|
||||
export ORIGIN=https://example.com
|
||||
```
|
||||
|
||||
## PORT
|
||||
|
||||
Defaults to 3000 if not specified
|
||||
|
||||
```shell
|
||||
```bash
|
||||
export PORT=4242
|
||||
```
|
||||
|
||||
## GH_TOKEN
|
||||
|
||||
A github token to read issues and create labels. This is required for **incident management**
|
||||
|
||||
```shell
|
||||
export GH_TOKEN=your-github-token
|
||||
```
|
||||
|
||||
## API_TOKEN
|
||||
|
||||
To talk to **kener apis** you will need to set up a token. It uses Bearer Authorization
|
||||
|
||||
```shell
|
||||
export API_TOKEN=sometoken
|
||||
```
|
||||
|
||||
## API_IP
|
||||
|
||||
While using API you can set this variable to accept request from a **specific IP**
|
||||
|
||||
```shell
|
||||
export API_IP=127.0.0.1
|
||||
```
|
||||
|
||||
## API_IP_REGEX
|
||||
|
||||
While using API you can set this variable to accept request from a specific IP that matches the regex. Below example shows an **IPv6 regex**
|
||||
|
||||
```shell
|
||||
export API_IP_REGEX=^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$
|
||||
```
|
||||
|
||||
If you set both API_IP and API_IP_REGEX, API_IP will be given preference
|
||||
|
||||
## KENER_BASE_PATH
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path.
|
||||
|
||||
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
|
||||
- Important: This env variable should be present during both build and run time
|
||||
- If you are using docker you will have to do your own build and set this env variable during `docker build`
|
||||
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
|
||||
- Important: This env variable should be present during both build and run time
|
||||
|
||||
```shell
|
||||
```bash
|
||||
export KENER_BASE_PATH=/status
|
||||
```
|
||||
|
||||
## RESEND_API_KEY
|
||||
|
||||
Kener uses [resend.com](https://resend.com) to send emails. Please make sure to sign up in [resend.com](https://resend.com) and get the API key. You will need to set the API key in the environment variable `RESEND_API_KEY`.
|
||||
|
||||
```bash
|
||||
export RESEND_API_KEY=re_sometoken
|
||||
```
|
||||
|
||||
## RESEND_SENDER_EMAIL
|
||||
|
||||
Set the sender email for the emails that are sent by Kener. This is required for sending emails. If you have not added a domain in resend, you can se something like `Some Name <onboarding@resend.dev>`. We recommend adding a domain in resend and using that email domain.
|
||||
|
||||
```bash
|
||||
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. If you do not want to use resend, you can set SMTP variables.
|
||||
</div>
|
||||
|
||||
## DATABASE_URL
|
||||
|
||||
Kener uses a database to store its data. By default, Kener uses sqlite. You can change the database by setting the `DATABASE_URL` environment variable. The connection string has to start with `sqlite`, `postgresql`, or `mysql`. Read more about [database configuration](/docs/database).
|
||||
|
||||
```bash
|
||||
export DATABASE_URL=sqlite://./database/awesomeKener.db
|
||||
```
|
||||
|
||||
## TZ
|
||||
|
||||
Set the timezone for the server. Set it to UTC.
|
||||
|
||||
```bash
|
||||
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 info ">
|
||||
|
||||
Generally, port 465 expects implicit SSL (SMTP_SECURE: 1), while port 587 and port 25 usually use STARTTLS (SMTP_SECURE: 0).
|
||||
|
||||
So, if you are using SMTP on port 465, set SMTP_SECURE: 1; otherwise, for ports 25 or 587, set SMTP_SECURE: 0.
|
||||
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div class=" note info ">
|
||||
|
||||
If your SMTP provider does require username and password, you can set `SMTP_USER` and `SMTP_PASS` to `-`.
|
||||
|
||||
</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
|
||||
|
||||
```shell
|
||||
```bash
|
||||
KENER_SECRET_KEY=please_change_me
|
||||
PORT=4242
|
||||
GH_TOKEN=your-github-token
|
||||
API_TOKEN=sometoken
|
||||
API_IP=
|
||||
API_IP_REGEX=
|
||||
RESEND_API_KEY=re_sometoken
|
||||
KENER_BASE_PATH=/status
|
||||
ORIGIN=http://localhost:3000
|
||||
```
|
||||
|
||||
## Secrets
|
||||
|
||||
Kener supports secrets in monitors. Let us say you have a monitor that is API based and you want to keep the API key secret. You can use the `secrets` key in the monitor to keep the API key secret.
|
||||
Kener supports secrets in monitors. Let us say you have a monitor that is API based and you want to keep the API key secret.
|
||||
|
||||
```yaml
|
||||
- name: Example Secret Monitor
|
||||
description: Monitor to show how to use secrets
|
||||
tag: "secret"
|
||||
api:
|
||||
method: GET
|
||||
url: https://api.example.com/users
|
||||
headers:
|
||||
Authorization: Bearer $CLIENT_SECRET
|
||||
```
|
||||
Example: `https://api.example.com/users` with a header `Authorization: Bearer $CLIENT_SECRET`
|
||||
|
||||
In the above example, the `CLIENT_SECRET` is a secret that you can set in the monitor. To properly make this work you will have to set up environment variables like below
|
||||
You should set the `CLIENT_SECRET` in the your environment variables.
|
||||
|
||||
```shell
|
||||
```bash
|
||||
export CLIENT_SECRET=your-api-key
|
||||
```
|
||||
|
||||
|
||||
+24
-8
@@ -9,13 +9,7 @@ Kener uses github for incident management. Issues created in github using certai
|
||||
|
||||
## Step 1: Create Github Repository
|
||||
|
||||
Create a Github Repository. It can be either public or private. After you have created a repository open `site.yaml` and add them like this
|
||||
|
||||
```yaml
|
||||
github:
|
||||
owner: "username"
|
||||
repo: "repository"
|
||||
```
|
||||
Create a [Github Repository](https://github.com/new). It can be either public or private.
|
||||
|
||||
## Step 2: Create Github Token
|
||||
|
||||
@@ -41,6 +35,28 @@ You can create either a classic token or personal access token
|
||||
|
||||
## Step 3: Set environment
|
||||
|
||||
```shell
|
||||
```bash
|
||||
export GH_TOKEN=github_pat_11AD3ZA3Y0
|
||||
```
|
||||
|
||||
## Step 4: Add to Kener
|
||||
|
||||
Add your repository details to kener.
|
||||
|
||||

|
||||
|
||||
### Github API URL
|
||||
|
||||
If you are on github enterprise you can set the github api url. For most users, it will be `https://api.github.com`
|
||||
|
||||
### Github Repo
|
||||
|
||||
The repository name you created in step 1
|
||||
|
||||
### Github Username
|
||||
|
||||
Your github username
|
||||
|
||||
### Incident History
|
||||
|
||||
It is in hours. It means if an issue is created before X hours then kener would not honor it. What it means, is that kener would not show it under active incidents nor it will update the uptime. Default is 30\*24 hours = 720 hours.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Home Page Setup | Kener
|
||||
description: Learn how to set up and work with the home page in kener.
|
||||
---
|
||||
|
||||
# Home Page
|
||||
|
||||
Home page is the first page that the user sees when they visit your status page. You can customize the home page to show the information that you want to show.
|
||||
|
||||
---
|
||||
|
||||
## Hero Section
|
||||
|
||||
### Title
|
||||
|
||||
A big text that will be shown on the hero section.
|
||||
|
||||
### Subtitle
|
||||
|
||||
A small text that will be shown below the title.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Navigation
|
||||
|
||||
You can navigation links to other urls. You can add as many as you want.
|
||||
|
||||

|
||||
|
||||
### Icon
|
||||
|
||||
A small image to be shown along with the link. Icon is optional
|
||||
|
||||
### Title
|
||||
|
||||
The title of the link.
|
||||
|
||||
### URL
|
||||
|
||||
The URL to redirect to when the link is clicked.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Internationalization
|
||||
|
||||
Add multiple languages to your status page. You can add a new language and add translations for all the text that is shown on the status page by following the steps [here](/docs/i18n).
|
||||
|
||||
- Adding more than one locales will enable a dropdown in the navbar to select the language.
|
||||
- Selected languages are stored in cookies and will be used when the user visits the site again.
|
||||
- There is no auto detection of the language. The user has to manually select the language.
|
||||
|
||||
### Default Language
|
||||
|
||||
The default language of the status page.
|
||||
|
||||
---
|
||||
|
||||
## Site Categories
|
||||
|
||||
Use this to group monitors. Byh default a readonly home category is created. You can add more categories and move monitors to those categories.
|
||||
|
||||
---
|
||||
|
||||
## Site Footer
|
||||
|
||||
Add footer text or html to the status page. You can add any text that you want to show in the footer. Make sure if you are adding html it is safe and valid.
|
||||
+32
-105
@@ -1,9 +1,9 @@
|
||||
---
|
||||
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 | Open Source Status Page
|
||||
description: Kener is a feature-rich and modern status page system built with SvelteKit and NodeJS. It is open-source and free to use.
|
||||
---
|
||||
|
||||
# Kener - A Sveltekit NodeJS Status Page System
|
||||
# Kener - A Feature Rich & Modern Status Page
|
||||
|
||||
<p align="center">
|
||||
<img src="/newbg.png" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
|
||||
@@ -36,114 +36,41 @@ description: Kener is an open-source Node.js status page tool, designed to make
|
||||
</picture>
|
||||
</div>
|
||||
<div class="flex gap-2 kener-home-links">
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing">Live Demo</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/quick-start">Quick Start</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/quick-start">Clone</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/deployment">Deploy</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/kener-apis">APIs</a>
|
||||
</div>
|
||||
<a href="https://kener.ing" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Live Demo
|
||||
</a>
|
||||
<a href="https://kener.ing/docs/quick-start" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Quick Start
|
||||
</a>
|
||||
<a href="https://github.com/rajnandan1/kener" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Clone
|
||||
</a>
|
||||
<a href="https://kener.ing/docs/deployment" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Deploy
|
||||
</a>
|
||||
<a href="https://kener.ing/docs/kener-apis" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
APIs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## What is Kener?
|
||||
## Forget Boring Status Pages. Hello Kener!
|
||||
|
||||
Kener is status page system built with Sveltekit and NodeJS. It does not try to replace the Datadogs and Atlassian of the world. It tries to help some who wants to come up with a status page that looks nice and minimum overhead, in a modern way.
|
||||
**Kener** isn't just another status page – it's a beautifully crafted, lightning-fast monitoring system that makes your services look good even when they're down. Built with modern tech (**SvelteKit** + **NodeJS**), it delivers enterprise-grade reliability monitoring without the enterprise price tag (it's completely free!).
|
||||
|
||||
It comes with all the basic asks for a status page. It is open-source and free to use.
|
||||
### Why Teams Are Switching to Kener
|
||||
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a cheaply available domain.
|
||||
- **📈 Up and running in minutes**, not days – from Docker to deployment in less time than it takes to make coffee
|
||||
- **🎭 Dark mode that actually looks good** – because your 3 AM incident responses shouldn't burn your retinas
|
||||
- **🧩 Ridiculously customizable** – make it match your brand or go wild with a unique design
|
||||
- **⚡ Blazing fast performance** – server-rendered for speed that both users and search engines love
|
||||
- **🌐 Works everywhere** – auto-adjusts to visitors' time zones and looks great on any device
|
||||
- **🔄 Real-time data** with gorgeous visualizations that make your uptime metrics pop
|
||||
- **🛠️ Modern tooling** without the bloat – everything you need, nothing you don't
|
||||
|
||||
## Features
|
||||
### Used by Companies That Value Simplicity and Power
|
||||
|
||||
### Monitoring and Tracking
|
||||
From indie developers to growing startups, teams are choosing Kener when they need reliability monitoring that doesn't require a dedicated DevOps team to maintain.
|
||||
|
||||
- Advanced application performance monitoring tools
|
||||
- Real-time network monitor software capabilities
|
||||
- Polls HTTP endpoint or Push data to monitor using Rest APIs
|
||||
- Handles Timezones for visitors
|
||||
- Categorize Monitors into different Sections
|
||||
- Cron-based scheduling for monitors. Minimum per minute
|
||||
- Flexible monitor configuration using YAML
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
"_My team set up Kener in less than an hour, and our customers love the transparency it provides. The embedded widgets on our pricing page increased conversions by showing our reliability upfront._" – A happy user
|
||||
|
||||
### Customization and Branding
|
||||
|
||||
- Customizable status page using yaml or code
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
|
||||
### Incident Management
|
||||
|
||||
- Create Incidents using Github Issues - Rich Text
|
||||
- Or use APIs to create Incidents
|
||||
|
||||
### User Experience and Design
|
||||
|
||||
- 100% Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
|
||||
## Technologies used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Inspired from
|
||||
|
||||
Kener draws inspiration from a comprehensive ecosystem of monitoring and status page solutions, reflecting the diverse landscape of network and application performance tools:
|
||||
Uptime and Status Page Platforms
|
||||
|
||||
- Upptime - GitHub-powered uptime monitoring
|
||||
- Statuspage - Incident communication platform
|
||||
- Cachet - Open-source status page system
|
||||
- Upptrends - Global website monitoring
|
||||
- Hexometer - Website monitoring and performance tracking
|
||||
|
||||
Enterprise Monitoring Solutions
|
||||
|
||||
- Pingdom - Website monitoring service
|
||||
- New Relic - Application performance monitoring
|
||||
- Datadog - Observability and monitoring platform
|
||||
- AppDynamics - Application performance management
|
||||
- Dynatrace - AI-powered full stack monitoring
|
||||
|
||||
Network and Infrastructure Monitoring
|
||||
|
||||
- StatusCake - Uptime and performance monitoring
|
||||
- UptimeRobot - Free website monitoring
|
||||
- Better Uptime - Monitoring and incident management
|
||||
- Site24x7 - Comprehensive monitoring solution
|
||||
- Nagios - Infrastructure and network monitoring
|
||||
- Zabbix - Enterprise-class monitoring solution
|
||||
- Prometheus - Monitoring and alerting toolkit
|
||||
- Sentry - Error tracking and performance monitoring
|
||||
|
||||
Cloud and Hybrid Monitoring Platforms
|
||||
|
||||
- CloudWatch - AWS monitoring service
|
||||
- Azure Monitor - Microsoft's monitoring solution
|
||||
- Google Cloud Monitoring - Cloud-native monitoring
|
||||
|
||||
## Support Me
|
||||
|
||||
If you are using Kener and want to support me, you can do so by sponsoring me on GitHub or buying me a coffee.
|
||||
|
||||
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
|
||||
|
||||
[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
## Ready to Transform Your Status Page?
|
||||
|
||||
@@ -12,7 +12,7 @@ Kener has two parts.
|
||||
|
||||
## Folder structure
|
||||
|
||||
```shell
|
||||
```bash
|
||||
├── src (svelte frontend files)
|
||||
├── static (things put here can be referenced directly example static/logo.png -> /logo.png)
|
||||
├── src
|
||||
|
||||
+176
-20
@@ -5,37 +5,193 @@ description: Kener supports multiple languages. You can add translations to your
|
||||
|
||||
# i18n
|
||||
|
||||
You can add translations to your site. By default it is set to `en`. Available translations are present in `/src/lib/locales/` folders in the root directory. You can add more translations by adding a new file in the `/src/lib/locales` folder.
|
||||
You can add translations to your site. By default it is set to `en`. Available translations are present in `src/lib/locales/` folders in the root directory. You can add more translations by adding a new file in the `src/lib/locales` folder.
|
||||
|
||||
## How to enable a translation
|
||||
|
||||
Once you have added a new translation file in the `locales` folder, you can enable it by adding the locale code in the `site.yaml` file.
|
||||
Kener has made it simple to add new translations.
|
||||
|
||||
Let us say you have added a `hi.json` file in the `locales` folder. You can enable it by adding the following to the `site.yaml` file.
|
||||
### Step 1
|
||||
|
||||
```yaml
|
||||
i18n:
|
||||
defaultLocale: en
|
||||
locales:
|
||||
en: English
|
||||
hi: हिन्दी
|
||||
Copy and update the translation file in the `locales` folder. The translation file should be a json file with the locale code as the name of the file. For example, `en.json`, `fr.json`, `de.json`.
|
||||
|
||||
<label for="exp2" class="accm">
|
||||
|
||||
<input type="checkbox" class="absolute opacity-0" id="exp2" />
|
||||
|
||||
<p class=" font-medium p-1 accmt rounded-md showaccm">
|
||||
<span>↓ Show File</span>
|
||||
<span>↑ Hide File</span>
|
||||
</p>
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||
```json
|
||||
{
|
||||
"%status for %duration": "%status for %duration",
|
||||
"120 Days": "120 Days",
|
||||
"14 Days": "14 Days",
|
||||
"150 Days": "150 Days",
|
||||
"180 Days": "180 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"7 Days": "7 Days",
|
||||
"90 Days": "90 Days",
|
||||
"Availability per Component": "Availability per Component",
|
||||
"All Systems are Degraded": "All Systems are Degraded",
|
||||
"All Systems are Down": "All Systems are Down",
|
||||
"All Systems are Operational": "All Systems are Operational",
|
||||
"All Systems are in Maintenance": "All Systems are in Maintenance",
|
||||
"Back": "Back",
|
||||
"Badge Copied": "Badge Copied",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Browse Events",
|
||||
"Change Email": "Change Email",
|
||||
"Code Copied": "Code Copied",
|
||||
"Confirm Login": "Confirm Login",
|
||||
"Copy Code": "Copy Code",
|
||||
"Copy Link": "Copy Link",
|
||||
"DEGRADED": "DEGRADED",
|
||||
"DOWN": "DOWN",
|
||||
"Dark": "Dark",
|
||||
"Days": "Days",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"Embed": "Embed",
|
||||
"Enter the code": "Enter the code",
|
||||
"Enter your email": "Enter your email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.",
|
||||
"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",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Incident Updates": "Incident Updates",
|
||||
"LIVE Status": "LIVE Status",
|
||||
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
|
||||
"Light": "Light",
|
||||
"Link Copied": "Link Copied",
|
||||
"Login": "Login",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"MONITORING": "MONITORING",
|
||||
"Maintenance Completed": "Maintenance Completed",
|
||||
"Maintenance in Progress": "Maintenance in Progress",
|
||||
"Manage Subscription": "Manage Subscription",
|
||||
"Mode": "Mode",
|
||||
"No Data": "No Data",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
"Pinging": "Pinging",
|
||||
"Please select specific monitors to receive updates from.": "Please select specific monitors to receive updates from.",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"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",
|
||||
"Some Systems are not working as expected": "Some Systems are not working as expected",
|
||||
"Some Systems Degraded": "Some Systems Degraded",
|
||||
"Some Systems Down": "Some Systems Down",
|
||||
"Some Systems in Maintenance": "Some Systems in Maintenance",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
"Started %startedAt, will last for %lastedFor more": "Started %startedAt, will last for %lastedFor more",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Subscribe": "Subscribe",
|
||||
"Subscribe to Updates": "Subscribe to Updates",
|
||||
"Subscribe to all monitors": "Subscribe to all monitors",
|
||||
"Switch to your timezone": "Switch to your timezone",
|
||||
"Theme": "Theme",
|
||||
"There are no upcoming maintenances": "There are no upcoming maintenances",
|
||||
"Timezone set to %tz": "Timezone set to %tz",
|
||||
"Today": "Today",
|
||||
"UP": "UP",
|
||||
"Update Subscription": "Update Subscription",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Upcoming Maintenances": "Upcoming Maintenances",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime",
|
||||
"View in detail": "View in detail",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "We have sent a code to your email. Please enter it below to confirm your login",
|
||||
"You are logged in as %email": "You are logged in as %email"
|
||||
}
|
||||
```
|
||||
|
||||
> **_defaultLocale:_** The default locale to be used. This will be the language used when a user visits the site for the first time. It is important to note that the default locale json file should be present in the locales folder.
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
### Step 2
|
||||
|
||||
Open `src/lib/locales/locales.json` and add the locale code and the name of the language.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"code": "en",
|
||||
"name": "English"
|
||||
},
|
||||
{
|
||||
"code": "hi",
|
||||
"name": "हिन्दी"
|
||||
},
|
||||
{
|
||||
"code": "zh-CN",
|
||||
"name": "中文"
|
||||
},
|
||||
{
|
||||
"code": "ja",
|
||||
"name": "日本語"
|
||||
},
|
||||
{
|
||||
"code": "vi",
|
||||
"name": "Tiếng Việt"
|
||||
},
|
||||
{
|
||||
"code": "fr",
|
||||
"name": "Français"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Step 3
|
||||
|
||||
Open `src/lib/i18n/client.js` and add the locale code to the `locales` array.
|
||||
|
||||
```js
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale"
|
||||
```
|
||||
|
||||
This should be valid for the date-fns library. You can find the list of locales [here](https://github.com/date-fns/date-fns/blob/9bb51691f201c3ec05ab832acbc5d478f2e5c47a/docs/i18nLocales.md).
|
||||
|
||||
Next add a mapping in the `locales` object.
|
||||
|
||||
```js
|
||||
const locales = { ru, en: enUS, hi, dk: de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko }
|
||||
```
|
||||
|
||||
### Step 4
|
||||
|
||||
Restart the server and you will see a dropdown in the navbar to select the language.
|
||||
|
||||
<div class="note info">
|
||||
DO NOT forget to raise a PR with the new translation file so that others can use it too.
|
||||
</div>
|
||||
|
||||
## Variables
|
||||
|
||||
There are few variables that you you should not change,
|
||||
|
||||
- %hours : This will be replaced by the hours
|
||||
- %minutes : This will be replaced by the minutes
|
||||
- %minute : This will be replaced by the minute
|
||||
- %status : This will be replaced by the status
|
||||
- %status : This will be replaced by the status
|
||||
- %lastedFor : This will be replaced by the duration the incident lasted
|
||||
- %startedAt : This will be replaced by the time the incident started
|
||||
- %date : This will be replaced by the date
|
||||
- %duration : This will be replaced by the duration
|
||||
|
||||
> **locales:\_** A list of locales that you want to enable. The key is the locale code and the value is the name of the language. The locale code should be the same as the json file name in the locales folder. `en` means `en.json` should be present in the locales folder.
|
||||
## Dates
|
||||
|
||||
Adding more than one locales will enable a dropdown in the navbar to select the language.
|
||||
|
||||
Selected languages are stored in cookies and will be used when the user visits the site again.
|
||||
|
||||
There is no auto detection of the language. The user has to manually select the language.
|
||||
The localization for dates are still not there. We are working on it and will be available soon.
|
||||
|
||||
+62
-20
@@ -5,34 +5,76 @@ description: Kener uses Github to power incident management using labels
|
||||
|
||||
# Incident Management
|
||||
|
||||
Kener uses Github to power incident management using labels
|
||||
Kener lets you manage incidents using its dashboard or APIs.
|
||||
|
||||
## Labels
|
||||
## Create Incident
|
||||
|
||||
Kener auto creates labels for your monitors using the `tag` parameter
|
||||
### Incident Title
|
||||
|
||||
- `incident`: If an issue is marked as incident it will show up in kener home page
|
||||
- `incident-down`: If an issue is marked as incident-down and incident kener would make that monitor down
|
||||
- `incident-degraded`: If an issue is marked as incident-degraded and incident then kener would make the monitor degraded
|
||||
- `resolved`: Use this tag to mark the incident has RESOLVED
|
||||
- `identified`: Use this tag to show that the root cause of the incident has been identified
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
## Creating Incident
|
||||
The title of the incident. This is used to identify the incident in the dashboard.
|
||||
|
||||
Kener uses Github issues to create incidents. Here is how you can create an incident
|
||||
### Incident Summary
|
||||
|
||||
### Using Github
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
- Go to your github repo of kener
|
||||
- Go to issues
|
||||
- Create an issue. Give it a title
|
||||
- In the body add [start_datetime:1702651340] and [end_datetime:1702651140] and add some description. Time is UTC
|
||||
- Add `incident`, `incident-down` and the monitor tag. This will make the monitor down for 4 minutes
|
||||
A brief summary of the incident. This is used to give a brief overview of the incident.
|
||||
|
||||
If you clone the repo it gives you an issue template to create incidents
|
||||
### Incident Start Date Time
|
||||
|
||||
Here is a [sample incident](https://github.com/rajnandan1/kener/issues/15) for your reference.
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
### Using API
|
||||
The start date and time of the incident.
|
||||
|
||||
You can also create incidents using the API. See the API [here](/docs/kener-apis#create-an-incident---api)
|
||||
<div class="note info">
|
||||
Each incident once created will be in INVESTIGATING status. You can update the status of the incident by adding updates as discussed below
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Update Incident
|
||||
|
||||
Add comments to the incident to update the status of the incident.
|
||||
|
||||
### Time Stamp
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
### Message
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The message to be added to the incident.
|
||||
|
||||
### Status
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The status of the incident. This can be `INVESTIGATING`, `IDENTIFIED`, `MONITORING`, `RESOLVED`.
|
||||
|
||||
### Close Incident
|
||||
|
||||
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. For any event type, this can be `DEGRADED` or `DOWN`. For maintenance event type, this can also be `MAINTENANCE`.
|
||||
|
||||
## 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.
|
||||
|
||||
+10
-408
@@ -5,26 +5,21 @@ description: Kener gives APIs to push data and create incident.
|
||||
|
||||
# Kener APIs
|
||||
|
||||
Kener also gives APIs to push data and create incident. Before you use kener apis you will have to set an authorization token called `API_TOKEN`. This also has to be set as an environment variable.
|
||||
Kener also gives APIs to push data and create incident. Before you use kener apis you will have to generate the API keys using the dashboard.
|
||||
|
||||
```shell
|
||||
export API_TOKEN=some-token-set-by-you
|
||||
```
|
||||
1. Go to manage kener dashboard
|
||||
2. Go to api keys
|
||||
3. Click on Create New API Key
|
||||
4. Enter a name for the API key
|
||||
5. Click on Create
|
||||
|
||||
Additonally you can set IP whitelisting by setting another environment token called `API_IP` or `API_IP_REGEX`. If you set both `API_IP` and `API_IP_REGEX`, `API_IP` will be given preference. Read more [here](/docs/environment-vars#api_ip)
|
||||
You can use the generated API key to authenticate the APIs. It will be not be shown again, so make sure to save it.
|
||||
|
||||
## Interactive API Reference
|
||||
## API Reference
|
||||
|
||||
<p class="border p-4 rounded-md">
|
||||
<picture class="inline">
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f916/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f916/512.gif" alt="🤖" width="32" height="32">
|
||||
</picture>
|
||||
<a href="/api-reference">
|
||||
Click here to view the interactive API reference
|
||||
</a>
|
||||
The current version is 3.0.0. We maintain just one version
|
||||
|
||||
</p>
|
||||
[API Reference](/api-reference)
|
||||
|
||||
You can download the openapi spec
|
||||
|
||||
@@ -32,396 +27,3 @@ You can download the openapi spec
|
||||
- [YAML](https://raw.githubusercontent.com/rajnandan1/kener/main/openapi.yaml)
|
||||
|
||||
---
|
||||
|
||||
## Update Status - API
|
||||
|
||||

|
||||
|
||||
The update status API can be used to manually update the state of a monitor from a remote server.
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------ |
|
||||
| status | `Required` Can be only UP/DOWN/DEGRADED |
|
||||
| latency | `Required` In Seconds. Leave 0 if not required |
|
||||
| timestampInSeconds | `Optional` Timestamp in UTC seconds. Defaults to now. Should between 90 Days and now |
|
||||
| tag | `Required` Monitor Tag set in monitors.yaml |
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/status \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"status": "DOWN",
|
||||
"latency": 1213,
|
||||
"timestampInSeconds": 1702405860,
|
||||
"tag": "google-search"
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 200,
|
||||
"message": "success at 1702405860"
|
||||
}
|
||||
```
|
||||
|
||||
This will update the status of the monitor with tag `google-search` to DOWN at UTC 1702405860
|
||||
|
||||
---
|
||||
|
||||
## Get Status - API
|
||||
|
||||

|
||||
|
||||
Use this API to get the status of a monitor.
|
||||
|
||||
### Request
|
||||
|
||||
Replace `google-search` with your monitor tag in query param
|
||||
|
||||
```shell
|
||||
curl --request GET \
|
||||
--url 'http://your-kener.host/api/status?tag=google-search' \
|
||||
--header 'Authorization: Bearer some-token-set-by-you'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "UP",
|
||||
"uptime": "9.0026",
|
||||
"lastUpdatedAt": 1706447160
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Create an Incident - API
|
||||
|
||||

|
||||
|
||||
Can be use to create an incident from a remote server
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------- | -------------------------------------------------------- |
|
||||
| startDatetime | `Optional` When did the incident start in UTC second |
|
||||
| endDatetime | `Optional` When did the incident end in UTC seconds |
|
||||
| title | `Required` Title of the incident |
|
||||
| body | `Optional` Body of the incident |
|
||||
| tags | `Required` Array of String, Monitor Tags of the incident |
|
||||
| impact | `Optional` Can be only DOWN/DEGRADED |
|
||||
| isMaintenance | `Optional` Boolean if incident is a maintenance |
|
||||
| isIdentified | `Optional` Incident identified |
|
||||
| isResolved | `Optional` Incident resolved |
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/incident \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"title": "Outage in Mumbai",
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"tags": ["google-search"],
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update an Incident - API
|
||||
|
||||

|
||||
|
||||
Can be use to update an incident from a remote server. It will clear values if not passed
|
||||
|
||||
### Request Param
|
||||
|
||||
- `incidentNumber`: Number of the incident
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------- | -------------------------------------------------------- |
|
||||
| startDatetime | `Optional` When did the incident start in UTC second |
|
||||
| endDatetime | `Optional` When did the incident end in UTC seconds |
|
||||
| title | `Required` Title of the incident |
|
||||
| body | `Optional` Body of the incident |
|
||||
| tags | `Required` Array of String, Monitor Tags of the incident |
|
||||
| impact | `Optional` Can be only DOWN/DEGRADED |
|
||||
| isMaintenance | `Optional` Boolean if incident is a maintenance |
|
||||
| isIdentified | `Optional` Incident identified |
|
||||
| isResolved | `Optional` Incident resolved |
|
||||
|
||||
```shell
|
||||
curl --request PATCH \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber} \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"title": "Outage in Mumbai",
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"tags": ["google-search"],
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get an Incident - API
|
||||
|
||||

|
||||
|
||||
Use `incidentNumber` to fetch an incident
|
||||
|
||||
### Request Body
|
||||
|
||||
```shell
|
||||
curl --request GET \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber} \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Add Comment - API
|
||||
|
||||

|
||||
|
||||
Add comments for incident using `incidentNumber`
|
||||
|
||||
### Request
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber}/comment \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"body": "comment 1"
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"commentID": 1873376745,
|
||||
"body": "comment 1",
|
||||
"createdAt": 1704123938
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get Comments - API
|
||||
|
||||

|
||||
|
||||
Use this API to fetch all the comments for an incident
|
||||
|
||||
### Request
|
||||
|
||||
```shell
|
||||
curl --request GET \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber}/comment \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"commentID": 1873372042,
|
||||
"body": "comment 1",
|
||||
"createdAt": 1704123116
|
||||
},
|
||||
{
|
||||
"commentID": 1873372169,
|
||||
"body": "comment 2",
|
||||
"createdAt": 1704123139
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update Incident Status - API
|
||||
|
||||

|
||||
|
||||
Use this to API to update the status of an ongoing incident.
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | ------------------------------------------------------------ |
|
||||
| isIdentified | `Optional` Boolean, set it when incident has been identified |
|
||||
| isResolved | `Optional` Boolean, set it when incident has been resolved |
|
||||
| endDatetime | `Optional` When did the incident end in UTC seconds |
|
||||
|
||||
### Request
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/incident/{incidentNumber}/status \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
"endDatetime": 1702405920
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Search Incidents - API
|
||||
|
||||

|
||||
|
||||
Use this to API to search incidents.
|
||||
|
||||
### Request Body
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| state | `Optional` open or closed. Default is open |
|
||||
| tags | `Optional` Comma separated monitor tags, example: earth,google-seach |
|
||||
| page | `Optional` Page number, starts with 1, defaults to 1 |
|
||||
| per_page | `Optional` Page size, defaults to 10, max is 100 |
|
||||
| created_after_utc | `Optional` timestamp in UTC seconds when the incident was created after. Example: 1702405920 |
|
||||
| created_before_utc | `Optional` timestamp in UTC seconds when the incident was created before . Example: 1702405920 |
|
||||
| title_like | `Optional` search incidents with title |
|
||||
|
||||
### Request
|
||||
|
||||
Search incidents that are closed and title contains `hello incident`
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://your-kener.host/api/incident?state=closed&title_like=Hello%20Incident \
|
||||
--header 'Authorization: Bearer some-token-set-by-you' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
"endDatetime": 1702405920
|
||||
}'
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"createdAt": 1703940450,
|
||||
"closedAt": null,
|
||||
"title": "Outage in Mumbai - Hello Incident",
|
||||
"tags": ["google-search"],
|
||||
"incidentNumber": 12,
|
||||
"startDatetime": 1702405740,
|
||||
"endDatetime": 1702405920,
|
||||
"body": "Login cluster is down in mumbai region",
|
||||
"impact": "DOWN",
|
||||
"isMaintenance": false,
|
||||
"isIdentified": true,
|
||||
"isResolved": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -152,7 +152,7 @@ Below example will call https://api.github.com/repos/rajnandan1/kener/issues. If
|
||||
})
|
||||
```
|
||||
|
||||
## With defaultStatus UP
|
||||
## With default_status UP
|
||||
|
||||
Each minute it will set the status as UP
|
||||
|
||||
@@ -160,7 +160,7 @@ Each minute it will set the status as UP
|
||||
- name: Earth
|
||||
description: Our Planet
|
||||
tag: "earth"
|
||||
defaultStatus: UP
|
||||
default_status: UP
|
||||
```
|
||||
|
||||
## With Category
|
||||
@@ -200,10 +200,10 @@ The below monitor will show DEGRADED if 3 or more degraded status in a day and D
|
||||
- name: Earth
|
||||
description: Our blue planet
|
||||
tag: "earth"
|
||||
defaultStatus: "UP"
|
||||
dayDegradedMinimumCount: 3
|
||||
dayDownMinimumCount: 2
|
||||
includeDegradedInDowntime: true
|
||||
default_status: "UP"
|
||||
day_degraded_minimum_count: 3
|
||||
day_down_minimum_count: 2
|
||||
include_degraded_in_downtime: true
|
||||
```
|
||||
|
||||
## Monitor with Alerts
|
||||
@@ -216,8 +216,8 @@ The below example will trigger an alert if the monitor is DOWN for 10 consecutiv
|
||||
- name: Earth
|
||||
description: Our blue planet
|
||||
tag: "earth"
|
||||
defaultStatus: "UP"
|
||||
includeDegradedInDowntime: true
|
||||
default_status: "UP"
|
||||
include_degraded_in_downtime: true
|
||||
alerts:
|
||||
DOWN:
|
||||
failureThreshold: 10
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
---
|
||||
title: API Monitors | Kener
|
||||
description: Learn how to set up and work with API monitors in kener.
|
||||
---
|
||||
|
||||
# API Monitors
|
||||
|
||||
API monitors are used to monitor APIs. You can use API monitors to monitor the uptime of your Website or APIs and get notified when they are down.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Timeout
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The timeout is used to define the time in milliseconds after which the monitor should timeout. If the monitor does not respond within the timeout period, the monitor will be marked as down. For example, `5000` will set the timeout to 5 seconds. It is required and has to be a number greater than 0.
|
||||
|
||||
## URL
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The URL is used to define the URL of the API that you want to monitor. It is required and has to be a valid URL.
|
||||
|
||||
## Method
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The method is used to define the HTTP method that should be used to make the request. It is required and has to be one of the following: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`.
|
||||
|
||||
## Headers
|
||||
|
||||
The headers are used to define the headers that should be sent with the request. It is optional and has to be a valid JSON object.
|
||||
|
||||
## 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 (statusCode, responseTime, responseRaw, modules) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
let status = 'DOWN'
|
||||
if(statusCodeShort >=2 && statusCodeShort <= 3) {
|
||||
status = 'UP',
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
|
||||
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
|
||||
- `responseRaw` **REQUIRED** is the raw response of the API
|
||||
- `modules` is an object that has [`cheerio`](https://www.npmjs.com/package/cheerio) that you can use to parse HTML. `const $ = modules.cheerio.load(responseRaw)`
|
||||
|
||||
|
||||
|
||||
### Example 1
|
||||
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the status code is 2XX then the status is UP, if the status code is 5XX then the status is DOWN. If the response contains the word `Unknown Error` then the status is DOWN. If the response time is greater than 2000 then the status is DEGRADED.
|
||||
|
||||
```javascript
|
||||
(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
|
||||
let status = "DOWN"
|
||||
|
||||
//if the status code is 2XX then the status is UP
|
||||
if (/^[2]\d{2}$/.test(statusCode)) {
|
||||
status = "UP"
|
||||
if (responseTime > 2000) {
|
||||
status = "DEGRADED"
|
||||
}
|
||||
}
|
||||
|
||||
//if the status code is 5XX then the status is DOWN
|
||||
if (/^[5]\d{2}$/.test(statusCode)) status = "DOWN"
|
||||
|
||||
if (responseRaw.includes("Unknown Error")) {
|
||||
status = "DOWN"
|
||||
}
|
||||
|
||||
return {
|
||||
status: status,
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Example 2
|
||||
|
||||
This next example shows how to call another API withing eval. It is scrapping the second last script tag from the response and checking if the heading is "No recent issues" then the status is UP else it is DOWN.
|
||||
|
||||
```js
|
||||
(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let htmlString = responseRaw;
|
||||
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g)
|
||||
if (scriptTags && scriptTags.length >= 2) {
|
||||
// Extract the second last script tag's src attribute
|
||||
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
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Example 3
|
||||
|
||||
The next example shows how to use cheerio to parse bitbucket status page and check if all the components are operational. If all the components are operational then the status is UP else it is DOWN.
|
||||
|
||||
```js
|
||||
(async function (statusCode, responseTime, responseDataBase64, modules) {
|
||||
let html = atob(responseDataBase64)
|
||||
const $ = modules.cheerio.load(html)
|
||||
const components = $(".components-section .components-container .component-container")
|
||||
let status = true
|
||||
components.each((index, element) => {
|
||||
const name = $(element).find(".component-name").text().trim()
|
||||
const statusText = $(element).find(".component-status").text().trim()
|
||||
if (statusText !== "Operational") {
|
||||
status = false
|
||||
}
|
||||
})
|
||||
return {
|
||||
status: status ? "UP" : "DOWN",
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Website Monitor
|
||||
|
||||
This is an example to monitor google every 5 minute.
|
||||
|
||||
<label for="websiteMonitor" class="accm">
|
||||
|
||||
<input type="checkbox" class="absolute opacity-0" id="websiteMonitor" />
|
||||
|
||||
<p class="font-medium p-1 accmt rounded-md showaccm">
|
||||
<span>↓ Show Example</span>
|
||||
<span>↑ Hide Example</span>
|
||||
</p>
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
### Get Request
|
||||
|
||||
Example to monitor a GET Request with an Authorization header set to `$SOME_TOKEN`.
|
||||
|
||||
`$SOME_TOKEN` is set in environment variables as `SOME_TOKEN`
|
||||
|
||||
```bash
|
||||
export SOME_TOKEN=some-token-example
|
||||
```
|
||||
|
||||
<label for="exp2" class="accm">
|
||||
|
||||
<input type="checkbox" class="absolute opacity-0" id="exp2" />
|
||||
|
||||
<p class=" font-medium p-1 accmt rounded-md showaccm">
|
||||
<span>↓ Show Example</span>
|
||||
<span>↑ Hide Example</span>
|
||||
</p>
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
### POST Request
|
||||
|
||||
Example showing setting up a POST request every minute with a timeout of 2 seconds.
|
||||
|
||||
<label for="exp3" class="accm">
|
||||
|
||||
<input type="checkbox" class="absolute opacity-0" id="exp3" />
|
||||
|
||||
<p class=" font-medium p-1 accmt rounded-md showaccm">
|
||||
<span>↓ Show Example</span>
|
||||
<span>↑ Hide Example</span>
|
||||
</p>
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
### Custom Response Eval
|
||||
|
||||
Example showing how to use a custom response eval function. It also shows how to set secrets in body for POST Request.
|
||||
The secrets have to be present in environment variables.
|
||||
|
||||
```bash
|
||||
export SERVICE_TOKEN=secret1_tone
|
||||
export SERVICE_SECRET=secret2_secret
|
||||
```
|
||||
|
||||
<label for="exp4" class="accm">
|
||||
|
||||
<input type="checkbox" class="absolute opacity-0" id="exp4" />
|
||||
|
||||
<p class=" font-medium p-1 accmt rounded-md showaccm">
|
||||
<span>↓ Show Example</span>
|
||||
<span>↑ Hide Example</span>
|
||||
</p>
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: DNS Monitors | Kener
|
||||
description: Learn how to set up and work with DNS monitors in kener.
|
||||
---
|
||||
|
||||
# DNS Monitors
|
||||
|
||||
DNS monitors are used to monitor DNS servers. Verify DNS queries for your server and match values with the expected values to get notified when they are different.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Host
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The host is used to define the host of the DNS server that you want to monitor. It is required and has to be a valid host.
|
||||
|
||||
## Lookup Record
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The query is used to define the query that you want to monitor. It is required and has to be a valid query. Example: `A`, `MX`, `TXT`, `CNAME`.
|
||||
|
||||
## Name Server
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The name server is the DNS server that you want to query. It is required and has to be a valid DNS server.
|
||||
|
||||
Few examples:
|
||||
|
||||
- 8.8.8.8 (Google)
|
||||
- 1.1.1.1 (Cloudflare)
|
||||
|
||||
## Match Type
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
Match type for DNS response. Can be ANY or ALL. If ANY is selected, the monitor will be marked as UP if any of the expected values match the response. If ALL is selected, all the expected values should match the response.
|
||||
|
||||
## Expected Values
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The expected values are the values that you expect the DNS server to return. You can add multiple values. Atleast one is required.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Gamedig Monitors | Kener
|
||||
description: Monitor games and services
|
||||
---
|
||||
|
||||
# Gamedig Monitors
|
||||
|
||||
Gamedig monitors are used to monitor games and services. Currently, 320+ games and some services are available. You can view the entire list [here](https://github.com/gamedig/node-gamedig/blob/HEAD/GAMES_LIST.md). More than just the state of the game/service, it provides information about your game/service, such as players list, world name, connection method, and so on.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Host
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The host is the domain name, IPv4 or IPv6 of the game/service you want to monitor. The host should be valid.
|
||||
|
||||
## Port
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The port is the query port of the game/service you want to monitor. Some games use a port, different from the one to connect, to accept queries, such as Minecraft with `query-port`.
|
||||
If you do not know the query port, you can let the monitor guess the port by checking the option `Guess port`.
|
||||
|
||||
## Timeout
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The timeout is used to define the time in milliseconds after which the monitor should timeout. If the monitor does not respond within the timeout period, the monitor will be marked as down. For example, 10000 will set the timeout to 10 seconds. It is required and has to be a number greater than 2000 (or 2 seconds).
|
||||
|
||||
## Request additional rules
|
||||
|
||||
Valve games can provide additional 'rules' to Gamedig monitors. If checked, they will be available in `reponseRaw.raw` (see [Eval](#eval)), beware that it may increase query time.
|
||||
|
||||
## 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 (responseTime, responseRaw) {
|
||||
return {
|
||||
status: "UP",
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `responseTime` **REQUIRED** is a number. It is the latency in milliseconds
|
||||
- `responseRaw` **REQUIRED** is the raw response of the game/service. It varies from game by game, we recommend first printing responseRaw with a simple `console.log`.
|
||||
|
||||
## Additional notes
|
||||
|
||||
Some games/services need more configuration on their part to accept correctly any query of the monitor. You can consult these notes [here](https://github.com/gamedig/node-gamedig/blob/HEAD/GAMES_LIST.md#games-with-additional-notes).
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Group Monitors | Kener
|
||||
description: Learn how to set up and work with Group monitors in kener.
|
||||
---
|
||||
|
||||
# Group Monitors
|
||||
|
||||
Group monitors are used to monitor multiple monitors at once. You can use Group monitors to monitor multiple monitors at once and get notified when they are down.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Timeout
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The timeout is used to define the time in milliseconds after which the group monitor should timeout.
|
||||
|
||||
Let us say the group monitor runs every minute, it will expect in the same minute all the other monitors to finish. It will wait till the timeout for them to complete. If not completed within that timeout, it will be marked as down.
|
||||
|
||||
## Monitors
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
You can add as many monitors as you want to monitor. The minimum number of monitors required is 2. The monitor can be any type of monitor.
|
||||
|
||||
## Hide
|
||||
|
||||
You can hide the monitors that are part of the group monitor. If you hide the monitors, the monitors inside the group will not be shown in the home page.
|
||||
|
||||
<div class="note">
|
||||
The group status will be the worst status of the monitors in the group.
|
||||
</div>
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Heartbeat Monitors | Kener
|
||||
description: Heartbeat monitors allow you to monitor systems using a "push" based approach
|
||||
---
|
||||
|
||||
# Heartbeat Monitors
|
||||
|
||||
Heartbeat monitors allow you to monitor systems using a "push" based approach, where your services send signals (or "heartbeats") to Kener on a regular basis. If Kener doesn't receive a heartbeat within the configured time period, it will mark the system as degraded or down.
|
||||
|
||||
This is useful for monitoring services that:
|
||||
|
||||
- Run on a schedule
|
||||
- Cannot be directly accessed for pull-based monitoring
|
||||
- Need to report their own health status
|
||||
|
||||
## How It Works
|
||||
|
||||
1. You configure a heartbeat monitor in Kener
|
||||
2. Kener provides a unique URL endpoint for your service to ping
|
||||
3. Your service sends HTTP requests to this endpoint at regular intervals
|
||||
4. If Kener doesn't receive a ping within the configured time window, it will mark the service as degraded or down
|
||||
|
||||
## Setting Up a Heartbeat Monitor
|
||||
|
||||
To create a heartbeat monitor:
|
||||
|
||||
1. Navigate to the Monitors page in the dashboard
|
||||
2. Click "Add Monitor"
|
||||
3. Select "Heartbeat" as the monitor type
|
||||
4. Configure the following settings:
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------- |
|
||||
| Secret String | A unique secret that authenticates your heartbeat requests |
|
||||
| Degraded After (Minutes) | Number of minutes after which the service is considered degraded if no heartbeat is received |
|
||||
| Down After (Minutes) | Number of minutes after which the service is considered down if no heartbeat is received |
|
||||
|
||||
## Sending Heartbeats
|
||||
|
||||
Your service needs to send HTTP requests to the Kener heartbeat URL. Both GET and POST requests are supported.
|
||||
|
||||
### Endpoint Format
|
||||
|
||||
```
|
||||
https://<your-kener-instance>/api/heartbeat/<tag>:<secretString>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<tag>` is the tag you configured for your monitor
|
||||
- `<secretString>` is the secret you configured during setup
|
||||
|
||||
### Example
|
||||
|
||||
If your monitor has:
|
||||
|
||||
- Tag: `nightly-backup`
|
||||
- Secret String: `super-secret-key`
|
||||
|
||||
Your service should send requests to:
|
||||
|
||||
```
|
||||
https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
|
||||
```
|
||||
|
||||
You can use any HTTP client to send these requests, such as curl:
|
||||
|
||||
```bash
|
||||
curl https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
|
||||
```
|
||||
|
||||
Or in a scheduled script:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Run your task here
|
||||
./perform-backup.sh
|
||||
|
||||
# Send heartbeat to Kener
|
||||
curl https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
|
||||
```
|
||||
|
||||
## Monitoring State
|
||||
|
||||
The monitor status works as follows:
|
||||
|
||||
- **UP**: Heartbeats are being received within the configured time windows
|
||||
- **DEGRADED**: No heartbeat has been received for longer than the "Degraded After" time period
|
||||
- **DOWN**: No heartbeat has been received for longer than the "Down After" time period
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Keep your secret string confidential, as anyone with knowledge of it can send heartbeats
|
||||
- Consider using HTTPS for your Kener instance to encrypt heartbeat communications
|
||||
- Rotate the secret string periodically by updating the monitor configuration
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Set the "Degraded After" time slightly longer than your expected interval between heartbeats
|
||||
- Set the "Down After" time based on your SLA requirements
|
||||
- For critical systems, consider sending heartbeats more frequently than required
|
||||
- Add logging in your service to track successful and failed heartbeat attempts
|
||||
|
||||
Heartbeat monitors provide a simple but effective way to ensure your scheduled tasks and services are running properly, without requiring external polling.
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
title: Ping Monitors | Kener
|
||||
description: Learn how to set up and work with Ping monitors in kener.
|
||||
---
|
||||
|
||||
# Ping Monitors
|
||||
|
||||
Ping monitors are used to monitor livenees of your servers. You can use Ping monitors to monitor the uptime of your servers and get notified when they are down.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</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.
|
||||
- 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.
|
||||
|
||||
## 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 (arrayOfPings) {
|
||||
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
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `arrayOfPings` **REQUIRED** is an array of Ping Response Objects as shown below.
|
||||
|
||||
```json
|
||||
/*
|
||||
[
|
||||
{
|
||||
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
|
||||
|
||||
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 (arrayOfPings) {
|
||||
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
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: SQL Monitors | Kener
|
||||
description: Monitor a SQL database
|
||||
---
|
||||
|
||||
# SQL Monitors
|
||||
|
||||
SQL monitors are used to monitor a SQL database. You can monitor a SQL database by adding the database connection details to the monitor. The SQL monitor will check the database connection and notify you if the database connection is down.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Database Type
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The database type is the type of the database you want to monitor. The database type should be one of the following values:
|
||||
|
||||
- mysql
|
||||
- postgres
|
||||
|
||||
Instead of giving the connection string as plain text you can add environment variables to the connection string. The environment variables should be in the format of `$`. The environment variables should be defined in the `.env` file.
|
||||
|
||||
Example: `mysql://$MYSQL_USER:$MYSQL_PASSWORD@$MYSQL_HOST:$MYSQL_PORT/$MYSQL_DATABASE`
|
||||
|
||||
Make sure to define the environment variables in the `.env` file as shown below
|
||||
|
||||
```env
|
||||
MYSQL_USER=root
|
||||
MYSQL_PASSWORD=password
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_DATABASE=database
|
||||
```
|
||||
|
||||
## Timeout
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The timeout is the time in milliseconds to wait for the database connection to respond.
|
||||
|
||||
## Connection String
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The connection string is the connection string of the database you want to monitor. The connection string should be a valid connection string for the database type you have selected.
|
||||
|
||||
## SQL Query
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The SQL query is the query you want to run on the database. The SQL query should be a valid SQL query for the database type you have selected.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: SSL Monitors | Kener
|
||||
description: Monitor the SSL certificate of a domain
|
||||
---
|
||||
|
||||
# SSL Monitors
|
||||
|
||||
SSL monitors are used to monitor the SSL certificate of a domain. You can monitor the SSL certificate of a domain by adding the domain name to the monitor. The SSL monitor will check the SSL certificate of the domain and notify you if the SSL certificate is about to expire.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Host
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The host is the domain name of the website you want to monitor. The host should be a valid domain name.
|
||||
|
||||
## Port
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The port is the ssl port of the website you want to monitor. The default ssl port is 443.
|
||||
|
||||
## Degraded If
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
Set this to the number of hours that still remains for the certificate to expire. If the certificate is about to expire in the given number of hours, the monitor status will be set to degraded. Example if you send the value to be 24, the monitor status will be set to degraded if the certificate is about to expire in 24 hours.
|
||||
|
||||
## Down If
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
Set this to the number of hours that still remains for the certificate to expire. If the certificate is about to expire in the given number of hours, the monitor status will be set to down. Example if you send the value to be 12, the monitor status will be set to down if the certificate is about to expire in 12 hours.
|
||||
|
||||
<div class="note">
|
||||
|
||||
`Degraded If` should be more than `Down If`.
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
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 liveness of your servers. You can use TCP monitors to monitor the uptime of your servers and get notified when they are down.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</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 (arrayOfPings) {
|
||||
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
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `arrayOfPings` **REQUIRED** is an array of TCP Response Objects as shown below.
|
||||
|
||||
```json
|
||||
/*
|
||||
[
|
||||
{
|
||||
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
|
||||
|
||||
Each object in the array represents the tcp response of a host.
|
||||
|
||||
- `host`: The host that was pinged.
|
||||
- `port`: The port that was pinged. Defaults to 80 if not provided.
|
||||
- `type`: The type of IP address. Can be `IP4` or `IP6`.
|
||||
- `status`: The status of the ping. Can be `open` , `error` or `timeout`.
|
||||
- `open`: The host is reachable.
|
||||
- `error`: There was an error while pinging the host.
|
||||
- `timeout`: The host did not respond in time.
|
||||
- `latency`: The time taken to ping the host. This is in milliseconds.
|
||||
|
||||
### 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 (arrayOfPings) {
|
||||
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
|
||||
}
|
||||
})
|
||||
```
|
||||
+116
-142
@@ -1,181 +1,155 @@
|
||||
---
|
||||
title: Monitors | monitors.yaml | Kener
|
||||
title: Monitors | Kener
|
||||
description: Monitors are the heart of Kener. This is where you define the monitors you want to show on your site.
|
||||
---
|
||||
|
||||
# Monitors
|
||||
|
||||
Inside `config/` folder there is a file called `monitors.yaml`. We will be adding our monitors here. Please note that your yaml must be valid. It is an array.
|
||||
Monitors are the heart of Kener. This is where you define the monitors you want to show on your site.
|
||||
|
||||
## Understanding monitors
|
||||
## Add Monitors
|
||||
|
||||
Each monitor runs at 1 minute interval by default. Monitor runs in below priority order.
|
||||
Click on the ➕ to add a monitor.
|
||||
|
||||
- `defaultStatus` Data. Used to set the default status of the monitor
|
||||
- PING/API/DNS call Data overrides above data(if present)
|
||||
- Pushed Status Data overrides status Data using [Kener Update Statue API](/docs/kener-apis#update-status---api)
|
||||
- [Manual Incident](/docs/incident-management) Data overrides Pushed Status Data
|
||||
<div class="border rounded-md">
|
||||
|
||||
## General Attributes
|
||||

|
||||
|
||||
A list of attributes that can be used in all types of monitors.
|
||||
</div>
|
||||
|
||||
| Key | Required? | Explanation |
|
||||
| ------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| name | Required + Unique | This will be shown in the UI to your users. Keep it short and unique |
|
||||
| description | Optional | This is a breif description for the monitor |
|
||||
| tag | Required + Unique | This is used to tag incidents created in Github using comments |
|
||||
| image | Optional | To show a logo before the name |
|
||||
| cron | Optional | Use a valid cron expression to specify the interval to run the monitors. Defaults to `* * * * *` i.e every minute |
|
||||
| defaultStatus | Optional | This will be the default status if no other way is specified to check the monitor. can be `UP`/`DOWN`/`DEGRADED` |
|
||||
| hidden | Optional | If set to `true` will not show the monitor in the UI |
|
||||
| category | Optional | Use this to group your monitors. Make sure you have defined category in `site.yaml` and use the `name` attribute. More about it [here](/docs/customize-site#categories). |
|
||||
| dayDegradedMinimumCount | Optional | Default is 1. It means, minimum this number of count for the day to be classified as DEGRADED(Yellow Bar) in 90 day view. Has to be `number` greater than 0 |
|
||||
| dayDownMinimumCount | Optional | Default is 1. It means, minimum this number of count for the day to be classified as DOWN(Red Bar) in 90 day view. Has to be `number` greater than 0 |
|
||||
| includeDegradedInDowntime | Optional | By deafault uptime percentage is calculated as (UP+DEGRADED/UP+DEGRADED+DOWN). Setting it as `true` will change the calculation to (UP/UP+DEGRADED+DOWN) |
|
||||
## Tag
|
||||
|
||||
### Example
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
UNIQUE
|
||||
</span>
|
||||
&
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
```yaml
|
||||
- name: "Google"
|
||||
description: "Google Search Engine"
|
||||
tag: "google"
|
||||
image: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
|
||||
defaultStatus: "UP"
|
||||
hidden: false
|
||||
dayDegradedMinimumCount: 2
|
||||
dayDownMinimumCount: 3
|
||||
includeDegradedInDowntime: false
|
||||
```
|
||||
The tag is unique to each monitor and is used to identify the monitor in the code. It is also used to generate the URL for the monitor.
|
||||
|
||||
`dayDegradedMinimumCount` and `dayDownMinimumCount` only works when `summaryStyle` is set as `DAY` in `site.yaml`. More about it [here](/docs/customize-site#summarystyle)
|
||||
## Name
|
||||
|
||||
## API Monitor Attributes
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
UNIQUE
|
||||
</span>
|
||||
&
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
A list of attributes that can be used in API monitors.
|
||||
Name is also unique to each monitor and is used to identify the monitor in the admin panel or in the status page.
|
||||
|
||||
| Key | Required? | Explanation |
|
||||
| ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| api.url | Required | HTTP URL |
|
||||
| api.method | Optional | HTTP Method. Default is `GET` |
|
||||
| api.headers | Optional | HTTP headers |
|
||||
| api.body | Optional | HTTP Body as string |
|
||||
| api.timeout | Optional | timeout for the api in milliseconds. Default is 10000(10 secs) |
|
||||
| api.eval | Optional | Evaluator written in JS, to parse HTTP response and calculate uptime and latency |
|
||||
| api.hideURLForGet | Optional | if the monitor is a GET URL and no headers are specified and the response body content-type is a text/html then kener shows a GET hyperlink in monitor description. To hide that set this as false. Default is `true` |
|
||||
## Image
|
||||
|
||||
### Eval
|
||||
The image is used to display a logo or an image for the monitor. It is displayed on the status page and in the admin panel. It is optional.
|
||||
|
||||
This is a anonymous JS function, by default it looks like this.
|
||||
## Description
|
||||
|
||||
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and lantecy(number)
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
The description is used to describe the monitor. It is displayed on the status page and in the admin panel. It is optional. You can also write html in the description and can use tailwind classes.
|
||||
|
||||
```javascript
|
||||
(function (statusCode, responseTime, responseDataBase64) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
let status = 'DOWN'
|
||||
if(statusCodeShort >=2 && statusCodeShort <= 3) {
|
||||
status = 'UP',
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})
|
||||
```
|
||||
## Cron
|
||||
|
||||
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
|
||||
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64);
|
||||
//let jsonResp = JSON.parse(decodedResp)
|
||||
```
|
||||
The cron is used to define the interval at which the monitor should run. It is a cron expression. For example, `* * * * *` will run the monitor every minute.
|
||||
|
||||
### Example
|
||||
## Default Status
|
||||
|
||||
```yaml
|
||||
- name: "Google"
|
||||
description: "Google Search Engine"
|
||||
tag: "google"
|
||||
image: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
|
||||
defaultStatus: "UP"
|
||||
hidden: false
|
||||
api:
|
||||
url: "https://www.google.com"
|
||||
method: "GET"
|
||||
headers:
|
||||
"Content-Type": "application/json"
|
||||
body: ""
|
||||
timeout: 10000
|
||||
eval: |
|
||||
(function (statusCode, responseTime, responseDataBase64) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
let status = 'DOWN'
|
||||
if(statusCodeShort >=2 && statusCodeShort <= 3) {
|
||||
status = 'UP',
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})
|
||||
```
|
||||
Using default status you can set a predefined status for the monitor. The predefined status can be `UP`, `DOWN`, or `DEGRADED`. If the monitor does not have api/dns/ping or if the monitor status is not updated using webbhook, manual incident this will the status of the monitor.
|
||||
|
||||
To view more examples of API monitors, please visit [here](/docs/monitors-examples)
|
||||
## Category Name
|
||||
|
||||
## PING Monitor Attributes
|
||||
The category name is used to group monitors. It is displayed on the status page and in the admin panel. It is optional. By a home category is already present. Adding monitor to the home category will show the monitor on the home page.
|
||||
|
||||
A list of attributes that can be used in PING monitors.
|
||||
## Day Degraded Min Count
|
||||
|
||||
| Key | Required? | Explanation |
|
||||
| ------------ | --------- | ----------------------------------------------------------------------- |
|
||||
| ping.hostsV4 | Required | Array of hosts / IP to monitor ping response. Either domain name or IP4 |
|
||||
| ping.hostsV6 | Required | Array of hosts / IP to monitor ping response. Either domain name or IP6 |
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
Either one of `hostsV4` or `hostsV6` is required
|
||||
The day degraded minimum count is used to define the minimum number of degraded incidents in a day to mark the monitor as degraded for the day. A degraded day means if you are using Monitor Style as FULL under Theme, the whole bar will be shown as Degraded (yellow color) . It is optional. It has to be more than equal to 1
|
||||
|
||||
### Example
|
||||
## Day Down Min Count
|
||||
|
||||
```yaml
|
||||
- name: "Ping All"
|
||||
description: "Ping All is where I ping all the hosts"
|
||||
tag: "pingall"
|
||||
defaultStatus: "UP"
|
||||
ping:
|
||||
hostsV4:
|
||||
- "www.rajnandan.com"
|
||||
- "103.125.217.243"
|
||||
```
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
You can find more examples of PING monitors [here](/docs/monitor-examples#ping-monitor)
|
||||
The day down minimum count is used to define the minimum number of down incidents in a day to mark the monitor as down for the day. A down day means if you are using Monitor Style as FULL under Theme, the whole bar will be shown as Down (red color) . It is optional. It has to be more than equal to 1
|
||||
|
||||
## DNS Monitor Attributes
|
||||
## Include Degraded
|
||||
|
||||
A list of attributes that can be used in DNS monitors.
|
||||
The include degraded is used to include the degraded checks in the total bad checks count. It is optional. By default uptime percentage is calculated as (UP+DEGRADED/UP+DEGRADED+DOWN). Setting it as `YES` will change the calculation to (UP/UP+DEGRADED+DOWN)
|
||||
|
||||
| Key | Required? | Explanation |
|
||||
| ---------------- | --------- | ------------------------------------------------------------------------ |
|
||||
| dns.hosts | Required | Array of hosts to monitor DNS response. Either domain name or IP4 or IP6 |
|
||||
| dns.lookupRecord | Required | DNS record type. |
|
||||
| dns.nameServer | Required | DNS server to use. |
|
||||
| dns.matchType | Required | Match type for DNS response. Can be `ANY` or `ALL`. Default is `ALL` |
|
||||
| dns.values | Required | Expected values for the DNS response. Array of string |
|
||||
## Monitor Type
|
||||
|
||||
### Example
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
```yaml
|
||||
- name: "DNS All"
|
||||
description: "DNS All is where I check all the DNS"
|
||||
tag: "dnsall"
|
||||
defaultStatus: "UP"
|
||||
dns:
|
||||
host: "www.rajnandan.com"
|
||||
lookupRecord: "CNAME"
|
||||
nameServer: "8.8.8.8"
|
||||
matchType: "ANY"
|
||||
values:
|
||||
- "rajnandan1.github.io"
|
||||
```
|
||||
### API
|
||||
|
||||
To monitor an API, you need to provide the URL of the API and the expected status code. If the status code is not received, the monitor will be marked as down. You can read more about API monitoring [here](/docs/monitors-api).
|
||||
|
||||
### DNS
|
||||
|
||||
To monitor the DNS records, you need to provide the domain name and the record type. If the record is not found, the monitor will be marked as down. You can read more about DNS monitoring [here](/docs/monitors-dns).
|
||||
|
||||
### PING
|
||||
|
||||
To monitor the ping, you need to provide the IP address or the domain name. If the ping is not successful, the monitor will be marked as down. You can read more about PING monitoring [here](/docs/monitors-ping).
|
||||
|
||||
### GAMEDIG
|
||||
|
||||
To monitor game/service, you need to provide the game or service to monitor, the IP address or the domain name and the port. If the query is not successful, the monitor will be marked as down. You can read more about GAMEDIG monitoring [here](/docs/monitors-gamedig).
|
||||
|
||||
---
|
||||
|
||||
## Triggers
|
||||
|
||||
To add a trigger to a monitor make sure you have created a trigger. You can read more about triggers [here](/docs/triggers).
|
||||
|
||||
- Click on the 🔔 icon on the top right corner of the monitor.
|
||||
- Add details for either DOWN or DEGRADED.
|
||||
- Failure Threshold(Required): The number of consecutive failures before the trigger is activated.
|
||||
- Success Threshold(Required): The number of consecutive successes before the trigger is deactivated.
|
||||
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in Github. So make sure you have set up the Github token in the environment variables. The incident will be closed when the monitor is back to UP.
|
||||
- Severity (Required): The severity of the incident. It can be `Critcal` or `Warning`.
|
||||
- Custom Message (Required): Add your owner alert message.
|
||||
- Choose Triggers: Choose the triggers you want to activate the trigger. You can choose multiple triggers.
|
||||
- It will take upto 1 minute for the trigger to be activated.
|
||||
|
||||
---
|
||||
|
||||
## Edit Monitors
|
||||
|
||||
Click on the ⚙️ to edit the monitor.
|
||||
|
||||
### Deactivate Monitor
|
||||
|
||||
You can deactivate the monitor by switching the toggle to off. Deactivation a monitor will stop the monitor from running. It will take one minute to deactivate.
|
||||
|
||||
### Activate Monitor
|
||||
|
||||
You can activate the monitor by switching the toggle to on. Activation a monitor will start the monitor. It will take one minute to activate.
|
||||
|
||||
<div class="note info">
|
||||
|
||||
Any changes in the a live monitor will take 2 minutes to reflect.
|
||||
|
||||
</div>
|
||||
|
||||
## Supported Monitors
|
||||
|
||||
- [API Monitors](/docs/monitors-api)
|
||||
- [DNS Monitors](/docs/monitors-dns)
|
||||
- [PING Monitors](/docs/monitors-ping)
|
||||
- [TCP Monitors](/docs/monitors-tcp)
|
||||
- [SQL Monitors](/docs/monitors-sql)
|
||||
- [SSL Monitors](/docs/monitors-ssl)
|
||||
- [HEARTBEAT Monitors](/docs/monitors-heartbeat)
|
||||
- [GROUP Monitors](/docs/monitors-group)
|
||||
|
||||
+48
-19
@@ -1,41 +1,37 @@
|
||||
---
|
||||
title: Quick Start | Kener
|
||||
title: Get started | Kener
|
||||
description: Get started with Kener
|
||||
---
|
||||
|
||||
# Quick Start
|
||||
# Get Started
|
||||
|
||||
Here is a demonstration of how to get started with Kener in seconds
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js Minimum version required is `v22.12.0`.
|
||||
- Git
|
||||
- sqlite3
|
||||
- Node.js Minimum version required is `v18`.
|
||||
- Git
|
||||
- sqlite3
|
||||
|
||||
## Clone the repository
|
||||
|
||||
```shell
|
||||
```bash
|
||||
git clone https://github.com/rajnandan1/kener.git
|
||||
cd kener
|
||||
```
|
||||
|
||||
## Install Dependencies
|
||||
|
||||
```shell
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Setup Configuration
|
||||
## Set up Environment Variables
|
||||
|
||||
- Rename `config/site.example.yaml` -> `config/site.yaml`
|
||||
- Rename `config/monitors.example.yaml` -> `config/monitors.yaml`
|
||||
- Rename `config/server.example.yaml` -> `config/server.yaml`
|
||||
Kener needs some environment variables to be set to run properly. [Here](/docs/environment-vars) are the list of environment variables that you need to set.
|
||||
|
||||
```shell
|
||||
cp config/site.example.yaml config/site.yaml
|
||||
cp config/monitors.example.yaml config/monitors.yaml
|
||||
cp config/server.example.yaml config/server.yaml
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## Start Kener
|
||||
@@ -44,10 +40,43 @@ cp config/server.example.yaml config/server.yaml
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Kener Development Server would be running at PORT 3000. Go to [http://localhost:3000](http://localhost:3000)
|
||||
Kener Development Server would be running at PORT 3000. Go to [http://localhost:3000](http://localhost:3000) to see the Kener in action.
|
||||
|
||||
<div class="note info">
|
||||
|
||||
Status page will be at [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
Status Page Manage Portal will be at [http://localhost:3000/manage/app/site](http://localhost:3000/manage/app/site)
|
||||
|
||||
</div>
|
||||
|
||||
## Create a new User
|
||||
|
||||
If this is the first time your are launching kener then you would be redirected to the [set up page](/manage/setup). Fill in the details and click on `Let's Go` button.
|
||||
|
||||
- **Name**: Your Name
|
||||
- **Email**: Your Email
|
||||
- **Password**: Your Password
|
||||
|
||||
Please note that the email should be a valid email address and password should be atleast 8 characters long with uppercase lowercase and numbers.
|
||||
|
||||
## Login
|
||||
|
||||
Once you have created the user, you can login with the credentials you have provided by going to the [login page](/manage/signin)
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
You can watch the video tutorial on how to get started with Kener
|
||||
|
||||
[](https://www.youtube.com/watch?v=4L9PnWPqMPw)
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Configure Site](/docs/customize-site)
|
||||
- [Add Monitors](/docs/monitors)
|
||||
- [Alerting](/docs/alerting)
|
||||
Learn how to configure kener by going through one of the topics
|
||||
|
||||
- [Monitors](/docs/monitors): Learn how to set up and work with monitors in kener.
|
||||
- [Triggers](/docs/triggers): Learn how to set up and work with triggers in kener.
|
||||
- [Environment Variables](/docs/environment-vars): Learn how to set up and work with environment variables in kener.
|
||||
- [API](/docs/kener-apis): Learn how to use the API in kener.
|
||||
- [Databases](/docs/database): Learn how to set up and work with databases in kener.
|
||||
- [Theme](/docs/theme): Learn how to set up and work with theme in kener.
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Kener Role Based Access Control (RBAC)
|
||||
description: Role-Based Access Control (RBAC) in Kener
|
||||
---
|
||||
|
||||
# Role-Based Access Control (RBAC) in Kener
|
||||
|
||||
Kener includes a comprehensive role-based access control system that allows you to manage user permissions and access to various features. This document explains the available roles, their permissions, and how to manage users effectively.
|
||||
|
||||
## Available Roles
|
||||
|
||||
Kener offers three different roles with varying levels of permissions:
|
||||
|
||||
| Role | Description |
|
||||
| ------ | ------------------------------------------------------------------------------ |
|
||||
| Admin | Full access to all features including user management and system configuration |
|
||||
| Editor | Can create and edit monitors, triggers, incidents, and other operational data |
|
||||
| Member | Read-only access with limited interaction capabilities |
|
||||
|
||||
## Role Permissions
|
||||
|
||||
### Admin
|
||||
|
||||
Admins have unrestricted access to the entire system:
|
||||
|
||||
- **User Management**:
|
||||
|
||||
- Create, update, and deactivate users
|
||||
- Change user roles
|
||||
- Reset passwords for other users
|
||||
- Send verification emails
|
||||
|
||||
- **System Configuration**:
|
||||
|
||||
- Configure all site settings
|
||||
- Manage API keys
|
||||
- Set up triggers and integrations
|
||||
|
||||
- **Operational Access**:
|
||||
- Full access to create and manage monitors
|
||||
- Create and update incidents
|
||||
- Run tests on monitors and triggers
|
||||
- Configure all notification channels
|
||||
|
||||
### Editor
|
||||
|
||||
Editors can manage most operational aspects but cannot administer users:
|
||||
|
||||
- **Content Management**:
|
||||
|
||||
- Create and edit monitors
|
||||
- Configure monitor settings
|
||||
- Create and manage triggers
|
||||
- Create and update incidents
|
||||
- Add incident updates and comments
|
||||
|
||||
- **Limited Access**:
|
||||
- Cannot manage users
|
||||
- Cannot change role assignments
|
||||
- Cannot access certain system-level configurations
|
||||
|
||||
### Member
|
||||
|
||||
Members have read-only access with minimal interaction capabilities:
|
||||
|
||||
- **View Access**:
|
||||
|
||||
- View all monitors and their status
|
||||
- View incidents and their history
|
||||
- See system configuration (but cannot modify)
|
||||
|
||||
- **Limited Interactions**:
|
||||
- Can test existing triggers but cannot create or edit them
|
||||
- Cannot create or update incidents
|
||||
- Cannot modify any system configuration
|
||||
|
||||
## Managing Users
|
||||
|
||||
### Adding New Users
|
||||
|
||||
Only Admins and Editors can add new users to the system:
|
||||
|
||||
1. Navigate to the **Users** page in the management dashboard
|
||||
2. Click the **Add New User** button
|
||||
3. Fill in the required information:
|
||||
- Name
|
||||
- Email
|
||||
- Password
|
||||
- Role (Member or Editor)
|
||||
4. Click **Add User** to create the account
|
||||
|
||||
When a new user is added, a verification email can be sent to confirm their email address if email sending is configured.
|
||||
|
||||
### User Settings
|
||||
|
||||
Admins can manage user accounts through the user settings page:
|
||||
|
||||
- **Email Verification**: Send verification emails to users
|
||||
- **Password Reset**: Reset a user's password
|
||||
- **Role Management**: Change a user's role between Member and Editor
|
||||
- **Account Status**: Activate or deactivate user accounts
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Follow the Principle of Least Privilege**:
|
||||
|
||||
- Assign the minimum necessary permissions for users to perform their job
|
||||
- Start users with the Member role and elevate as needed
|
||||
|
||||
2. **Regular Access Reviews**:
|
||||
|
||||
- Periodically review user access and roles
|
||||
- Remove access for users who no longer need it
|
||||
|
||||
3. **Admin Accounts**:
|
||||
- Limit the number of Admin accounts
|
||||
- Use strong passwords for Admin accounts
|
||||
- Consider using email verification for all users
|
||||
|
||||
## Role Limitations
|
||||
|
||||
- The Admin role can only be assigned during initial setup
|
||||
- Members cannot create or modify content
|
||||
- Users cannot modify their own role (only an Admin can change roles)
|
||||
|
||||
## Email Verification
|
||||
|
||||
When email sending is configured, users can verify their email addresses:
|
||||
|
||||
1. Admins can send verification emails from the user management interface
|
||||
2. Users receive an email with a verification link
|
||||
3. After clicking the link, the user's email is marked as verified
|
||||
|
||||
Email verification improves security and ensures that users have provided valid email addresses for notifications.
|
||||
+14
-8
@@ -20,18 +20,24 @@ description: Roadmap for Kener
|
||||
|
||||
☑ ~~Support for github enterprise~~
|
||||
|
||||
☐ Add support for fly.io deployment
|
||||
|
||||
☐ Add to digital ocean marketplace
|
||||
|
||||
☐ Add to aws marketplace
|
||||
|
||||
☐ Add Rate limiting to APIs
|
||||
|
||||
☐ Add support for custom themes
|
||||
|
||||
☐ Create Admin UI
|
||||
☑ ~~Handle uploaded images for docker containers~~
|
||||
|
||||
☑ ~~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~~
|
||||
|
||||
☐ Monitor Update Screen
|
||||
|
||||
☐ i18n for admin dashboard
|
||||
|
||||
---
|
||||
|
||||
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)
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: SEO Setup | Kener
|
||||
description: Learn how to set up and work with SEO in kener.
|
||||
---
|
||||
|
||||
# SEO
|
||||
|
||||
SEO is important for your status page to be found on the internet. Kener provides you with the ability to set up SEO for your status page.
|
||||
|
||||
## Analytics
|
||||
|
||||
You can add Google Analytics, Amplitude, Mixpanel. Basic events and page tracking are supported.
|
||||
|
||||
### Google Analytics
|
||||
|
||||
ADD ID. It is the measurement ID that you get from Google Analytics. You can find it in the admin section of your Google Analytics account.
|
||||
|
||||
### Amplitude
|
||||
|
||||
Add API key of your Amplitude account
|
||||
|
||||
### Mixpanel
|
||||
|
||||
Add project token of your Mixpanel account.
|
||||
|
||||
### Plausible
|
||||
|
||||
- Add domain of your Plausible account. Example: `example.com`
|
||||
- Add API key of your Plausible account. Default is `https://plausible.io/api/event`
|
||||
- Add Script Source of your Plausible account. Default is `https://plausible.io/js/script.pageview-props.tagged-events.js`
|
||||
|
||||
### Microsoft Clarity
|
||||
|
||||
Add Project ID of your Microsoft Clarity account.
|
||||
|
||||
## Search Engine Optimization
|
||||
|
||||
You can add meta tags for SEO. You can add any `meta` that you want to add to the head of the page. Example: `og:image`, `og:title`, `og:description`, `twitter:image`, `twitter:title`, `twitter:description`.
|
||||
|
||||
Once added it will become
|
||||
|
||||
```html
|
||||
<meta property="og:image" content="https://example.com/image.png" />
|
||||
```
|
||||
|
||||
## Sitemap
|
||||
|
||||
Kener auto generates a sitemap for your status page. You can view it at `/sitemap.xml`
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Site Info Setup | Kener
|
||||
description: Learn how to set up and work with site info in kener.
|
||||
---
|
||||
|
||||
# Site
|
||||
|
||||
Site info is used to define the site information that will be shown on the status page.
|
||||
|
||||
## Site Title
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The site title is used to define the title of the site. It is required and has to be a string. It what will be shown on the browser tab. This will become
|
||||
|
||||
Example: `Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management`
|
||||
|
||||
```html
|
||||
<title>
|
||||
Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management
|
||||
</title>
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Site Name
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
This will be shown as a brand name on the status page on the nav bar top left.
|
||||
|
||||

|
||||
|
||||
## Home Location
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
Link to redirect to when Site Name or logo is clicked. Can be your main website. or it can be `/` to be current page. If you are using a base path it should be `/base-path` to keep the the user on the same page.
|
||||
|
||||
## Logo
|
||||
|
||||
Same as Site Name, it is placed left of Site Name as shown in image above
|
||||
|
||||
## Favicon
|
||||
|
||||
Favicon to be shown in browser tab.
|
||||
@@ -51,7 +51,7 @@ You can set custom label for the status badge. If you do not set a label it will
|
||||
|
||||
Shows the last health check was UP/DOWN/DEGRADED as SVG dot
|
||||
|
||||
```shell
|
||||
```bash
|
||||
http://[hostname]/badge/[tag]/dot
|
||||
#or
|
||||
http://[hostname]/badge/[tag]/dot?animate=ping
|
||||
|
||||
+195
-127
@@ -1,129 +1,197 @@
|
||||
{
|
||||
"sidebar": [
|
||||
{
|
||||
"sectionTitle": "Getting Started",
|
||||
"children": [
|
||||
{
|
||||
"title": "Introduction",
|
||||
"link": "/docs/home",
|
||||
"file": "/home.md"
|
||||
},
|
||||
{
|
||||
"title": "Quick Start",
|
||||
"link": "/docs/quick-start",
|
||||
"file": "/docs/quick-start.md"
|
||||
},
|
||||
{
|
||||
"title": "Changelogs",
|
||||
"link": "/docs/changelogs",
|
||||
"file": "/changelogs.md"
|
||||
},
|
||||
{
|
||||
"title": "Roadmap",
|
||||
"link": "/docs/roadmap",
|
||||
"file": "/roadmap.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Core Concepts",
|
||||
"children": [
|
||||
{
|
||||
"title": "Customization (site.yaml)",
|
||||
"link": "/docs/customize-site",
|
||||
"file": "/customize-site.md"
|
||||
},
|
||||
{
|
||||
"title": "Monitors (monitors.yaml)",
|
||||
"link": "/docs/monitors",
|
||||
"file": "/monitors.md"
|
||||
},
|
||||
{
|
||||
"title": "Alerting (server.yaml)",
|
||||
"link": "/docs/alerting",
|
||||
"file": "/alerting.md"
|
||||
},
|
||||
{
|
||||
"title": "Database (server.yaml)",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
},
|
||||
{
|
||||
"title": "Monitors Examples",
|
||||
"link": "/docs/monitor-examples",
|
||||
"file": "/monitor-examples.md"
|
||||
},
|
||||
{
|
||||
"title": "Incident Management",
|
||||
"link": "/docs/incident-management",
|
||||
"file": "/incident-management.md"
|
||||
},
|
||||
{
|
||||
"title": "Localization",
|
||||
"link": "/docs/i18n",
|
||||
"file": "/i18n.md"
|
||||
},
|
||||
{
|
||||
"title": "Badges",
|
||||
"link": "/docs/status-badges",
|
||||
"file": "/status-badges.md"
|
||||
},
|
||||
{
|
||||
"title": "Embed",
|
||||
"link": "/docs/embed",
|
||||
"file": "/embed.md"
|
||||
},
|
||||
{
|
||||
"title": "Analytics",
|
||||
"link": "/docs/site-analytics",
|
||||
"file": "/site-analytics.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Deployment",
|
||||
"children": [
|
||||
{
|
||||
"title": "Environment Setup",
|
||||
"link": "/docs/environment-vars",
|
||||
"file": "/environment-vars.md"
|
||||
},
|
||||
{
|
||||
"title": "Deployment",
|
||||
"link": "/docs/deployment",
|
||||
"file": "/deployment.md"
|
||||
},
|
||||
{
|
||||
"title": "Github Setup",
|
||||
"link": "/docs/gh-setup",
|
||||
"file": "/gh-setup.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "API Reference",
|
||||
"children": [
|
||||
{
|
||||
"title": "Kener APIs",
|
||||
"link": "/docs/kener-apis",
|
||||
"file": "/kener-apis.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Guides",
|
||||
"children": [
|
||||
{
|
||||
"title": "Categorize Monitors",
|
||||
"link": "/docs/categorize-guide",
|
||||
"file": "/categorize-guide.md"
|
||||
},
|
||||
{
|
||||
"title": "Custom JS/CSS",
|
||||
"link": "/docs/custom-js-css-guide",
|
||||
"file": "/custom-js-css-guide.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"sidebar": [
|
||||
{
|
||||
"sectionTitle": "Getting Started",
|
||||
"children": [
|
||||
{
|
||||
"title": "Introduction",
|
||||
"link": "/docs/home",
|
||||
"file": "/home.md"
|
||||
},
|
||||
{
|
||||
"title": "Get Started",
|
||||
"link": "/docs/quick-start",
|
||||
"file": "/quick-start.md"
|
||||
},
|
||||
{
|
||||
"title": "Concepts",
|
||||
"link": "/docs/concepts",
|
||||
"file": "/concepts.md"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Deployment",
|
||||
"link": "/docs/deployment",
|
||||
"file": "/deployment.md"
|
||||
},
|
||||
{
|
||||
"title": "Databases",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
},
|
||||
{
|
||||
"title": "Access Control",
|
||||
"link": "/docs/rbac",
|
||||
"file": "/rbac.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Guides",
|
||||
"children": [
|
||||
{
|
||||
"title": "Setup Environment",
|
||||
"link": "/docs/environment-vars",
|
||||
"file": "/environment-vars.md"
|
||||
},
|
||||
{
|
||||
"title": "Use Badges",
|
||||
"link": "/docs/status-badges",
|
||||
"file": "/status-badges.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Monitors",
|
||||
"link": "/docs/monitors",
|
||||
"file": "/monitors.md"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Setup Triggers",
|
||||
"link": "/docs/triggers",
|
||||
"file": "/triggers.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Site",
|
||||
"link": "/docs/site",
|
||||
"file": "/site.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup SEO",
|
||||
"link": "/docs/seo",
|
||||
"file": "/seo.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Home",
|
||||
"link": "/docs/home-page",
|
||||
"file": "/home-page.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Theme",
|
||||
"link": "/docs/theme",
|
||||
"file": "/theme.md"
|
||||
},
|
||||
{
|
||||
"title": "View Alerts",
|
||||
"link": "/docs/alerts",
|
||||
"file": "/alerts.md"
|
||||
},
|
||||
{
|
||||
"title": "API Keys",
|
||||
"link": "/docs/apikeys",
|
||||
"file": "/apikeys.md"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Incident Management",
|
||||
"link": "/docs/incident-management",
|
||||
"file": "/incident-management.md"
|
||||
},
|
||||
{
|
||||
"title": "Embed",
|
||||
"link": "/docs/embed",
|
||||
"file": "/embed.md"
|
||||
},
|
||||
{
|
||||
"title": "Custom JS/CSS",
|
||||
"link": "/docs/custom-js-css-guide",
|
||||
"file": "/custom-js-css-guide.md"
|
||||
},
|
||||
{
|
||||
"title": "Internationalization",
|
||||
"link": "/docs/i18n",
|
||||
"file": "/i18n.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Monitors",
|
||||
"children": [
|
||||
{
|
||||
"title": "API/Website Monitor",
|
||||
"link": "/docs/monitors-api",
|
||||
"file": "/monitors-api.md"
|
||||
},
|
||||
{
|
||||
"title": "Ping Monitor",
|
||||
"link": "/docs/monitors-ping",
|
||||
"file": "/monitors-ping.md"
|
||||
},
|
||||
{
|
||||
"title": "TCP Monitor",
|
||||
"link": "/docs/monitors-tcp",
|
||||
"file": "/monitors-tcp.md"
|
||||
},
|
||||
{
|
||||
"title": "DNS Monitor",
|
||||
"link": "/docs/monitors-dns",
|
||||
"file": "/monitors-dns.md"
|
||||
},
|
||||
{
|
||||
"title": "Group Monitor",
|
||||
"link": "/docs/monitors-group",
|
||||
"file": "/monitors-group.md"
|
||||
},
|
||||
{
|
||||
"title": "SSL Monitor",
|
||||
"link": "/docs/monitors-ssl",
|
||||
"file": "/monitors-ssl.md"
|
||||
},
|
||||
{
|
||||
"title": "SQL Monitor",
|
||||
"link": "/docs/monitors-sql",
|
||||
"file": "/monitors-sql.md"
|
||||
},
|
||||
{
|
||||
"title": "Heartbeat Monitor",
|
||||
"link": "/docs/monitors-heartbeat",
|
||||
"file": "/monitors-heartbeat.md"
|
||||
},
|
||||
{
|
||||
"title": "Gamedig Monitor",
|
||||
"link": "/docs/monitors-gamedig",
|
||||
"file": "/monitors-gamedig.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "API Reference",
|
||||
"children": [
|
||||
{
|
||||
"title": "Kener APIs",
|
||||
"link": "/docs/kener-apis",
|
||||
"file": "/kener-apis.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Help",
|
||||
"children": [
|
||||
{
|
||||
"title": "Fonts",
|
||||
"link": "/docs/custom-fonts",
|
||||
"file": "/custom-fonts.md"
|
||||
},
|
||||
{
|
||||
"title": "Changelogs",
|
||||
"link": "/docs/changelogs",
|
||||
"file": "/changelogs.md"
|
||||
},
|
||||
{
|
||||
"title": "Roadmap",
|
||||
"link": "/docs/roadmap",
|
||||
"file": "/roadmap.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Theme Customization | Kener
|
||||
description: Learn how to customize the theme of your status page in kener.
|
||||
---
|
||||
|
||||
# Theme Customization
|
||||
|
||||
Kener provides you with the ability to customize the theme of your status page. You can change the colors, fonts, and other styles of the status page.
|
||||
|
||||
---
|
||||
|
||||
## Home Page Pattern
|
||||
|
||||
Kener can show a subtle pattern in all your pages. It is either square or dots. Right now you cannot modify the color of the pattern. However, you can disable it by choosing none
|
||||
|
||||
---
|
||||
|
||||
## Default Theme
|
||||
|
||||
Kener comes with both dark and light theme. You can set a default theme that will be used every time the user visits.
|
||||
|
||||
Setting it to light or dark will make the theme light or dark respectively. Setting it to none will make the theme change based on the user's system preference. Setting it to system will make the theme change based on the user's system preference.
|
||||
|
||||
If you want to force a theme to be light or dark you can set it to light or dark and uncheck `Let Users toggle theme between light and dark`
|
||||
|
||||
---
|
||||
|
||||
## Monitor Style
|
||||
|
||||
You can change how the bars and summary of a monitor looks like.
|
||||
|
||||
### Status Bar
|
||||
|
||||
#### Partial
|
||||
|
||||
The status bar will be a gradient from green to red/yellow based on the status of the monitor.
|
||||
|
||||

|
||||
|
||||
#### Full
|
||||
|
||||
The status bar will be a solid color based on the status of the monitor.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Roundness
|
||||
|
||||
Adjust the roundness of the status bar.
|
||||
|
||||
#### SHARP
|
||||
|
||||

|
||||
|
||||
#### ROUNDED
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Summary Type
|
||||
|
||||
Control how the summary of the monitor looks like.
|
||||
|
||||
#### Current
|
||||
|
||||
Shows the current live value of the monitor. Example: If it is DOWN currently it will say `DOWN for x minutes`, if it is UP it will say `Status Ok`
|
||||
|
||||
#### DAY
|
||||
|
||||
Shows the day's impact. Example: If it was down at 10:00 AM and it is 11:00 AM, it will say `DOWN for 1 hour`. If there was no DOWN or DEGRADED state it will say `Status Ok`
|
||||
|
||||
---
|
||||
|
||||
## Colors
|
||||
|
||||
You can change the colors of UP/DEGRADED/DOWN/MAINTENANCE states of the monitor. By default, it shows a shade of green, yellow, red and blue respectively.
|
||||
|
||||
---
|
||||
|
||||
## Font
|
||||
|
||||
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
|
||||
|
||||
Add font url
|
||||
|
||||
### Font Family
|
||||
|
||||
Add font family name
|
||||
|
||||
### Custom CSS
|
||||
|
||||
You can add custom CSS to your status page. This will be added to the head of the page. You can add custom CSS to your status page. This will be added to the head of the page.
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
<div class="note danger">
|
||||
Do not include <style> tags.
|
||||
</div>
|
||||
@@ -0,0 +1,471 @@
|
||||
---
|
||||
title: Triggers | Kener
|
||||
description: Learn how to set up and work with triggers in kener.
|
||||
---
|
||||
|
||||
## Triggers
|
||||
|
||||
Triggers are used to trigger actions based on the status of your monitors. You can use triggers to send notifications, or call webhooks when a monitor goes down or up.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Name
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The name is used to define the name of the webhook. It is required and has to be a string.
|
||||
|
||||
### Description
|
||||
|
||||
The description is used to define the description of the webhook. It is optional and has to be a string.
|
||||
|
||||
Kener supports the following triggers:
|
||||
|
||||
- [Webhook](#webhook)
|
||||
- [Discord](#discord)
|
||||
- [Slack](#slack)
|
||||
- [Email](#email)
|
||||
|
||||
### Trigger Variables
|
||||
|
||||
You can use the following variables in the triggers using mustache syntax.
|
||||
|
||||
| Key | Description | Mustache Variable |
|
||||
| ------------- | ------------------------------------------------------------ | ----------------- |
|
||||
| site_name | [string] Name of your site | {{site_name}} |
|
||||
| logo_url | [string] Logo of your site | {{logo_url}} |
|
||||
| site_url | [string] Url of your site | {{site_url}} |
|
||||
| alert_name | [string] Name of the alert | {{alert_name}} |
|
||||
| status | [string] Status of the alert. Can be `TRIGGERED`, `RESOLVED` | {{status}} |
|
||||
| is_resolved | [bool] True if alert is `RESOLVED` | {{is_resolved}} |
|
||||
| is_triggered | [bool] True if alert is `TRIGGERED` | {{is_triggered}} |
|
||||
| description | [string] Description of the alert. | {{description}} |
|
||||
| action_text | [string] CTA for next action on the alert | {{action_text}} |
|
||||
| action_url | [string] URL for next action on the alert | {{action_url}} |
|
||||
| metric | [string] Name of the monitor | {{metric}} |
|
||||
| severity | [string] Severity of the alert. Can be `critical`, `warn` | {{severity}} |
|
||||
| id | [string] Id of the alert | {{id}} |
|
||||
| current_value | [number] Current count of failures | {{current_value}} |
|
||||
| threshold | [number] Threshold Failue | {{threshold}} |
|
||||
| source | [string] Source of the alert | {{source}} |
|
||||
| timestamp | [string] Timestamp **ISO 8601** formatted | {{timestamp}} |
|
||||
|
||||
---
|
||||
|
||||
## Webhook
|
||||
|
||||
Webhook triggers are used to send a HTTP POST request to a URL when a monitor goes down or up.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### URL
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The URL is used to define the URL of the webhook. It is required and has to be a valid URL.
|
||||
|
||||
You can also pass secrets that are set in the environment variables.
|
||||
|
||||
Example: `https://example.com/webhook?secret=$SECRET_X`. Make sure `$SECRET_X` is set in the environment variables.
|
||||
|
||||
### Method
|
||||
|
||||
Method will always be `POST`
|
||||
|
||||
### Headers
|
||||
|
||||
The headers are used to define the headers that should be sent with the request. It is optional and has to be a valid JSON object. You can add secrets that are set in the environment variables.
|
||||
|
||||
Example: `Authorization: Bearer $SECRET_Y`. Make sure `$SECRET_Y` is set in the environment variables.
|
||||
|
||||
While sending webhook kener will add two more headers: `Content-Type: application/json` and `User-Agent: Kener/3.0.0`.
|
||||
|
||||
### Body
|
||||
|
||||
Body of the webhook will be sent as below:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mockoon-9",
|
||||
"alert_name": "Mockoon DOWN",
|
||||
"severity": "critical",
|
||||
"status": "TRIGGERED",
|
||||
"source": "Kener",
|
||||
"timestamp": "2024-11-27T04:55:00.369Z",
|
||||
"description": "🚨 **Service Alert**: Check the details below",
|
||||
"details": {
|
||||
"metric": "Mockoon",
|
||||
"current_value": 1,
|
||||
"threshold": 1
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"text": "View Monitor",
|
||||
"url": "https://kener.ing/monitor-mockoon"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Body
|
||||
|
||||
Using [mustache variables](#triggers-trigger-variables) you can customize the body of the webhook. If you are not using a json body then please make sure you are using the right content-type by setting custom headers.
|
||||
|
||||
### Examples
|
||||
|
||||
#### 1. 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>.
|
||||
|
||||
#### 2. Conditional Webhook
|
||||
|
||||
You can use `is_triggered` and `is_resolved` to create conditional webhooks.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Monitor Alert for {{metric}} has been {{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}. Current value is {{current_value}} and threshold set is {{threshold}}"
|
||||
}
|
||||
```
|
||||
|
||||
The output will be
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Monitor Alert for Mockoon has been 🔴 Triggered. Current value is 1 and threshold set is 1"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Monitor Alert for Mockoon has been 🟢 Resolved. Current value is 0 and threshold set is 1"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Combing mustache with env variables
|
||||
|
||||
Combine the above example with `NODE_ENV` environment variable.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Monitor Alert for {{metric}} in environment $NODE_ENV has been {{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}. Current value is {{current_value}} and threshold set is {{threshold}}."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Discord
|
||||
|
||||
Discord triggers are used to send a message to a discord channel when a monitor goes down or up.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Discord URL
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The Discord URL is used to define the URL of the discord webhook. It is required and has to be a valid URL.
|
||||
|
||||
#### How to get the Discord URL?
|
||||
|
||||
1. Go to your discord server
|
||||
2. Right-click on the channel you want to send the messages
|
||||
3. Click on `Edit Channel`
|
||||
4. Go to `Integrations`
|
||||
5. Click on `Create Webhook`
|
||||
6. Copy the URL
|
||||
|
||||
#### Discord Message
|
||||
|
||||
The discord message when alert is `TRIGGERED` will look like this
|
||||
|
||||

|
||||
|
||||
The discord message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||
|
||||
### Modify Discord Message
|
||||
|
||||
You can modify the discord message by using [mustache variables](#triggers-trigger-variables).
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "{{site_name}}",
|
||||
"avatar_url": "{{{logo_url}}}",
|
||||
"content": "## {{alert_name}}\n{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}\n{{description}}\nClick [MY CTA]({{{action_url}}}) for more.",
|
||||
"embeds": [
|
||||
{
|
||||
"title": "❌{{alert_name}}❌",
|
||||
"description": "{{description}}",
|
||||
"url": "{{{action_url}}}",
|
||||
"color": "{{#is_triggered}}13250616{{/is_triggered}}{{#is_resolved}}5156244{{/is_resolved}}",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Monitor",
|
||||
"value": "{{metric}}",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "Severity",
|
||||
"value": "{{severity}}",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "Alert ID",
|
||||
"value": "{{id}}",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "Current Value",
|
||||
"value": "{{current_value}}",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"name": "Threshold",
|
||||
"value": "{{threshold}}",
|
||||
"inline": true
|
||||
}
|
||||
],
|
||||
"footer": {
|
||||
"text": "{{source}}",
|
||||
"icon_url": "{{{logo_url}}}"
|
||||
},
|
||||
"timestamp": "{{timestamp}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Slack
|
||||
|
||||
Slack triggers are used to send a message to a slack channel when a monitor goes down or up.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Slack URL
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The Slack URL is used to define the URL of the slack webhook. It is required and has to be a valid URL.
|
||||
|
||||
#### How to get the Slack URL?
|
||||
|
||||
1. Go to your slack workspace
|
||||
2. Click on `Apps` on the left sidebar
|
||||
3. Search for `Incoming Webhooks`
|
||||
4. Click on `Add to Slack`
|
||||
5. Select the channel you want to send the messages
|
||||
6. Click on `Add Incoming Webhook Integration`
|
||||
7. Copy the URL
|
||||
|
||||
#### Slack Message
|
||||
|
||||
The slack message when alert is `TRIGGERED` will look like this
|
||||
|
||||

|
||||
|
||||
The slack message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||
|
||||
### Modify Slack Message
|
||||
|
||||
You can modify the slack message by using [mustache variables](#triggers-trigger-variables).
|
||||
|
||||
```json
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Hello Alert {{alert_name}}",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "{{description}}\n*Source:* {{source}}\n*Severity:* {{severity}}\n*Status:* {{status}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Metric:*\n{{metric}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Current Value:*\n{{current_value}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Threshold:*\n{{threshold}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Environment:*\n$_NODE_ENV"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Timestamp:*\n<!date^{{timestamp_unix}}^{date} at {time}|{{timestamp}}>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "{{action_text}}"
|
||||
},
|
||||
"url": "{{{action_url}}}",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Email
|
||||
|
||||
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">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Resend
|
||||
|
||||
To send emails using Resend you just need to set `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>
|
||||
|
||||
<div class=" note info ">
|
||||
|
||||
If your SMTP provider does require username and password, you can set `SMTP_USER` and `SMTP_PASS` to `-`.
|
||||
|
||||
</div>
|
||||
|
||||
### To
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The email addresses to which the email should be sent. It is required and has to be a valid email addresses. You can pass multiple email addresses separated by a comma.
|
||||
|
||||
### Sender
|
||||
|
||||
The email address from which the email should be sent.
|
||||
|
||||
It should be in the format `Name <email@address.com>`
|
||||
|
||||
If you have not connected your domain with resend, then use `Some Name <onboarding@resend.dev>`
|
||||
|
||||
### Subject
|
||||
|
||||
Subject of the email when `TRIGGERED`
|
||||
|
||||
```text
|
||||
[TRIGGERED] Mockoon DOWN at 2024-12-27T04:42:01.430Z
|
||||
```
|
||||
|
||||
Subject of the email when `RESOLVED`
|
||||
|
||||
```text
|
||||
[RESOLVED] Mockoon DOWN at 2024-12-27T04:42:01.430Z
|
||||
```
|
||||
|
||||
### Body
|
||||
|
||||
The emaik message when alert is `TRIGGERED` will look like this
|
||||
|
||||

|
||||
|
||||
The emaik message when alert is `RESOLVED` will look like this
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Edit Triggers
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #111;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script
|
||||
async
|
||||
src="http://localhost:3000/embed/monitor-earth/js?theme=light&monitor=http://localhost:3000/embed/monitor-earth&locale=fr&tz=Asia/Tokyo"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Automatically set PUBLIC_WHITE_LABEL based on WHITE_LABEL
|
||||
export PUBLIC_WHITE_LABEL="${WHITE_LABEL}"
|
||||
|
||||
# Replace shell with the given command (from CMD or runtime args)
|
||||
exec "$@"
|
||||
+2
-1
@@ -9,7 +9,8 @@
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"noImplicitAny": false
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
const databaseURL = process.env.DATABASE_URL || "sqlite://./database/kener.sqlite.db";
|
||||
|
||||
const databaseURLParts = databaseURL.split("://");
|
||||
const databaseType = databaseURLParts[0];
|
||||
const databasePath = databaseURLParts[1];
|
||||
|
||||
const knexOb = {
|
||||
migrations: {
|
||||
directory: "./migrations",
|
||||
},
|
||||
seeds: {
|
||||
directory: "./seeds",
|
||||
},
|
||||
databaseType,
|
||||
};
|
||||
if (databaseType === "sqlite") {
|
||||
knexOb.client = "better-sqlite3";
|
||||
knexOb.connection = {
|
||||
filename: databasePath,
|
||||
};
|
||||
knexOb.useNullAsDefault = true;
|
||||
} else if (databaseType === "postgresql") {
|
||||
knexOb.client = "pg";
|
||||
knexOb.connection = databaseURL;
|
||||
} else if (databaseType === "mysql") {
|
||||
knexOb.client = "mysql2";
|
||||
knexOb.connection = databaseURL;
|
||||
} else {
|
||||
console.error("Invalid database type");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export default knexOb;
|
||||
@@ -3,11 +3,18 @@ import { apiReference } from "@scalar/express-api-reference";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import express from "express";
|
||||
import sitemap from "./sitemap.js";
|
||||
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";
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const base = process.env.KENER_BASE_PATH || "";
|
||||
|
||||
const app = express();
|
||||
const db = knex(knexOb);
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.startsWith("/embed")) {
|
||||
res.setHeader("Content-Security-Policy", "frame-ancestors *");
|
||||
@@ -15,9 +22,19 @@ app.use((req, res, next) => {
|
||||
res.setHeader("X-Powered-By", "Kener");
|
||||
next();
|
||||
});
|
||||
app.get("/healthcheck", (req, res) => {
|
||||
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
|
||||
process.env.UPLOAD_PATH = "./uploads";
|
||||
|
||||
app.use(base + "/uploads", express.static("uploads"));
|
||||
|
||||
try {
|
||||
const openapiJSON = fs.readFileSync("./openapi.json", "utf-8");
|
||||
@@ -48,13 +65,35 @@ try {
|
||||
} catch (e) {
|
||||
console.warn("Error loading openapi.json, but that is okay.");
|
||||
}
|
||||
app.get("/sitemap.xml", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/xml");
|
||||
res.end(sitemap);
|
||||
});
|
||||
|
||||
app.use(handler);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
//migrations
|
||||
async function runMigrations() {
|
||||
try {
|
||||
console.log("Running migrations...");
|
||||
await db.migrate.latest(); // Runs migrations to the latest state
|
||||
console.log("Migrations completed successfully!");
|
||||
} catch (err) {
|
||||
console.error("Error running migrations:", err);
|
||||
}
|
||||
}
|
||||
|
||||
//seed
|
||||
async function runSeed() {
|
||||
try {
|
||||
console.log("Running seed...");
|
||||
await db.seed.run(); // Runs seed to the latest state
|
||||
console.log("Seed completed successfully!");
|
||||
} catch (err) {
|
||||
console.error("Error running seed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
app.listen(PORT, async () => {
|
||||
await runMigrations();
|
||||
await runSeed();
|
||||
await db.destroy();
|
||||
Startup();
|
||||
console.log("Kener is running on port " + PORT + "!");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// migrations/YYYYMMDDHHMMSS_create_monitoring_tables.js
|
||||
|
||||
export function up(knex) {
|
||||
return (
|
||||
knex.schema
|
||||
// Create monitoring_data table
|
||||
.createTable("monitoring_data", (table) => {
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.integer("timestamp").notNullable();
|
||||
table.text("status");
|
||||
table.float("latency", 8, 2);
|
||||
table.text("type");
|
||||
table.primary(["monitor_tag", "timestamp"]);
|
||||
})
|
||||
// Create monitor_alerts table
|
||||
.createTable("monitor_alerts", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_status", 255).notNullable();
|
||||
table.string("alert_status", 255).notNullable();
|
||||
table.integer("health_checks").notNullable();
|
||||
table.integer("incident_number").defaultTo(0);
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
// Add index to monitor_alerts table
|
||||
.raw(
|
||||
"CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)"
|
||||
)
|
||||
.createTable("site_data", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("key", 255).notNullable().unique();
|
||||
table.text("value").notNullable();
|
||||
table.string("data_type", 255).notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("tag", 255).notNullable().unique();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.text("description");
|
||||
table.text("image");
|
||||
table.string("cron", 255);
|
||||
table.string("default_status", 255);
|
||||
table.string("status", 255);
|
||||
table.string("category_name", 255);
|
||||
table.string("monitor_type", 255);
|
||||
table.string("down_trigger", 255);
|
||||
table.string("degraded_trigger", 255);
|
||||
table.text("type_data");
|
||||
table.integer("day_degraded_minimum_count");
|
||||
table.integer("day_down_minimum_count");
|
||||
table.string("include_degraded_in_downtime", 255).defaultTo("NO");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("triggers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.string("trigger_type", 255);
|
||||
table.text("trigger_desc");
|
||||
table.string("trigger_status", 255);
|
||||
table.text("trigger_meta");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("users", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("email", 255).notNullable().unique();
|
||||
table.string("name", 255).notNullable();
|
||||
table.string("password_hash", 255).notNullable();
|
||||
table.integer("is_active").defaultTo(1);
|
||||
table.integer("is_verified").defaultTo(0);
|
||||
table.string("role", 255).defaultTo("user");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("api_keys", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.string("hashed_key", 255).notNullable().unique();
|
||||
table.string("masked_key", 255).notNullable();
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("incidents", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("title", 255).notNullable();
|
||||
table.integer("start_date_time").notNullable();
|
||||
table.integer("end_date_time");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.string("state", 255).defaultTo("INVESTIGATING");
|
||||
})
|
||||
.createTable("incident_monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_impact", 255);
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.integer("incident_id").notNullable();
|
||||
table.unique(["monitor_tag", "incident_id"]);
|
||||
})
|
||||
.createTable("incident_comments", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.text("comment").notNullable();
|
||||
table.integer("incident_id").notNullable();
|
||||
table.integer("commented_at").notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.string("state", 255).defaultTo("INVESTIGATING");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function down(knex) {
|
||||
return (
|
||||
knex.schema
|
||||
// Drop tables in reverse order
|
||||
.dropTableIfExists("monitor_alerts")
|
||||
.dropTableIfExists("monitoring_data")
|
||||
.dropTableIfExists("site_data")
|
||||
.dropTableIfExists("monitors")
|
||||
.dropTableIfExists("triggers")
|
||||
.dropTableIfExists("users")
|
||||
.dropTableIfExists("api_keys")
|
||||
.dropTableIfExists("incidents")
|
||||
.dropTableIfExists("incident_monitors")
|
||||
.dropTableIfExists("incident_comments")
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function up(knex) {
|
||||
return knex.schema.alterTable("incidents", function (table) {
|
||||
table.text("incident_source").defaultTo("DASHBOARD");
|
||||
});
|
||||
}
|
||||
|
||||
export function down(knex) {
|
||||
return knex.schema.alterTable("incidents", function (table) {
|
||||
table.dropColumn("incident_source");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
export function up(knex) {
|
||||
return knex.schema.createTable("invitations", (table) => {
|
||||
// Primary key
|
||||
table.increments("id").primary();
|
||||
|
||||
// Core invitation fields
|
||||
table.string("invitation_token").unique().notNullable();
|
||||
table.string("invitation_type").notNullable();
|
||||
table.integer("invited_user_id").nullable();
|
||||
table.integer("invited_by_user_id").notNullable();
|
||||
|
||||
// Additional data fields
|
||||
table.text("invitation_meta").nullable(); // For storing JSON or other metadata
|
||||
table.timestamp("invitation_expiry").notNullable();
|
||||
table.string("invitation_status").notNullable().defaultTo("PENDING");
|
||||
|
||||
// Timestamps
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Indexes
|
||||
table.index("invitation_status");
|
||||
table.index("invitation_expiry");
|
||||
table.index(["invited_by_user_id", "invitation_status"]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
export function down(knex) {
|
||||
return knex.schema.dropTableIfExists("invitations");
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export function up(knex) {
|
||||
return knex.schema
|
||||
.createTable("subscribers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("subscriber_send").notNullable();
|
||||
table.text("subscriber_meta").nullable();
|
||||
table.string("subscriber_type").notNullable();
|
||||
table.string("subscriber_status").notNullable();
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Add unique constraint on subscriber_send and subscriber_type
|
||||
table.unique(["subscriber_send", "subscriber_type"]);
|
||||
|
||||
// Add index on subscriber_send for better query performance
|
||||
table.index(["subscriber_send"]);
|
||||
})
|
||||
.createTable("subscriptions", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("subscriber_id").unsigned().notNullable();
|
||||
table.string("subscriptions_status").notNullable();
|
||||
table.string("subscriptions_monitors").notNullable();
|
||||
table.text("subscriptions_meta").nullable();
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Add unique constraint on subscriber_id and subscriptions_monitors
|
||||
// This constraint also creates an index that will be used for queries
|
||||
table.unique(["subscriber_id", "subscriptions_monitors"]);
|
||||
|
||||
// Add index to optimize queries filtering by status and monitors
|
||||
table.index(["subscriptions_status", "subscriptions_monitors"]);
|
||||
})
|
||||
.createTable("subscription_triggers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("subscription_trigger_type").notNullable().unique();
|
||||
table.string("subscription_trigger_status").notNullable();
|
||||
table.text("config").nullable();
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
export function down(knex) {
|
||||
return knex.schema
|
||||
.dropTableIfExists("subscription_triggers")
|
||||
.dropTableIfExists("subscriptions")
|
||||
.dropTableIfExists("subscribers");
|
||||
}
|
||||
+1248
-219
File diff suppressed because it is too large
Load Diff
+312
-216
@@ -1,9 +1,9 @@
|
||||
---
|
||||
info:
|
||||
title: Kener API
|
||||
version: 1.0.0
|
||||
version: 3.0.0
|
||||
description: |
|
||||
# Kener Self-hosted node js status page
|
||||
# Kener Selfhost node js status page
|
||||

|
||||
API specification for Kener status page and incident management system. This API spec was created using [Frogment](https://www.frogment.app)
|
||||
contact:
|
||||
@@ -15,7 +15,7 @@ info:
|
||||
url: https://opensource.org/licenses/MIT
|
||||
openapi: 3.0.0
|
||||
servers:
|
||||
- url: https://your-kener-host.com
|
||||
- url: https://{host}
|
||||
description: Kener host URL
|
||||
tags:
|
||||
- name: Monitors
|
||||
@@ -76,130 +76,138 @@ components:
|
||||
uptime:
|
||||
type: string
|
||||
example: '100'
|
||||
lastUpdatedAt:
|
||||
last_updated_at:
|
||||
type: integer
|
||||
example: 1731251760
|
||||
example:
|
||||
status: UP
|
||||
lastUpdatedAt: 1731251760
|
||||
last_updated_at: 1731251760
|
||||
uptime: '100'
|
||||
Incident:
|
||||
type: object
|
||||
description: body of an incident
|
||||
required:
|
||||
- title
|
||||
- tags
|
||||
properties:
|
||||
startDatetime:
|
||||
start_date_time:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds
|
||||
example: 1731901920
|
||||
endDatetime:
|
||||
end_date_time:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds
|
||||
nullable: true
|
||||
example: 1704123938
|
||||
title:
|
||||
type: string
|
||||
example: Outage in mumbai
|
||||
description: title of the incident
|
||||
body:
|
||||
created_at:
|
||||
type: string
|
||||
example: body of the incident
|
||||
description: body of the incident
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: earth
|
||||
description: comma separated tags of monitors
|
||||
example:
|
||||
- earth
|
||||
- google
|
||||
impact:
|
||||
example: '2025-01-09 04:12:01'
|
||||
description: created at time
|
||||
updated_at:
|
||||
type: string
|
||||
example: DOWN
|
||||
description: Impact of the incident
|
||||
example: '2025-01-09 06:10:00'
|
||||
description: updated at time
|
||||
status:
|
||||
type: string
|
||||
example: OPEN
|
||||
description: delete or not deleted incident
|
||||
enum:
|
||||
- DOWN
|
||||
- DEGRADED
|
||||
isMaintenance:
|
||||
type: boolean
|
||||
description: is this incident because of maintenance
|
||||
example: false
|
||||
isIdentified:
|
||||
type: boolean
|
||||
description: has the incident been indentified
|
||||
example: true
|
||||
isResolved:
|
||||
type: boolean
|
||||
description: has the incident been resovled
|
||||
example: true
|
||||
- OPEN
|
||||
- CLOSED
|
||||
state:
|
||||
type: string
|
||||
description: the current status of the incident
|
||||
example: INVESTIGATING
|
||||
enum:
|
||||
- INVESTIGATING
|
||||
- IDENTIFIED
|
||||
- MONITORING
|
||||
- RESOLVED
|
||||
example:
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
start_date_time: 1731901920
|
||||
end_date_time: 1704123938
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
created_at: '2025-01-09 04:12:01'
|
||||
updated_at: '2025-01-09 04:12:01'
|
||||
state: INVESTIGATING
|
||||
status: OPEN
|
||||
id: 4
|
||||
IncidentResponse:
|
||||
description: Incident response schema
|
||||
allOf:
|
||||
- "$ref": "#/components/schemas/Incident"
|
||||
- type: object
|
||||
properties:
|
||||
createdAt:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds, incident created at
|
||||
example: 1731901920
|
||||
closedAt:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: UTC timestamp in seconds, incident closed at
|
||||
example: 1731901920
|
||||
incidentNumber:
|
||||
type: integer
|
||||
description: id of the incident
|
||||
example: 4
|
||||
example:
|
||||
createdAt: 1731901920
|
||||
closedAt: 1731901920
|
||||
incidentNumber: 4
|
||||
Comment:
|
||||
type: object
|
||||
description: Comment of an incident
|
||||
required:
|
||||
- body
|
||||
properties:
|
||||
body:
|
||||
comment:
|
||||
type: string
|
||||
example: comment 1
|
||||
commented_at:
|
||||
type: integer
|
||||
example: 1736398336
|
||||
state:
|
||||
type: string
|
||||
example: IDENTIFIED
|
||||
example:
|
||||
body: comment 2
|
||||
commented_at: 1736398336
|
||||
state: IDENTIFIED
|
||||
CommentResponse:
|
||||
type: object
|
||||
description: Comment Response
|
||||
properties:
|
||||
commentID:
|
||||
id:
|
||||
type: integer
|
||||
description: ID of the comment
|
||||
example: 1873376745
|
||||
body:
|
||||
example: 60
|
||||
comment:
|
||||
type: string
|
||||
description: body of the comment
|
||||
example: comment 3
|
||||
createdAt:
|
||||
description: The content of the comment
|
||||
example: Sometimes, you want all the goodness of moment#from but you don't
|
||||
want to have to create two moments, you just want to display a length
|
||||
of time.
|
||||
incident_id:
|
||||
type: integer
|
||||
description: timestamp when comment was created
|
||||
example: 1704123938
|
||||
description: ID of the associated incident
|
||||
example: 24
|
||||
commented_at:
|
||||
type: integer
|
||||
description: Timestamp when the comment was added
|
||||
example: 1736398336
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Timestamp when the comment was created in ISO 8601 format
|
||||
example: '2025-01-09 04:52:16'
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Timestamp when the comment was last updated in ISO 8601 format
|
||||
example: '2025-01-09 04:52:16'
|
||||
status:
|
||||
type: string
|
||||
description: Current status of the comment
|
||||
example: ACTIVE
|
||||
state:
|
||||
type: string
|
||||
description: the current status of the incident
|
||||
example: INVESTIGATING
|
||||
enum:
|
||||
- INVESTIGATING
|
||||
- IDENTIFIED
|
||||
- MONITORING
|
||||
- RESOLVED
|
||||
example:
|
||||
commentID: 1873376745
|
||||
body: comment 4
|
||||
createdAt: 1704123938
|
||||
id: 60
|
||||
comment: Sometimes, you want all the goodness of moment
|
||||
incident_id: 24
|
||||
commented_at: 1736398336
|
||||
created_at: '2025-01-09 04:52:16'
|
||||
updated_at: '2025-01-09 04:52:16'
|
||||
status: ACTIVE
|
||||
state: MONITORING
|
||||
IncidentStatus:
|
||||
type: object
|
||||
description: Status of the incident
|
||||
@@ -220,6 +228,25 @@ components:
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
endDatetime: 1731901920
|
||||
IncidentCreateRequest:
|
||||
title: Incident Create Request Body
|
||||
type: object
|
||||
description: body of an incident
|
||||
required:
|
||||
- title
|
||||
- start_date_time
|
||||
properties:
|
||||
start_date_time:
|
||||
type: integer
|
||||
description: UTC timestamp in seconds
|
||||
example: 1731901920
|
||||
title:
|
||||
type: string
|
||||
example: Outage in mumbai
|
||||
description: title of the incident
|
||||
example:
|
||||
start_date_time: 1731901920
|
||||
title: title of the incident
|
||||
responses:
|
||||
Response401:
|
||||
description: Bad API keys response
|
||||
@@ -267,102 +294,94 @@ components:
|
||||
summary: create incident response
|
||||
description: create incident response
|
||||
value:
|
||||
createdAt: 1731901920
|
||||
closedAt: 1731901920
|
||||
incidentNumber: 4
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
start_date_time: 1731901920
|
||||
end_date_time: 1731901920
|
||||
id: 4
|
||||
created_at: '2025-01-09 04:12:01'
|
||||
updated_at: '2025-01-10 04:12:01'
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
status: OPEN
|
||||
state: INVESTIGATING
|
||||
CreateIncidentRequest:
|
||||
summary: create incident request body
|
||||
description: Some Description
|
||||
description: create incident request body
|
||||
value:
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
start_date_time: 1731901920
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
SearchIncidentsResponse:
|
||||
summary: array of incidents
|
||||
description: Some Description
|
||||
value:
|
||||
- createdAt: 1731901920
|
||||
closedAt: 1731901920
|
||||
incidentNumber: 4
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
- createdAt: 1731901920
|
||||
closedAt: 1731901920
|
||||
incidentNumber: 4
|
||||
startDatetime: 1731901920
|
||||
endDatetime: 1704123938
|
||||
title: title of the incident
|
||||
body: body of the incident
|
||||
tags:
|
||||
- earth
|
||||
- google
|
||||
impact: DOWN
|
||||
isMaintenance: false
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
- id: 2
|
||||
title: future wala
|
||||
start_date_time: 1736774486
|
||||
end_date_time: 1737033787
|
||||
created_at: '2025-01-12 13:21:32'
|
||||
updated_at: '2025-01-12 13:23:15'
|
||||
status: OPEN
|
||||
state: RESOLVED
|
||||
- id: 1
|
||||
title: internatiinal asda
|
||||
start_date_time: 1736684329
|
||||
end_date_time:
|
||||
created_at: '2025-01-12 12:18:53'
|
||||
updated_at: '2025-01-12 13:21:32'
|
||||
status: OPEN
|
||||
state: INVESTIGATING
|
||||
CommentsResponse:
|
||||
summary: list of comments
|
||||
description: Some Description
|
||||
value:
|
||||
- commentID: 1873376745
|
||||
body: comment 4
|
||||
createdAt: 1704123938
|
||||
- commentID: 1873376745
|
||||
body: comment 4
|
||||
createdAt: 1704123938
|
||||
- id: 58
|
||||
comment: idensad
|
||||
incident_id: 24
|
||||
commented_at: 1736398295
|
||||
created_at: '2025-01-09 04:51:37'
|
||||
updated_at: '2025-01-09 04:51:37'
|
||||
status: ACTIVE
|
||||
state: IDENTIFIED
|
||||
- id: 57
|
||||
comment: Sometimes, you want all the goodness of moment#from but you don't
|
||||
want to have to create two moments, you just want to display a length of
|
||||
time.
|
||||
incident_id: 24
|
||||
commented_at: 1736398279
|
||||
created_at: '2025-01-09 04:51:19'
|
||||
updated_at: '2025-01-09 04:51:19'
|
||||
status: ACTIVE
|
||||
state: INVESTIGATING
|
||||
CommentRequestBody:
|
||||
summary: request body for a comment
|
||||
description: request body for a comment to add in an incident
|
||||
summary: request body for a update
|
||||
description: request body for a update to add in an incident
|
||||
value:
|
||||
body: This is a comment
|
||||
comment: This is a comment
|
||||
commented_at: 1736398336
|
||||
state: IDENTIFIED
|
||||
CreateCommentResponse:
|
||||
summary: create comment response body sample
|
||||
description: create comment response body sample
|
||||
summary: create update response body sample
|
||||
description: create update response body sample
|
||||
value:
|
||||
commentID: 1873376745
|
||||
body: comment 4
|
||||
createdAt: 1704123938
|
||||
id: 60
|
||||
comment: Sometimes, you want all the goodness of moment
|
||||
incident_id: 24
|
||||
commented_at: 1736398336
|
||||
created_at: '2025-01-09 04:52:16'
|
||||
updated_at: '2025-01-09 04:52:16'
|
||||
status: ACTIVE
|
||||
state: MONITORING
|
||||
paths:
|
||||
"/api/status":
|
||||
post:
|
||||
operationId: updateMonitorStatus
|
||||
summary: Update status of a monitor
|
||||
description: Update status of an incident at a given timestamp
|
||||
description: Update status of a monitor at a given timestamp UTC
|
||||
tags:
|
||||
- Monitors
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: request body to update an incident
|
||||
description: request body to update status of a monitor
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -403,7 +422,7 @@ paths:
|
||||
- name: tag
|
||||
in: query
|
||||
required: true
|
||||
description: monitor tag to get an incident
|
||||
description: monitor tag to get status of it
|
||||
schema:
|
||||
type: string
|
||||
example: earth
|
||||
@@ -436,7 +455,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Incident"
|
||||
"$ref": "#/components/schemas/IncidentCreateRequest"
|
||||
examples:
|
||||
sample:
|
||||
"$ref": "#/components/examples/CreateIncidentRequest"
|
||||
@@ -463,6 +482,17 @@ paths:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: status
|
||||
in: query
|
||||
description: status of the incident. Can be open or close. close means deleted
|
||||
schema:
|
||||
type: string
|
||||
description: status of the incident. Can be open or close. close means deleted
|
||||
enum:
|
||||
- OPEN
|
||||
- CLOSED
|
||||
default: OPEN
|
||||
example: OPEN
|
||||
- name: state
|
||||
in: query
|
||||
description: state of the incident. Can be open or close
|
||||
@@ -470,17 +500,12 @@ paths:
|
||||
type: string
|
||||
description: state of the incident. Can be open or close
|
||||
enum:
|
||||
- open
|
||||
- closed
|
||||
default: open
|
||||
example: open
|
||||
- name: tags
|
||||
in: query
|
||||
description: Comma separated monitor tags
|
||||
schema:
|
||||
type: string
|
||||
description: Comma separated monitor tags
|
||||
example: earth,google
|
||||
- INVESTIGATING
|
||||
- IDENTIFIED
|
||||
- MONITORING
|
||||
- RESOLVED
|
||||
default: INVESTIGATING
|
||||
example: IDENTIFIED
|
||||
- name: page
|
||||
in: query
|
||||
description: page number
|
||||
@@ -490,7 +515,7 @@ paths:
|
||||
default: 1
|
||||
minimum: 1
|
||||
example: 1
|
||||
- name: per_page
|
||||
- name: limit
|
||||
in: query
|
||||
description: how many per page
|
||||
schema:
|
||||
@@ -499,27 +524,20 @@ paths:
|
||||
description: how many per page
|
||||
maximum: 100
|
||||
example: 10
|
||||
- name: created_after_utc
|
||||
- name: start_date_time
|
||||
description: start time
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: start time
|
||||
example: 1731866475
|
||||
- name: created_before_utc
|
||||
- name: end_date_time
|
||||
description: end time
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
description: end time
|
||||
example: 1731866475
|
||||
- name: title_like
|
||||
description: title of the incident
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: title of the incident
|
||||
example: outage
|
||||
responses:
|
||||
'200':
|
||||
description: Search results retrieved successfully
|
||||
@@ -536,19 +554,19 @@ paths:
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
"/api/incident/{incidentNumber}":
|
||||
"/api/incident/{incident_id}":
|
||||
parameters:
|
||||
- name: incidentNumber
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
description: incident number as an integer to get incident by id
|
||||
description: incident id as an integer to get incident by id
|
||||
schema:
|
||||
type: integer
|
||||
example: 4
|
||||
get:
|
||||
operationId: getIncident
|
||||
summary: Get an incident by number
|
||||
description: API to get an incident by incident number
|
||||
summary: Get an incident by id
|
||||
description: API to get an incident by incident id
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
@@ -570,7 +588,7 @@ paths:
|
||||
patch:
|
||||
operationId: updateIncident
|
||||
summary: Update an incident
|
||||
description: API to update an incident by incident number
|
||||
description: API to update an incident by incident id
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
@@ -599,26 +617,26 @@ paths:
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
"/api/incident/{incidentNumber}/comment":
|
||||
"/api/incident/{incident_id}/updates":
|
||||
parameters:
|
||||
- name: incidentNumber
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
description: incident number to fetch comment for
|
||||
description: incident id to fetch comment for
|
||||
schema:
|
||||
type: integer
|
||||
example: 4
|
||||
post:
|
||||
operationId: addIncidentComment
|
||||
summary: Add a comment to an incident
|
||||
description: API to create comment for an incident
|
||||
summary: Add an update to an incident
|
||||
description: API to create update for an incident
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
description: body to add a comment
|
||||
description: body to add an update
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -642,15 +660,15 @@ paths:
|
||||
"$ref": "#/components/responses/Response401"
|
||||
get:
|
||||
operationId: getIncidentComments
|
||||
summary: Get comments for an incident
|
||||
description: API to get comments for an incident
|
||||
summary: Get updates for an incident
|
||||
description: API to get updates for an incident
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Comments retrieved successfully
|
||||
description: updates retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -664,45 +682,123 @@ paths:
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
"/api/incident/{incidentNumber}/status":
|
||||
"/api/incident/{incident_id}/monitors":
|
||||
parameters:
|
||||
- name: incidentNumber
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
description: incident number to fetch status for
|
||||
description: incident id to fetch comment for
|
||||
schema:
|
||||
type: integer
|
||||
example: 4
|
||||
post:
|
||||
operationId: updateIncidentStatus
|
||||
summary: Update the status of an incident
|
||||
description: API to update status of an incident
|
||||
get:
|
||||
summary: Get monitors for an incident
|
||||
operationId: getIncidentMonitors
|
||||
tags:
|
||||
- Incidents
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
parameters:
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
description: request body to update status of an incident
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/IncidentStatus"
|
||||
example:
|
||||
isIdentified: true
|
||||
isResolved: true
|
||||
endDatetime: 1731901920
|
||||
schema:
|
||||
type: integer
|
||||
description: The ID of the incident
|
||||
responses:
|
||||
'200':
|
||||
description: Incident status updated successfully
|
||||
description: List of monitors for the incident
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/IncidentResponse"
|
||||
examples:
|
||||
success:
|
||||
"$ref": "#/components/examples/CreateIncidentResponse"
|
||||
'400':
|
||||
"$ref": "#/components/responses/Response400"
|
||||
'401':
|
||||
"$ref": "#/components/responses/Response401"
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
monitor_tag:
|
||||
type: string
|
||||
description: The tag of the monitor
|
||||
example: earth
|
||||
monitor_impact:
|
||||
type: string
|
||||
description: The impact status of the monitor
|
||||
example: DOWN
|
||||
security:
|
||||
- bearerAuth: []
|
||||
post:
|
||||
summary: Add a monitor to an incident
|
||||
operationId: addIncidentMonitor
|
||||
tags:
|
||||
- Incidents
|
||||
parameters:
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: The ID of the incident
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
tag:
|
||||
type: string
|
||||
description: The tag for the monitor
|
||||
example: okbookmarks
|
||||
impact:
|
||||
type: string
|
||||
description: The impact status of the monitor
|
||||
example: DEGRADED
|
||||
responses:
|
||||
'200':
|
||||
description: Monitor added successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
monitor_tag:
|
||||
type: string
|
||||
description: The tag of the monitor
|
||||
example: okbookmarks
|
||||
monitor_impact:
|
||||
type: string
|
||||
description: The impact status of the monitor
|
||||
example: DEGRADED
|
||||
security:
|
||||
- bearerAuth: []
|
||||
delete:
|
||||
summary: Delete a monitor from an incident
|
||||
operationId: deleteIncidentMonitor
|
||||
tags:
|
||||
- Incidents
|
||||
parameters:
|
||||
- name: incident_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: The ID of the incident
|
||||
- name: tag
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The tag of the monitor to delete
|
||||
responses:
|
||||
'200':
|
||||
description: Monitor deleted successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: Confirmation message
|
||||
example: Monitor deleted successfully
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
Generated
+9529
-6330
File diff suppressed because it is too large
Load Diff
+124
-94
@@ -1,96 +1,126 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "2.0.0",
|
||||
"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",
|
||||
"predevschedule": "node build.js",
|
||||
"configure": "node build.js",
|
||||
"devschedule": "node src/lib/server/startup.js",
|
||||
"schedule": "node src/lib/server/startup.js",
|
||||
"predevelopment": "node delay.js",
|
||||
"development": "vite dev",
|
||||
"dev": "npm-run-all --parallel devschedule development",
|
||||
"prettify": "prettier --write .",
|
||||
"start": "node main.js",
|
||||
"prod": "npm-run-all --parallel schedule start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.27.4",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"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-check": "^3.6.0",
|
||||
"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",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"bits-ui": "^0.9.9",
|
||||
"clsx": "^2.0.0",
|
||||
"croner": "^7.0.5",
|
||||
"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",
|
||||
"lucide-svelte": "^0.292.0",
|
||||
"marked": "^11.1.1",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"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",
|
||||
"svelte-legos": "^0.2.5",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-variants": "^0.1.18"
|
||||
}
|
||||
"name": "kener",
|
||||
"version": "3.2.18",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"description": "Kener is a modern, open-source status page application built with Node.js. It provides real-time monitoring, uptime tracking, incident management, and beautiful dashboards. Perfect for DevOps teams, SaaS providers, and businesses needing reliable service status communication with minimal setup.",
|
||||
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
|
||||
"keywords": [
|
||||
"status page",
|
||||
"uptime monitoring",
|
||||
"incident management",
|
||||
"DevOps tools",
|
||||
"service reliability",
|
||||
"open source",
|
||||
"Node.js",
|
||||
"dashboard",
|
||||
"system monitoring",
|
||||
"status alerts",
|
||||
"outage communication",
|
||||
"API monitoring",
|
||||
"SaaS status",
|
||||
"performance metrics",
|
||||
"status reporting"
|
||||
],
|
||||
"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",
|
||||
"generate-readme": "node scripts/generate-readme.js",
|
||||
"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": {
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@number-flow/svelte": "^0.2.1",
|
||||
"@scalar/express-api-reference": "^0.4.167",
|
||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||
"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",
|
||||
"cheerio": "^1.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"croner": "^7.0.5",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dns2": "^2.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"figlet": "^1.8.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"gamedig": "^5.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "^3.1.0",
|
||||
"lucide-svelte": "^0.483.0",
|
||||
"marked": "^11.1.1",
|
||||
"mobile-detect": "^1.4.5",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mustache": "^4.2.0",
|
||||
"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-codemirror-editor": "^1.4.1",
|
||||
"svelte-legos": "^0.2.5",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-variants": "^0.1.18",
|
||||
"vite-plugin-package-version": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import fs from "fs";
|
||||
import Mustache from "mustache";
|
||||
import dotenv from "dotenv";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
|
||||
// Resolve paths correctly in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Load the template
|
||||
const templatePath = path.resolve(__dirname, "../README.template.md");
|
||||
const template = fs.readFileSync(templatePath, "utf-8");
|
||||
|
||||
// Load environment variables and provide default values
|
||||
const data = {
|
||||
kener_full_version: process.env.BUILD_FULL_VERSION || "N/A",
|
||||
kener_major_version: process.env.BUILD_MAJOR_VERSION || "N/A",
|
||||
kener_major_minor_version: process.env.BUILD_MAJOR_MINOR_VERSION || "N/A",
|
||||
};
|
||||
|
||||
// Render README.md
|
||||
const output = Mustache.render(template, data);
|
||||
|
||||
// Write to README.md
|
||||
fs.writeFileSync(path.resolve(__dirname, "../README.md"), output);
|
||||
|
||||
console.log("✅ README.md generated successfully!");
|
||||
@@ -0,0 +1,32 @@
|
||||
import monitorSeed from "../src/lib/server/db/seedMonitorData.js";
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
export async function seed(knex) {
|
||||
// Check if the table is empty
|
||||
const count = await knex("monitors").count("id as CNT").first();
|
||||
if (count.CNT == 0) {
|
||||
// Deletes ALL existing entries
|
||||
for (const monitor of monitorSeed) {
|
||||
await knex("monitors").insert({
|
||||
tag: monitor.tag,
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
image: monitor.image,
|
||||
cron: monitor.cron,
|
||||
default_status: monitor.default_status,
|
||||
status: monitor.status,
|
||||
category_name: monitor.category_name,
|
||||
monitor_type: monitor.monitor_type,
|
||||
type_data: monitor.type_data,
|
||||
day_degraded_minimum_count: monitor.day_degraded_minimum_count,
|
||||
day_down_minimum_count: monitor.day_down_minimum_count,
|
||||
include_degraded_in_downtime: monitor.include_degraded_in_downtime,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import seedSiteData from "../src/lib/server/db/seedSiteData.js";
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
export async function seed(knex) {
|
||||
// Check if the table is empty
|
||||
const count = await knex("site_data").count("id as CNT").first();
|
||||
|
||||
for (const key in seedSiteData) {
|
||||
if (Object.prototype.hasOwnProperty.call(seedSiteData, key)) {
|
||||
let value = seedSiteData[key];
|
||||
let data_type = typeof value;
|
||||
if (data_type === "object") {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
const existingEntry = await knex("site_data").where({ key: key }).first();
|
||||
if (!existingEntry) {
|
||||
await knex("site_data").insert([{ key: key, value: value, data_type: data_type }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
//read from process.env.PUBLIC_KENER_FOLDER / site.json
|
||||
//read from process.env.PUBLIC_KENER_FOLDER / monitors.json
|
||||
// create sitemap.xml
|
||||
import fs from "fs-extra";
|
||||
let siteMap = "";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
const site = fs.readJSONSync("./database/site.json", "utf8");
|
||||
const monitors = fs.readJSONSync("./database/monitors.json", "utf8");
|
||||
if (site.siteURL !== undefined && site.siteURL !== null && site.siteURL !== "") {
|
||||
if (monitors.length > 0) {
|
||||
siteMap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://www.sitemaps.org/schemas/sitemap/0.9
|
||||
https://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
${monitors
|
||||
.map((monitor) => {
|
||||
return `
|
||||
<url>
|
||||
<loc>${site.siteURL}/incident/${monitor.folderName}</loc>
|
||||
<lastmod>${new Date().toISOString()}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>`;
|
||||
})
|
||||
.join("\n")}
|
||||
${monitors
|
||||
.map((monitor) => {
|
||||
return `
|
||||
<url>
|
||||
<loc>${site.siteURL}/monitor-${encodeURIComponent(monitor.tag)}</loc>
|
||||
<lastmod>${new Date().toISOString()}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>`;
|
||||
})
|
||||
.join("\n")}
|
||||
</urlset>`;
|
||||
}
|
||||
}
|
||||
|
||||
//export default siteMap
|
||||
export default siteMap;
|
||||
@@ -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>
|
||||
|
||||
+64
-56
@@ -1,82 +1,90 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss/base";
|
||||
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background-kener: hsl(0, 0%, 100%);
|
||||
--background-kener-rgba: rgba(255, 255, 255, 0.5);
|
||||
--foreground: 240 10% 4%;
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background-kener: hsl(0, 0%, 100%);
|
||||
--background-kener-rgba: rgba(255, 255, 255, 0.5);
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--foreground: 240 10% 4%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 4%;
|
||||
--muted: 0 0% 92.9%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 4%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 4%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--card: 0 0% 99%;
|
||||
--card-foreground: 240 10% 4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--date-picker-background: hsl(0 0% 99%);
|
||||
--date-picker-foreground: hsl(240 10% 4%);
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--secondary: 0 0% 92.9%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--ring: 240 10% 4%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 4%;
|
||||
--background-kener: hsl(240, 10%, 4%);
|
||||
--background-kener-rgba: rgba(9, 9, 11, 0.35);
|
||||
--foreground: 210 40% 98%;
|
||||
--ring: 240 10% 4%;
|
||||
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
--popover: 240 10% 4%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
.dark {
|
||||
--background: 240 7% 11%;
|
||||
--background-kener: hsl(240 7% 11%);
|
||||
--background-kener-rgba: rgba(30, 30, 26, 0.35);
|
||||
--foreground: 60 10% 86%;
|
||||
|
||||
--card: 240 10% 4%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 0 0% 40%;
|
||||
|
||||
--border: 240 4% 16%;
|
||||
--input: 240 4% 16%;
|
||||
--popover: 230 9% 14%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--card: 230 9% 14%;
|
||||
--card-foreground: 60 10% 86%;
|
||||
|
||||
--secondary: 240 4% 16%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--border: 60 1% 17%;
|
||||
--input: 60 6% 17%;
|
||||
|
||||
--accent: 240 4% 16%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--date-picker-background: hsl(240 4% 16%);
|
||||
--date-picker-foreground: hsl(60 10% 86%);
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--primary: 60 10% 86%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--ring: hsl(212.7, 26.8%, 83.9);
|
||||
}
|
||||
--secondary: 240 1% 18%;
|
||||
--secondary-foreground: 60 10% 86%;
|
||||
|
||||
--accent: 240 4% 16%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: hsl(212.7, 26.8%, 83.9);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
+67
-12
@@ -1,24 +1,79 @@
|
||||
code:not([class^="language-"]) {
|
||||
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.sidebar-item.active,
|
||||
.sidebar-item:hover {
|
||||
color: #ed702d;
|
||||
color: #ed702d;
|
||||
}
|
||||
|
||||
.w-585px {
|
||||
width: 585px;
|
||||
width: 585px;
|
||||
}
|
||||
|
||||
main {
|
||||
scroll-behavior: smooth;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.kener-home-links a {
|
||||
text-decoration: none;
|
||||
background-color: var(--bg-background);
|
||||
text-decoration: none;
|
||||
background-color: var(--bg-background);
|
||||
}
|
||||
.kener-home-links > div:hover {
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 0 8px 1.5px #3e9a4b;
|
||||
.kener-home-links > a:hover {
|
||||
transition: all 0.3s;
|
||||
border-color: #ed702d;
|
||||
background-color: #ed702d;
|
||||
}
|
||||
|
||||
.accm input:checked ~ div {
|
||||
display: block;
|
||||
}
|
||||
.accm input ~ div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.accm input {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.accmt {
|
||||
background-color: hsl(223, 10%, 14%);
|
||||
}
|
||||
|
||||
.accm input:checked ~ .showaccm span:first-child {
|
||||
display: none;
|
||||
}
|
||||
.accm input:checked ~ .showaccm span:last-child {
|
||||
display: block;
|
||||
}
|
||||
.accm input ~ .showaccm span:first-child {
|
||||
display: block;
|
||||
}
|
||||
.accm input ~ .showaccm span:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.note {
|
||||
@apply mt-4 rounded-md border bg-background p-3 text-sm shadow-sm;
|
||||
}
|
||||
.note.danger {
|
||||
border: 1px solid #e3342f;
|
||||
color: #e3342f;
|
||||
}
|
||||
|
||||
.note.info {
|
||||
border: 1px solid #3490dc;
|
||||
color: #3490dc;
|
||||
}
|
||||
|
||||
.copybtn .copy-btn {
|
||||
transform: scale(1);
|
||||
}
|
||||
.copybtn .check-btn {
|
||||
transform: scale(0);
|
||||
}
|
||||
.copybtn:focus .copy-btn {
|
||||
transform: scale(0);
|
||||
}
|
||||
.copybtn:focus .check-btn {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-hairline.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-hairline.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-hairline.ttf") format("truetype");
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-hairline-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-hairline-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-hairline-italic.ttf") format("truetype");
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-light.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-light.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-light.ttf") format("truetype");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-light-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-light-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-light-italic.ttf") format("truetype");
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-regular.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-regular.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-regular.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-regular-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-regular-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-regular-italic.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-bold.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-bold.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-bold.ttf") format("truetype");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-bold-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-bold-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-bold-italic.ttf") format("truetype");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-black.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-black.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-black.ttf") format("truetype");
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("./fonts/lato/latin_subset/lato-black-italic.woff2") format("woff2"),
|
||||
url("./fonts/lato/latin_subset/lato-black-italic.woff") format("woff"),
|
||||
url("./fonts/lato/latin_subset/lato-black-italic.ttf") format("truetype");
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ event, resolve }) {
|
||||
const response = await resolve(event);
|
||||
response.headers.delete("Link");
|
||||
return response;
|
||||
}
|
||||
+833
-235
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
||||
<script>
|
||||
import { onMount, onDestroy, beforeUpdate } from "svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let monitor;
|
||||
export let theme = "light";
|
||||
export let bgc = "transparent";
|
||||
export let locale = "en";
|
||||
|
||||
let iframe, listeners;
|
||||
let containerId = `embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||
let isMounted = false;
|
||||
let prevMonitor, prevTheme, prevBgc, prevLocale;
|
||||
|
||||
onMount(() => {
|
||||
if (!monitor) return;
|
||||
|
||||
isMounted = true;
|
||||
prevMonitor = monitor;
|
||||
prevTheme = theme;
|
||||
prevBgc = bgc;
|
||||
prevLocale = locale;
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const uid = "KENER_" + ~~(new Date().getTime() / 86400000);
|
||||
const uriEmbedded = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
|
||||
|
||||
iframe = document.createElement("iframe");
|
||||
iframe.src = uriEmbedded;
|
||||
iframe.id = uid;
|
||||
iframe.width = "0%";
|
||||
iframe.height = "0";
|
||||
iframe.frameBorder = "0";
|
||||
iframe.allowTransparency = true;
|
||||
iframe.sandbox =
|
||||
"allow-modals allow-forms allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation allow-downloads";
|
||||
iframe.allow = "midi; geolocation; microphone; camera; display-capture; encrypted-media;";
|
||||
|
||||
container.appendChild(iframe);
|
||||
|
||||
const setHeight = (data) => {
|
||||
if (data.height !== undefined) {
|
||||
iframe.height = data.height;
|
||||
}
|
||||
};
|
||||
|
||||
const setWidth = (data) => {
|
||||
if (data.width !== undefined) {
|
||||
iframe.width = data.width;
|
||||
}
|
||||
};
|
||||
|
||||
listeners = (event) => {
|
||||
if (event.data && event.data.height !== undefined) {
|
||||
setHeight(event.data);
|
||||
}
|
||||
if (event.data && event.data.width !== undefined) {
|
||||
setWidth(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", listeners, false);
|
||||
});
|
||||
|
||||
beforeUpdate(() => {
|
||||
if (
|
||||
isMounted &&
|
||||
iframe &&
|
||||
(monitor !== prevMonitor || theme !== prevTheme || bgc !== prevBgc || locale !== prevLocale)
|
||||
) {
|
||||
iframe.src = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
|
||||
|
||||
// Update previous values
|
||||
prevMonitor = monitor;
|
||||
prevTheme = theme;
|
||||
prevBgc = bgc;
|
||||
prevLocale = locale;
|
||||
dispatch("update", {
|
||||
monitor: prevMonitor,
|
||||
theme: prevTheme,
|
||||
bgc: prevBgc,
|
||||
locale: prevLocale
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (iframe) {
|
||||
window.removeEventListener("message", listeners);
|
||||
iframe.remove();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Dynamic container with unique ID -->
|
||||
<div id={containerId}></div>
|
||||
@@ -0,0 +1,356 @@
|
||||
[
|
||||
{ "id": "sdtd", "name": "7 Days to Die", "isValve": true },
|
||||
{ "id": "arma2", "name": "ARMA 2", "isValve": true },
|
||||
{ "id": "a2oa", "name": "ARMA 2: Operation Arrowhead", "isValve": true },
|
||||
{ "id": "arma3", "name": "ARMA 3", "isValve": true },
|
||||
{ "id": "aaa", "name": "ARMA: Armed Assault" },
|
||||
{ "id": "acwa", "name": "ARMA: Cold War Assault" },
|
||||
{ "id": "armareforger", "name": "ARMA: Reforger", "isValve": true },
|
||||
{ "id": "armaresistance", "name": "ARMA: Resistance" },
|
||||
{ "id": "abioticfactor", "name": "Abiotic Factor", "isValve": true },
|
||||
{ "id": "aosc", "name": "Ace of Spades Classic" },
|
||||
{ "id": "ahl", "name": "Action Half-Life", "isValve": true },
|
||||
{ "id": "actionsource", "name": "Action: Source", "isValve": true },
|
||||
{ "id": "aoc", "name": "Age of Chivalry", "isValve": true },
|
||||
{ "id": "aoe2", "name": "Age of Empires 2" },
|
||||
{ "id": "alienarena", "name": "Alien Arena" },
|
||||
{ "id": "alienswarm", "name": "Alien Swarm", "isValve": true },
|
||||
{ "id": "avp2", "name": "Aliens versus Predator 2" },
|
||||
{ "id": "avp2010", "name": "Aliens vs. Predator 2010", "isValve": true },
|
||||
{ "id": "americasarmy", "name": "America's Army" },
|
||||
{ "id": "americasarmy2", "name": "America's Army 2" },
|
||||
{ "id": "americasarmy3", "name": "America's Army 3", "isValve": true },
|
||||
{ "id": "aapg", "name": "America's Army: Proving Grounds", "isValve": true },
|
||||
{ "id": "asr08", "name": "Arca Sim Racing '08" },
|
||||
{ "id": "asa", "name": "Ark: Survival Ascended" },
|
||||
{ "id": "ase", "name": "Ark: Survival Evolved", "isValve": true },
|
||||
{ "id": "armagetronadvanced", "name": "Armagetron Advanced" },
|
||||
{ "id": "assettocorsa", "name": "Assetto Corsa" },
|
||||
{ "id": "atlas", "name": "Atlas", "isValve": true },
|
||||
{ "id": "avorion", "name": "Avorion", "isValve": true },
|
||||
{ "id": "brokeprotocol", "name": "BROKE PROTOCOL" },
|
||||
{ "id": "baldursgate", "name": "Baldur's Gate" },
|
||||
{ "id": "ballisticoverkill", "name": "Ballistic Overkill", "isValve": true },
|
||||
{ "id": "barotrauma", "name": "Barotrauma", "isValve": true },
|
||||
{ "id": "basedefense", "name": "Base Defense", "isValve": true },
|
||||
{ "id": "battalion1944", "name": "Battalion 1944", "isValve": true },
|
||||
{ "id": "battlefield1942", "name": "Battlefield 1942" },
|
||||
{ "id": "battlefield2", "name": "Battlefield 2" },
|
||||
{ "id": "battlefield2142", "name": "Battlefield 2142" },
|
||||
{ "id": "battlefield3", "name": "Battlefield 3" },
|
||||
{ "id": "battlefield4", "name": "Battlefield 4" },
|
||||
{ "id": "battlefieldhardline", "name": "Battlefield Hardline" },
|
||||
{ "id": "battlefieldvietnam", "name": "Battlefield Vietnam" },
|
||||
{ "id": "bbc2", "name": "Battlefield: Bad Company 2" },
|
||||
{ "id": "beammp", "name": "BeamMP (2021)" },
|
||||
{ "id": "blackmesa", "name": "Black Mesa", "isValve": true },
|
||||
{ "id": "bladesymphony", "name": "Blade Symphony", "isValve": true },
|
||||
{ "id": "brainbread", "name": "BrainBread", "isValve": true },
|
||||
{ "id": "brainbread2", "name": "BrainBread 2", "isValve": true },
|
||||
{ "id": "breach", "name": "Breach", "isValve": true },
|
||||
{ "id": "breed", "name": "Breed" },
|
||||
{ "id": "brink", "name": "Brink", "isValve": true },
|
||||
{ "id": "bas", "name": "Build and Shoot" },
|
||||
{ "id": "c2d", "name": "CS2D" },
|
||||
{ "id": "cod", "name": "Call of Duty" },
|
||||
{ "id": "cod2", "name": "Call of Duty 2" },
|
||||
{ "id": "cod3", "name": "Call of Duty 3" },
|
||||
{ "id": "cod4mw", "name": "Call of Duty 4: Modern Warfare" },
|
||||
{ "id": "codbo3", "name": "Call of Duty: Black Ops 3", "isValve": true },
|
||||
{ "id": "codmw2", "name": "Call of Duty: Modern Warfare 2" },
|
||||
{ "id": "codmw3", "name": "Call of Duty: Modern Warfare 3", "isValve": true },
|
||||
{ "id": "coduo", "name": "Call of Duty: United Offensive" },
|
||||
{ "id": "codwaw", "name": "Call of Duty: World at War" },
|
||||
{ "id": "coj", "name": "Call of Juarez" },
|
||||
{ "id": "chaser", "name": "Chaser" },
|
||||
{ "id": "cmw", "name": "Chivalry: Medieval Warfare", "isValve": true },
|
||||
{ "id": "chrome", "name": "Chrome" },
|
||||
{ "id": "codenamecure", "name": "Codename CURE", "isValve": true },
|
||||
{ "id": "codenameeagle", "name": "Codename Eagle" },
|
||||
{ "id": "colonysurvival", "name": "Colony Survival", "isValve": true },
|
||||
{ "id": "cacr", "name": "Command and Conquer: Renegade" },
|
||||
{ "id": "c3db", "name": "Commandos 3: Destination Berlin" },
|
||||
{ "id": "conanexiles", "name": "Conan Exiles", "isValve": true },
|
||||
{ "id": "contagion", "name": "Contagion", "isValve": true },
|
||||
{ "id": "contractjack", "name": "Contract J.A.C.K." },
|
||||
{ "id": "corekeeper", "name": "Core Keeper", "isValve": true },
|
||||
{ "id": "counterstrike15", "name": "Counter-Strike 1.5" },
|
||||
{ "id": "counterstrike16", "name": "Counter-Strike 1.6", "isValve": true },
|
||||
{ "id": "counterstrike2", "name": "Counter-Strike 2", "isValve": true },
|
||||
{ "id": "cscz", "name": "Counter-Strike: Condition Zero", "isValve": true },
|
||||
{ "id": "csgo", "name": "Counter-Strike: Global Offensive", "isValve": true },
|
||||
{ "id": "css", "name": "Counter-Strike: Source", "isValve": true },
|
||||
{ "id": "creativerse", "name": "Creativerse", "isValve": true },
|
||||
{ "id": "crce", "name": "Cross Racing Championship Extreme" },
|
||||
{ "id": "crysis", "name": "Crysis" },
|
||||
{ "id": "crysis2", "name": "Crysis 2" },
|
||||
{ "id": "crysiswars", "name": "Crysis Wars" },
|
||||
{ "id": "daikatana", "name": "Daikatana" },
|
||||
{ "id": "dmomam", "name": "Dark Messiah of Might and Magic", "isValve": true },
|
||||
{ "id": "dal", "name": "Dark and Light", "isValve": true },
|
||||
{ "id": "dhe4445", "name": "Darkest Hour: Europe '44-'45" },
|
||||
{ "id": "dod", "name": "Day of Defeat", "isValve": true },
|
||||
{ "id": "dods", "name": "Day of Defeat: Source", "isValve": true },
|
||||
{ "id": "dayofdragons", "name": "Day of Dragons", "isValve": true },
|
||||
{ "id": "doi", "name": "Day of Infamy", "isValve": true },
|
||||
{ "id": "dayz", "name": "DayZ", "isValve": true },
|
||||
{ "id": "dayzmod", "name": "DayZ Mod", "isValve": true },
|
||||
{ "id": "dow", "name": "Days of War", "isValve": true },
|
||||
{ "id": "ddpt", "name": "Deadly Dozen: Pacific Theater" },
|
||||
{ "id": "deathmatchclassic", "name": "Deathmatch Classic", "isValve": true },
|
||||
{ "id": "deerhunter2005", "name": "Deer Hunter 2005" },
|
||||
{ "id": "descent3", "name": "Descent 3" },
|
||||
{ "id": "deusex", "name": "Deus Ex" },
|
||||
{ "id": "devastation", "name": "Devastation" },
|
||||
{ "id": "ddd", "name": "Dino D-Day", "isValve": true },
|
||||
{ "id": "dtr2", "name": "Dirt Track Racing 2" },
|
||||
{ "id": "discord", "name": "Discord" },
|
||||
{ "id": "dst", "name": "Don't Starve Together", "isValve": true },
|
||||
{ "id": "doom3", "name": "Doom 3" },
|
||||
{ "id": "dota2", "name": "Dota 2", "isValve": true },
|
||||
{ "id": "dab", "name": "Double Action: Boogaloo", "isValve": true },
|
||||
{ "id": "dootf", "name": "Drakan: Order of the Flame" },
|
||||
{ "id": "dnf2001", "name": "Duke Nukem Forever 2001" },
|
||||
{ "id": "dystopia", "name": "Dystopia", "isValve": true },
|
||||
{ "id": "eco", "name": "Eco" },
|
||||
{ "id": "empiresmod", "name": "Empires Mod", "isValve": true },
|
||||
{ "id": "egs", "name": "Empyrion - Galactic Survival", "isValve": true },
|
||||
{ "id": "etqw", "name": "Enemy Territory: Quake Wars" },
|
||||
{ "id": "ets2", "name": "Euro Truck Simulator 2", "isValve": true },
|
||||
{ "id": "exfil", "name": "Exfil", "isValve": true },
|
||||
{ "id": "fear", "name": "F.E.A.R." },
|
||||
{ "id": "f1c9902", "name": "F1 Challenge '99-'02" },
|
||||
{ "id": "foundry", "name": "FOUNDRY", "isValve": true },
|
||||
{ "id": "factorio", "name": "Factorio" },
|
||||
{ "id": "farcry", "name": "Far Cry" },
|
||||
{ "id": "farcry2", "name": "Far Cry 2" },
|
||||
{ "id": "farmingsimulator19", "name": "Farming Simulator 19" },
|
||||
{ "id": "farmingsimulator22", "name": "Farming Simulator 22" },
|
||||
{ "id": "farmingsimulator25", "name": "Farming Simulator 25" },
|
||||
{ "id": "fof", "name": "Fistful of Frags", "isValve": true },
|
||||
{ "id": "formulaone2002", "name": "Formula One 2002" },
|
||||
{ "id": "fortressforever", "name": "Fortress Forever", "isValve": true },
|
||||
{ "id": "ffow", "name": "Frontlines: Fuel of War" },
|
||||
{ "id": "garrysmod", "name": "Garry's Mod", "isValve": true },
|
||||
{ "id": "geneshift", "name": "Geneshift" },
|
||||
{ "id": "gck", "name": "Giants: Citizen Kabuto" },
|
||||
{ "id": "globaloperations", "name": "Global Operations" },
|
||||
{ "id": "goldeneyesource", "name": "GoldenEye: Source", "isValve": true },
|
||||
{ "id": "gus", "name": "Gore: Ultimate Soldier" },
|
||||
{ "id": "gta5f", "name": "Grand Theft Auto V - FiveM" },
|
||||
{ "id": "gta5r", "name": "Grand Theft Auto V - RageMP" },
|
||||
{ "id": "gta5am", "name": "Grand Theft Auto V - alt:V Multiplayer" },
|
||||
{ "id": "gtasamta", "name": "Grand Theft Auto: San Andreas - Multi Theft Auto" },
|
||||
{ "id": "gtasam", "name": "Grand Theft Auto: San Andreas Multiplayer" },
|
||||
{ "id": "gtasao", "name": "Grand Theft Auto: San Andreas OpenMP" },
|
||||
{ "id": "gtavcmta", "name": "Grand Theft Auto: Vice City - Multi Theft Auto" },
|
||||
{ "id": "groundbreach", "name": "Ground Breach", "isValve": true },
|
||||
{ "id": "gunmanchronicles", "name": "Gunman Chronicles", "isValve": true },
|
||||
{ "id": "hl2d", "name": "Half-Life 2: Deathmatch", "isValve": true },
|
||||
{ "id": "hld", "name": "Half-Life Deathmatch", "isValve": true },
|
||||
{ "id": "hlds", "name": "Half-Life Deathmatch: Source", "isValve": true },
|
||||
{ "id": "hlof", "name": "Half-Life: Opposing Force", "isValve": true },
|
||||
{ "id": "halo", "name": "Halo" },
|
||||
{ "id": "halo2", "name": "Halo 2" },
|
||||
{ "id": "eldewrito", "name": "Halo Online - ElDewrito" },
|
||||
{ "id": "hawakening", "name": "Hawakening" },
|
||||
{ "id": "hll", "name": "Hell Let Loose", "isValve": true },
|
||||
{ "id": "heretic2", "name": "Heretic II" },
|
||||
{ "id": "hexen2", "name": "Hexen II" },
|
||||
{ "id": "hiddendangerous2", "name": "Hidden & Dangerous 2" },
|
||||
{ "id": "homefront", "name": "Homefront", "isValve": true },
|
||||
{ "id": "homeworld2", "name": "Homeworld 2" },
|
||||
{ "id": "hurtworld", "name": "Hurtworld", "isValve": true },
|
||||
{ "id": "i2cs", "name": "IGI 2: Covert Strike" },
|
||||
{ "id": "i2s", "name": "IL-2 Sturmovik" },
|
||||
{ "id": "icarus", "name": "Icarus", "isValve": true },
|
||||
{ "id": "insurgency", "name": "Insurgency", "isValve": true },
|
||||
{ "id": "imic", "name": "Insurgency: Modern Infantry Combat", "isValve": true },
|
||||
{ "id": "insurgencysandstorm", "name": "Insurgency: Sandstorm", "isValve": true },
|
||||
{ "id": "ironstorm", "name": "Iron Storm" },
|
||||
{ "id": "jb0n", "name": "James Bond 007: Nightfire" },
|
||||
{ "id": "jc2m", "name": "Just Cause 2 - Multiplayer" },
|
||||
{ "id": "jc3m", "name": "Just Cause 3 - Multiplayer", "isValve": true },
|
||||
{ "id": "kspd", "name": "Kerbal Space Program - DMP" },
|
||||
{ "id": "killingfloor", "name": "Killing Floor" },
|
||||
{ "id": "killingfloor2", "name": "Killing Floor 2", "isValve": true },
|
||||
{ "id": "kloc", "name": "Kingpin: Life of Crime" },
|
||||
{ "id": "kpctnc", "name": "Kiss: Psycho Circus: The Nightmare Child" },
|
||||
{ "id": "kreedzclimbing", "name": "Kreedz Climbing", "isValve": true },
|
||||
{ "id": "l4d", "name": "Left 4 Dead", "isValve": true },
|
||||
{ "id": "l4d2", "name": "Left 4 Dead 2", "isValve": true },
|
||||
{ "id": "m2m", "name": "Mafia II - Multiplayer" },
|
||||
{ "id": "m2o", "name": "Mafia II - Online" },
|
||||
{ "id": "moh", "name": "Medal of Honor" },
|
||||
{ "id": "moha", "name": "Medal of Honor: Airborne" },
|
||||
{ "id": "mohaa", "name": "Medal of Honor: Allied Assault" },
|
||||
{ "id": "mohaab", "name": "Medal of Honor: Allied Assault Breakthrough" },
|
||||
{ "id": "mohaas", "name": "Medal of Honor: Allied Assault Spearhead" },
|
||||
{ "id": "mohpa", "name": "Medal of Honor: Pacific Assault" },
|
||||
{ "id": "mohw", "name": "Medal of Honor: Warfighter" },
|
||||
{ "id": "medievalengineers", "name": "Medieval Engineers", "isValve": true },
|
||||
{ "id": "minecraft", "name": "Minecraft" },
|
||||
{ "id": "mbe", "name": "Minecraft: Bedrock Edition" },
|
||||
{ "id": "minetest", "name": "Minetest" },
|
||||
{ "id": "mnc", "name": "Monday Night Combat", "isValve": true },
|
||||
{ "id": "mordhau", "name": "Mordhau", "isValve": true },
|
||||
{ "id": "mumble", "name": "Mumble" },
|
||||
{ "id": "mgm", "name": "Mumble - GT Murmur" },
|
||||
{ "id": "mutantfactions", "name": "Mutant Factions" },
|
||||
{ "id": "moe", "name": "Myth of Empires", "isValve": true },
|
||||
{ "id": "nascarthunder2004", "name": "NASCAR Thunder 2004" },
|
||||
{ "id": "naturalselection", "name": "Natural Selection", "isValve": true },
|
||||
{ "id": "naturalselection2", "name": "Natural Selection 2", "isValve": true },
|
||||
{ "id": "nfshp2", "name": "Need for Speed: Hot Pursuit 2" },
|
||||
{ "id": "nab", "name": "Nerf Arena Blast" },
|
||||
{ "id": "neverwinternights", "name": "Neverwinter Nights" },
|
||||
{ "id": "neverwinternights2", "name": "Neverwinter Nights 2" },
|
||||
{ "id": "nexuiz", "name": "Nexuiz" },
|
||||
{ "id": "nitrofamily", "name": "Nitro Family" },
|
||||
{ "id": "nmrih", "name": "No More Room in Hell", "isValve": true },
|
||||
{ "id": "nolf2asihw", "name": "No One Lives Forever 2: A Spy in H.A.R.M.'s Way" },
|
||||
{ "id": "nla", "name": "Nova-Life: Amboise", "isValve": true },
|
||||
{ "id": "nucleardawn", "name": "Nuclear Dawn", "isValve": true },
|
||||
{ "id": "onset", "name": "Onset", "isValve": true },
|
||||
{ "id": "openarena", "name": "OpenArena" },
|
||||
{ "id": "openttd", "name": "OpenTTD" },
|
||||
{ "id": "ofcwc", "name": "Operation Flashpoint: Cold War Crisis" },
|
||||
{ "id": "ofr", "name": "Operation Flashpoint: Resistance" },
|
||||
{ "id": "ohd", "name": "Operation: Harsh Doorstop", "isValve": true },
|
||||
{ "id": "painkiller", "name": "Painkiller" },
|
||||
{ "id": "palworld", "name": "Palworld" },
|
||||
{ "id": "pvak2", "name": "Pirates, Vikings, and Knights II", "isValve": true },
|
||||
{ "id": "pixark", "name": "PixARK", "isValve": true },
|
||||
{ "id": "postscriptum", "name": "Post Scriptum", "isValve": true },
|
||||
{ "id": "postal2", "name": "Postal 2" },
|
||||
{ "id": "prey", "name": "Prey" },
|
||||
{ "id": "pce", "name": "Primal Carnage: Extinction", "isValve": true },
|
||||
{ "id": "projectcars", "name": "Project Cars", "isValve": true },
|
||||
{ "id": "projectcars2", "name": "Project Cars 2", "isValve": true },
|
||||
{ "id": "prb2", "name": "Project Reality: Battlefield 2" },
|
||||
{ "id": "projectzomboid", "name": "Project Zomboid", "isValve": true },
|
||||
{ "id": "quake", "name": "Quake" },
|
||||
{ "id": "quake2", "name": "Quake 2" },
|
||||
{ "id": "q3a", "name": "Quake 3: Arena" },
|
||||
{ "id": "quake4", "name": "Quake 4" },
|
||||
{ "id": "quakelive", "name": "Quake Live", "isValve": true },
|
||||
{ "id": "rdkf", "name": "Rag Doll Kung Fu", "isValve": true },
|
||||
{ "id": "rainbowsix", "name": "Rainbow Six" },
|
||||
{ "id": "rs2rs", "name": "Rainbow Six 2: Rogue Spear" },
|
||||
{ "id": "rs3rs", "name": "Rainbow Six 3: Raven Shield" },
|
||||
{ "id": "rallisportchallenge", "name": "RalliSport Challenge" },
|
||||
{ "id": "rallymasters", "name": "Rally Masters" },
|
||||
{ "id": "rdr2r", "name": "Red Dead Redemption 2 - RedM" },
|
||||
{ "id": "redorchestra", "name": "Red Orchestra" },
|
||||
{ "id": "redorchestra2", "name": "Red Orchestra 2", "isValve": true },
|
||||
{ "id": "roo4145", "name": "Red Orchestra: Ostfront 41-45" },
|
||||
{ "id": "redline", "name": "Redline" },
|
||||
{ "id": "renegade10", "name": "Renegade X" },
|
||||
{ "id": "renown", "name": "Renown" },
|
||||
{ "id": "rtcw", "name": "Return to Castle Wolfenstein" },
|
||||
{ "id": "ricochet", "name": "Ricochet", "isValve": true },
|
||||
{ "id": "ron", "name": "Rise of Nations" },
|
||||
{ "id": "rs2v", "name": "Rising Storm 2: Vietnam", "isValve": true },
|
||||
{ "id": "risingworld", "name": "Rising World", "isValve": true },
|
||||
{ "id": "ror2", "name": "Risk of Rain 2", "isValve": true },
|
||||
{ "id": "rune", "name": "Rune" },
|
||||
{ "id": "rust", "name": "Rust", "isValve": true },
|
||||
{ "id": "stalker", "name": "S.T.A.L.K.E.R." },
|
||||
{ "id": "swat4", "name": "SWAT 4" },
|
||||
{ "id": "satisfactory", "name": "Satisfactory" },
|
||||
{ "id": "s2ats", "name": "Savage 2: A Tortured Soul" },
|
||||
{ "id": "serioussam", "name": "Serious Sam" },
|
||||
{ "id": "serioussam2", "name": "Serious Sam 2" },
|
||||
{ "id": "sstse", "name": "Serious Sam: The Second Encounter" },
|
||||
{ "id": "shatteredhorizon", "name": "Shattered Horizon", "isValve": true },
|
||||
{ "id": "shogo", "name": "Shogo" },
|
||||
{ "id": "shootmania", "name": "Shootmania" },
|
||||
{ "id": "sin", "name": "SiN" },
|
||||
{ "id": "sinepisodes", "name": "SiN Episodes", "isValve": true },
|
||||
{ "id": "soldat", "name": "Soldat" },
|
||||
{ "id": "sof", "name": "Soldier of Fortune" },
|
||||
{ "id": "sof2", "name": "Soldier of Fortune 2" },
|
||||
{ "id": "sotf", "name": "Sons Of The Forest", "isValve": true },
|
||||
{ "id": "soulmask", "name": "Soulmask", "isValve": true },
|
||||
{ "id": "spaceengineers", "name": "Space Engineers", "isValve": true },
|
||||
{ "id": "squad", "name": "Squad", "isValve": true },
|
||||
{ "id": "stbc", "name": "Star Trek: Bridge Commander" },
|
||||
{ "id": "stvef", "name": "Star Trek: Voyager - Elite Force" },
|
||||
{ "id": "stvef2", "name": "Star Trek: Voyager - Elite Force 2" },
|
||||
{ "id": "swjk2jo", "name": "Star Wars Jedi Knight II: Jedi Outcast" },
|
||||
{ "id": "swjkja", "name": "Star Wars Jedi Knight: Jedi Academy" },
|
||||
{ "id": "swb", "name": "Star Wars: Battlefront" },
|
||||
{ "id": "swb2", "name": "Star Wars: Battlefront 2" },
|
||||
{ "id": "swrc", "name": "Star Wars: Republic Commando" },
|
||||
{ "id": "starmade", "name": "StarMade" },
|
||||
{ "id": "starbound", "name": "Starbound", "isValve": true },
|
||||
{ "id": "starsiege", "name": "Starsiege" },
|
||||
{ "id": "suicidesurvival", "name": "Suicide Survival", "isValve": true },
|
||||
{ "id": "stn", "name": "Survive the Nights", "isValve": true },
|
||||
{ "id": "svencoop", "name": "Sven Coop", "isValve": true },
|
||||
{ "id": "synergy", "name": "Synergy", "isValve": true },
|
||||
{ "id": "toxikk", "name": "TOXIKK" },
|
||||
{ "id": "tacticalops", "name": "Tactical Ops" },
|
||||
{ "id": "toh", "name": "Take On Helicopters" },
|
||||
{ "id": "teamfactor", "name": "Team Factor" },
|
||||
{ "id": "teamfortress2", "name": "Team Fortress 2", "isValve": true },
|
||||
{ "id": "tfc", "name": "Team Fortress Classic", "isValve": true },
|
||||
{ "id": "teamspeak2", "name": "Teamspeak 2" },
|
||||
{ "id": "teamspeak3", "name": "Teamspeak 3" },
|
||||
{ "id": "terminus", "name": "Terminus" },
|
||||
{ "id": "terrariatshock", "name": "Terraria - TShock" },
|
||||
{ "id": "theforest", "name": "The Forest", "isValve": true },
|
||||
{ "id": "thefront", "name": "The Front", "isValve": true },
|
||||
{ "id": "thehidden", "name": "The Hidden", "isValve": true },
|
||||
{ "id": "theisle", "name": "The Isle", "isValve": true },
|
||||
{ "id": "tie", "name": "The Isle Evrima" },
|
||||
{ "id": "tonolf", "name": "The Operative: No One Lives Forever" },
|
||||
{ "id": "theship", "name": "The Ship", "isValve": true },
|
||||
{ "id": "thespecialists", "name": "The Specialists", "isValve": true },
|
||||
{ "id": "tcgraw", "name": "Tom Clancy's Ghost Recon Advanced Warfighter" },
|
||||
{ "id": "tcgraw2", "name": "Tom Clancy's Ghost Recon Advanced Warfighter 2" },
|
||||
{ "id": "thps3", "name": "Tony Hawk's Pro Skater 3" },
|
||||
{ "id": "thps4", "name": "Tony Hawk's Pro Skater 4" },
|
||||
{ "id": "thu2", "name": "Tony Hawk's Underground 2" },
|
||||
{ "id": "towerunite", "name": "Tower Unite", "isValve": true },
|
||||
{ "id": "trackmania2", "name": "Trackmania 2" },
|
||||
{ "id": "trackmaniaforever", "name": "Trackmania Forever" },
|
||||
{ "id": "tremulous", "name": "Tremulous" },
|
||||
{ "id": "t1s", "name": "Tribes 1: Starsiege" },
|
||||
{ "id": "tribesvengeance", "name": "Tribes: Vengeance" },
|
||||
{ "id": "tron20", "name": "Tron 2.0" },
|
||||
{ "id": "turok2", "name": "Turok 2" },
|
||||
{ "id": "universalcombat", "name": "Universal Combat" },
|
||||
{ "id": "unreal", "name": "Unreal" },
|
||||
{ "id": "u2tax", "name": "Unreal 2: The Awakening - XMP" },
|
||||
{ "id": "unrealtournament", "name": "Unreal Tournament" },
|
||||
{ "id": "unrealtournament2003", "name": "Unreal Tournament 2003" },
|
||||
{ "id": "unrealtournament2004", "name": "Unreal Tournament 2004" },
|
||||
{ "id": "unrealtournament3", "name": "Unreal Tournament 3" },
|
||||
{ "id": "urbanterror", "name": "Urban Terror" },
|
||||
{ "id": "vrising", "name": "V Rising", "isValve": true },
|
||||
{ "id": "v8sc", "name": "V8 Supercar Challenge" },
|
||||
{ "id": "valheim", "name": "Valheim", "isValve": true },
|
||||
{ "id": "vampireslayer", "name": "Vampire Slayer", "isValve": true },
|
||||
{ "id": "ventrilo", "name": "Ventrilo" },
|
||||
{ "id": "vcm", "name": "Vice City Multiplayer" },
|
||||
{ "id": "vietcong", "name": "Vietcong" },
|
||||
{ "id": "vietcong2", "name": "Vietcong 2" },
|
||||
{ "id": "vintagestory", "name": "Vintage Story" },
|
||||
{ "id": "warfork", "name": "Warfork" },
|
||||
{ "id": "warsow", "name": "Warsow" },
|
||||
{ "id": "wot", "name": "Wheel of Time" },
|
||||
{ "id": "wolfenstein", "name": "Wolfenstein" },
|
||||
{ "id": "wet", "name": "Wolfenstein: Enemy Territory" },
|
||||
{ "id": "wop", "name": "World Of Padman" },
|
||||
{ "id": "wurmunlimited", "name": "Wurm Unlimited", "isValve": true },
|
||||
{ "id": "xonotic", "name": "Xonotic" },
|
||||
{ "id": "xpandrally", "name": "Xpand Rally" },
|
||||
{ "id": "zombiemaster", "name": "Zombie Master", "isValve": true },
|
||||
{ "id": "zps", "name": "Zombie Panic: Source", "isValve": true },
|
||||
{ "id": "enshrouded", "name": "enshrouded", "isValve": true },
|
||||
{ "id": "netpanzer", "name": "netPanzer" },
|
||||
{ "id": "rfactor", "name": "rFactor" },
|
||||
{ "id": "rfactor2", "name": "rFactor 2", "isValve": true },
|
||||
{ "id": "unturned", "name": "unturned", "isValve": true }
|
||||
]
|
||||
@@ -1,13 +0,0 @@
|
||||
// @ts-ignore
|
||||
export const analyticsEvent = (event, data) => {
|
||||
// Do something with the event and data
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("analyticsEvent", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
event,
|
||||
data
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
export const DiscordJSONTemplate = JSON.stringify(
|
||||
{
|
||||
username: "{{site_name}}",
|
||||
avatar_url: "{{{logo_url}}}",
|
||||
content:
|
||||
"## {{alert_name}}\n{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}\n{{description}}\nClick [{{action_text}}]({{{action_url}}}) for more.",
|
||||
embeds: [
|
||||
{
|
||||
title: "{{alert_name}}",
|
||||
description: "{{description}}",
|
||||
url: "{{{action_url}}}",
|
||||
color: "{{#is_triggered}}13250616{{/is_triggered}}{{#is_resolved}}5156244{{/is_resolved}}",
|
||||
fields: [
|
||||
{
|
||||
name: "Monitor",
|
||||
value: "{{metric}}",
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Severity",
|
||||
value: "{{severity}}",
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Alert ID",
|
||||
value: "{{id}}",
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Current Value",
|
||||
value: "{{current_value}}",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Threshold",
|
||||
value: "{{threshold}}",
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: "{{source}}",
|
||||
icon_url: "{{{logo_url}}}",
|
||||
},
|
||||
timestamp: "{{timestamp}}",
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
export const WebhookJSONTemplate = 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,
|
||||
);
|
||||
|
||||
export const SlackJSONTemplate = JSON.stringify(
|
||||
{
|
||||
blocks: [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "{{alert_name}}",
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}",
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: "{{description}}\n*Source:* {{source}}\n*Severity:* {{severity}}\n*Status:* {{status}}",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
fields: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "*Metric:*\n{{metric}}",
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "*Current Value:*\n{{current_value}}",
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "*Threshold:*\n{{threshold}}",
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "*Timestamp:*\n<!date^{{timestamp_unix}}^{date} at {time}|{{timestamp}}>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "{{action_text}}",
|
||||
},
|
||||
url: "{{{action_url}}}",
|
||||
style: "primary",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
export const DefaultAPIEval = `(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
return {
|
||||
status: 'UP',
|
||||
latency: responseTime,
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})`;
|
||||
|
||||
export const DefaultPingEval = `(async function (arrayOfPings) {
|
||||
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,
|
||||
}
|
||||
})`;
|
||||
|
||||
export const DefaultTCPEval = `(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.status === "open";
|
||||
}, true);
|
||||
|
||||
return {
|
||||
status: alive ? 'UP' : 'DOWN',
|
||||
latency: latencyTotal / arrayOfPings.length,
|
||||
}
|
||||
})`;
|
||||
|
||||
export const DefaultGamedigEval = `(async function (responseTime, responseRaw) {
|
||||
return {
|
||||
status: 'UP',
|
||||
latency: responseTime,
|
||||
}
|
||||
})`;
|
||||
export const GAMEDIG_TIMEOUT = 10 * 1000; // 10 seconds
|
||||
export const GAMEDIG_SOCKET_TIMEOUT = 2 * 1000; // 2 seconds
|
||||
|
||||
export const ErrorSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="60" viewBox="0 0 120 60" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="30" cy="24" r="10"/>
|
||||
<path d="M26 27h8"/>
|
||||
<path d="M26 21h2"/>
|
||||
<path d="M32 21h2"/>
|
||||
<text x="80" y="29" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" fill="currentColor" font-weight="300">Not Found</text>
|
||||
</svg>`;
|
||||
@@ -0,0 +1,17 @@
|
||||
// @ts-ignore
|
||||
export const analyticsEvent = (event, data) => {
|
||||
// Do something with the event and data
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
event = "kener_" + event;
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("analyticsEvent", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
event,
|
||||
data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,306 @@
|
||||
// @ts-nocheck
|
||||
import { base } from "$app/paths";
|
||||
function siteDataExtractFromDb(data, obj) {
|
||||
let requestedObject = { ...obj };
|
||||
for (const key in requestedObject) {
|
||||
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
|
||||
const element = data[key];
|
||||
if (data[key]) {
|
||||
requestedObject[key] = data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
//remove any keys that are still null or empty
|
||||
for (const key in requestedObject) {
|
||||
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
|
||||
const element = requestedObject[key];
|
||||
if (element === null || element === "") {
|
||||
delete requestedObject[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return requestedObject;
|
||||
}
|
||||
|
||||
//a function to make an api call to /manage/api/ to store site data
|
||||
function storeSiteData(data) {
|
||||
return fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ action: "storeSiteData", data }),
|
||||
});
|
||||
}
|
||||
const allRecordTypes = {
|
||||
A: 1,
|
||||
NS: 2,
|
||||
MD: 3,
|
||||
MF: 4,
|
||||
CNAME: 5,
|
||||
SOA: 6,
|
||||
MB: 7,
|
||||
MG: 8,
|
||||
MR: 9,
|
||||
NULL: 10,
|
||||
WKS: 11,
|
||||
PTR: 12,
|
||||
HINFO: 13,
|
||||
MINFO: 14,
|
||||
MX: 15,
|
||||
TXT: 16,
|
||||
RP: 17,
|
||||
AFSDB: 18,
|
||||
X25: 19,
|
||||
ISDN: 20,
|
||||
RT: 21,
|
||||
NSAP: 22,
|
||||
NSAP_PTR: 23,
|
||||
SIG: 24,
|
||||
KEY: 25,
|
||||
PX: 26,
|
||||
GPOS: 27,
|
||||
AAAA: 28,
|
||||
LOC: 29,
|
||||
NXT: 30,
|
||||
EID: 31,
|
||||
NIMLOC: 32,
|
||||
SRV: 33,
|
||||
ATMA: 34,
|
||||
NAPTR: 35,
|
||||
KX: 36,
|
||||
CERT: 37,
|
||||
A6: 38,
|
||||
DNAME: 39,
|
||||
SINK: 40,
|
||||
OPT: 41,
|
||||
APL: 42,
|
||||
DS: 43,
|
||||
SSHFP: 44,
|
||||
IPSECKEY: 45,
|
||||
RRSIG: 46,
|
||||
NSEC: 47,
|
||||
DNSKEY: 48,
|
||||
DHCID: 49,
|
||||
NSEC3: 50,
|
||||
NSEC3PARAM: 51,
|
||||
TLSA: 52,
|
||||
SMIMEA: 53,
|
||||
HIP: 55,
|
||||
NINFO: 56,
|
||||
RKEY: 57,
|
||||
TALINK: 58,
|
||||
CDS: 59,
|
||||
CDNSKEY: 60,
|
||||
OPENPGPKEY: 61,
|
||||
CSYNC: 62,
|
||||
SPF: 99,
|
||||
UINFO: 100,
|
||||
UID: 101,
|
||||
GID: 102,
|
||||
UNSPEC: 103,
|
||||
NID: 104,
|
||||
L32: 105,
|
||||
L64: 106,
|
||||
LP: 107,
|
||||
EUI48: 108,
|
||||
EUI64: 109,
|
||||
TKEY: 249,
|
||||
TSIG: 250,
|
||||
IXFR: 251,
|
||||
AXFR: 252,
|
||||
MAILB: 253,
|
||||
MAILA: 254,
|
||||
ANY: 255,
|
||||
};
|
||||
const ValidateIpAddress = function (input) {
|
||||
// Check if input is a valid IPv4 address with an optional port
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(input)) {
|
||||
return "IP4";
|
||||
}
|
||||
|
||||
// Improved IPv6 regex that better handles compressed notation
|
||||
const ipv6Regex =
|
||||
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){0,7}:|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){6}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|(?:[0-9a-fA-F]{1,4}:){1,7}(?::|:[0-9a-fA-F]{1,4})|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$/;
|
||||
if (ipv6Regex.test(input)) {
|
||||
return "IP6";
|
||||
}
|
||||
|
||||
// Check if input is a valid domain name with an optional port
|
||||
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
if (domainRegex.test(input)) {
|
||||
return "DOMAIN";
|
||||
}
|
||||
|
||||
// If none of the above conditions match, the input is invalid
|
||||
return "Invalid";
|
||||
};
|
||||
|
||||
function IsValidHost(domain) {
|
||||
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
return regex.test(domain);
|
||||
}
|
||||
function IsValidNameServer(nameServer) {
|
||||
//8.8.8.8 example
|
||||
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
return regex.test(nameServer);
|
||||
}
|
||||
const IsValidURL = function (url) {
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
};
|
||||
function ValidateCronExpression(cronExp) {
|
||||
// Check if expression is provided and is a string
|
||||
if (!cronExp || typeof cronExp !== "string") {
|
||||
return { isValid: false, message: "Cron expression must be a non-empty string" };
|
||||
}
|
||||
|
||||
// Split the expression into its components
|
||||
const fields = cronExp.trim().split(/\s+/);
|
||||
|
||||
// Standard cron should have 5 or 6 fields
|
||||
// minute hour day-of-month month day-of-week [year]
|
||||
if (fields.length < 5 || fields.length > 6) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "Cron expression must have 5 or 6 fields",
|
||||
};
|
||||
}
|
||||
|
||||
// Define field constraints
|
||||
const fieldConstraints = [
|
||||
{ name: "minute", min: 0, max: 59 },
|
||||
{ name: "hour", min: 0, max: 23 },
|
||||
{ name: "day", min: 1, max: 31 },
|
||||
{ name: "month", min: 1, max: 12 },
|
||||
{ name: "weekday", min: 0, max: 6 },
|
||||
{ name: "year", min: 1970, max: 2099 }, // Optional field
|
||||
];
|
||||
|
||||
// Valid characters in cron expressions
|
||||
const validChars = /^[\d/*,\-]+$/;
|
||||
|
||||
// Validate each field
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i];
|
||||
const constraint = fieldConstraints[i];
|
||||
|
||||
// Check for valid characters
|
||||
if (!validChars.test(field)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid characters in ${constraint.name} field`,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle special characters
|
||||
if (field === "*") {
|
||||
continue; // Asterisk is valid for all fields
|
||||
}
|
||||
|
||||
// Handle lists (comma-separated values)
|
||||
if (field.includes(",")) {
|
||||
const values = field.split(",");
|
||||
for (const value of values) {
|
||||
if (!isValidRange(value, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${value}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle ranges (with hyphens)
|
||||
if (field.includes("-")) {
|
||||
const [start, end] = field.split("-").map(Number);
|
||||
if (start == null || end == null || start < constraint.min || end > constraint.max || start > end) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${field}`,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle steps (with forward slash)
|
||||
if (field.includes("/")) {
|
||||
const [range, step] = field.split("/");
|
||||
if (range !== "*" && !isValidRange(range, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${range}`,
|
||||
};
|
||||
}
|
||||
if (!isValidRange(step, 1, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid step value in ${constraint.name} field: ${step}`,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle plain numbers
|
||||
if (!isValidRange(field, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${field}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, message: "Valid cron expression" };
|
||||
}
|
||||
|
||||
function isValidRange(value, min, max) {
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && num >= min && num <= max;
|
||||
}
|
||||
function IsValidPort(port) {
|
||||
return isValidRange(port, 1, 65535);
|
||||
}
|
||||
|
||||
function SortMonitor(monitorSort, resp) {
|
||||
let monitors = [];
|
||||
if (!!monitorSort && monitorSort.length > 0) {
|
||||
monitors = monitorSort.map((id) => resp.find((m) => m.id == id)).filter((m) => !!m);
|
||||
//append any new monitors
|
||||
monitors = [...monitors, ...resp.filter((m) => !monitorSort.includes(m.id))];
|
||||
} else {
|
||||
monitors = resp;
|
||||
}
|
||||
return monitors;
|
||||
}
|
||||
//js function to generate 32 character random string
|
||||
function RandomString(length) {
|
||||
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let result = "";
|
||||
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive game's data from its id.
|
||||
* @param {string} id Id
|
||||
* @return Returns game's data if found, undefined instead.
|
||||
*/
|
||||
function GetGameFromId(list, id) {
|
||||
return list.find((game) => game.id === id);
|
||||
}
|
||||
|
||||
export {
|
||||
siteDataExtractFromDb,
|
||||
storeSiteData,
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
IsValidHost,
|
||||
IsValidNameServer,
|
||||
IsValidURL,
|
||||
IsValidPort,
|
||||
ValidateCronExpression,
|
||||
SortMonitor,
|
||||
RandomString,
|
||||
GetGameFromId,
|
||||
};
|
||||
+11
-9
@@ -1,13 +1,15 @@
|
||||
// @ts-nocheck
|
||||
import { siteStore } from "./server/stores/site.js";
|
||||
import { get } from "svelte/store";
|
||||
const site = get(siteStore);
|
||||
import { GetAllSiteData } from "$lib/server/controllers/controller.js";
|
||||
|
||||
const StatusColor = {
|
||||
UP: site.colors?.UP || "#00dfa2",
|
||||
DEGRADED: site.colors?.DEGRADED || "#e6ca61",
|
||||
DOWN: site.colors?.DOWN || "#ca3038",
|
||||
NO_DATA: "#f1f5f8"
|
||||
};
|
||||
async function StatusColor(status) {
|
||||
let site = await GetAllSiteData();
|
||||
return {
|
||||
UP: site.colors?.UP || "#00dfa2",
|
||||
DEGRADED: site.colors?.DEGRADED || "#e6ca61",
|
||||
DOWN: site.colors?.DOWN || "#ca3038",
|
||||
MAINTENANCE: site.colors?.MAINTENANCE || "#6679cc",
|
||||
NO_DATA: "#f1f5f8"
|
||||
};
|
||||
}
|
||||
|
||||
export default StatusColor;
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
<script>
|
||||
import { formatDistanceToNow, formatDistance } from "date-fns";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
import Copy from "lucide-svelte/icons/copy";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import * as Accordion from "$lib/components/ui/accordion";
|
||||
import { l, f, fd, fdn } from "$lib/i18n/client";
|
||||
import { base } from "$app/paths";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { marked } from "marked";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let incident;
|
||||
export let index;
|
||||
export let allowCollapse = true;
|
||||
let startTime = new Date(incident.start_date_time * 1000);
|
||||
let endTime = new Date();
|
||||
let nowTime = new Date();
|
||||
if (incident.end_date_time) {
|
||||
endTime = new Date(incident.end_date_time * 1000);
|
||||
}
|
||||
let incidentType = incident.incident_type;
|
||||
const lastedFor = fd(startTime, endTime, $page.data.selectedLang);
|
||||
const remainingTime = fd(nowTime, endTime, $page.data.selectedLang);
|
||||
const startedAt = fdn(startTime, $page.data.selectedLang);
|
||||
|
||||
let incidentTimeStatus = "";
|
||||
if (nowTime < startTime) {
|
||||
incidentTimeStatus = "YET_TO_START";
|
||||
} else if (nowTime >= startTime && nowTime <= endTime) {
|
||||
incidentTimeStatus = "ONGOING";
|
||||
} else if (nowTime > endTime) {
|
||||
incidentTimeStatus = "COMPLETED";
|
||||
}
|
||||
|
||||
let accordionValue = "incident-0";
|
||||
if ($page.data.site.incidentGroupView == "COLLAPSED" && allowCollapse) {
|
||||
accordionValue = "incident-collapse";
|
||||
} else if ($page.data.site.incidentGroupView == "EXPANDED") {
|
||||
accordionValue = index;
|
||||
}
|
||||
|
||||
let incidentDateSummary = "";
|
||||
let maintenanceBadge = "";
|
||||
let maintenanceBadgeColor = "";
|
||||
|
||||
if (incidentTimeStatus == "YET_TO_START") {
|
||||
incidentDateSummary = l($page.data.lang, "Starts %startedAt", { startedAt });
|
||||
if (incidentType === "MAINTENANCE") {
|
||||
incidentDateSummary = l($page.data.lang, "Starts %startedAt, will last for %lastedFor", {
|
||||
startedAt,
|
||||
lastedFor
|
||||
});
|
||||
maintenanceBadge = "Upcoming Maintenance";
|
||||
maintenanceBadgeColor = "text-upcoming-maintenance";
|
||||
}
|
||||
} else if (incidentTimeStatus == "ONGOING") {
|
||||
incidentDateSummary = l($page.data.lang, "Started %startedAt, still ongoing", {
|
||||
startedAt
|
||||
});
|
||||
if (incidentType === "MAINTENANCE") {
|
||||
incidentDateSummary = l($page.data.lang, "Started %startedAt, will last for %lastedFor more", {
|
||||
startedAt,
|
||||
lastedFor: remainingTime
|
||||
});
|
||||
maintenanceBadge = "Maintenance in Progress";
|
||||
maintenanceBadgeColor = "text-maintenance-in-progress";
|
||||
}
|
||||
} else if (incidentTimeStatus == "COMPLETED") {
|
||||
incidentDateSummary = l($page.data.lang, "Started %startedAt, lasted for %lastedFor", {
|
||||
startedAt,
|
||||
lastedFor
|
||||
});
|
||||
maintenanceBadge = "Maintenance Completed";
|
||||
maintenanceBadgeColor = "text-maintenance-completed";
|
||||
}
|
||||
let pathMonitorLink = "";
|
||||
onMount(async () => {
|
||||
let protocol = window.location.protocol;
|
||||
let domain = window.location.host;
|
||||
pathMonitorLink = `${protocol}//${domain}${base}/view/events/${incident.incident_type}-${incident.id}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="newincident relative grid w-full grid-cols-12 gap-2 px-0 py-0 last:border-b-0">
|
||||
<div class="col-span-12">
|
||||
<Accordion.Root
|
||||
bind:value={index}
|
||||
class="accor {allowCollapse === false ? 'hide-chevron' : ''}"
|
||||
disabled={!allowCollapse}
|
||||
>
|
||||
<Accordion.Item value={accordionValue}>
|
||||
<Accordion.Trigger
|
||||
class="rounded-md px-4 hover:bg-muted hover:no-underline"
|
||||
on:click={() => analyticsEvent("incident_open", { incident_title: incident.title })}
|
||||
>
|
||||
<div class="w-full text-left hover:no-underline">
|
||||
<div class="flex justify-start gap-x-2">
|
||||
<p class="flex gap-x-2 text-xs font-semibold">
|
||||
{#if incidentType == "INCIDENT"}
|
||||
<span class="badge-{incident.state}">
|
||||
{l($page.data.lang, incident.state)}
|
||||
</span>
|
||||
{:else if incidentType == "MAINTENANCE"}
|
||||
<span class="{maintenanceBadgeColor} ">
|
||||
{l($page.data.lang, maintenanceBadge)}
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
<div class="flex justify-end gap-x-2">
|
||||
{#if $page.data.isLoggedIn}
|
||||
<Button
|
||||
href="{base}/manage/app/events#{incident.id}"
|
||||
class="rotate-once h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
variant="link"
|
||||
>
|
||||
<Settings class="h-4 w-4 " />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !!pathMonitorLink}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="link"
|
||||
class="copybtn relative h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(pathMonitorLink);
|
||||
analyticsEvent("incident_copy_link", { incident_title: incident.title });
|
||||
}}
|
||||
>
|
||||
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
|
||||
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="font-medium">
|
||||
{incident.title}
|
||||
</p>
|
||||
{#if !!incidentDateSummary}
|
||||
<Tooltip.Root openDelay={100} side="bottom" align="end">
|
||||
<Tooltip.Trigger
|
||||
class=" text-ellipsis whitespace-nowrap text-xs font-medium tracking-normal text-muted-foreground"
|
||||
>
|
||||
{incidentDateSummary}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class=" z-20 mt-11" side="bottom" align="end">
|
||||
<div
|
||||
class="items-center justify-center rounded border bg-primary px-1.5 py-1 text-xs font-medium text-primary-foreground shadow-popover"
|
||||
>
|
||||
{f(
|
||||
new Date(incident.start_date_time * 1000),
|
||||
"MMMM do yyyy, h:mm:ss a",
|
||||
$page.data.selectedLang,
|
||||
$page.data.localTz
|
||||
)}
|
||||
{#if incident.end_date_time}
|
||||
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
|
||||
{f(
|
||||
new Date(incident.end_date_time * 1000),
|
||||
"MMMM do yyyy, h:mm:ss a",
|
||||
$page.data.selectedLang,
|
||||
$page.data.localTz
|
||||
)}
|
||||
{:else}
|
||||
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
|
||||
<span class="dots-animation inline-block w-6 text-left"></span>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
<div class="px-4 pt-2">
|
||||
{#if incident.monitors.length > 0}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each incident.monitors as monitor}
|
||||
<div class="tag-affected-text flex gap-x-2 rounded-md bg-secondary px-1 py-1 pr-2">
|
||||
<div
|
||||
class="bg-api-{monitor.impact_type.toLowerCase()} rounded px-1.5 py-1 text-xs font-semibold text-primary-foreground"
|
||||
>
|
||||
{monitor.impact_type}
|
||||
</div>
|
||||
{#if monitor.image}
|
||||
<GMI src={monitor.image} classList="mt-1 h-4 w-4" />
|
||||
{/if}
|
||||
<div class="mt-0.5 font-medium">
|
||||
{monitor.name}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<p class="my-3 text-xs font-semibold uppercase text-muted-foreground">
|
||||
{l($page.data.lang, "Updates")}
|
||||
</p>
|
||||
{#if incident.comments.length > 0}
|
||||
{#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">
|
||||
<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($page.data.lang, comment.state)}
|
||||
</div>
|
||||
|
||||
<time class=" mb-1 text-sm font-medium leading-none text-muted-foreground">
|
||||
{f(
|
||||
new Date(comment.commented_at * 1000),
|
||||
"MMMM do yyyy, h:mm:ss a",
|
||||
$page.data.selectedLang,
|
||||
$page.data.localTz
|
||||
)}
|
||||
</time>
|
||||
|
||||
<div class="mb-4 text-sm font-normal">
|
||||
<div
|
||||
class="kener-md 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"
|
||||
>
|
||||
{@html marked.parse(comment.comment)}
|
||||
</div>
|
||||
</div>
|
||||
</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",
|
||||
$page.data.selectedLang,
|
||||
$page.data.localTz
|
||||
)}
|
||||
</time>
|
||||
|
||||
<div class="mb-2 text-sm font-normal">
|
||||
<div
|
||||
class="kener-md 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"
|
||||
>
|
||||
{@html marked.parse(comment.comment)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-sm font-medium">
|
||||
{l($page.data.lang, "No Updates Yet")}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
import { base } from "$app/paths";
|
||||
export let src = "";
|
||||
export let alt = "GMI";
|
||||
export let classList = "";
|
||||
export let style = "";
|
||||
export let srcset = "";
|
||||
if (!src.startsWith("http")) src = `${base}${src}`;
|
||||
</script>
|
||||
|
||||
<img {src} {alt} class={classList} {style} {srcset} />
|
||||
@@ -1,224 +0,0 @@
|
||||
<script>
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { ChevronDown, Info, CalendarCheck, Hammer } from "lucide-svelte";
|
||||
import * as Collapsible from "$lib/components/ui/collapsible";
|
||||
import { l } from "$lib/i18n/client";
|
||||
import axios from "axios";
|
||||
import { Skeleton } from "$lib/components/ui/skeleton";
|
||||
import { base } from "$app/paths";
|
||||
|
||||
import { tooltipAction } from "svelte-legos";
|
||||
|
||||
export let incident;
|
||||
export let variant = "title+body+comments+monitor";
|
||||
export let state = "open";
|
||||
export let monitor;
|
||||
export let lang;
|
||||
const StatusObj = {
|
||||
UP: "api-up",
|
||||
DEGRADED: "api-degraded",
|
||||
DOWN: "api-down",
|
||||
NO_DATA: "api-nodata"
|
||||
};
|
||||
let blinker = "bg-transparent";
|
||||
let incidentPriority = "";
|
||||
let incidentDuration = 0;
|
||||
if (incident.labels.includes("incident-down")) {
|
||||
blinker = "bg-red-500";
|
||||
incidentPriority = "DOWN";
|
||||
} else if (incident.labels.includes("incident-degraded")) {
|
||||
blinker = "bg-yellow-500";
|
||||
incidentPriority = "DEGRADED";
|
||||
}
|
||||
let incidentState = incident.state;
|
||||
let incidentClosedAt = incident.incident_end_time;
|
||||
let incidentCreatedAt = incident.incident_start_time;
|
||||
let incidentMessage = "";
|
||||
if (!!incidentClosedAt && !!incidentCreatedAt) {
|
||||
//incidentDuration between closed_at and created_at
|
||||
incidentDuration = moment(incidentClosedAt * 1000)
|
||||
.add(1, "minutes")
|
||||
.diff(moment(incidentCreatedAt * 1000), "minutes");
|
||||
} else if (!!incidentCreatedAt) {
|
||||
//incidentDuration between now and created_at
|
||||
incidentDuration = moment().diff(moment(incidentCreatedAt * 1000), "minutes");
|
||||
}
|
||||
|
||||
//find a replace /\[start_datetime:(\d+)\]/ empty in incident.body
|
||||
//find a replace /\[end_datetime:(\d+)\]/ empty in incident.body
|
||||
incident.body = incident.body.replace(/\[start_datetime:(\d+)\]/g, "");
|
||||
incident.body = incident.body.replace(/\[end_datetime:(\d+)\]/g, "");
|
||||
|
||||
//fetch comments
|
||||
incident.comments = [];
|
||||
let commentsLoading = true;
|
||||
|
||||
function getComments() {
|
||||
if (incident.comments.length > 0) return;
|
||||
if (commentsLoading === false) return;
|
||||
axios
|
||||
.get(`${base}/incident/${incident.number}/comments`)
|
||||
.then((response) => {
|
||||
incident.comments = response.data;
|
||||
commentsLoading = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
// console.log(error);
|
||||
});
|
||||
}
|
||||
$: {
|
||||
if (state == "open") {
|
||||
getComments();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="incident-div relative mb-8 grid w-full grid-cols-3 gap-4">
|
||||
<div class="col-span-3">
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-2 pt-3">
|
||||
<Card.Title class="relative mb-0">
|
||||
{#if incidentPriority != "" && incidentDuration > 0}
|
||||
<p class="absolute -top-8 -translate-y-1 leading-10">
|
||||
<Badge
|
||||
class="-ml-4 bg-card text-xs font-semibold text-[rgba(0,0,0,.6)] text-{StatusObj[
|
||||
incidentPriority
|
||||
]} "
|
||||
>
|
||||
{incidentPriority} for {incidentDuration} Minute{incidentDuration >
|
||||
1
|
||||
? "s"
|
||||
: ""}
|
||||
</Badge>
|
||||
</p>
|
||||
{/if}
|
||||
{#if monitor.image}
|
||||
<img
|
||||
src={monitor.image.startsWith("/")
|
||||
? base + monitor.image
|
||||
: monitor.image}
|
||||
class="absolute left-0 top-1 inline h-5 w-5"
|
||||
alt=""
|
||||
srcset=""
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="px-8">
|
||||
<div class="scroll-m-20 text-xl font-medium tracking-tight">
|
||||
{#if variant.includes("monitor")}
|
||||
{monitor.name} -
|
||||
{/if}
|
||||
{#if variant.includes("title")}
|
||||
{incident.title}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if incidentState == "open"}
|
||||
<span
|
||||
class="absolute -left-[26px] -top-[16px] inline-flex h-[8px] w-[8px] animate-ping rounded-full {blinker} opacity-75"
|
||||
></span>
|
||||
{/if}
|
||||
{#if variant.includes("body") || variant.includes("comments")}
|
||||
<div class="toggle absolute right-4 {state}">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-6 w-6 rounded-full"
|
||||
size="icon"
|
||||
on:click={() => {
|
||||
state = state == "open" ? "close" : "open";
|
||||
}}
|
||||
>
|
||||
<ChevronDown class="text-muted-foreground" size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Title>
|
||||
<Card.Description class="-mt-4 px-8 text-xs ">
|
||||
{moment(incidentCreatedAt * 1000).format("MMMM Do YYYY, h:mm:ss a")}
|
||||
|
||||
<p class="mt-0 flex gap-2 leading-8">
|
||||
{#if incident.labels.includes("identified")}
|
||||
<span
|
||||
class="tag-identified-text"
|
||||
title={l(lang, "incident.identified")}
|
||||
>
|
||||
<Info class="inline h-4 w-4" />
|
||||
</span>
|
||||
{/if}
|
||||
{#if incident.labels.includes("resolved")}
|
||||
<span class="tag-resolved-text" title={l(lang, "incident.resolved")}>
|
||||
<CalendarCheck class="inline h-4 w-4" />
|
||||
</span>
|
||||
{/if}
|
||||
{#if incident.labels.includes("maintenance")}
|
||||
<span
|
||||
class="tag-maintenance-text"
|
||||
title={l(lang, "incident.maintenance")}
|
||||
>
|
||||
<Hammer class="inline h-4 w-4" />
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
{#if (variant.includes("body") || variant.includes("comments")) && state == "open"}
|
||||
<Card.Content class="px-14">
|
||||
{#if variant.includes("body")}
|
||||
<div
|
||||
class="prose prose-stone max-w-none text-sm dark:prose-invert prose-code:rounded prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm"
|
||||
>
|
||||
{@html incident.body}
|
||||
</div>
|
||||
{/if}
|
||||
{#if variant.includes("comments") && incident.comments?.length > 0}
|
||||
<div class="ml-4 mt-8 px-4">
|
||||
<ol class="relative border-s border-secondary">
|
||||
{#each incident.comments as comment}
|
||||
<li class="mb-10 ms-4">
|
||||
<div
|
||||
class="absolute -start-1.5 mt-1.5 h-3 w-3 rounded-full border border-secondary bg-secondary"
|
||||
></div>
|
||||
<time
|
||||
class="mb-1 text-xs font-normal leading-none text-muted-foreground"
|
||||
>
|
||||
{moment(comment.created_at).format(
|
||||
"MMMM Do YYYY, h:mm:ss a"
|
||||
)}
|
||||
</time>
|
||||
<div
|
||||
class="wysiwyg prose prose-stone mb-4 max-w-none text-base text-sm font-normal dark:prose-invert prose-code:rounded prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm"
|
||||
>
|
||||
{@html comment.body}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
{:else if commentsLoading}
|
||||
<Skeleton class="h-[20px] w-[100px] rounded-full" />
|
||||
{/if}
|
||||
</Card.Content>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toggle {
|
||||
display: none;
|
||||
}
|
||||
.toggle {
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
.toggle.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.incident-div:hover .toggle {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
export let data;
|
||||
let pageNo = 1;
|
||||
let limit = 20;
|
||||
let alerts = [];
|
||||
let totalPages = 0;
|
||||
let loadingData = false;
|
||||
|
||||
let incidentURL = "";
|
||||
if (data.siteData.github && data.siteData.github.owner && data.siteData.github.repo) {
|
||||
incidentURL = `https://github.com/${data.siteData.github.owner}/${data.siteData.github.repo}/issues`;
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loadingData = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "getAllAlertsPaginated",
|
||||
data: { page: pageNo, limit }
|
||||
})
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
alerts = resp.alerts;
|
||||
totalPages = Math.ceil(resp.total.count / limit);
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loadingData = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<div>
|
||||
{#if loadingData}
|
||||
<Loader class="mt-6 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
{#if totalPages > 1}
|
||||
<nav class="flex max-w-2xl items-center gap-x-1" aria-label="Pagination">
|
||||
<button
|
||||
disabled={pageNo === 1}
|
||||
type="button"
|
||||
class="inline-flex min-h-[38px] min-w-[38px] items-center justify-center gap-x-2 rounded-lg border border-transparent px-2.5 py-2 text-sm text-gray-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
|
||||
aria-label="Previous"
|
||||
on:click={() => {
|
||||
pageNo--;
|
||||
loadData();
|
||||
}}
|
||||
>
|
||||
<ChevronLeft class="size-3.5 shrink-0" />
|
||||
<span class="sr-only">Previous</span>
|
||||
</button>
|
||||
<div class="flex max-w-[380px] items-center gap-x-1 overflow-x-auto">
|
||||
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
|
||||
<button
|
||||
on:click={() => {
|
||||
pageNo = page;
|
||||
loadData();
|
||||
}}
|
||||
type="button"
|
||||
class="flex min-h-[38px] min-w-[38px] items-center justify-center rounded-lg border {page == pageNo
|
||||
? 'border-gray-200'
|
||||
: 'border-transparent'} px-3 py-2 text-sm text-gray-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={pageNo === totalPages}
|
||||
class="inline-flex min-h-[38px] min-w-[38px] items-center justify-center gap-x-2 rounded-lg border border-transparent px-2.5 py-2 text-sm text-gray-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
|
||||
aria-label="Next"
|
||||
on:click={() => {
|
||||
pageNo++;
|
||||
loadData();
|
||||
}}
|
||||
>
|
||||
<span class="sr-only">Next</span>
|
||||
<ChevronRight class="size-3.5 shrink-0" />
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
<!-- End Pagination -->
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="-m-1.5 overflow-x-auto">
|
||||
<div class="inline-block min-w-full p-1.5 align-middle">
|
||||
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500">ID</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Type</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Monitor</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Status</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Checks</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Incident</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Created At</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{#each alerts as alert}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium">
|
||||
{alert.id}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
|
||||
{#if alert.monitor_status === "DOWN"}
|
||||
<span class="text-red-500">
|
||||
{alert.monitor_status}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-yellow-500">
|
||||
{alert.monitor_status}
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
{alert.monitor_tag}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
|
||||
{#if alert.alert_status === "RESOLVED"}
|
||||
<span class="text-blue-500">
|
||||
{alert.alert_status}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-fuchsia-500">
|
||||
{alert.alert_status}
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
{alert.health_checks}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
|
||||
{#if !!alert.incident_number}
|
||||
<a
|
||||
rel="external"
|
||||
href="{base}/manage/app/events#{alert.incident_number}"
|
||||
class="text-cyan-500 hover:underline focus:outline-none"
|
||||
>
|
||||
{alert.incident_number}
|
||||
</a>
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||
{moment.utc(alert.created_at, "YYYY-MM-DD HH:mm:ss").local().format("YYYY-MM-DD HH:mm:ss")}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user