mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
418 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebc30b674f | |||
| bf079798d3 | |||
| 480cf64738 | |||
| 65f039b7bd | |||
| 5c7b62e9f7 | |||
| 5e6e891ea6 | |||
| 82c904608b | |||
| f5260ceb86 | |||
| abb32fdd6f | |||
| 97aef5c5f5 | |||
| c3eb8ae7f4 | |||
| 72f74f9164 | |||
| 958676e5a7 | |||
| 51134670e5 | |||
| ecea2b220d | |||
| 212f4d5e7a | |||
| 2edb09bd13 | |||
| 6b9f073cb7 | |||
| 9f933a0f1c | |||
| 8a6f721bb4 | |||
| eeadcca6ed | |||
| 8e89c10906 | |||
| 833d16351c | |||
| 0c10552fa7 | |||
| 443afe2f6e | |||
| db48d98da7 | |||
| bf3150434a | |||
| 71c4bd26d2 | |||
| 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 | |||
| 7f33f6ddfd |
@@ -1,4 +1,7 @@
|
||||
node_modules
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
dist
|
||||
build
|
||||
.env
|
||||
|
||||
@@ -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
|
||||
+23
-9
@@ -1,12 +1,26 @@
|
||||
KENER_SECRET_KEY=please_change_me
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
KENER_BASE_PATH=""
|
||||
RESEND_API_KEY=
|
||||
ORIGIN=http://localhost:3000
|
||||
TZ=Etc/UTC
|
||||
KENER_SECRET_KEY=please_change_me_to_something_secure
|
||||
|
||||
# For SQLite database...
|
||||
DATABASE_URL=sqlite://./database/kener.sqlite.db
|
||||
TZ=UTC
|
||||
|
||||
# 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>
|
||||
|
||||
# DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase
|
||||
# DATABASE_URL=mysql://root:password@127.0.0.1:3306/kener
|
||||
# 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
|
||||
@@ -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,6 +9,7 @@ config/server.yaml
|
||||
/src/lib/.kener
|
||||
/package
|
||||
.env
|
||||
.vscode
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
+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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+128
-32
@@ -1,52 +1,148 @@
|
||||
FROM node:23
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Install necessary packages including tzdata for timezone setting
|
||||
# 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
|
||||
|
||||
#==========================================================#
|
||||
# STAGE 1: BUILD STAGE #
|
||||
#==========================================================#
|
||||
|
||||
FROM node:${DEBIAN_VERSION_TAG} AS builder-debian
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
libnode108 \
|
||||
nodejs \
|
||||
python3 \
|
||||
sqlite3 \
|
||||
libsqlite3-dev \
|
||||
make \
|
||||
node-gyp \
|
||||
g++ \
|
||||
tzdata \
|
||||
iputils-ping && \
|
||||
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/*
|
||||
|
||||
# Set the timezone environment variable and the application environment
|
||||
ARG KENER_BASE_PATH=
|
||||
ENV TZ=Etc/UTC
|
||||
ENV KENER_BASE_PATH=${KENER_BASE_PATH}
|
||||
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
|
||||
|
||||
FROM builder-${VARIANT} AS builder
|
||||
|
||||
# Set environment variables
|
||||
ENV NPM_CONFIG_LOGLEVEL=error \
|
||||
VITE_BUILD_ENV=production
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files and install dependencies
|
||||
# Copy package files for dependency installation
|
||||
COPY package*.json ./
|
||||
RUN npm install && npm cache clean --force
|
||||
|
||||
# Copy the rest of the application code
|
||||
# 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
|
||||
|
||||
# Copy application source code
|
||||
COPY . .
|
||||
|
||||
# remove dir src/routes/(docs)
|
||||
RUN rm -rf src/routes/\(docs\)
|
||||
# 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
|
||||
|
||||
# Ensure /app/uploads and /app/database have rw permissions
|
||||
RUN mkdir -p /app/uploads /app/database && \
|
||||
chmod -R 777 /app/uploads /app/database
|
||||
# Build the application and copy templates to build directory
|
||||
RUN npm run build && \
|
||||
mkdir -p build/server/templates && \
|
||||
cp -r src/lib/server/templates/* build/server/templates/ && \
|
||||
npm prune --omit=dev
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
#==========================================================#
|
||||
# STAGE 2: PRODUCTION/FINAL STAGE #
|
||||
#==========================================================#
|
||||
|
||||
# Argument for the port
|
||||
ARG PORT=3000
|
||||
# Set the environment variable for the port
|
||||
ENV PORT=$PORT
|
||||
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/*
|
||||
|
||||
FROM node:${ALPINE_VERSION_TAG} AS final-alpine
|
||||
RUN apk add --no-cache --update \
|
||||
iputils \
|
||||
sqlite \
|
||||
tzdata
|
||||
|
||||
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
|
||||
|
||||
# Set the command to run the application
|
||||
# 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"]
|
||||
@@ -7,9 +7,27 @@
|
||||
<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>
|
||||
<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">
|
||||
@@ -30,13 +48,19 @@
|
||||
|
||||
## What is 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** 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 is carefully crafted to be easy to use and customize.
|
||||
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.
|
||||
|
||||
It comes with all the basic asks for a status page. It is open-source and free to use.
|
||||
### Why 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.
|
||||
✅ 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
|
||||
|
||||
@@ -48,14 +72,78 @@ git clone https://github.com/rajnandan1/kener.git
|
||||
cd kener
|
||||
npm install
|
||||
cp .env.example .env
|
||||
npm run build
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
The latest image is available on DockerHub at [`rajnandan1/kener:latest`](https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest).
|
||||
Download and use the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
|
||||
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.18"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.18-blue" alt="Kener latest stable version: 3.2.18" /></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.18)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.18" target="_blank"><code>3.2.18</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.18) 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.18) 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.18)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.18-alpine" target="_blank"><code>3.2.18-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.18) 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.18) 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
|
||||
|
||||
@@ -65,69 +153,64 @@ Download and use the sample [docker-compose.yml](https://github.com/rajnandan1/k
|
||||
|
||||
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
|
||||
|
||||
- Advanced application performance monitoring tools
|
||||
- Real-time network monitor software capabilities
|
||||
- Polls HTTP endpoint or Push data to monitor using Rest APIs
|
||||
- Adjusts Timezones for visitors
|
||||
- Categorize Monitors into different Sections
|
||||
- Cron-based scheduling for monitors. Minimum per minute
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
- Automatically adjusts timezones for visitors
|
||||
- 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
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
- Beautifully Crafted Status Page
|
||||
- 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
|
||||
|
||||
- Incident Management
|
||||
- Incident Communication
|
||||
- Comprehensive APIs for Incident Management
|
||||
- **Incident tracking & communication** tools
|
||||
- **Comprehensive APIs** for incident management
|
||||
|
||||
### User Experience and Design
|
||||
|
||||
- Good Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
- Server Side Rendering
|
||||
### 🧑💻 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"/>
|
||||
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
|
||||
</div>
|
||||
|
||||
|
||||
## Technologies used
|
||||
## Technologies Used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Support Me
|
||||
|
||||
If you are using Kener and want to support me, you can do so by sponsoring me on GitHub or buying me a coffee.
|
||||
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 [Contributing Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
|
||||
If you want to contribute to Kener, please read the [Contribution Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
@@ -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)
|
||||
+51
-16
@@ -1,26 +1,61 @@
|
||||
# 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
|
||||
- NODE_ENV=production
|
||||
#- PORT=3000
|
||||
#- KENER_BASE_PATH=
|
||||
#- RESEND_API_KEY=
|
||||
#- ORIGIN=
|
||||
#- KENER_SECRET_KEY=
|
||||
#- RESEND_SENDER_EMAIL=
|
||||
#- DATABASE_URL=
|
||||
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:
|
||||
|
||||
### 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:
|
||||
- '.:/app/database:rw'
|
||||
- '.:/app/uploads:rw'
|
||||
- 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
|
||||
|
||||
+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
|
||||
|
||||
|
||||
+80
-90
@@ -7,7 +7,36 @@ description: Changelogs for Kener
|
||||
|
||||
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
|
||||
|
||||
## v3.0.9
|
||||
## 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">
|
||||
@@ -16,111 +45,72 @@ Here are the changelogs for Kener. Changelogs are only published when there are
|
||||
|
||||
### Features
|
||||
|
||||
- Support of SMTP for email notifications. Read more [here](/docs/triggers/#email-smtp)
|
||||
- Introduction of event type `MAINTENANCE` for incidents.
|
||||
- You can write eval function for ping now in monitors. Read more [here](/docs/monitors-ping/#eval)
|
||||
- Added category filter for monitor management.
|
||||
- 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
|
||||
|
||||
### Fixes
|
||||
### Improvements
|
||||
|
||||
- Support longer TLD in `siteURL` example `https://example.network`
|
||||
- Remove googleapis preconnect and preload
|
||||
- Fixed wrong action url in webhook.
|
||||
- Improved build time by migrating lucid-svelte to individual components
|
||||
|
||||
## v3.0.1
|
||||
### 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
|
||||
|
||||
- Support for i18n in dates.
|
||||
- Support of i18n in monitor embeds. Read more [here](/docs/embed#javascript-parameters)
|
||||
- **New Heartbeat Monitors**
|
||||
|
||||
## v3.0.0
|
||||
- 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
|
||||
|
||||
<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>
|
||||
- **Data Interpolation Improvements**
|
||||
- Fixed data interpolation issues for more accurate uptime calculations
|
||||
- Improved handling of timestamp boundaries
|
||||
|
||||
Here are the changes in this release
|
||||
### Fixes & Improvements
|
||||
|
||||
### Features
|
||||
- **UI Enhancements**
|
||||
|
||||
- New APIs for creating incidents and pushing updates. Read more [here](/docs/kener-apis)
|
||||
- Incident management is now part of the admin UI and removed from the config file.
|
||||
- The UI colors have been updated to be more muted.
|
||||
- Email Notifications for incidents using [resend](https://resend.com).
|
||||
- New Kener management portal. No monitors.yaml or site.yaml needed anymore
|
||||
- Login Page and Setup Page
|
||||
- Remove Github dependency
|
||||
- Options to disable square or dot pattern
|
||||
- Support for new languages
|
||||
- Multiple DB support (mysql, postgres, sqlite3)
|
||||
- New API reference
|
||||
- New documentation site
|
||||
- Better mobile responsiveness for theme toggles and controls
|
||||
- Fixed layout issues with bottom navigation buttons
|
||||
- Improved timezone selector display
|
||||
|
||||
## v2.0.0
|
||||
- **Data Processing**
|
||||
|
||||
<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>
|
||||
- Fixed edge case issues with uptime calculations
|
||||
- Improved handling of "No Data" status in summaries
|
||||
- More accurate time range display across timezones
|
||||
|
||||
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`
|
||||
|
||||
### Migration
|
||||
|
||||
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)
|
||||
|
||||
## v0.0.16
|
||||
|
||||
<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)
|
||||
|
||||
### Migration
|
||||
|
||||
#### Source
|
||||
|
||||
- Move data from `PUBLIC_KENER_FOLDER` to `/database` file.
|
||||
- Move `site.yaml` to `/config` folder
|
||||
- Move `monitors.yaml` to `/config` folder
|
||||
|
||||
#### Docker
|
||||
|
||||
- Use `-v $(pwd)/database:/app/database` and `-v $(pwd)/config:/app/config` in your docker run command
|
||||
- **Documentation**
|
||||
- Added comprehensive documentation for Heartbeat Monitors
|
||||
- Updated API documentation with new endpoints
|
||||
|
||||
@@ -33,10 +33,53 @@ To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS the
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
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'
|
||||
```
|
||||
|
||||
|
||||
+22
-2
@@ -56,7 +56,7 @@ ghcr.io/rajnandan1/kener:latest
|
||||
|
||||
[Environment variables](/docs/environment-vars) can be passed with `-e` An example `docker run` command:
|
||||
|
||||
Make sure `./database` directory is present in the root directory.
|
||||
Make sure `./database` and `./uploads` directories are present in the root directory.
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -65,7 +65,10 @@ This example is for sqlite. You can also use postgres. Read more about it [here]
|
||||
#### sqlite
|
||||
|
||||
```bash
|
||||
mkdir database
|
||||
mkdir database uploads
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-v $(pwd)/database:/app/database \
|
||||
-v $(pwd)/uploads:/app/uploads \
|
||||
@@ -79,6 +82,10 @@ docker run \
|
||||
|
||||
You can also use a .env file
|
||||
|
||||
```bash
|
||||
mkdir database uploads
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-v $(pwd)/database:/app/database \
|
||||
@@ -92,6 +99,12 @@ Or use **Docker Compose** with the example [docker-compose.yaml](https://raw.git
|
||||
|
||||
#### Postgres
|
||||
|
||||
```bash
|
||||
mkdir uploads
|
||||
```
|
||||
|
||||
Database folder is not required for postgres.
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-p 3000:3000 \
|
||||
@@ -104,6 +117,12 @@ docker run \
|
||||
|
||||
#### MySQL
|
||||
|
||||
```bash
|
||||
mkdir uploads
|
||||
```
|
||||
|
||||
Database folder is not required for mysql.
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-p 3000:3000 \
|
||||
@@ -122,6 +141,7 @@ By default kener runs on `/` but you can change it to `/status` or any other pat
|
||||
|
||||
- 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>
|
||||
|
||||
|
||||
+12
-31
@@ -12,29 +12,24 @@ 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/monitor-[tag]/js?theme=light&monitor=http://[hostname]/embed/monitor-[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/monitor-earth/js?theme=light&monitor=https://kener.ing/embed/monitor-earth"
|
||||
></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. 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
|
||||
- `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.
|
||||
|
||||
@@ -49,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/monitor-[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/monitor-earth?theme=light"
|
||||
width="100%"
|
||||
height="100"
|
||||
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.
|
||||
@@ -78,9 +59,9 @@ 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. 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
|
||||
- `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
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ Set this to the origin of your website(protocl + hostname + port if there). This
|
||||
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
|
||||
@@ -37,8 +44,8 @@ export PORT=4242
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
```bash
|
||||
export KENER_BASE_PATH=/status
|
||||
@@ -93,10 +100,24 @@ 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
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@ A small text that will be shown below the title.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ A small text that will be shown below the title.
|
||||
|
||||
You can navigation links to other urls. You can add as many as you want.
|
||||
|
||||

|
||||

|
||||
|
||||
### Icon
|
||||
|
||||
@@ -45,7 +45,7 @@ The title of the link.
|
||||
|
||||
The URL to redirect to when the link is clicked.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
+30
-74
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Kener Documentation
|
||||
title: Kener Documentation | Open Source Status Page
|
||||
description: Kener is a feature-rich and modern status page system built with SvelteKit and NodeJS. It is open-source and free to use.
|
||||
---
|
||||
|
||||
@@ -36,85 +36,41 @@ description: Kener is a feature-rich and modern status page system built with Sv
|
||||
</picture>
|
||||
</div>
|
||||
<div class="flex gap-2 kener-home-links">
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing">Live Demo</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/quick-start">Quick Start</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://github.com/rajnandan1/kener">Clone</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/deployment">Deploy</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/kener-apis">APIs</a>
|
||||
</div>
|
||||
<a href="https://kener.ing" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Live Demo
|
||||
</a>
|
||||
<a href="https://kener.ing/docs/quick-start" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Quick Start
|
||||
</a>
|
||||
<a href="https://github.com/rajnandan1/kener" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Clone
|
||||
</a>
|
||||
<a href="https://kener.ing/docs/deployment" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Deploy
|
||||
</a>
|
||||
<a href="https://kener.ing/docs/kener-apis" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
APIs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## What is Kener?
|
||||
## Forget Boring Status Pages. Hello Kener!
|
||||
|
||||
Kener is status page system built with Sveltekit and NodeJS. It does not try to replace the Datadogs and Atlassian of the world. It tries to help some who wants to come up with a status page that looks nice and minimum overhead, in a modern way.
|
||||
**Kener** isn't just another status page – it's a beautifully crafted, lightning-fast monitoring system that makes your services look good even when they're down. Built with modern tech (**SvelteKit** + **NodeJS**), it delivers enterprise-grade reliability monitoring without the enterprise price tag (it's completely free!).
|
||||
|
||||
It comes with all the basic asks for a status page. It is open-source and free to use.
|
||||
### Why Teams Are Switching to Kener
|
||||
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a
|
||||
cheaply available domain.
|
||||
- **📈 Up and running in minutes**, not days – from Docker to deployment in less time than it takes to make coffee
|
||||
- **🎭 Dark mode that actually looks good** – because your 3 AM incident responses shouldn't burn your retinas
|
||||
- **🧩 Ridiculously customizable** – make it match your brand or go wild with a unique design
|
||||
- **⚡ Blazing fast performance** – server-rendered for speed that both users and search engines love
|
||||
- **🌐 Works everywhere** – auto-adjusts to visitors' time zones and looks great on any device
|
||||
- **🔄 Real-time data** with gorgeous visualizations that make your uptime metrics pop
|
||||
- **🛠️ Modern tooling** without the bloat – everything you need, nothing you don't
|
||||
|
||||
## Quick Deployment
|
||||
### Used by Companies That Value Simplicity and Power
|
||||
|
||||
[](https://railway.com/template/spSvic?referralCode=1Pn7vs)
|
||||
From indie developers to growing startups, teams are choosing Kener when they need reliability monitoring that doesn't require a dedicated DevOps team to maintain.
|
||||
|
||||
## Features
|
||||
"_My team set up Kener in less than an hour, and our customers love the transparency it provides. The embedded widgets on our pricing page increased conversions by showing our reliability upfront._" – A happy user
|
||||
|
||||
### Monitoring and Tracking
|
||||
|
||||
- Advanced application performance monitoring tools
|
||||
- Real-time network monitor software capabilities
|
||||
- Polls HTTP endpoint or Push data to monitor using Rest APIs
|
||||
- Handles Timezones for visitors
|
||||
- Categorize Monitors into different Sections
|
||||
- Cron-based scheduling for monitors. Minimum per minute
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
|
||||
### Customization and Branding
|
||||
|
||||
- Customizable status page
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
- Beautifully Crafted Status Page
|
||||
|
||||
### Incident Management
|
||||
|
||||
- Incident Management
|
||||
- Incident Communication
|
||||
- Comprehensive APIs for Incident Management
|
||||
|
||||
### User Experience and Design
|
||||
|
||||
- 100% Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
- Server Side Rendering
|
||||
|
||||
## Technologies used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Support Me
|
||||
|
||||
If you are using Kener and want to support me, you can do so by sponsoring me on GitHub or buying me a coffee.
|
||||
|
||||
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
|
||||
|
||||
[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
## Ready to Transform Your Status Page?
|
||||
|
||||
+121
-90
@@ -28,65 +28,96 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
|
||||
```json
|
||||
{
|
||||
"%status for %duration": "%status for %duration",
|
||||
"14 Days": "14 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"7 Days": "7 Days",
|
||||
"90 Days": "90 Days",
|
||||
"Availability per Component": "Availability per Component",
|
||||
"Back": "Back",
|
||||
"Badge Copied": "Badge Copied",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Browse Events",
|
||||
"Code Copied": "Code Copied",
|
||||
"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",
|
||||
"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",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"MONITORING": "MONITORING",
|
||||
"Maintenance Completed": "Maintenance Completed",
|
||||
"Maintenance in Progress": "Maintenance in Progress",
|
||||
"Mode": "Mode",
|
||||
"No Data": "No Data",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
"Pinging": "Pinging",
|
||||
"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",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Theme",
|
||||
"Today": "Today",
|
||||
"UP": "UP",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"%status for %duration": "%status for %duration",
|
||||
"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"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -100,30 +131,30 @@ Open `src/lib/locales/locales.json` and add the locale code and the name of the
|
||||
|
||||
```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"
|
||||
}
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -132,7 +163,7 @@ Open `src/lib/locales/locales.json` and add the locale code and the name of the
|
||||
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";
|
||||
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).
|
||||
@@ -140,7 +171,7 @@ This should be valid for the date-fns library. You can find the list of locales
|
||||
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 };
|
||||
const locales = { ru, en: enUS, hi, dk: de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko }
|
||||
```
|
||||
|
||||
### Step 4
|
||||
@@ -155,11 +186,11 @@ DO NOT forget to raise a PR with the new translation file so that others can use
|
||||
|
||||
There are few variables that you you should not change,
|
||||
|
||||
- %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
|
||||
- %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
|
||||
|
||||
## Dates
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ To close an incident, you need to add a message and set the status to `RESOLVED`
|
||||
|
||||
## Add Monitors
|
||||
|
||||
To add monitors to the incident, you need to add the monitor tag to the incident. This will automatically add the monitor to the incident. You will also get the status of the monitor in the incident. This can be `DEGRADED` or `DOWN`.
|
||||
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
|
||||
|
||||
|
||||
+82
-60
@@ -9,7 +9,7 @@ API monitors are used to monitor APIs. You can use API monitors to monitor the u
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
(async function (statusCode, responseTime, responseDataBase64) {
|
||||
(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
let status = 'DOWN'
|
||||
if(statusCodeShort >=2 && statusCodeShort <= 3) {
|
||||
@@ -64,76 +64,98 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
})
|
||||
```
|
||||
|
||||
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
|
||||
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it
|
||||
- `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)`
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64);
|
||||
//if the response is a json object
|
||||
//let jsonResp = JSON.parse(decodedResp)
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
### Example 1
|
||||
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the status code is 2XX then the status is UP, if the status code is 5XX then the status is DOWN. If the response contains the word `Unknown Error` then the status is DOWN. If the response time is greater than 2000 then the status is DEGRADED.
|
||||
|
||||
```javascript
|
||||
(async function (statusCode, responseTime, responseDataBase64) {
|
||||
const resp = atob(responseDataBase64); //convert base64 to string
|
||||
(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
|
||||
let status = "DOWN";
|
||||
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 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 the status code is 5XX then the status is DOWN
|
||||
if (/^[5]\d{2}$/.test(statusCode)) status = "DOWN"
|
||||
|
||||
if (resp.includes("Unknown Error")) {
|
||||
status = "DOWN";
|
||||
}
|
||||
if (responseRaw.includes("Unknown Error")) {
|
||||
status = "DOWN"
|
||||
}
|
||||
|
||||
return {
|
||||
status: status,
|
||||
latency: responseTime
|
||||
};
|
||||
});
|
||||
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.
|
||||
|
||||
```javascript
|
||||
(async function raj(statusCode, responseTime, responseDataBase64) {
|
||||
let htmlString = atob(responseDataBase64);
|
||||
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g);
|
||||
if (scriptTags && scriptTags.length >= 2) {
|
||||
// Extract the second last script tag's src attribute
|
||||
const secondLastScript = scriptTags[scriptTags.length - 2];
|
||||
const srcMatch = secondLastScript.match(/src="([^"]+)"/);
|
||||
const secondLastScriptSrc = srcMatch ? srcMatch[1] : null;
|
||||
```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
|
||||
};
|
||||
});
|
||||
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
|
||||
@@ -153,7 +175,7 @@ This is an example to monitor google every 5 minute.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -180,7 +202,7 @@ export SOME_TOKEN=some-token-example
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -201,7 +223,7 @@ Example showing setting up a POST request every minute with a timeout of 2 secon
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -228,7 +250,7 @@ export SERVICE_SECRET=secret2_secret
|
||||
|
||||
<div class="border rounded-md p-1">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ DNS monitors are used to monitor DNS servers. Verify DNS queries for your server
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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.
|
||||
+80
-93
@@ -9,29 +9,18 @@ Ping monitors are used to monitor livenees of your servers. You can use Ping mon
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Host V4
|
||||
## Hosts
|
||||
|
||||
You can add as many IP addresses as you want to monitor. The IP address should be a valid IPv4 address.
|
||||
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.
|
||||
|
||||
Example of IP4 address is `106.12.43.232`, with the port number `80`. The port number is optional and defaults to `80`.
|
||||
|
||||
Example of IP4 address with port number is `66.51.120.219:465`.
|
||||
|
||||
Example of domain name with port number is `www.rajnandan.com:80`.
|
||||
|
||||
## Host V6
|
||||
|
||||
You can add as many IP addresses as you want to monitor. The IP address should be a valid IPv6 address. Example of IP6 address is `2001:0db8:85a3:0000:0000:8a2e:0370`.
|
||||
|
||||
Example of IP6 address with port number is `2606:50c0:8000::153:443`.
|
||||
|
||||
<p class="note danger">
|
||||
Please note that at least one of the Host V4 or Host V6 is required.
|
||||
<p>
|
||||
- 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
|
||||
|
||||
@@ -43,102 +32,100 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64));
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
(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);
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive
|
||||
}, true)
|
||||
|
||||
return {
|
||||
status: alive ? "UP" : "DOWN",
|
||||
latency: parseInt(latencyTotal / arrayOfPings.length)
|
||||
};
|
||||
});
|
||||
return {
|
||||
status: alive ? "UP" : "DOWN",
|
||||
latency: latencyTotal / arrayOfPings.length
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
|
||||
- `arrayOfPings` **REQUIRED** is an array of Ping Response Objects as shown below.
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64);
|
||||
let jsonResp = JSON.parse(decodedResp);
|
||||
console.log(jsonResp);
|
||||
```json
|
||||
/*
|
||||
[
|
||||
{
|
||||
"host": "smtp.resend.com",
|
||||
"port": 587,
|
||||
"type": "IP4",
|
||||
"status": "open",
|
||||
"latency": 36.750917
|
||||
},
|
||||
{
|
||||
"host": "66.51.120.219",
|
||||
"port": 465,
|
||||
"type": "IP4",
|
||||
"status": "open",
|
||||
"latency": 27.782792
|
||||
},
|
||||
{
|
||||
"host": "2606:4700:4700::1111",
|
||||
"port": 443,
|
||||
"status": "open",
|
||||
"type": "IP6",
|
||||
"latency": 5.684375
|
||||
}
|
||||
{
|
||||
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
|
||||
|
||||
- `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.
|
||||
Each object in the array represents the ping response of a host.
|
||||
|
||||
- `alive` is a boolean. It is true if the host is alive and false if the host is down.
|
||||
- `min` is a string. It is the minimum latency of the pings.
|
||||
- `max` is a string. It is the maximum latency of the pings.
|
||||
- `avg` is a string. It is the average latency of the pings.
|
||||
- `latencies` is an array of numbers. It is the latency of each ping.
|
||||
- `latency` is a number. It is the average latency of the pings.
|
||||
- `host` is a string. It is the host that was pinged.
|
||||
- `type` is a string. It is the type of the host. It can be either `IP4` or `IP6` or `DOMAIN`.
|
||||
|
||||
### Example
|
||||
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
|
||||
|
||||
```javascript
|
||||
(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64));
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
(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 areAllOpen = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive
|
||||
}, true)
|
||||
|
||||
let avgLatency = latencyTotal / arrayOfPings.length;
|
||||
let avgLatency = latencyTotal / arrayOfPings.length
|
||||
|
||||
if (areAllOpen && avgLatency > 10) {
|
||||
return {
|
||||
status: "DEGRADED",
|
||||
latency: avgLatency
|
||||
};
|
||||
}
|
||||
if (areAllOpen && avgLatency > 10) {
|
||||
return {
|
||||
status: "DEGRADED",
|
||||
latency: avgLatency
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: areAllOpen ? "UP" : "DOWN",
|
||||
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
|
||||
}
|
||||
})
|
||||
```
|
||||
+26
-11
@@ -13,7 +13,7 @@ Click on the ➕ to add a monitor.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -103,21 +103,25 @@ To monitor the DNS records, you need to provide the domain name and the record t
|
||||
|
||||
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.
|
||||
- Click on the 🔔 icon on the top right corner of the monitor.
|
||||
- Add details for either DOWN or DEGRADED.
|
||||
- Failure Threshold(Required): The number of consecutive failures before the trigger is activated.
|
||||
- Success Threshold(Required): The number of consecutive successes before the trigger is deactivated.
|
||||
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in Github. So make sure you have set up the Github token in the environment variables. The incident will be closed when the monitor is back to UP.
|
||||
- Severity (Required): The severity of the incident. It can be `Critcal` or `Warning`.
|
||||
- Custom Message (Required): Add your owner alert message.
|
||||
- Choose Triggers: Choose the triggers you want to activate the trigger. You can choose multiple triggers.
|
||||
- It will take upto 1 minute for the trigger to be activated.
|
||||
|
||||
---
|
||||
|
||||
@@ -135,6 +139,17 @@ You can activate the monitor by switching the toggle to on. Activation a monitor
|
||||
|
||||
<div class="note info">
|
||||
|
||||
Any changes in the a live monitor will take one minute to reflect.
|
||||
Any changes in the a live monitor will take 2 minutes to reflect.
|
||||
|
||||
</div>
|
||||
|
||||
## Supported Monitors
|
||||
|
||||
- [API Monitors](/docs/monitors-api)
|
||||
- [DNS Monitors](/docs/monitors-dns)
|
||||
- [PING Monitors](/docs/monitors-ping)
|
||||
- [TCP Monitors](/docs/monitors-tcp)
|
||||
- [SQL Monitors](/docs/monitors-sql)
|
||||
- [SSL Monitors](/docs/monitors-ssl)
|
||||
- [HEARTBEAT Monitors](/docs/monitors-heartbeat)
|
||||
- [GROUP Monitors](/docs/monitors-group)
|
||||
|
||||
+18
-12
@@ -9,9 +9,9 @@ Here is a demonstration of how to get started with Kener in seconds
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js Minimum version required is `v18`.
|
||||
- Git
|
||||
- sqlite3
|
||||
- Node.js Minimum version required is `v18`.
|
||||
- Git
|
||||
- sqlite3
|
||||
|
||||
## Clone the repository
|
||||
|
||||
@@ -54,9 +54,9 @@ Status Page Manage Portal will be at [http://localhost:3000/manage/app/site](htt
|
||||
|
||||
If this is the first time your are launching kener then you would be redirected to the [set up page](/manage/setup). Fill in the details and click on `Let's Go` button.
|
||||
|
||||
- **Name**: Your Name
|
||||
- **Email**: Your Email
|
||||
- **Password**: Your Password
|
||||
- **Name**: Your Name
|
||||
- **Email**: Your Email
|
||||
- **Password**: Your Password
|
||||
|
||||
Please note that the email should be a valid email address and password should be atleast 8 characters long with uppercase lowercase and numbers.
|
||||
|
||||
@@ -64,13 +64,19 @@ Please note that the email should be a valid email address and password should b
|
||||
|
||||
Once you have created the user, you can login with the credentials you have provided by going to the [login page](/manage/signin)
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
You can watch the video tutorial on how to get started with Kener
|
||||
|
||||
[](https://www.youtube.com/watch?v=4L9PnWPqMPw)
|
||||
|
||||
## Next Steps
|
||||
|
||||
Learn how to configure kener by going through one of the topics
|
||||
|
||||
- [Monitors](/docs/monitors): Learn how to set up and work with monitors in kener.
|
||||
- [Triggers](/docs/triggers): Learn how to set up and work with triggers in kener.
|
||||
- [Environment Variables](/docs/environment-vars): Learn how to set up and work with environment variables in kener.
|
||||
- [API](/docs/kener-apis): Learn how to use the API in kener.
|
||||
- [Databases](/docs/database): Learn how to set up and work with databases in kener.
|
||||
- [Theme](/docs/theme): Learn how to set up and work with theme in kener.
|
||||
- [Monitors](/docs/monitors): Learn how to set up and work with monitors in kener.
|
||||
- [Triggers](/docs/triggers): Learn how to set up and work with triggers in kener.
|
||||
- [Environment Variables](/docs/environment-vars): Learn how to set up and work with environment variables in kener.
|
||||
- [API](/docs/kener-apis): Learn how to use the API in kener.
|
||||
- [Databases](/docs/database): Learn how to set up and work with databases in kener.
|
||||
- [Theme](/docs/theme): Learn how to set up and work with theme in kener.
|
||||
|
||||
+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.
|
||||
+1
-1
@@ -40,4 +40,4 @@ description: Roadmap for Kener
|
||||
|
||||
---
|
||||
|
||||
Request a feature or start a discussion [here](https://github.com/rajnandan1/kener/discussions/119)
|
||||
Request a feature or start a discussion [here](https://github.com/rajnandan1/kener/discussions/new/choose)
|
||||
|
||||
+11
-1
@@ -21,7 +21,17 @@ Add API key of your Amplitude account
|
||||
|
||||
### Mixpanel
|
||||
|
||||
Add token of your Mixpanel account.
|
||||
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
|
||||
|
||||
|
||||
+3
-3
@@ -19,11 +19,11 @@ Example: `Kener - Open-Source and Modern looking Node.js Status Page for Effortl
|
||||
|
||||
```html
|
||||
<title>
|
||||
Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management
|
||||
Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management
|
||||
</title>
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## Site Name
|
||||
|
||||
@@ -33,7 +33,7 @@ Example: `Kener - Open-Source and Modern looking Node.js Status Page for Effortl
|
||||
|
||||
This will be shown as a brand name on the status page on the nav bar top left.
|
||||
|
||||

|
||||

|
||||
|
||||
## Home Location
|
||||
|
||||
|
||||
+193
-157
@@ -1,161 +1,197 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "API/Website Monitor",
|
||||
"link": "/docs/monitors-api",
|
||||
"file": "/monitors-api.md"
|
||||
},
|
||||
{
|
||||
"title": "Ping Monitor",
|
||||
"link": "/docs/monitors-ping",
|
||||
"file": "/monitors-ping.md"
|
||||
},
|
||||
{
|
||||
"title": "DNS Monitor",
|
||||
"link": "/docs/monitors-dns",
|
||||
"file": "/monitors-dns.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Triggers",
|
||||
"link": "/docs/triggers",
|
||||
"file": "/triggers.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Site",
|
||||
"link": "/docs/site",
|
||||
"file": "/site.md"
|
||||
},
|
||||
{
|
||||
"title": "Setup Github",
|
||||
"link": "/docs/gh-setup",
|
||||
"file": "/gh-setup.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": "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": "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": "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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+8
-8
@@ -11,7 +11,7 @@ Kener provides you with the ability to customize the theme of your status page.
|
||||
|
||||
## Home Page Pattern
|
||||
|
||||
Kener can show a subtle pattern in all your pages. It is either sqaure or dots. Right now you cannot modify the color of the pattern. However, you can disable it by choosing none
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
@@ -35,13 +35,13 @@ You can change how the bars and summary of a monitor looks like.
|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -51,11 +51,11 @@ Adjust the roundness of the status bar.
|
||||
|
||||
#### SHARP
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -75,13 +75,13 @@ Shows the day's impact. Example: If it was down at 10:00 AM and it is 11:00 AM,
|
||||
|
||||
## Colors
|
||||
|
||||
You can change the colors of UP/DEGRADED/DOWN states of the monitor. You show have a shade of green, yellow, and red respectively.
|
||||
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 fonts.
|
||||
You can change the font of the status page. You can choose from a list of google, or [bunny fonts](https://fonts.bunny.net/) (which is a fully GDPR-compliant, zero-tracking/no-logging CDN font service).
|
||||
|
||||
### Font URL
|
||||
|
||||
@@ -97,7 +97,7 @@ You can add custom CSS to your status page. This will be added to the head of th
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+254
-72
@@ -3,13 +3,13 @@ title: Triggers | Kener
|
||||
description: Learn how to set up and work with triggers in kener.
|
||||
---
|
||||
|
||||
# Triggers
|
||||
## 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>
|
||||
|
||||
@@ -27,10 +27,36 @@ The description is used to define the description of the webhook. It is optional
|
||||
|
||||
Kener supports the following triggers:
|
||||
|
||||
- [Webhook](#webhook)
|
||||
- [Discord](#discord)
|
||||
- [Slack](#slack)
|
||||
- [Email](#email)
|
||||
- [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
|
||||
|
||||
@@ -38,7 +64,7 @@ Webhook triggers are used to send a HTTP POST request to a URL when a monitor go
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -72,45 +98,86 @@ 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Description | Variable |
|
||||
| --------------------- | ----------------------------------------------------------- | ----------------------------- |
|
||||
| id | Unique ID of the alert | ${id} |
|
||||
| alert_name | Name of the alert | ${alert_name} |
|
||||
| severity | Severity of the alert. Can be `critical`, `warn` | ${severity} |
|
||||
| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` | ${status} |
|
||||
| source | Source of the alert. Can be `Kener` | ${source} |
|
||||
| timestamp | Timestamp of the alert | ${timestamp} |
|
||||
| description | Description of the alert. This you can customize. See below | ${description} |
|
||||
| details | Details of the alert. | - |
|
||||
| details.metric | Name of the monitor | ${metric} |
|
||||
| details.current_value | Current value of the monitor | ${current_value} |
|
||||
| details.threshold | Alert trigger threshold of the monitor | ${threshold} |
|
||||
| actions | Actions to be taken. Link to view the monitor. | ${action_text}, ${action_url} |
|
||||
|
||||
### Custom Body
|
||||
|
||||
You can customize the body of the webhook. You can use the variables mentioned above. If you are not using a json body then please make sure you are using the right content-type by setting custom headers. See examples below.
|
||||
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
|
||||
|
||||
@@ -118,7 +185,7 @@ Discord triggers are used to send a message to a discord channel when a monitor
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -147,7 +214,61 @@ 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
|
||||
|
||||
@@ -155,7 +276,7 @@ Slack triggers are used to send a message to a slack channel when a monitor goes
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -185,7 +306,82 @@ 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
|
||||
|
||||
@@ -193,7 +389,7 @@ Email triggers are used to send an email when a monitor goes down or up. Kener s
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -205,10 +401,10 @@ To send emails using Resend you just need to set `RESEND_API_KEY` in the environ
|
||||
|
||||
To send emails using SMTP, please enter
|
||||
|
||||
- Host: SMTP server host
|
||||
- Port: SMTP server port
|
||||
- User: SMTP server username
|
||||
- Password: SMTP server password
|
||||
- Host: SMTP server host
|
||||
- Port: SMTP server port
|
||||
- User: SMTP server username
|
||||
- Password: SMTP server password
|
||||
|
||||
<div class="note danger">
|
||||
|
||||
@@ -216,6 +412,12 @@ Since the password will be stored as plain text we encourage to use environment
|
||||
|
||||
</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">
|
||||
@@ -250,11 +452,11 @@ Subject of the email when `RESOLVED`
|
||||
|
||||
The emaik message when alert is `TRIGGERED` will look like this
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -267,23 +469,3 @@ Click on the ⚙️ to edit the trigger.
|
||||
You can deactivate the trigger by switching the toggle to off. You cannot send message to a deactivated trigger. Any monitor with this trigger will not send any notifications.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Telegram
|
||||
|
||||
You can use the webhook trigger to send a message to a telegram channel. Enable `Use a custom webhook body`.
|
||||
|
||||
Set the URL to `https://api.telegram.org/bot[BOT_TOKEN]/sendMessage`. Replace [BOT_TOKEN] with your bot token.
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_id": "[CHAT_ID]", // Replace [CHAT_ID] with your chat id
|
||||
"text": "<b>${alert_name}</b>\n\n<b>Severity:</b> <code>${severity}</code>\n<b>Status:</b> ${status}\n<b>Source:</b> Kener\n<b>Time:</b> ${timestamp}\n\n📌 <b>Details:</b>\n- <b>Metric:</b>${metric}\n- <b>Current Value:</b> <code>${current_value}</code>\n- <b>Threshold:</b> <code>${threshold}</code>\n\n🔍 <a href=\"${action_url}\">${action_text}</a>",
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
```
|
||||
|
||||
If you want to send a message to a group, then replace `[CHAT_ID]` with the group id.
|
||||
|
||||
You can also use environment variables to store the bot token and chat id. In that case the URL will be `https://api.telegram.org/bot$BOT_TOKEN/sendMessage`. In the body you can use `"chat_id": "$CHAT_ID"`. Make sure you have set the `BOT_TOKEN` and `CHAT_ID` in the <a href="/docs/environment-vars#secrets">environment variables</a>.
|
||||
|
||||
+16
-17
@@ -1,20 +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>
|
||||
Hello world
|
||||
<script
|
||||
async
|
||||
src="http://localhost:3000/embed/monitor-earth/js?theme=dark&bgc=111&locale=hi&monitor=http://localhost:3000/embed/monitor-earth"
|
||||
></script>
|
||||
</body>
|
||||
<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
|
||||
//
|
||||
|
||||
+18
-17
@@ -9,28 +9,29 @@ const databaseType = databaseURLParts[0];
|
||||
const databasePath = databaseURLParts[1];
|
||||
|
||||
const knexOb = {
|
||||
migrations: {
|
||||
directory: "./migrations"
|
||||
},
|
||||
seeds: {
|
||||
directory: "./seeds"
|
||||
}
|
||||
migrations: {
|
||||
directory: "./migrations",
|
||||
},
|
||||
seeds: {
|
||||
directory: "./seeds",
|
||||
},
|
||||
databaseType,
|
||||
};
|
||||
if (databaseType === "sqlite") {
|
||||
knexOb.client = "better-sqlite3";
|
||||
knexOb.connection = {
|
||||
filename: databasePath
|
||||
};
|
||||
knexOb.useNullAsDefault = true;
|
||||
knexOb.client = "better-sqlite3";
|
||||
knexOb.connection = {
|
||||
filename: databasePath,
|
||||
};
|
||||
knexOb.useNullAsDefault = true;
|
||||
} else if (databaseType === "postgresql") {
|
||||
knexOb.client = "pg";
|
||||
knexOb.connection = databaseURL;
|
||||
knexOb.client = "pg";
|
||||
knexOb.connection = databaseURL;
|
||||
} else if (databaseType === "mysql") {
|
||||
knexOb.client = "mysql2";
|
||||
knexOb.connection = databaseURL;
|
||||
knexOb.client = "mysql2";
|
||||
knexOb.connection = databaseURL;
|
||||
} else {
|
||||
console.error("Invalid database type");
|
||||
process.exit(1);
|
||||
console.error("Invalid database type");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export default knexOb;
|
||||
|
||||
@@ -15,6 +15,10 @@ const base = process.env.KENER_BASE_PATH || "";
|
||||
const app = express();
|
||||
const db = knex(knexOb);
|
||||
|
||||
if (process.env.ORIGIN) {
|
||||
process.env.SVELTEKIT_ORIGIN = process.env.ORIGIN;
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.startsWith("/embed")) {
|
||||
res.setHeader("Content-Security-Policy", "frame-ancestors *");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
+901
-3
@@ -25,6 +25,10 @@
|
||||
"name": "Monitors",
|
||||
"description": "APIs to interact with monitors"
|
||||
},
|
||||
{
|
||||
"name": "MonitorStatus",
|
||||
"description": "APIs to interact with monitor status"
|
||||
},
|
||||
{
|
||||
"name": "Incidents",
|
||||
"description": "APIs to integrate incidents"
|
||||
@@ -457,7 +461,7 @@
|
||||
"operationId": "updateMonitorStatus",
|
||||
"summary": "Update status of a monitor",
|
||||
"description": "Update status of a monitor at a given timestamp UTC",
|
||||
"tags": ["Monitors"],
|
||||
"tags": ["MonitorStatus"],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
@@ -516,7 +520,7 @@
|
||||
"operationId": "getMonitorStatus",
|
||||
"summary": "Get status of a monitor",
|
||||
"description": "get status of a monitor at timestamp",
|
||||
"tags": ["Monitors"],
|
||||
"tags": ["MonitorStatus"],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
@@ -1097,6 +1101,900 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/monitor": {
|
||||
"get": {
|
||||
"summary": "Search Monitor",
|
||||
"description": "Search Monitor",
|
||||
"operationId": "searchMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tag",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"example": "{{tag}}"
|
||||
},
|
||||
"description": "Optional tag to filter results"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response200",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T11:14:49.822Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 15
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "SSL"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github SSL Certificate"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "GithubSSL"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"Response200": {
|
||||
"value": [
|
||||
{
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create Monitor",
|
||||
"description": "API to create monitors",
|
||||
"operationId": "createMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-22T10:10:00.546Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 15
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "API"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "Github"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-22T10:10:00.546Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Create Monitor": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-22T10:10:00.546Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "API",
|
||||
"name": "Github",
|
||||
"status": "ACTIVE",
|
||||
"tag": "Github",
|
||||
"type_data": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}",
|
||||
"updated_at": "2025-04-22T10:10:00.546Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Response201",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:35:21.030Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 16
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "API"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "Github"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:35:21.030Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response201": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T12:35:21.030Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 16,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "API",
|
||||
"name": "Github",
|
||||
"status": "ACTIVE",
|
||||
"tag": "Github",
|
||||
"type_data": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}",
|
||||
"updated_at": "2025-04-23T12:35:21.030Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Response400",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "monitor id must be empty or 0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response400": {
|
||||
"value": {
|
||||
"error": "monitor id must be empty or 0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/monitor/{monitorId}": {
|
||||
"get": {
|
||||
"summary": "Get Monitor",
|
||||
"description": "API to get a specified monitor",
|
||||
"operationId": "getMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response200",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T11:14:49.822Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 15
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "SSL"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github SSL Certificate"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "GithubSSL"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response200": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Response404",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response404": {
|
||||
"value": {
|
||||
"error": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"summary": "Update Monitor",
|
||||
"description": "API to update a specified monitor",
|
||||
"operationId": "updateMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T11:14:49.822Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 20
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "SSL"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github SSL Certificate"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "GithubSSL"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Update Monitor": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 20,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response200",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T11:14:49.822Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 15
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "SSL"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github SSL Certificate"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "GithubSSL"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T13:38:22.965Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response200": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T13:38:22.965Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Response400",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "id mismatch: payload: 15, url: 14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response400": {
|
||||
"value": {
|
||||
"error": "id mismatch: payload: 15, url: 14"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Response404",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response404": {
|
||||
"value": {
|
||||
"error": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Delete Monitor",
|
||||
"description": "API to delete a specified monitor",
|
||||
"operationId": "deleteMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response200",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"examples": {
|
||||
"Response200": {
|
||||
"value": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Response404",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response404": {
|
||||
"value": {
|
||||
"error": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "monitorId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+9529
-7893
File diff suppressed because it is too large
Load Diff
+124
-109
@@ -1,111 +1,126 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.0.9",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
|
||||
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
|
||||
"keywords": [
|
||||
"Node.js application",
|
||||
"Open-source status page",
|
||||
"Service monitoring tool",
|
||||
"Real-time incident management",
|
||||
"Customizable reporting",
|
||||
"Service outage tracker",
|
||||
"User-friendly dashboard",
|
||||
"Incident communication platform",
|
||||
"Scalable monitoring solution",
|
||||
"Community-driven software",
|
||||
"Website status tracker",
|
||||
"Incident response tool",
|
||||
"System status monitoring",
|
||||
"Service reliability management",
|
||||
"Incident alert system"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rajnandan1/kener.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"configure": "node build.js",
|
||||
"migrate": "npx knex migrate:latest",
|
||||
"preseed": "npx knex migrate:latest",
|
||||
"seed": "npx knex seed:run",
|
||||
"predev": "npm run seed",
|
||||
"devschedule": "node src/lib/server/startup.js",
|
||||
"schedule": "node src/lib/server/startup.js",
|
||||
"development": "vite dev",
|
||||
"dev": "npm-run-all --parallel devschedule development",
|
||||
"prettify": "prettier --write .",
|
||||
"start": "node main.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.27.4",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@zerodevx/svelte-toast": "^0.9.6",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-picker-svelte": "^2.15.1",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-awesome-color-picker": "^3.1.4",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte-dnd-action": "^0.9.55",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@number-flow/svelte": "^0.2.1",
|
||||
"@scalar/express-api-reference": "^0.4.167",
|
||||
"analytics": "^0.8.14",
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"bits-ui": "^0.9.9",
|
||||
"clsx": "^2.0.0",
|
||||
"croner": "^7.0.5",
|
||||
"date-fns": "^4.1.0",
|
||||
"dns2": "^2.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"figlet": "^1.8.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "^3.1.0",
|
||||
"lucide-svelte": "^0.292.0",
|
||||
"marked": "^11.1.1",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mysql2": "^3.12.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.10.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pg": "^8.13.1",
|
||||
"pg-pool": "^3.7.0",
|
||||
"ping": "^0.4.4",
|
||||
"queue": "^7.0.0",
|
||||
"randomstring": "^1.3.0",
|
||||
"resend": "^4.0.1",
|
||||
"svelte-legos": "^0.2.5",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-variants": "^0.1.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
"name": "kener",
|
||||
"version": "3.2.19",
|
||||
"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!");
|
||||
@@ -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.tag}</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;
|
||||
+62
-60
@@ -1,88 +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: 0 0% 92.9%;
|
||||
--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% 99%;
|
||||
--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%;
|
||||
|
||||
--date-picker-background: hsl(0 0% 99%);
|
||||
--date-picker-foreground: hsl(240 10% 4%);
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--date-picker-background: hsl(0 0% 99%);
|
||||
--date-picker-foreground: hsl(240 10% 4%);
|
||||
|
||||
--secondary: 0 0% 92.9%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 0 0% 92.9%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--ring: 240 10% 4%;
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
--ring: 240 10% 4%;
|
||||
|
||||
.dark {
|
||||
--background: 240 7% 11%;
|
||||
--background-kener: hsl(240 7% 11%);
|
||||
--background-kener-rgba: rgba(30, 30, 26, 0.35);
|
||||
--foreground: 60 10% 86%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 0 0% 40%;
|
||||
.dark {
|
||||
--background: 240 7% 11%;
|
||||
--background-kener: hsl(240 7% 11%);
|
||||
--background-kener-rgba: rgba(30, 30, 26, 0.35);
|
||||
--foreground: 60 10% 86%;
|
||||
|
||||
--popover: 230 9% 14%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 0 0% 40%;
|
||||
|
||||
--card: 230 9% 14%;
|
||||
--card-foreground: 60 10% 86%;
|
||||
--popover: 230 9% 14%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--border: 60 1% 17%;
|
||||
--input: 60 6% 17%;
|
||||
--card: 230 9% 14%;
|
||||
--card-foreground: 60 10% 86%;
|
||||
|
||||
--date-picker-background: hsl(240 4% 16%);
|
||||
--date-picker-foreground: hsl(60 10% 86%);
|
||||
--border: 60 1% 17%;
|
||||
--input: 60 6% 17%;
|
||||
|
||||
--primary: 60 10% 86%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--date-picker-background: hsl(240 4% 16%);
|
||||
--date-picker-foreground: hsl(60 10% 86%);
|
||||
|
||||
--secondary: 240 1% 18%;
|
||||
--secondary-foreground: 60 10% 86%;
|
||||
--primary: 60 10% 86%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 240 4% 16%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--secondary: 240 1% 18%;
|
||||
--secondary-foreground: 60 10% 86%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--accent: 240 4% 16%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--ring: hsl(212.7, 26.8%, 83.9);
|
||||
}
|
||||
--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;
|
||||
}
|
||||
}
|
||||
|
||||
+36
-26
@@ -1,69 +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;
|
||||
display: block;
|
||||
}
|
||||
.accm input ~ div {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.accm input {
|
||||
visibility: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.accmt {
|
||||
background-color: hsl(223, 10%, 14%);
|
||||
background-color: hsl(223, 10%, 14%);
|
||||
}
|
||||
|
||||
.accm input:checked ~ .showaccm span:first-child {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
.accm input:checked ~ .showaccm span:last-child {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
.accm input ~ .showaccm span:first-child {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
.accm input ~ .showaccm span:last-child {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
background: transparent !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.note {
|
||||
@apply mt-4 rounded-md border bg-background p-3 text-sm shadow-sm;
|
||||
@apply mt-4 rounded-md border bg-background p-3 text-sm shadow-sm;
|
||||
}
|
||||
.note.danger {
|
||||
border: 1px solid #e3342f;
|
||||
color: #e3342f;
|
||||
border: 1px solid #e3342f;
|
||||
color: #e3342f;
|
||||
}
|
||||
|
||||
.note.info {
|
||||
border: 1px solid #3490dc;
|
||||
color: #3490dc;
|
||||
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;
|
||||
}
|
||||
+742
-626
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 }
|
||||
]
|
||||
@@ -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>`;
|
||||
+14
-10
@@ -1,13 +1,17 @@
|
||||
// @ts-ignore
|
||||
export const analyticsEvent = (event, data) => {
|
||||
// Do something with the event and data
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("analyticsEvent", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+263
-245
@@ -1,288 +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;
|
||||
let requestedObject = { ...obj };
|
||||
for (const key in requestedObject) {
|
||||
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
|
||||
const element = data[key];
|
||||
if (data[key]) {
|
||||
requestedObject[key] = data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
//remove any keys that are still null or empty
|
||||
for (const key in requestedObject) {
|
||||
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
|
||||
const element = requestedObject[key];
|
||||
if (element === null || element === "") {
|
||||
delete requestedObject[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return requestedObject;
|
||||
}
|
||||
|
||||
//a function to make an api call to /manage/api/ to store site data
|
||||
function storeSiteData(data) {
|
||||
return fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ action: "storeSiteData", data })
|
||||
});
|
||||
return fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ action: "storeSiteData", data }),
|
||||
});
|
||||
}
|
||||
const allRecordTypes = {
|
||||
A: 1,
|
||||
NS: 2,
|
||||
MD: 3,
|
||||
MF: 4,
|
||||
CNAME: 5,
|
||||
SOA: 6,
|
||||
MB: 7,
|
||||
MG: 8,
|
||||
MR: 9,
|
||||
NULL: 10,
|
||||
WKS: 11,
|
||||
PTR: 12,
|
||||
HINFO: 13,
|
||||
MINFO: 14,
|
||||
MX: 15,
|
||||
TXT: 16,
|
||||
RP: 17,
|
||||
AFSDB: 18,
|
||||
X25: 19,
|
||||
ISDN: 20,
|
||||
RT: 21,
|
||||
NSAP: 22,
|
||||
NSAP_PTR: 23,
|
||||
SIG: 24,
|
||||
KEY: 25,
|
||||
PX: 26,
|
||||
GPOS: 27,
|
||||
AAAA: 28,
|
||||
LOC: 29,
|
||||
NXT: 30,
|
||||
EID: 31,
|
||||
NIMLOC: 32,
|
||||
SRV: 33,
|
||||
ATMA: 34,
|
||||
NAPTR: 35,
|
||||
KX: 36,
|
||||
CERT: 37,
|
||||
A6: 38,
|
||||
DNAME: 39,
|
||||
SINK: 40,
|
||||
OPT: 41,
|
||||
APL: 42,
|
||||
DS: 43,
|
||||
SSHFP: 44,
|
||||
IPSECKEY: 45,
|
||||
RRSIG: 46,
|
||||
NSEC: 47,
|
||||
DNSKEY: 48,
|
||||
DHCID: 49,
|
||||
NSEC3: 50,
|
||||
NSEC3PARAM: 51,
|
||||
TLSA: 52,
|
||||
SMIMEA: 53,
|
||||
HIP: 55,
|
||||
NINFO: 56,
|
||||
RKEY: 57,
|
||||
TALINK: 58,
|
||||
CDS: 59,
|
||||
CDNSKEY: 60,
|
||||
OPENPGPKEY: 61,
|
||||
CSYNC: 62,
|
||||
SPF: 99,
|
||||
UINFO: 100,
|
||||
UID: 101,
|
||||
GID: 102,
|
||||
UNSPEC: 103,
|
||||
NID: 104,
|
||||
L32: 105,
|
||||
L64: 106,
|
||||
LP: 107,
|
||||
EUI48: 108,
|
||||
EUI64: 109,
|
||||
TKEY: 249,
|
||||
TSIG: 250,
|
||||
IXFR: 251,
|
||||
AXFR: 252,
|
||||
MAILB: 253,
|
||||
MAILA: 254,
|
||||
ANY: 255
|
||||
A: 1,
|
||||
NS: 2,
|
||||
MD: 3,
|
||||
MF: 4,
|
||||
CNAME: 5,
|
||||
SOA: 6,
|
||||
MB: 7,
|
||||
MG: 8,
|
||||
MR: 9,
|
||||
NULL: 10,
|
||||
WKS: 11,
|
||||
PTR: 12,
|
||||
HINFO: 13,
|
||||
MINFO: 14,
|
||||
MX: 15,
|
||||
TXT: 16,
|
||||
RP: 17,
|
||||
AFSDB: 18,
|
||||
X25: 19,
|
||||
ISDN: 20,
|
||||
RT: 21,
|
||||
NSAP: 22,
|
||||
NSAP_PTR: 23,
|
||||
SIG: 24,
|
||||
KEY: 25,
|
||||
PX: 26,
|
||||
GPOS: 27,
|
||||
AAAA: 28,
|
||||
LOC: 29,
|
||||
NXT: 30,
|
||||
EID: 31,
|
||||
NIMLOC: 32,
|
||||
SRV: 33,
|
||||
ATMA: 34,
|
||||
NAPTR: 35,
|
||||
KX: 36,
|
||||
CERT: 37,
|
||||
A6: 38,
|
||||
DNAME: 39,
|
||||
SINK: 40,
|
||||
OPT: 41,
|
||||
APL: 42,
|
||||
DS: 43,
|
||||
SSHFP: 44,
|
||||
IPSECKEY: 45,
|
||||
RRSIG: 46,
|
||||
NSEC: 47,
|
||||
DNSKEY: 48,
|
||||
DHCID: 49,
|
||||
NSEC3: 50,
|
||||
NSEC3PARAM: 51,
|
||||
TLSA: 52,
|
||||
SMIMEA: 53,
|
||||
HIP: 55,
|
||||
NINFO: 56,
|
||||
RKEY: 57,
|
||||
TALINK: 58,
|
||||
CDS: 59,
|
||||
CDNSKEY: 60,
|
||||
OPENPGPKEY: 61,
|
||||
CSYNC: 62,
|
||||
SPF: 99,
|
||||
UINFO: 100,
|
||||
UID: 101,
|
||||
GID: 102,
|
||||
UNSPEC: 103,
|
||||
NID: 104,
|
||||
L32: 105,
|
||||
L64: 106,
|
||||
LP: 107,
|
||||
EUI48: 108,
|
||||
EUI64: 109,
|
||||
TKEY: 249,
|
||||
TSIG: 250,
|
||||
IXFR: 251,
|
||||
AXFR: 252,
|
||||
MAILB: 253,
|
||||
MAILA: 254,
|
||||
ANY: 255,
|
||||
};
|
||||
const ValidateIpAddress = function (input) {
|
||||
// Check if input is a valid IPv4 address with an optional port
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}(:\d{1,5})?$/;
|
||||
if (ipv4Regex.test(input)) {
|
||||
return "IPv4";
|
||||
}
|
||||
// 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}|:))(?::\d{1,5})?$/;
|
||||
if (ipv6Regex.test(input)) {
|
||||
return "IPv6";
|
||||
}
|
||||
// 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,}(:\d{1,5})?$/;
|
||||
if (domainRegex.test(input)) {
|
||||
return "Domain Name";
|
||||
}
|
||||
// Check if input is a valid domain name with an optional port
|
||||
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
if (domainRegex.test(input)) {
|
||||
return "DOMAIN";
|
||||
}
|
||||
|
||||
// If none of the above conditions match, the input is invalid
|
||||
return "Invalid";
|
||||
// If none of the above conditions match, the input is invalid
|
||||
return "Invalid";
|
||||
};
|
||||
|
||||
function IsValidHost(domain) {
|
||||
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
return regex.test(domain);
|
||||
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
return regex.test(domain);
|
||||
}
|
||||
function IsValidNameServer(nameServer) {
|
||||
//8.8.8.8 example
|
||||
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
return regex.test(nameServer);
|
||||
//8.8.8.8 example
|
||||
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
return regex.test(nameServer);
|
||||
}
|
||||
const IsValidURL = function (url) {
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
};
|
||||
function ValidateCronExpression(cronExp) {
|
||||
// Check if expression is provided and is a string
|
||||
if (!cronExp || typeof cronExp !== "string") {
|
||||
return { isValid: false, message: "Cron expression must be a non-empty string" };
|
||||
}
|
||||
// Check if expression is provided and is a string
|
||||
if (!cronExp || typeof cronExp !== "string") {
|
||||
return { isValid: false, message: "Cron expression must be a non-empty string" };
|
||||
}
|
||||
|
||||
// Split the expression into its components
|
||||
const fields = cronExp.trim().split(/\s+/);
|
||||
// Split the expression into its components
|
||||
const fields = cronExp.trim().split(/\s+/);
|
||||
|
||||
// Standard cron should have 5 or 6 fields
|
||||
// minute hour day-of-month month day-of-week [year]
|
||||
if (fields.length < 5 || fields.length > 6) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "Cron expression must have 5 or 6 fields"
|
||||
};
|
||||
}
|
||||
// Standard cron should have 5 or 6 fields
|
||||
// minute hour day-of-month month day-of-week [year]
|
||||
if (fields.length < 5 || fields.length > 6) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "Cron expression must have 5 or 6 fields",
|
||||
};
|
||||
}
|
||||
|
||||
// Define field constraints
|
||||
const fieldConstraints = [
|
||||
{ name: "minute", min: 0, max: 59 },
|
||||
{ name: "hour", min: 0, max: 23 },
|
||||
{ name: "day", min: 1, max: 31 },
|
||||
{ name: "month", min: 1, max: 12 },
|
||||
{ name: "weekday", min: 0, max: 6 },
|
||||
{ name: "year", min: 1970, max: 2099 } // Optional field
|
||||
];
|
||||
// Define field constraints
|
||||
const fieldConstraints = [
|
||||
{ name: "minute", min: 0, max: 59 },
|
||||
{ name: "hour", min: 0, max: 23 },
|
||||
{ name: "day", min: 1, max: 31 },
|
||||
{ name: "month", min: 1, max: 12 },
|
||||
{ name: "weekday", min: 0, max: 6 },
|
||||
{ name: "year", min: 1970, max: 2099 }, // Optional field
|
||||
];
|
||||
|
||||
// Valid characters in cron expressions
|
||||
const validChars = /^[\d/*,\-]+$/;
|
||||
// Valid characters in cron expressions
|
||||
const validChars = /^[\d/*,\-]+$/;
|
||||
|
||||
// Validate each field
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i];
|
||||
const constraint = fieldConstraints[i];
|
||||
// Validate each field
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i];
|
||||
const constraint = fieldConstraints[i];
|
||||
|
||||
// Check for valid characters
|
||||
if (!validChars.test(field)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid characters in ${constraint.name} field`
|
||||
};
|
||||
}
|
||||
// Check for valid characters
|
||||
if (!validChars.test(field)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid characters in ${constraint.name} field`,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle special characters
|
||||
if (field === "*") {
|
||||
continue; // Asterisk is valid for all fields
|
||||
}
|
||||
// Handle special characters
|
||||
if (field === "*") {
|
||||
continue; // Asterisk is valid for all fields
|
||||
}
|
||||
|
||||
// Handle lists (comma-separated values)
|
||||
if (field.includes(",")) {
|
||||
const values = field.split(",");
|
||||
for (const value of values) {
|
||||
if (!isValidRange(value, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${value}`
|
||||
};
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Handle lists (comma-separated values)
|
||||
if (field.includes(",")) {
|
||||
const values = field.split(",");
|
||||
for (const value of values) {
|
||||
if (!isValidRange(value, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${value}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle ranges (with hyphens)
|
||||
if (field.includes("-")) {
|
||||
const [start, end] = field.split("-").map(Number);
|
||||
if (
|
||||
start == null ||
|
||||
end == null ||
|
||||
start < constraint.min ||
|
||||
end > constraint.max ||
|
||||
start > end
|
||||
) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${field}`
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Handle ranges (with hyphens)
|
||||
if (field.includes("-")) {
|
||||
const [start, end] = field.split("-").map(Number);
|
||||
if (start == null || end == null || start < constraint.min || end > constraint.max || start > end) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${field}`,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle steps (with forward slash)
|
||||
if (field.includes("/")) {
|
||||
const [range, step] = field.split("/");
|
||||
if (range !== "*" && !isValidRange(range, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${range}`
|
||||
};
|
||||
}
|
||||
if (!isValidRange(step, 1, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid step value in ${constraint.name} field: ${step}`
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Handle steps (with forward slash)
|
||||
if (field.includes("/")) {
|
||||
const [range, step] = field.split("/");
|
||||
if (range !== "*" && !isValidRange(range, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${range}`,
|
||||
};
|
||||
}
|
||||
if (!isValidRange(step, 1, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid step value in ${constraint.name} field: ${step}`,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle plain numbers
|
||||
if (!isValidRange(field, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${field}`
|
||||
};
|
||||
}
|
||||
}
|
||||
// Handle plain numbers
|
||||
if (!isValidRange(field, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${field}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, message: "Valid cron expression" };
|
||||
return { isValid: true, message: "Valid cron expression" };
|
||||
}
|
||||
|
||||
function isValidRange(value, min, max) {
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && num >= min && num <= max;
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && num >= min && num <= max;
|
||||
}
|
||||
function 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;
|
||||
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,
|
||||
ValidateCronExpression,
|
||||
SortMonitor
|
||||
siteDataExtractFromDb,
|
||||
storeSiteData,
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
IsValidHost,
|
||||
IsValidNameServer,
|
||||
IsValidURL,
|
||||
IsValidPort,
|
||||
ValidateCronExpression,
|
||||
SortMonitor,
|
||||
RandomString,
|
||||
GetGameFromId,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ async function StatusColor(status) {
|
||||
UP: site.colors?.UP || "#00dfa2",
|
||||
DEGRADED: site.colors?.DEGRADED || "#e6ca61",
|
||||
DOWN: site.colors?.DOWN || "#ca3038",
|
||||
MAINTENANCE: site.colors?.MAINTENANCE || "#6679cc",
|
||||
NO_DATA: "#f1f5f8"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,175 +1,269 @@
|
||||
<script>
|
||||
import { formatDistanceToNow, formatDistance } from "date-fns";
|
||||
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";
|
||||
|
||||
import * as Accordion from "$lib/components/ui/accordion";
|
||||
import { l, f, fd, fdn } from "$lib/i18n/client";
|
||||
import { base } from "$app/paths";
|
||||
export let incident;
|
||||
export let index;
|
||||
export let lang;
|
||||
export let selectedLang = "en";
|
||||
let startTime = new Date(incident.start_date_time * 1000);
|
||||
let endTime = new Date();
|
||||
let nowTime = new Date();
|
||||
if (incident.end_date_time) {
|
||||
endTime = new Date(incident.end_date_time * 1000);
|
||||
}
|
||||
let incidentType = incident.incident_type;
|
||||
const lastedFor = fd(startTime, endTime, selectedLang);
|
||||
const startedAt = fdn(startTime, selectedLang);
|
||||
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 isFuture = false;
|
||||
//is future incident
|
||||
if (nowTime < startTime) {
|
||||
isFuture = true;
|
||||
}
|
||||
let incidentTimeStatus = "";
|
||||
if (nowTime < startTime) {
|
||||
incidentTimeStatus = "YET_TO_START";
|
||||
} else if (nowTime >= startTime && nowTime <= endTime) {
|
||||
incidentTimeStatus = "ONGOING";
|
||||
} else if (nowTime > endTime) {
|
||||
incidentTimeStatus = "COMPLETED";
|
||||
}
|
||||
|
||||
let incidentDateSummary = "";
|
||||
let maintenanceBadge = "";
|
||||
let maintenanceBadgeColor = "";
|
||||
if (!isFuture && incident.state != "RESOLVED") {
|
||||
incidentDateSummary = l(lang, "Started %startedAt, still ongoing", {
|
||||
startedAt
|
||||
});
|
||||
maintenanceBadge = "Maintenance in Progress";
|
||||
maintenanceBadgeColor = "text-maintenance-in-progress";
|
||||
} else if (!isFuture && incident.state == "RESOLVED") {
|
||||
incidentDateSummary = l(lang, "Started %startedAt, lasted for %lastedFor", {
|
||||
startedAt,
|
||||
lastedFor
|
||||
});
|
||||
maintenanceBadge = "Maintenance Completed";
|
||||
maintenanceBadgeColor = "text-maintenance-completed";
|
||||
} else if (isFuture && incident.state != "RESOLVED") {
|
||||
incidentDateSummary = l(lang, "Starts %startedAt", { startedAt });
|
||||
maintenanceBadge = "Upcoming Maintenance";
|
||||
maintenanceBadgeColor = "text-upcoming-maintenance";
|
||||
} else if (isFuture && incident.state == "RESOLVED") {
|
||||
incidentDateSummary = l(lang, "Starts %startedAt, will last for %lastedFor", {
|
||||
startedAt,
|
||||
lastedFor
|
||||
});
|
||||
maintenanceBadge = "Upcoming Maintenance";
|
||||
maintenanceBadgeColor = "text-upcoming-maintenance";
|
||||
}
|
||||
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">
|
||||
<Accordion.Item value="incident-0">
|
||||
<Accordion.Trigger class="px-4 hover:bg-muted hover:no-underline">
|
||||
<div class="w-full text-left hover:no-underline">
|
||||
{#if incidentType == "INCIDENT"}
|
||||
<p
|
||||
class="scroll-m-20 text-xs font-semibold leading-5 tracking-normal badge-{incident.state}"
|
||||
>
|
||||
{l(lang, incident.state)}
|
||||
</p>
|
||||
{:else if incidentType == "MAINTENANCE"}
|
||||
<p
|
||||
class="{maintenanceBadgeColor} scroll-m-20 text-xs font-semibold leading-5 tracking-normal"
|
||||
>
|
||||
{l(lang, maintenanceBadge)}
|
||||
</p>
|
||||
{/if}
|
||||
<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="scroll-m-20 text-lg font-medium tracking-tight">
|
||||
{incident.title}
|
||||
</p>
|
||||
{#if !!incidentDateSummary}
|
||||
<p
|
||||
class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground"
|
||||
>
|
||||
{incidentDateSummary}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
<div class="px-4 pt-2">
|
||||
{#if incident.monitors.length > 0}
|
||||
<div class="flex 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}
|
||||
<img src={base + monitor.image} class="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(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(lang, comment.state)}
|
||||
</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",
|
||||
selectedLang
|
||||
)}
|
||||
</time>
|
||||
<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>
|
||||
|
||||
<p class="mb-4 text-sm font-normal">
|
||||
{comment.comment}
|
||||
</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{:else if incidentType == "MAINTENANCE"}
|
||||
<ol class="relative mt-2 pl-0">
|
||||
{#each incident.comments as comment}
|
||||
<li class="relative pb-2 last:border-0">
|
||||
<time
|
||||
class=" mb-1 text-sm font-medium leading-none text-muted-foreground"
|
||||
>
|
||||
{f(
|
||||
new Date(comment.commented_at * 1000),
|
||||
"MMMM do yyyy, h:mm:ss a",
|
||||
selectedLang
|
||||
)}
|
||||
</time>
|
||||
<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>
|
||||
|
||||
<p class="mb-2 text-sm font-normal">
|
||||
{comment.comment}
|
||||
</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-sm font-medium">
|
||||
{l(lang, "No Updates Yet")}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
</div>
|
||||
<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,209 +1,205 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||
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;
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
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
|
||||
target="_blank"
|
||||
href="{incidentURL}/{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 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>
|
||||
|
||||
@@ -1,271 +1,251 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Settings, Bell, Loader, Copy, Check } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import Bell from "lucide-svelte/icons/bell";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Copy from "lucide-svelte/icons/copy";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
let apiKeys = [];
|
||||
let loaderLoadingAll = false;
|
||||
let loaderCreateNew = false;
|
||||
let newAPIKeyName = "";
|
||||
let newKeyResp = {};
|
||||
let showCreateModal = false;
|
||||
let apiKeys = [];
|
||||
let loaderLoadingAll = false;
|
||||
let loaderCreateNew = false;
|
||||
let newAPIKeyName = "";
|
||||
let newKeyResp = {};
|
||||
let showCreateModal = false;
|
||||
|
||||
async function loadAPIKeys() {
|
||||
loaderLoadingAll = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "getAPIKeys",
|
||||
data: {}
|
||||
})
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
apiKeys = resp;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderLoadingAll = false;
|
||||
}
|
||||
}
|
||||
async function loadAPIKeys() {
|
||||
loaderLoadingAll = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "getAPIKeys",
|
||||
data: {}
|
||||
})
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
apiKeys = resp;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderLoadingAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createNew() {
|
||||
newKeyResp = {};
|
||||
loaderCreateNew = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "createNewApiKey",
|
||||
data: {
|
||||
name: newAPIKeyName
|
||||
}
|
||||
})
|
||||
});
|
||||
newKeyResp = await apiResp.json();
|
||||
loadAPIKeys();
|
||||
showCreateModal = false;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderCreateNew = false;
|
||||
}
|
||||
}
|
||||
async function createNew() {
|
||||
newKeyResp = {};
|
||||
loaderCreateNew = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "createNewApiKey",
|
||||
data: {
|
||||
name: newAPIKeyName
|
||||
}
|
||||
})
|
||||
});
|
||||
newKeyResp = await apiResp.json();
|
||||
loadAPIKeys();
|
||||
showCreateModal = false;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderCreateNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadAPIKeys();
|
||||
});
|
||||
onMount(() => {
|
||||
loadAPIKeys();
|
||||
});
|
||||
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(newKeyResp.apiKey);
|
||||
}
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(newKeyResp.apiKey);
|
||||
}
|
||||
|
||||
function updateStatus(apiKey) {
|
||||
apiKey.status = apiKey.status == "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "updateApiKeyStatus",
|
||||
data: {
|
||||
id: apiKey.id,
|
||||
status: apiKey.status
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
function updateStatus(apiKey) {
|
||||
apiKey.status = apiKey.status == "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "updateApiKeyStatus",
|
||||
data: {
|
||||
id: apiKey.id,
|
||||
status: apiKey.status
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<div>
|
||||
{#if loaderLoadingAll}
|
||||
<Loader class="mt-6 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
on:click={(e) => {
|
||||
showCreateModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" /> Create New API Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 flex justify-between">
|
||||
<div>
|
||||
{#if loaderLoadingAll}
|
||||
<Loader class="mt-6 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button
|
||||
on:click={(e) => {
|
||||
showCreateModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" /> Create New API Key
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if !!newKeyResp && !!newKeyResp.apiKey}
|
||||
<div class="my-4 rounded-lg border border-green-700 bg-green-800 bg-opacity-20 p-4">
|
||||
<p class="font-medium">
|
||||
<picture class="mr-1 inline-block">
|
||||
<source
|
||||
srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.webp"
|
||||
type="image/webp"
|
||||
/>
|
||||
<img
|
||||
src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif"
|
||||
alt="🎉"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</picture>
|
||||
API Key Created
|
||||
</p>
|
||||
<p
|
||||
class="relative my-2 rounded-sm border bg-card px-4 py-2 pr-8 font-mono text-sm font-medium"
|
||||
>
|
||||
{newKeyResp.apiKey}
|
||||
<div class="my-4 rounded-lg border border-green-700 bg-green-800 bg-opacity-20 p-4">
|
||||
<p class="font-medium">
|
||||
<picture class="mr-1 inline-block">
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.webp" type="image/webp" />
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif" alt="🎉" width="24" height="24" />
|
||||
</picture>
|
||||
API Key Created
|
||||
</p>
|
||||
<p class="relative my-2 rounded-sm border bg-card px-4 py-2 pr-8 font-mono text-sm font-medium">
|
||||
{newKeyResp.apiKey}
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="copybtn absolute right-2 top-2 h-5 w-5 p-1"
|
||||
on:click={copyKey}
|
||||
>
|
||||
<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>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Your new API key has been created. It will be <b class="uppercase underline"
|
||||
>not be shown again</b
|
||||
>, so make sure to save it.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="icon" variant="ghost" class="copybtn absolute right-2 top-2 h-5 w-5 p-1" on:click={copyKey}>
|
||||
<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>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Your new API key has been created. It will be <b class="uppercase underline">not be shown again</b>, so make sure
|
||||
to save it.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<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"
|
||||
>Name</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Key</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
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{#each apiKeys as apiKey}
|
||||
<tr>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{apiKey.name}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{apiKey.masked_key.slice(-32)}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{moment
|
||||
.utc(apiKey.created_at, "YYYY-MM-DD HH:mm:ss")
|
||||
.local()
|
||||
.format("YYYY-MM-DD HH:mm:ss")}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
value=""
|
||||
class="peer sr-only"
|
||||
checked={apiKey.status == "ACTIVE"}
|
||||
on:change={() => {
|
||||
updateStatus(apiKey);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
|
||||
></div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">Name</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500">Key</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
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{#each apiKeys as apiKey}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200">
|
||||
{apiKey.name}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200">
|
||||
{apiKey.masked_key.slice(-32)}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200">
|
||||
{moment.utc(apiKey.created_at, "YYYY-MM-DD HH:mm:ss").local().format("YYYY-MM-DD HH:mm:ss")}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200">
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={$page.data.user.role == "member"}
|
||||
value=""
|
||||
class="peer sr-only"
|
||||
checked={apiKey.status == "ACTIVE"}
|
||||
on:change={() => {
|
||||
updateStatus(apiKey);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 peer-disabled:opacity-55 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
|
||||
></div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if showCreateModal}
|
||||
<div
|
||||
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
showCreateModal = false;
|
||||
}}
|
||||
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
|
||||
>
|
||||
<X class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Create a new API Key</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
API keys are used to authenticate your requests to the API. They are unique to
|
||||
your account and should be kept secret.
|
||||
</p>
|
||||
<hr class="my-4" />
|
||||
<form on:submit|preventDefault={createNew}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label for="newAPIKeyName">Name</Label>
|
||||
<Input
|
||||
bind:value={newAPIKeyName}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="newAPIKeyName"
|
||||
placeholder="eg. My API Key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Button variant="secondary" type="submit" disabled={loaderCreateNew}>
|
||||
Create
|
||||
{#if loaderCreateNew}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm">
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
showCreateModal = false;
|
||||
}}
|
||||
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
|
||||
>
|
||||
<X class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Create a new API Key</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
API keys are used to authenticate your requests to the API. They are unique to your account and should be kept
|
||||
secret.
|
||||
</p>
|
||||
<hr class="my-4" />
|
||||
<form on:submit|preventDefault={createNew}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label for="newAPIKeyName">Name</Label>
|
||||
<Input
|
||||
bind:value={newAPIKeyName}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="newAPIKeyName"
|
||||
required
|
||||
placeholder="eg. My API Key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Button variant="secondary" type="submit" disabled={loaderCreateNew}>
|
||||
Create
|
||||
{#if loaderCreateNew}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,221 +1,355 @@
|
||||
<script>
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Plus, X } from "lucide-svelte";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { base } from "$app/paths";
|
||||
import { Loader, Info } from "lucide-svelte";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { base } from "$app/paths";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import { Tooltip } from "bits-ui";
|
||||
|
||||
export let data;
|
||||
export let data;
|
||||
|
||||
let metaTags = [];
|
||||
function addNewRow() {
|
||||
metaTags = [
|
||||
...metaTags,
|
||||
{
|
||||
key: "",
|
||||
value: ""
|
||||
}
|
||||
];
|
||||
}
|
||||
let analyticsSupported = [
|
||||
{
|
||||
id: "",
|
||||
type: "GA",
|
||||
name: "Google Analytics",
|
||||
script: "https://unpkg.com/@analytics/google-analytics@1.0.7/dist/@analytics/google-analytics.min.js"
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
type: "AMPLITUDE",
|
||||
name: "Amplitude",
|
||||
script: "https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js"
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
type: "MIXPANEL",
|
||||
name: "MixPanel",
|
||||
script: "https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js"
|
||||
}
|
||||
];
|
||||
function removeRow(i) {
|
||||
metaTags = metaTags.filter((_, index) => index !== i);
|
||||
}
|
||||
let metaTags = [];
|
||||
function addNewRow() {
|
||||
metaTags = [
|
||||
...metaTags,
|
||||
{
|
||||
key: "",
|
||||
value: ""
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
metaTags = siteDataExtractFromDb(data.siteData, {
|
||||
metaTags: JSON.stringify(metaTags)
|
||||
}).metaTags;
|
||||
function removeRow(i) {
|
||||
metaTags = metaTags.filter((_, index) => index !== i);
|
||||
}
|
||||
|
||||
let analytics = siteDataExtractFromDb(data.siteData, {
|
||||
analytics: []
|
||||
}).analytics;
|
||||
metaTags = siteDataExtractFromDb(data.siteData, {
|
||||
metaTags: JSON.stringify(metaTags)
|
||||
}).metaTags;
|
||||
|
||||
//merge analyticsSupported with analytics
|
||||
analytics = analyticsSupported.map((supported) => {
|
||||
let found = analytics.find((a) => a.type === supported.type);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
return supported;
|
||||
});
|
||||
let formState = "idle";
|
||||
let formStateAnalytics = "idle";
|
||||
|
||||
let formState = "idle";
|
||||
let formStateAnalytics = "idle";
|
||||
async function formSubmit() {
|
||||
formState = "loading";
|
||||
let resp = await storeSiteData({
|
||||
metaTags: JSON.stringify(metaTags)
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let metaError = "";
|
||||
async function formSubmit() {
|
||||
metaError = "";
|
||||
formState = "loading";
|
||||
let resp = await storeSiteData({
|
||||
metaTags: JSON.stringify(metaTags)
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
metaError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let seoError = "";
|
||||
async function formSubmitAnalytics() {
|
||||
seoError = "";
|
||||
formStateAnalytics = "loading";
|
||||
|
||||
async function formSubmitAnalytics() {
|
||||
formStateAnalytics = "loading";
|
||||
let resp = await storeSiteData({
|
||||
analytics: JSON.stringify(analytics)
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formStateAnalytics = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let dataToSave = {};
|
||||
selectedAnalytics.requirements.forEach((req) => {
|
||||
dataToSave[req.label] = req.value;
|
||||
});
|
||||
let finalData = {};
|
||||
finalData[selectedAnalytics.key] = JSON.stringify({
|
||||
requirements: dataToSave,
|
||||
isEnabled: selectedAnalytics.isEnabled
|
||||
});
|
||||
let resp = await storeSiteData(finalData);
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formStateAnalytics = "idle";
|
||||
if (data.error) {
|
||||
seoError = data.error;
|
||||
return;
|
||||
} else {
|
||||
selectedAnalytics.activeInSite = selectedAnalytics.isEnabled;
|
||||
//update activeInSite in analyticsDropdownItems also
|
||||
let index = analyticsDropdownItems.findIndex((item) => item.label === selectedAnalytics.label);
|
||||
analyticsDropdownItems[index].activeInSite = true;
|
||||
}
|
||||
}
|
||||
let selectedAnalytics;
|
||||
let analyticsDropdownItems = [
|
||||
{
|
||||
label: "Google Analytics",
|
||||
logo: `/ga.png`,
|
||||
key: "analytics.googleTagManager",
|
||||
isEnabled: false,
|
||||
requirements: [
|
||||
{
|
||||
label: "Measurement ID",
|
||||
type: "text",
|
||||
placeholder: "G-S05E5E5E5E5",
|
||||
required: true,
|
||||
value: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Plausible",
|
||||
logo: `/plausible.png`,
|
||||
key: "analytics.plausible",
|
||||
isEnabled: false,
|
||||
requirements: [
|
||||
{
|
||||
label: "Domain",
|
||||
type: "text",
|
||||
placeholder: "kener.ing",
|
||||
required: true,
|
||||
value: new URL(data.siteData.siteURL).hostname
|
||||
},
|
||||
{
|
||||
label: "API",
|
||||
type: "text",
|
||||
placeholder: "https://plausible.io/api/event",
|
||||
required: true,
|
||||
value: "https://plausible.io/api/event"
|
||||
},
|
||||
{
|
||||
label: "Script Source",
|
||||
type: "text",
|
||||
placeholder: "https://plausible.io/js/script.pageview-props.tagged-events.js",
|
||||
required: true,
|
||||
value: "https://plausible.io/js/script.pageview-props.tagged-events.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "MixPanel",
|
||||
logo: `/mixpanel.png`,
|
||||
key: "analytics.mixpanel",
|
||||
isEnabled: false,
|
||||
requirements: [
|
||||
{
|
||||
label: "Project Token",
|
||||
type: "text",
|
||||
placeholder: "YOUR_PROJECT_TOKEN",
|
||||
required: true,
|
||||
value: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Amplitude",
|
||||
logo: `/amplitude.png`,
|
||||
key: "analytics.amplitude",
|
||||
isEnabled: false,
|
||||
requirements: [
|
||||
{
|
||||
label: "Amplitude API Key",
|
||||
type: "text",
|
||||
placeholder: "API key for your Amplitude project",
|
||||
required: true,
|
||||
value: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Microsoft Clarity",
|
||||
logo: `/clarity.png`,
|
||||
key: "analytics.clarity",
|
||||
isEnabled: false,
|
||||
requirements: [
|
||||
{
|
||||
label: "Project ID",
|
||||
type: "text",
|
||||
placeholder: "Project ID for your Microsoft Clarity project",
|
||||
required: true,
|
||||
value: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Umami",
|
||||
logo: `/umami.png`,
|
||||
key: "analytics.umami",
|
||||
isEnabled: false,
|
||||
requirements: [
|
||||
{
|
||||
label: "Website ID",
|
||||
type: "text",
|
||||
placeholder: "Website ID for your umami script and domain",
|
||||
required: true,
|
||||
value: ""
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
analyticsDropdownItems = analyticsDropdownItems.map((item) => {
|
||||
let dbData = data.siteData[item.key];
|
||||
if (!!dbData) {
|
||||
item.isEnabled = dbData.isEnabled;
|
||||
item.requirements.forEach((req) => {
|
||||
req.value = dbData.requirements[req.label];
|
||||
});
|
||||
item.activeInSite = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
selectedAnalytics = analyticsDropdownItems[0];
|
||||
</script>
|
||||
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Analytics</Card.Title>
|
||||
<Card.Description>
|
||||
Add your analytics ID/Key here. You can add multiple analytics providers.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitAnalytics}>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each analytics as analytic}
|
||||
<div class="col-span-1">
|
||||
<Label for={analytic.type}>
|
||||
{analytic.name}
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Add your {analytic.name} ID/Key here.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={analytic.id}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id={analytic.type}
|
||||
placeholder="{analytic.type} ID/Key"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Analytics</Card.Title>
|
||||
<Card.Description>Add your analytics ID/Key here. You can add multiple analytics providers.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="mt-4 grid grid-cols-4 overflow-hidden rounded-md border">
|
||||
<div class="col-span-1 flex flex-col border-r">
|
||||
{#each analyticsDropdownItems as item}
|
||||
<Button
|
||||
variant={!!selectedAnalytics && selectedAnalytics.label === item.label ? "primary" : "ghost"}
|
||||
class="flex items-center justify-between gap-x-2 rounded-none border-b text-sm last:border-none"
|
||||
on:click={() => (selectedAnalytics = item)}
|
||||
>
|
||||
<div class="flex flex-row gap-x-3">
|
||||
<img src={base + item.logo} class="w-5" alt={item.label} />
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
{#if item.activeInSite && item.isEnabled}
|
||||
<div class="rounded-sm">
|
||||
<span class="relative flex size-2">
|
||||
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
|
||||
></span>
|
||||
<span class="relative inline-flex size-2 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-end">
|
||||
<Button type="submit" disabled={formStateAnalytics === "loading"}>
|
||||
Save
|
||||
{#if formStateAnalytics === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
<form on:submit|preventDefault={formSubmitAnalytics} class="col-span-3 flex flex-col justify-between p-3 py-2">
|
||||
{#if !!selectedAnalytics}
|
||||
<div>
|
||||
<div class="mb-2 text-sm font-medium text-muted-foreground">
|
||||
Add details for your {selectedAnalytics.label} account
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
{#each selectedAnalytics.requirements as req}
|
||||
<div class="col-span-2 flex flex-col gap-y-2">
|
||||
<Label for={req.label}>{req.label}</Label>
|
||||
<Input
|
||||
bind:value={req.value}
|
||||
class=""
|
||||
type={req.type}
|
||||
placeholder={req.placeholder}
|
||||
bind:required={req.required}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mt-2 flex flex-row justify-between">
|
||||
<div class="flex flex-row gap-x-2">
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="peer sr-only"
|
||||
checked={selectedAnalytics.isEnabled}
|
||||
on:change={() => {
|
||||
selectedAnalytics.isEnabled = !selectedAnalytics.isEnabled;
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
|
||||
></div>
|
||||
</label>
|
||||
<div class="pt-2.5 text-sm font-medium text-muted-foreground">
|
||||
{selectedAnalytics.label} is
|
||||
<i class="underline">{!selectedAnalytics.isEnabled ? "disabled" : "enabled"}</i> for your site
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button type="submit" disabled={formStateAnalytics === "loading"}>
|
||||
Save Changes for {selectedAnalytics.label}
|
||||
{#if formStateAnalytics === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if !!seoError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{seoError}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Search Engine Optimization</Card.Title>
|
||||
<Card.Description>
|
||||
Add your meta tags here. You can add multiple meta tags.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" use:autoAnimate on:submit|preventDefault={formSubmit}>
|
||||
{#each metaTags as metaTag, i}
|
||||
<div class="flex w-full flex-row justify-between gap-2">
|
||||
<div class="grid grid-cols-12 gap-x-2">
|
||||
<div class="col-span-4">
|
||||
<Label for="key">
|
||||
Meta Name
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
<meta name="{metaTag.key}" content="{metaTag.value}">
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={metaTag.key}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="key"
|
||||
placeholder="eg. kener"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-7">
|
||||
<Label for="value">Content</Label>
|
||||
<Input
|
||||
bind:value={metaTag.value}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="value"
|
||||
placeholder="eg. kener"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Button variant="ghost" class="mt-8" on:click={() => removeRow(i)}>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="relative">
|
||||
<hr class="border-1 border-border-input relative top-4 h-px border-dashed" />
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Search Engine Optimization</Card.Title>
|
||||
<Card.Description>Add your meta tags here. You can add multiple meta tags.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" use:autoAnimate on:submit|preventDefault={formSubmit}>
|
||||
{#each metaTags as metaTag, i}
|
||||
<div class="flex w-full flex-row justify-between gap-2">
|
||||
<div class="grid grid-cols-12 gap-x-2">
|
||||
<div class="col-span-4">
|
||||
<Label for="key">
|
||||
Meta Name
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
<meta name="{metaTag.key}" content="{metaTag.value}">
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input bind:value={metaTag.key} class="mt-2" type="text" id="key" placeholder="eg. kener" />
|
||||
</div>
|
||||
<div class="col-span-7">
|
||||
<Label for="value">Content</Label>
|
||||
<Input bind:value={metaTag.value} class="mt-2" type="text" id="value" placeholder="eg. kener" />
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Button variant="ghost" class="mt-8" on:click={() => removeRow(i)}>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="relative">
|
||||
<hr class="border-1 border-border-input relative top-4 h-px border-dashed" />
|
||||
|
||||
<Button
|
||||
on:click={addNewRow}
|
||||
variant="secondary"
|
||||
class="absolute left-1/2 h-8 w-8 -translate-x-1/2 p-0 "
|
||||
>
|
||||
<Plus class="h-4 w-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex w-full justify-end" style="margin-top:32px">
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
<Button on:click={addNewRow} variant="secondary" class="absolute left-1/2 h-8 w-8 -translate-x-1/2 p-0 ">
|
||||
<Plus class="h-4 w-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex w-full justify-end gap-x-2" style="margin-top:32px">
|
||||
{#if !!metaError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{metaError}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save Meta Tags
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -1,381 +1,452 @@
|
||||
<script>
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { tooltipAction } from "svelte-legos";
|
||||
import { base } from "$app/paths";
|
||||
import { Loader, Info, X } from "lucide-svelte";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { tooltipAction } from "svelte-legos";
|
||||
import { base } from "$app/paths";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import * as Alert from "$lib/components/ui/alert";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
|
||||
export let data;
|
||||
export let data;
|
||||
|
||||
let siteInformation = {
|
||||
title: "",
|
||||
siteName: "",
|
||||
siteURL: "",
|
||||
home: "",
|
||||
logo: "",
|
||||
favicon: ""
|
||||
};
|
||||
let siteInformation = {
|
||||
title: "",
|
||||
siteName: "",
|
||||
siteURL: "",
|
||||
home: "",
|
||||
logo: "",
|
||||
favicon: ""
|
||||
};
|
||||
|
||||
let logoFile;
|
||||
let logoFile;
|
||||
|
||||
let formErrorMessage = "";
|
||||
let formErrorMessage = "";
|
||||
|
||||
siteInformation = siteDataExtractFromDb(data.siteData, siteInformation);
|
||||
siteInformation = siteDataExtractFromDb(data.siteData, siteInformation);
|
||||
|
||||
let formState = "idle";
|
||||
async function formSubmit() {
|
||||
formErrorMessage = "";
|
||||
formState = "loading";
|
||||
let resp = await storeSiteData(siteInformation);
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
formErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let formState = "idle";
|
||||
async function formSubmit() {
|
||||
formErrorMessage = "";
|
||||
formState = "loading";
|
||||
let resp = await storeSiteData(siteInformation);
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
formErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileChangeFavicon(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
siteInformation.favicon = reader.result;
|
||||
uploadFunction(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
let uploadLogoStatus = "";
|
||||
let uploadingLogo = false;
|
||||
async function handleFileChangeLogo(event) {
|
||||
event.preventDefault();
|
||||
function handleFileChangeFavicon(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
siteInformation.favicon = reader.result;
|
||||
uploadFunction(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
let uploadLogoStatus = "";
|
||||
let uploadingLogo = false;
|
||||
async function handleFileChangeLogo(event) {
|
||||
event.preventDefault();
|
||||
|
||||
uploadLogoStatus = "";
|
||||
const file = event.target.files[0];
|
||||
if (!file) {
|
||||
uploadLogoStatus = "Please select a file to upload.";
|
||||
return;
|
||||
}
|
||||
uploadLogoStatus = "";
|
||||
const file = event.target.files[0];
|
||||
if (!file) {
|
||||
uploadLogoStatus = "Please select a file to upload.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 100000) {
|
||||
uploadLogoStatus = "File size should be less than 100KB.";
|
||||
return;
|
||||
}
|
||||
if (file.size > 102400) {
|
||||
uploadLogoStatus = "File size should be less than 100KB.";
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
uploadingLogo = true;
|
||||
try {
|
||||
const response = await fetch(base + "/manage/app/upload", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
uploadingLogo = true;
|
||||
try {
|
||||
const response = await fetch(base + "/manage/app/upload", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
siteInformation.logo = "/uploads/" + result.filename;
|
||||
} else {
|
||||
uploadLogoStatus = "Failed to upload file.";
|
||||
}
|
||||
} catch (error) {
|
||||
uploadLogoStatus = "An error occurred while uploading the file.";
|
||||
} finally {
|
||||
uploadingLogo = false;
|
||||
}
|
||||
}
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
siteInformation.logo = "/uploads/" + result.filename;
|
||||
} else {
|
||||
uploadLogoStatus = "Failed to upload file.";
|
||||
}
|
||||
} catch (error) {
|
||||
uploadLogoStatus = "An error occurred while uploading the file.";
|
||||
} finally {
|
||||
uploadingLogo = false;
|
||||
}
|
||||
}
|
||||
|
||||
let uploadFaviconStatus = "";
|
||||
let uploadingFavicon = false;
|
||||
async function handleFaviconChangeLogo(event) {
|
||||
event.preventDefault();
|
||||
let uploadFaviconStatus = "";
|
||||
let uploadingFavicon = false;
|
||||
async function handleFaviconChangeLogo(event) {
|
||||
event.preventDefault();
|
||||
|
||||
uploadFaviconStatus = "";
|
||||
const file = event.target.files[0];
|
||||
if (!file) {
|
||||
uploadFaviconStatus = "Please select a file to upload.";
|
||||
return;
|
||||
}
|
||||
uploadFaviconStatus = "";
|
||||
const file = event.target.files[0];
|
||||
if (!file) {
|
||||
uploadFaviconStatus = "Please select a file to upload.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 20000) {
|
||||
uploadFaviconStatus = "File size should be less than 20KB.";
|
||||
return;
|
||||
}
|
||||
if (file.size > 20480) {
|
||||
uploadFaviconStatus = "File size should be less than 20KB.";
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
uploadingFavicon = true;
|
||||
try {
|
||||
const response = await fetch(base + "/manage/app/upload", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
uploadingFavicon = true;
|
||||
try {
|
||||
const response = await fetch(base + "/manage/app/upload", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
siteInformation.favicon = "/uploads/" + result.filename;
|
||||
} else {
|
||||
uploadFaviconStatus = "Failed to upload file.";
|
||||
}
|
||||
} catch (error) {
|
||||
uploadFaviconStatus = "An error occurred while uploading the file.";
|
||||
} finally {
|
||||
uploadingFavicon = false;
|
||||
}
|
||||
}
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
siteInformation.favicon = "/uploads/" + result.filename;
|
||||
} else {
|
||||
uploadFaviconStatus = "Failed to upload file.";
|
||||
}
|
||||
} catch (error) {
|
||||
uploadFaviconStatus = "An error occurred while uploading the file.";
|
||||
} finally {
|
||||
uploadingFavicon = false;
|
||||
}
|
||||
}
|
||||
let typeOfLogoUpload = siteInformation.logo.startsWith("http") ? "URL" : "FILE";
|
||||
let typeOfFaviconUpload = siteInformation.favicon.startsWith("http") ? "URL" : "FILE";
|
||||
</script>
|
||||
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Site Configuration</Card.Title>
|
||||
<Card.Description>Configure your site information here.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmit}>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="title" class="">
|
||||
Site Title
|
||||
<span class="text-red-500">*</span>
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Site Configuration</Card.Title>
|
||||
<Card.Description>Configure your site information here.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if !!base}
|
||||
<Alert.Root class="my-2">
|
||||
<Alert.Title>Sub path</Alert.Title>
|
||||
<Alert.Description>Your site is running in a subpath <span class="">{base}</span></Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmit}>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="title" class="">
|
||||
Site Title
|
||||
<span class="text-red-500">*</span>
|
||||
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Site Title will be the page title of your site.
|
||||
<title></title>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={siteInformation.title}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="title"
|
||||
placeholder="eg. kener"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<Label for="siteName">
|
||||
Site Name
|
||||
<span class="text-red-500">*</span>
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Site Title will be the page title of your site. <title></title>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={siteInformation.title}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="title"
|
||||
placeholder="eg. kener"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<Label for="siteName">
|
||||
Site Name
|
||||
<span class="text-red-500">*</span>
|
||||
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
This is the name of your site.<br /> It will be shown in the nav
|
||||
bar as brand name.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={siteInformation.siteName}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="siteName"
|
||||
placeholder="eg. Kener.ing"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="siteURL">
|
||||
Site URL
|
||||
<span class="text-red-500">*</span>
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
The URL your kener is hosted on.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={siteInformation.siteURL}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="siteURL"
|
||||
placeholder="eg. https://status.example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="siteURL">
|
||||
Home Location
|
||||
<span class="text-red-500">*</span>
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Where to go when someone clicks on the brand logo in the navbar.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
This is the name of your site.<br /> It will be shown in the nav bar as brand name.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={siteInformation.siteName}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="siteName"
|
||||
placeholder="eg. Kener.ing"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="siteURL">
|
||||
Site URL
|
||||
<span class="text-red-500">*</span>
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
The URL your kener is hosted on.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Input
|
||||
bind:value={siteInformation.siteURL}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="siteURL"
|
||||
placeholder="eg. https://status.example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="siteURL">
|
||||
Home Location
|
||||
<span class="text-red-500">*</span>
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Where to go when someone clicks on the brand logo in the navbar.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
bind:value={siteInformation.home}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="home"
|
||||
placeholder="/ or https://example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
bind:value={siteInformation.home}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="home"
|
||||
placeholder="/ or https://example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col justify-evenly gap-2">
|
||||
<div>
|
||||
<Label for="logo">Logo</Label>
|
||||
</div>
|
||||
<div class="flex gap-x-2">
|
||||
{#if !!siteInformation.logo}
|
||||
<div class="relative">
|
||||
<Label
|
||||
for="logo"
|
||||
class="inline-block h-[60px] w-[60px] cursor-pointer rounded-sm border p-1"
|
||||
>
|
||||
<img
|
||||
src={base + siteInformation.logo}
|
||||
class="w-fit hover:scale-95"
|
||||
alt=""
|
||||
/>
|
||||
</Label>
|
||||
<div class="flex w-full flex-col justify-evenly gap-2">
|
||||
<div>
|
||||
<Label for="logo">Logo</Label>
|
||||
</div>
|
||||
<div class="flex gap-x-4">
|
||||
{#if !!siteInformation.logo}
|
||||
<div class="relative mt-2.5">
|
||||
<Label for="logo" class="inline-block h-[60px] w-[60px] cursor-pointer rounded-sm border p-1">
|
||||
<GMI src={siteInformation.logo} classList="w-fit hover:scale-95" alt="" />
|
||||
</Label>
|
||||
|
||||
<div class="absolute -right-2 -top-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="h-5 w-5 rounded-full border p-1"
|
||||
size="icon"
|
||||
on:click={() => (siteInformation.logo = "")}
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if uploadingLogo}
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform rounded-sm bg-[rgba(0,0,0,0.5)] p-0.5"
|
||||
>
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<Input
|
||||
class=""
|
||||
id="logo"
|
||||
type="file"
|
||||
accept=".jpg, .jpeg, .png"
|
||||
disabled={uploadingLogo}
|
||||
on:change={handleFileChangeLogo}
|
||||
/>
|
||||
<p class="mt-1 text-xs font-medium">
|
||||
Please upload a square image of max size 100KB
|
||||
</p>
|
||||
<p class="mt-1 text-xs font-medium text-destructive">
|
||||
{uploadLogoStatus}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="favicon">Favicon</Label>
|
||||
</div>
|
||||
<div class="flex gap-x-2">
|
||||
{#if !!siteInformation.favicon}
|
||||
<div class="relative">
|
||||
<Label
|
||||
for="favicon"
|
||||
class="inline-block h-[40px] w-[40px] cursor-pointer rounded-sm border p-1"
|
||||
>
|
||||
<img
|
||||
src={base + siteInformation.favicon}
|
||||
class="w-fit hover:scale-95"
|
||||
alt=""
|
||||
/>
|
||||
</Label>
|
||||
<div class="absolute -right-2 -top-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="h-5 w-5 rounded-full border p-1"
|
||||
size="icon"
|
||||
on:click={() => (siteInformation.logo = "")}
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if uploadingLogo}
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform rounded-sm bg-[rgba(0,0,0,0.5)] p-0.5"
|
||||
>
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-x-2">
|
||||
<div>
|
||||
<Label for="typeOfLogoUpload" class="text-sm font-medium">Method</Label>
|
||||
<Select.Root
|
||||
portal={null}
|
||||
onSelectedChange={(e) => {
|
||||
typeOfLogoUpload = e.value;
|
||||
}}
|
||||
selected={{
|
||||
value: typeOfLogoUpload,
|
||||
label: typeOfLogoUpload
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="mt-2 w-[200px]" id="typeOfLogoUpload">
|
||||
<Select.Value bind:value={typeOfLogoUpload} placeholder="Logo Type" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Logo Type</Select.Label>
|
||||
<Select.Item value="URL" label="URL" class="text-sm font-medium">URL</Select.Item>
|
||||
<Select.Item value="FILE" label="FILE" class="text-sm font-medium">FILE</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
{#if typeOfLogoUpload === "URL"}
|
||||
<div>
|
||||
<Label for="logo" class="text-sm font-medium">Please provide a URL of the image</Label>
|
||||
<Input
|
||||
bind:value={siteInformation.logo}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="logo"
|
||||
placeholder="https://example.com/logo.png"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Label for="logo" class="text-sm font-medium">Please upload a square image of max size 100KB</Label>
|
||||
<Input
|
||||
class="mt-2"
|
||||
id="logo"
|
||||
type="file"
|
||||
accept=".jpg, .jpeg, .png, .svg"
|
||||
disabled={uploadingLogo}
|
||||
on:change={handleFileChangeLogo}
|
||||
/>
|
||||
|
||||
<div class="absolute -right-2 -top-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="h-5 w-5 rounded-full border p-1"
|
||||
size="icon"
|
||||
on:click={() => (siteInformation.favicon = "")}
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if uploadingFavicon}
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform rounded-sm bg-[rgba(0,0,0,0.5)] p-0.5"
|
||||
>
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<Input
|
||||
class=""
|
||||
id="logo"
|
||||
type="file"
|
||||
accept=".jpg, .jpeg, .png, .ico"
|
||||
disabled={uploadingFavicon}
|
||||
on:change={handleFaviconChangeLogo}
|
||||
/>
|
||||
<p class="mt-1 text-xs font-medium">
|
||||
Please upload a square image of max size 20KB
|
||||
</p>
|
||||
<p class="mt-1 text-xs font-medium text-destructive">
|
||||
{uploadFaviconStatus}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<p class="px-4 pt-3 text-sm">
|
||||
<span class="text-red-500">
|
||||
{formErrorMessage}
|
||||
</span>
|
||||
</p>
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
<p class="mt-1 text-xs font-medium text-destructive">
|
||||
{uploadLogoStatus}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="favicon">Favicon</Label>
|
||||
</div>
|
||||
<div class="flex gap-x-4">
|
||||
{#if !!siteInformation.favicon}
|
||||
<div class="relative mt-8">
|
||||
<Label for="favicon" class="inline-block h-[40px] w-[40px] cursor-pointer rounded-sm border p-1">
|
||||
<GMI src={siteInformation.favicon} classList="w-fit hover:scale-95" alt="" />
|
||||
</Label>
|
||||
|
||||
<div class="absolute -right-2 -top-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="h-5 w-5 rounded-full border p-1"
|
||||
size="icon"
|
||||
on:click={() => (siteInformation.favicon = "")}
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if uploadingFavicon}
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform rounded-sm bg-[rgba(0,0,0,0.5)] p-0.5"
|
||||
>
|
||||
<Loader class="h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-x-2">
|
||||
<div>
|
||||
<Label for="typeOfFaviconUpload" class="text-sm font-medium">Method</Label>
|
||||
<Select.Root
|
||||
portal={null}
|
||||
onSelectedChange={(e) => {
|
||||
typeOfFaviconUpload = e.value;
|
||||
}}
|
||||
selected={{
|
||||
value: typeOfFaviconUpload,
|
||||
label: typeOfFaviconUpload
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="mt-2 w-[200px]" id="typeOfFaviconUpload">
|
||||
<Select.Value bind:value={typeOfFaviconUpload} placeholder="Favicon Type" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Favicon Type</Select.Label>
|
||||
<Select.Item value="URL" label="URL" class="text-sm font-medium">URL</Select.Item>
|
||||
<Select.Item value="FILE" label="FILE" class="text-sm font-medium">FILE</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
{#if typeOfFaviconUpload === "URL"}
|
||||
<div>
|
||||
<Label for="favicon" class="text-sm font-medium">Please provide a URL of the image</Label>
|
||||
<Input
|
||||
bind:value={siteInformation.favicon}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="favicon"
|
||||
placeholder="https://example.com/favicon.png"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Label for="favicon" class="text-sm font-medium">Please upload a square image of max size 20KB</Label>
|
||||
<Input
|
||||
class="mt-2"
|
||||
id="favicon"
|
||||
type="file"
|
||||
accept=".jpg, .jpeg, .png, .svg"
|
||||
disabled={uploadingFavicon}
|
||||
on:change={handleFaviconChangeLogo}
|
||||
/>
|
||||
|
||||
<p class="mt-1 text-xs font-medium text-destructive">
|
||||
{uploadFaviconStatus}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
<p class="py-2 text-sm font-medium text-destructive">
|
||||
{formErrorMessage}
|
||||
</p>
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- GitHub Settings -->
|
||||
|
||||
@@ -1,381 +1,449 @@
|
||||
<script>
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Plus, X, Info } from "lucide-svelte";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { base } from "$app/paths";
|
||||
import { Loader } from "lucide-svelte";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import ColorPicker from "svelte-awesome-color-picker";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { base } from "$app/paths";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import ColorPicker from "svelte-awesome-color-picker";
|
||||
|
||||
export let data;
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let formState = "idle";
|
||||
export let data;
|
||||
|
||||
let themeData = {
|
||||
pattern: "none",
|
||||
theme: "light",
|
||||
themeToggle: "YES",
|
||||
barStyle: "PARTIAL",
|
||||
barRoundness: "ROUNDED",
|
||||
summaryStyle: "CURRENT",
|
||||
colorsJ: {
|
||||
UP: "#4ead94",
|
||||
DOWN: "#ca3038",
|
||||
DEGRADED: "#e6ca61"
|
||||
},
|
||||
fontJ: {
|
||||
cssSrc: "https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@0,100..900;1,100..900&display=swap",
|
||||
family: "Albert Sans"
|
||||
},
|
||||
colors: "",
|
||||
font: "",
|
||||
customCSS: ""
|
||||
};
|
||||
let formState = "idle";
|
||||
let themeError = "";
|
||||
|
||||
themeData = siteDataExtractFromDb(data.siteData, themeData);
|
||||
let themeData = {
|
||||
pattern: "none",
|
||||
theme: "light",
|
||||
themeToggle: "YES",
|
||||
barStyle: "PARTIAL",
|
||||
barRoundness: "ROUNDED",
|
||||
summaryStyle: "CURRENT",
|
||||
kenerTheme: "default",
|
||||
colorsJ: {
|
||||
UP: "#4ead94",
|
||||
DOWN: "#ca3038",
|
||||
DEGRADED: "#e6ca61",
|
||||
MAINTENANCE: "#6679cc"
|
||||
},
|
||||
fontJ: {
|
||||
cssSrc:
|
||||
"https://fonts.bunny.net/css?family=albert-sans:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i&display=swap",
|
||||
family: "Albert Sans"
|
||||
},
|
||||
colors: "",
|
||||
font: "",
|
||||
customCSS: ""
|
||||
};
|
||||
|
||||
if (data.siteData.colors) {
|
||||
themeData.colorsJ = data.siteData.colors;
|
||||
}
|
||||
if (data.siteData.font) {
|
||||
themeData.fontJ = data.siteData.font;
|
||||
}
|
||||
themeData = siteDataExtractFromDb(data.siteData, themeData);
|
||||
|
||||
async function formSubmit() {
|
||||
formState = "loading";
|
||||
let themeDataAPI = { ...themeData };
|
||||
themeDataAPI.colors = JSON.stringify(themeDataAPI.colorsJ);
|
||||
themeDataAPI.font = JSON.stringify(themeDataAPI.fontJ);
|
||||
delete themeDataAPI.colorsJ;
|
||||
delete themeDataAPI.fontJ;
|
||||
let resp = await storeSiteData(themeDataAPI);
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (data.siteData.colors) {
|
||||
if (data.siteData.colors.MAINTENANCE === undefined) {
|
||||
data.siteData.colors.MAINTENANCE = "#6679cc"; // default value for MAINTENANCE
|
||||
}
|
||||
themeData.colorsJ = JSON.parse(JSON.stringify(data.siteData.colors));
|
||||
}
|
||||
if (data.siteData.font) {
|
||||
themeData.fontJ = data.siteData.font;
|
||||
}
|
||||
|
||||
async function formSubmit() {
|
||||
formState = "loading";
|
||||
themeError = "";
|
||||
let themeDataAPI = { ...themeData };
|
||||
themeDataAPI.colors = JSON.stringify(themeDataAPI.colorsJ);
|
||||
themeDataAPI.font = JSON.stringify(themeDataAPI.fontJ);
|
||||
delete themeDataAPI.colorsJ;
|
||||
delete themeDataAPI.fontJ;
|
||||
let resp = await storeSiteData(themeDataAPI);
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
themeError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Theme Customization</Card.Title>
|
||||
<Card.Description>
|
||||
You can customize the theme of your site here. You can change the pattern, theme,
|
||||
colors, font, and monitor style.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmit}>
|
||||
<p class="font-medium">
|
||||
Home Page Pattern
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the pattern for the home page. You can choose between squares,
|
||||
dots, or none.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Theme Customization</Card.Title>
|
||||
<Card.Description>
|
||||
You can customize the theme of your site here. You can change the pattern, theme, colors, font, and monitor style.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmit}>
|
||||
<p class="font-medium">
|
||||
Home Page Pattern
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the pattern for the home page. You can choose between squares, dots, or none.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
|
||||
<div class="flex">
|
||||
<RadioGroup.Root
|
||||
bind:value={themeData.pattern}
|
||||
onValueChange={(e) => (themeData.pattern = e)}
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="squares" id="rsq" />
|
||||
<Label for="rsq">Squares</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="dots" id="rdot" />
|
||||
<Label for="rdot">Dots</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="none" id="rnone" />
|
||||
<Label for="rnone">None</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<hr />
|
||||
<p class="font-medium">
|
||||
Default Theme
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the default theme for the site. You can choose between light,
|
||||
dark, or system.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex">
|
||||
<RadioGroup.Root
|
||||
bind:value={themeData.theme}
|
||||
onValueChange={(e) => (themeData.theme = e)}
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="none" id="none_theme" />
|
||||
<Label for="none_theme">None</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="light" id="light_theme" />
|
||||
<Label for="light_theme">Light</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="dark" id="dark_theme" />
|
||||
<Label for="dark_theme">Dark</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="system" id="system_theme" />
|
||||
<Label for="system_theme">System</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<p class="font-medium">
|
||||
<label>
|
||||
<input
|
||||
on:change={(e) => {
|
||||
themeData.themeToggle = e.target.checked === true ? "YES" : "NO";
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={themeData.themeToggle === "YES"}
|
||||
/>
|
||||
Let Users toggle theme between light and dark
|
||||
</label>
|
||||
</p>
|
||||
<hr />
|
||||
<p class="font-medium">Monitor Style</p>
|
||||
<div class="flex">
|
||||
<RadioGroup.Root
|
||||
class="flex flex-1 flex-wrap justify-start gap-4"
|
||||
bind:value={themeData.pattern}
|
||||
onValueChange={(e) => (themeData.pattern = e)}
|
||||
>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="squares" id="rsq" />
|
||||
<Label for="rsq">Squares</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="dots" id="rdot" />
|
||||
<Label for="rdot">Dots</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="tiles" id="tiles" />
|
||||
<Label for="tiles">Tiles</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="radial-blue" id="radial-blue" />
|
||||
<Label for="radial-blue">Radial Blue</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="radial-mono" id="radial-mono" />
|
||||
<Label for="radial-mono">Radial Mono</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="radial-midnight" id="radial-midnight" />
|
||||
<Label for="radial-midnight">Radial Midnight</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="circle-mono" id="circle-mono" />
|
||||
<Label for="circle-mono">Circle Mono</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="carbon-fibre" id="carbon-fibre" />
|
||||
<Label for="carbon-fibre">Carbon Fibre</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="texture-sky" id="texture-sky" />
|
||||
<Label for="texture-sky">Texture Sky</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="angular-mono" id="angular-mono" />
|
||||
<Label for="angular-mono">Angular Mono</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="angular-spring" id="angular-spring" />
|
||||
<Label for="angular-spring">Angular Spring</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="angular-bloom" id="angular-bloom" />
|
||||
<Label for="angular-bloom">Angular Bloom</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="pets" id="pets" />
|
||||
<Label for="pets">Pets</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="none" id="rnone" />
|
||||
<Label for="rnone">None</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<hr />
|
||||
<p class="font-medium">
|
||||
Default Theme
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the default theme for the site. You can choose between light, dark, or system.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex">
|
||||
<RadioGroup.Root bind:value={themeData.theme} onValueChange={(e) => (themeData.theme = e)}>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="none" id="none_theme" />
|
||||
<Label for="none_theme">None</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="light" id="light_theme" />
|
||||
<Label for="light_theme">Light</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="dark" id="dark_theme" />
|
||||
<Label for="dark_theme">Dark</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="system" id="system_theme" />
|
||||
<Label for="system_theme">System</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<p class="font-medium">
|
||||
<label>
|
||||
<input
|
||||
on:change={(e) => {
|
||||
themeData.themeToggle = e.target.checked === true ? "YES" : "NO";
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={themeData.themeToggle === "YES"}
|
||||
/>
|
||||
Let Users toggle theme between light and dark
|
||||
</label>
|
||||
</p>
|
||||
<hr />
|
||||
<p class="font-medium">Monitor Style</p>
|
||||
|
||||
<div class="flex gap-x-24">
|
||||
<RadioGroup.Root
|
||||
bind:value={themeData.barStyle}
|
||||
onValueChange={(e) => (themeData.barStyle = e)}
|
||||
>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Status bar
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the style for the monitor. You can choose between partial
|
||||
or full.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="PARTIAL" id="barStylePARTIAL" />
|
||||
<Label for="barStylePARTIAL">PARTIAL</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="FULL" id="barStyleFULL" />
|
||||
<Label for="barStyleFULL">FULL</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
<RadioGroup.Root
|
||||
bind:value={themeData.barRoundness}
|
||||
onValueChange={(e) => (themeData.barRoundness = e)}
|
||||
>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Roundness
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the roundness for the monitor. You can choose between
|
||||
rounded or sharp.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="SHARP" id="barRoundnessSHARP" />
|
||||
<Label for="barRoundnessSHARP">SHARP</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="ROUNDED" id="barRoundnessRound" />
|
||||
<Label for="barRoundnessRound">ROUNDED</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
<RadioGroup.Root
|
||||
bind:value={themeData.summaryStyle}
|
||||
onValueChange={(e) => (themeData.summaryStyle = e)}
|
||||
>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Summary Type
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the summary type for the monitor. You can choose between
|
||||
current or day.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="CURRENT" id="summaryStyle1" />
|
||||
<Label for="summaryStyle1">CURRENT</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="DAY" id="summaryStyle2" />
|
||||
<Label for="summaryStyle2">DAY</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<hr />
|
||||
<p class="font-medium">
|
||||
Colors
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the colors for the monitor. You can choose between UP, DEGRADED,
|
||||
and DOWN.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="relative w-full">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.UP}
|
||||
position="responsive"
|
||||
label="UP"
|
||||
--cp-input-color="#777"
|
||||
--cp-button-hover-color="#767676"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.UP = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.DEGRADED}
|
||||
position="responsive"
|
||||
label="DEGRADED"
|
||||
--cp-input-color="#777"
|
||||
--cp-button-hover-color="#767676"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.DEGRADED = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.DOWN}
|
||||
position="responsive"
|
||||
label="DOWN"
|
||||
--cp-input-color="#777"
|
||||
--cp-button-hover-color="#767676"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.DOWN = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<p class="font-medium">
|
||||
Font
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the font for the monitor. You can choose the font URL and font
|
||||
family name.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="cssSrc" class="text-sm text-muted-foreground">Font URL</Label>
|
||||
<Input
|
||||
bind:value={themeData.fontJ.cssSrc}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="cssSrc"
|
||||
placeholder="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row justify-start gap-2">
|
||||
<div class="w-48">
|
||||
<Label for="fontName" class="text-sm text-muted-foreground"
|
||||
>Font Family Name</Label
|
||||
>
|
||||
<Input
|
||||
bind:value={themeData.fontJ.family}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="fontName"
|
||||
placeholder="Lato"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<p class="font-medium">Custom CSS</p>
|
||||
<div class="flex w-full flex-row justify-start gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="customCSS" class="text-sm text-muted-foreground">
|
||||
You can add custom CSS to style your site. Do not include style tags.
|
||||
</Label>
|
||||
<textarea
|
||||
bind:value={themeData.customCSS}
|
||||
id="customCSS"
|
||||
class="mt-1 h-48 w-full rounded-sm border p-2"
|
||||
placeholder=".className{color: red;}"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex w-full justify-end">
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
<div class="flex gap-x-24">
|
||||
<RadioGroup.Root bind:value={themeData.barStyle} onValueChange={(e) => (themeData.barStyle = e)}>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Status bar
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the style for the monitor. You can choose between partial or full.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="PARTIAL" id="barStylePARTIAL" />
|
||||
<Label for="barStylePARTIAL">PARTIAL</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="FULL" id="barStyleFULL" />
|
||||
<Label for="barStyleFULL">FULL</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
<RadioGroup.Root bind:value={themeData.barRoundness} onValueChange={(e) => (themeData.barRoundness = e)}>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Roundness
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the roundness for the monitor. You can choose between rounded or sharp.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="SHARP" id="barRoundnessSHARP" />
|
||||
<Label for="barRoundnessSHARP">SHARP</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="ROUNDED" id="barRoundnessRound" />
|
||||
<Label for="barRoundnessRound">ROUNDED</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
<RadioGroup.Root bind:value={themeData.summaryStyle} onValueChange={(e) => (themeData.summaryStyle = e)}>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Summary Type
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the summary type for the monitor. You can choose between current or day.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="CURRENT" id="summaryStyle1" />
|
||||
<Label for="summaryStyle1">CURRENT</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="DAY" id="summaryStyle2" />
|
||||
<Label for="summaryStyle2">DAY</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<p class="font-medium">Status Colors</p>
|
||||
<p class=" text-xs font-semibold text-muted-foreground">
|
||||
Choose the colors for the monitor. You can choose between UP, DEGRADED, DOWN and MAINTENANCE.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-row justify-start gap-x-8">
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.UP}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
label="UP"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.UP = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.DEGRADED}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
label="DEGRADED"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.DEGRADED = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.DOWN}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
label="DOWN"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.DOWN = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.MAINTENANCE}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
label="MAINTENANCE"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.MAINTENANCE = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<p class="font-medium">
|
||||
Font
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the font for the monitor. You can choose the font URL and font family name.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="cssSrc" class="text-sm text-muted-foreground">Font URL</Label>
|
||||
<Input
|
||||
bind:value={themeData.fontJ.cssSrc}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="cssSrc"
|
||||
placeholder="https://fonts.bunny.net/css?family=lato:100,100i,300,300i,400,400i,700,700i,900,900i&display=swap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row justify-start gap-2">
|
||||
<div class="w-48">
|
||||
<Label for="fontName" class="text-sm text-muted-foreground">Font Family Name</Label>
|
||||
<Input bind:value={themeData.fontJ.family} class="mt-2" type="text" id="fontName" placeholder="Lato" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Want to upload and use custom font? Read more about it <a
|
||||
href="https://kener.ing/docs/custom-fonts/"
|
||||
class="text-card-foreground"
|
||||
target="_blank">here</a
|
||||
>.
|
||||
</p>
|
||||
<hr />
|
||||
<p class="font-medium">Custom CSS</p>
|
||||
<div class="flex w-full flex-row justify-start gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="customCSS" class="text-sm text-muted-foreground">
|
||||
You can add custom CSS to style your site. Do not include style tags. Go to <a
|
||||
href="https://kener.ing/docs/custom-js-css-guide#grid-layout"
|
||||
class="text-card-foreground"
|
||||
target="_blank">this link</a
|
||||
> to learn more about it.
|
||||
</Label>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={themeData.customCSS}
|
||||
lang={css()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "320px",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!themeError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{themeError}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save Theme Settings
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+415
-389
@@ -1,408 +1,434 @@
|
||||
<script>
|
||||
import * as Popover from "$lib/components/ui/popover";
|
||||
import { onMount } from "svelte";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { base } from "$app/paths";
|
||||
import { sub, startOfDay, getUnixTime } from "date-fns";
|
||||
import * as Popover from "$lib/components/ui/popover";
|
||||
import { onMount } from "svelte";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { base } from "$app/paths";
|
||||
import { sub, startOfDay, getUnixTime } from "date-fns";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import Share2 from "lucide-svelte/icons/share-2";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import TrendingUp from "lucide-svelte/icons/trending-up";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { afterUpdate } from "svelte";
|
||||
import axios from "axios";
|
||||
import { l, summaryTime, f } from "$lib/i18n/client";
|
||||
import { hoverAction, clickOutsideAction, slide } from "svelte-legos";
|
||||
import LoaderBoxes from "$lib/components/loaderbox.svelte";
|
||||
import NumberFlow from "@number-flow/svelte";
|
||||
import Incident from "$lib/components/IncidentNew.svelte";
|
||||
|
||||
import {
|
||||
Share2,
|
||||
Link,
|
||||
CopyCheck,
|
||||
Code,
|
||||
TrendingUp,
|
||||
Percent,
|
||||
Loader,
|
||||
ChevronLeft,
|
||||
ChevronRight
|
||||
} from "lucide-svelte";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { afterUpdate } from "svelte";
|
||||
import axios from "axios";
|
||||
import { l, summaryTime, f } from "$lib/i18n/client";
|
||||
import { hoverAction, clickOutsideAction, slide } from "svelte-legos";
|
||||
import LoaderBoxes from "$lib/components/loaderbox.svelte";
|
||||
import NumberFlow from "@number-flow/svelte";
|
||||
import Incident from "$lib/components/IncidentNew.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let monitor;
|
||||
export let localTz;
|
||||
export let lang;
|
||||
export let embed = false;
|
||||
export let selectedLang = "en";
|
||||
|
||||
export let monitor;
|
||||
let _0Day = {};
|
||||
let _90Day = monitor.pageData._90Day;
|
||||
let uptime90Day = monitor.pageData.uptime90Day;
|
||||
let incidents = {};
|
||||
let dayIncidentsFull = [];
|
||||
let homeDataMaxDays = monitor.pageData.homeDataMaxDays;
|
||||
|
||||
export let localTz;
|
||||
export let lang;
|
||||
export let embed = false;
|
||||
export let selectedLang = "en";
|
||||
let dimension = {
|
||||
x1: 6,
|
||||
x2: 4
|
||||
};
|
||||
|
||||
let _0Day = {};
|
||||
let _90Day = monitor.pageData._90Day;
|
||||
let uptime90Day = monitor.pageData.uptime90Day;
|
||||
let incidents = {};
|
||||
let dayIncidentsFull = [];
|
||||
dimension.x1 = ($page.data.isMobile ? 346 : 546) / homeDataMaxDays.maxDays;
|
||||
dimension.x2 = (4 / 6) * dimension.x1;
|
||||
|
||||
function loadIncidents() {
|
||||
axios
|
||||
.post(`${base}/api/today/incidents`, {
|
||||
tag: monitor.tag,
|
||||
startTs: monitor.pageData.midnight90DaysAgo,
|
||||
endTs: monitor.pageData.midnightTomorrow,
|
||||
localTz: localTz
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
incidents = response.data;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
function loadIncidents() {
|
||||
axios
|
||||
.post(`${base}/api/today/incidents`, {
|
||||
tag: monitor.tag,
|
||||
startTs: monitor.pageData.midnight90DaysAgo,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp,
|
||||
localTz: localTz
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
incidents = response.data;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
function getToday(startTs, incidentIDs) {
|
||||
axios
|
||||
.post(`${base}/api/today`, {
|
||||
monitor: monitor,
|
||||
localTz: localTz,
|
||||
startTs: startTs,
|
||||
incidentIDs: incidentIDs
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
_0Day = response.data._0Day;
|
||||
dayUptime = response.data.uptime;
|
||||
dayIncidentsFull = response.data.incidents;
|
||||
}
|
||||
loadingDayData = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
loadingDayData = false;
|
||||
});
|
||||
}
|
||||
function getToday(startTs, incidentIDs) {
|
||||
let endTs = Math.min(startTs + 86400, monitor.pageData.maxDateTodayTimestamp);
|
||||
|
||||
function scrollToRight() {
|
||||
setTimeout(() => {
|
||||
let divs = document.querySelectorAll(".daygrid90");
|
||||
divs.forEach((div) => {
|
||||
div.scrollLeft = div.scrollWidth;
|
||||
});
|
||||
}, 1000 * 0.2);
|
||||
}
|
||||
let uptimesRollers = [
|
||||
{
|
||||
text: `${l(lang, "90 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 90 }))),
|
||||
value: uptime90Day
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "60 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 59 })))
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "30 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 29 })))
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "14 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 13 })))
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "7 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 })))
|
||||
},
|
||||
{
|
||||
text: l(lang, "Today"),
|
||||
startTs: getUnixTime(startOfDay(new Date()))
|
||||
}
|
||||
];
|
||||
axios
|
||||
.post(`${base}/api/today`, {
|
||||
monitor: monitor,
|
||||
localTz: localTz,
|
||||
endTs: endTs,
|
||||
startTs: startTs,
|
||||
incidentIDs: incidentIDs
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data) {
|
||||
_0Day = response.data._0Day;
|
||||
dayUptime = response.data.uptime;
|
||||
dayIncidentsFull = response.data.incidents;
|
||||
}
|
||||
loadingDayData = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
loadingDayData = false;
|
||||
});
|
||||
}
|
||||
|
||||
//start of the week moment
|
||||
let rolledAt = 0;
|
||||
let rollerLoading = false;
|
||||
async function rollSummary(r) {
|
||||
let newRolledAt = (rolledAt + r) % uptimesRollers.length;
|
||||
function scrollToRight() {
|
||||
setTimeout(() => {
|
||||
let divs = document.querySelectorAll(".daygrid90");
|
||||
divs.forEach((div) => {
|
||||
div.scrollLeft = div.scrollWidth;
|
||||
});
|
||||
}, 1000 * 0.2);
|
||||
}
|
||||
|
||||
if (uptimesRollers[newRolledAt].value === undefined) {
|
||||
rollerLoading = true;
|
||||
uptimesRollers[newRolledAt].loading = true;
|
||||
function returnUptimeRollers() {
|
||||
let rollers = homeDataMaxDays.selectableDays;
|
||||
//sort descending
|
||||
rollers.sort((a, b) => b - a);
|
||||
let ret = [];
|
||||
for (let i = 0; i < rollers.length; i++) {
|
||||
let roller = rollers[i];
|
||||
if (roller == 1) {
|
||||
ret.push({
|
||||
text: `${l(lang, "Today")}`,
|
||||
startTs: monitor.pageData.startOfTheDay,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
});
|
||||
} else {
|
||||
ret.push({
|
||||
text: `${roller} ${l(lang, "Days")}`,
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * (roller - 1),
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
});
|
||||
}
|
||||
//if last index
|
||||
if (i == 0) {
|
||||
ret[i].value = uptime90Day;
|
||||
}
|
||||
}
|
||||
|
||||
let resp = await axios.post(`${base}/api/today/aggregated`, {
|
||||
monitor: monitor,
|
||||
startTs: uptimesRollers[newRolledAt].startTs
|
||||
});
|
||||
uptimesRollers[newRolledAt].value = resp.data.uptime;
|
||||
rollerLoading = false;
|
||||
}
|
||||
rolledAt = newRolledAt;
|
||||
for (const key in _90Day) {
|
||||
if (Object.prototype.hasOwnProperty.call(_90Day, key)) {
|
||||
const element = _90Day[key];
|
||||
if (key >= uptimesRollers[rolledAt].startTs) {
|
||||
_90Day[key].border = true;
|
||||
} else {
|
||||
_90Day[key].border = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
scrollToRight();
|
||||
loadIncidents();
|
||||
});
|
||||
afterUpdate(() => {
|
||||
dispatch("heightChange", {});
|
||||
});
|
||||
function show90Inline(e, bar) {
|
||||
if (e.detail.hover) {
|
||||
_90Day[bar.timestamp].showDetails = true;
|
||||
} else {
|
||||
_90Day[bar.timestamp].showDetails = false;
|
||||
}
|
||||
}
|
||||
let showDailyDataModal = false;
|
||||
let dateFetchedFor = "";
|
||||
let dayUptime = "NA";
|
||||
let loadingDayData = false;
|
||||
function dailyDataGetter(e, bar, incidentObj) {
|
||||
if (embed) {
|
||||
return;
|
||||
}
|
||||
let incidentIDs = incidentObj?.ids || [];
|
||||
dayUptime = "NA";
|
||||
dateFetchedFor = f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang);
|
||||
showDailyDataModal = true;
|
||||
loadingDayData = true;
|
||||
dayIncidentsFull = [];
|
||||
setTimeout(() => {
|
||||
getToday(bar.timestamp, incidentIDs);
|
||||
}, 750);
|
||||
}
|
||||
let uptimesRollers = returnUptimeRollers();
|
||||
|
||||
//start of the week moment
|
||||
let rolledAt = 0;
|
||||
let rollerLoading = false;
|
||||
async function rollSummary(r) {
|
||||
let newRolledAt = r;
|
||||
analyticsEvent("monitor_interval_switch", {
|
||||
tag: monitor.tag,
|
||||
interval: uptimesRollers[newRolledAt].text
|
||||
});
|
||||
|
||||
if (uptimesRollers[newRolledAt].value === undefined) {
|
||||
rollerLoading = true;
|
||||
uptimesRollers[newRolledAt].loading = true;
|
||||
|
||||
let resp = await axios.post(`${base}/api/today/aggregated`, {
|
||||
monitor: {
|
||||
tag: monitor.tag,
|
||||
include_degraded_in_downtime: monitor.include_degraded_in_downtime
|
||||
},
|
||||
startTs: uptimesRollers[newRolledAt].startTs,
|
||||
endTs: uptimesRollers[newRolledAt].endTs
|
||||
});
|
||||
uptimesRollers[newRolledAt].value = resp.data.uptime;
|
||||
rollerLoading = false;
|
||||
}
|
||||
rolledAt = newRolledAt;
|
||||
for (const key in _90Day) {
|
||||
if (Object.prototype.hasOwnProperty.call(_90Day, key)) {
|
||||
const element = _90Day[key];
|
||||
if (key >= uptimesRollers[rolledAt].startTs) {
|
||||
_90Day[key].border = true;
|
||||
} else {
|
||||
_90Day[key].border = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
scrollToRight();
|
||||
loadIncidents();
|
||||
});
|
||||
afterUpdate(() => {
|
||||
dispatch("heightChange", {});
|
||||
});
|
||||
function show90Inline(e, bar) {
|
||||
if (e.detail.hover) {
|
||||
_90Day[bar.timestamp].showDetails = true;
|
||||
} else {
|
||||
_90Day[bar.timestamp].showDetails = false;
|
||||
}
|
||||
}
|
||||
let showDailyDataModal = false;
|
||||
let dateFetchedFor = "";
|
||||
let dayUptime = "NA";
|
||||
let loadingDayData = false;
|
||||
|
||||
function dailyDataGetter(e, bar, incidentObj) {
|
||||
if (embed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let incidentIDs = incidentObj?.ids || [];
|
||||
dayUptime = "NA";
|
||||
dateFetchedFor = f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang, $page.data.localTz);
|
||||
showDailyDataModal = true;
|
||||
|
||||
loadingDayData = true;
|
||||
dayIncidentsFull = [];
|
||||
analyticsEvent("monitor_day_data", {
|
||||
tag: monitor.tag,
|
||||
data_date: dateFetchedFor
|
||||
});
|
||||
setTimeout(() => {
|
||||
getToday(bar.timestamp, incidentIDs);
|
||||
}, 50);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="monitor relative grid w-full grid-cols-12 gap-2 pb-2 pt-0 md:w-[655px]">
|
||||
{#if !!!embed}
|
||||
<div class="col-span-12 md:w-[546px]">
|
||||
<div class="pt-0">
|
||||
<div class="scroll-m-20 pr-5 text-xl font-medium tracking-tight">
|
||||
{#if monitor.image}
|
||||
<img
|
||||
src={base + monitor.image}
|
||||
class="absolute left-6 top-6 inline h-5 w-5"
|
||||
alt={monitor.name}
|
||||
srcset=""
|
||||
/>
|
||||
{/if}
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{monitor.name}
|
||||
</p>
|
||||
<div class="monitor relative grid w-full max-w-[655px] grid-cols-12 gap-2 pb-2 pt-0">
|
||||
{#if !!!embed}
|
||||
<div class="col-span-12 md:w-[546px]">
|
||||
<div class="pt-0">
|
||||
<div class="scroll-m-20 pr-5 text-xl font-medium tracking-tight">
|
||||
{#if monitor.image}
|
||||
<GMI
|
||||
src={monitor.image}
|
||||
classList="absolute left-6 top-6 inline h-5 w-5 hidden md:block"
|
||||
alt={monitor.name}
|
||||
srcset=""
|
||||
/>
|
||||
{/if}
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{monitor.name}
|
||||
</p>
|
||||
|
||||
<p class="mt-1 text-xs font-medium text-muted-foreground">
|
||||
{#if !!monitor.description}
|
||||
{@html monitor.description}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="absolute right-14 top-5">
|
||||
<Button
|
||||
class="wiggle h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
variant="link"
|
||||
on:click={(e) => {
|
||||
dispatch("show_shareMenu", {
|
||||
monitor: monitor
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Share2 class="h-4 w-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-span-12 min-h-[94px] pt-2 md:w-[546px]">
|
||||
<div class="col-span-12">
|
||||
<div class="flex justify-between">
|
||||
<div class=" ">
|
||||
<div class="flex gap-x-1">
|
||||
{#if rolledAt > 0}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="h-5 w-5 p-0"
|
||||
on:click={() => rollSummary(-1)}
|
||||
>
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex text-xs font-semibold">
|
||||
<span>
|
||||
{uptimesRollers[rolledAt].text}
|
||||
</span>
|
||||
|
||||
<span class="">
|
||||
{#if rollerLoading}
|
||||
<Loader class="mx-1 -mt-0.5 inline h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<TrendingUp class="mx-1 -mt-0.5 inline h-3 w-3" />
|
||||
{/if}
|
||||
{#if isNaN(uptimesRollers[rolledAt].value)}
|
||||
<span class="text-muted-foreground">-</span>
|
||||
{:else}
|
||||
<NumberFlow
|
||||
value={uptimesRollers[rolledAt].value}
|
||||
format={{
|
||||
notation: "standard",
|
||||
minimumFractionDigits: 4,
|
||||
maximumFractionDigits: 4
|
||||
}}
|
||||
suffix="%"
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{#if rolledAt < uptimesRollers.length - 1}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="h-5 w-5 p-0"
|
||||
on:click={() => rollSummary(1)}
|
||||
>
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-0.5 text-right">
|
||||
<div
|
||||
class="text-api-up truncate text-xs font-semibold text-{monitor.pageData
|
||||
.summaryColorClass}"
|
||||
>
|
||||
{monitor.pageData.summaryStatus}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative col-span-12 mt-1"
|
||||
use:clickOutsideAction
|
||||
on:clickoutside={(e) => {
|
||||
showDailyDataModal = false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="daygrid90 flex min-h-[60px] justify-start overflow-x-auto overflow-y-hidden py-1"
|
||||
>
|
||||
{#each Object.entries(_90Day) as [ts, bar]}
|
||||
<a
|
||||
data-ts={ts}
|
||||
use:hoverAction
|
||||
on:hover={(e) => {
|
||||
show90Inline(e, bar);
|
||||
}}
|
||||
on:click={(e) => {
|
||||
dailyDataGetter(e, bar, incidents[ts]);
|
||||
}}
|
||||
href="#"
|
||||
class="oneline h-[34px] w-[6px]
|
||||
<p class="mt-1 text-xs font-medium text-muted-foreground">
|
||||
{#if !!monitor.description}
|
||||
{@html monitor.description}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="absolute right-4 top-5 flex gap-x-2 md:right-14">
|
||||
{#if $page.data.isLoggedIn}
|
||||
<Button
|
||||
href="{base}/manage/app/monitors#{monitor.tag}"
|
||||
class=" rotate-once h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
variant="link"
|
||||
rel="external"
|
||||
>
|
||||
<Settings class="h-4 w-4 " />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
class="wiggle h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
variant="link"
|
||||
on:click={(e) => {
|
||||
dispatch("show_shareMenu", {
|
||||
monitor: monitor
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Share2 class="h-4 w-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-span-12 min-h-[94px] pt-2 md:w-[546px]">
|
||||
<div class="col-span-12">
|
||||
<div class="flex flex-wrap justify-between gap-x-1">
|
||||
<div class="">
|
||||
<DropdownMenu.Root class="">
|
||||
<DropdownMenu.Trigger class="mr-2 flex ">
|
||||
<Button variant="secondary" class="h-6 px-2 py-2 text-xs">
|
||||
{uptimesRollers[rolledAt].text}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class={!!embed ? "h-[60px] overflow-y-auto" : ""}>
|
||||
{#each uptimesRollers as roller, i}
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item
|
||||
class="text-xs {rolledAt == i ? 'bg-secondary' : ''} font-semibold"
|
||||
on:click={() => rollSummary(i)}
|
||||
>
|
||||
{roller.text}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
<div class="flex gap-x-2 pt-2 text-right">
|
||||
{#if rollerLoading}
|
||||
<Loader class=" mt-0.5 inline h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
{/if}
|
||||
{#if !isNaN(uptimesRollers[rolledAt].value)}
|
||||
<NumberFlow
|
||||
class="border-r pr-2 text-xs font-semibold"
|
||||
value={uptimesRollers[rolledAt].value}
|
||||
format={{
|
||||
notation: "standard",
|
||||
minimumFractionDigits: 4,
|
||||
maximumFractionDigits: 4
|
||||
}}
|
||||
suffix="%"
|
||||
/>
|
||||
{/if}
|
||||
<div class="truncate text-xs font-semibold text-{monitor.pageData.summaryColorClass}">
|
||||
{monitor.pageData.summaryStatus}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative col-span-12 mt-0.5"
|
||||
use:clickOutsideAction
|
||||
on:clickoutside={(e) => {
|
||||
showDailyDataModal = false;
|
||||
}}
|
||||
>
|
||||
<div class="daygrid90 flex min-h-[60px] justify-between overflow-x-auto overflow-y-hidden py-1">
|
||||
{#each Object.entries(_90Day) as [ts, bar]}
|
||||
<button
|
||||
data-ts={ts}
|
||||
use:hoverAction
|
||||
on:hover={(e) => {
|
||||
show90Inline(e, bar);
|
||||
}}
|
||||
on:click={(e) => {
|
||||
dailyDataGetter(e, bar, incidents[ts]);
|
||||
}}
|
||||
class="oneline h-[34px]
|
||||
{bar.border ? 'opacity-100' : 'opacity-20'} pb-1"
|
||||
>
|
||||
<div
|
||||
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto w-[4px] rounded-{monitor.pageData.barRoundness.toUpperCase() ==
|
||||
'SHARP'
|
||||
? 'none'
|
||||
: 'sm'}"
|
||||
></div>
|
||||
{#if !!incidents[ts]}
|
||||
<div
|
||||
style="transition-delay: {Math.floor(
|
||||
Math.random() * (1500 - 500 + 1)
|
||||
) + 500}ms;"
|
||||
class="bg-api-{incidents[
|
||||
ts
|
||||
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] left-[1px] h-[4px] w-[4px] rounded-full"
|
||||
></div>
|
||||
{/if}
|
||||
</a>
|
||||
{#if bar.showDetails}
|
||||
<div class="show-hover absolute text-sm">
|
||||
<div class="text-{bar.textClass} text-xs font-semibold">
|
||||
{f(
|
||||
new Date(bar.timestamp * 1000),
|
||||
"EEEE, MMMM do, yyyy",
|
||||
selectedLang
|
||||
)}
|
||||
-
|
||||
{bar.summaryStatus}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if showDailyDataModal}
|
||||
<div
|
||||
transition:slide={{ direction: "bottom" }}
|
||||
class="absolute -left-2 top-10 z-10 mx-auto rounded-sm border bg-card px-[7px] py-[7px] shadow-lg md:w-[560px]"
|
||||
>
|
||||
<div class="mb-2 flex justify-between text-xs font-semibold">
|
||||
<span>{dateFetchedFor}</span>
|
||||
{#if !loadingDayData}
|
||||
<span>
|
||||
<TrendingUp class="mx-1 inline" size={12} />
|
||||
{dayUptime}%</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if dayIncidentsFull.length > 0}
|
||||
<div class="-mx-2 mb-4 grid grid-cols-1">
|
||||
<div class="col-span-1 px-2">
|
||||
<Badge variant="outline" class="border-0 pl-0">
|
||||
{l(lang, "Incident Updates")}
|
||||
</Badge>
|
||||
</div>
|
||||
{#each dayIncidentsFull as incident, index}
|
||||
<div class="col-span-1">
|
||||
<Incident {incident} {lang} index="incident-{index}" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-wrap">
|
||||
{#if loadingDayData}
|
||||
<LoaderBoxes />
|
||||
{:else}
|
||||
{#each Object.entries(_0Day) as [ts, bar]}
|
||||
<div
|
||||
data-index={bar.index}
|
||||
class="bg-{bar.cssClass} today-sq m-[1px] h-[10px] w-[10px]"
|
||||
></div>
|
||||
<div class="hiddenx relative">
|
||||
<div
|
||||
data-index={ts.index}
|
||||
class="message rounded border bg-black p-2 text-xs font-semibold text-white"
|
||||
>
|
||||
<p>
|
||||
<span class="text-{bar.cssClass}"> ● </span>
|
||||
style="width: {dimension.x1}px"
|
||||
>
|
||||
<div
|
||||
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto rounded-{monitor.pageData.barRoundness.toUpperCase() ==
|
||||
'SHARP'
|
||||
? 'none'
|
||||
: 'sm'}"
|
||||
style="width: {dimension.x2}px"
|
||||
></div>
|
||||
<!-- incident dot -->
|
||||
{#if !!incidents[ts]}
|
||||
<div
|
||||
class="bg-api-{incidents[
|
||||
ts
|
||||
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] h-[4px] w-[4px] rounded-full"
|
||||
style="left: {dimension.x1 / 2 - 2}px"
|
||||
></div>
|
||||
{/if}
|
||||
</button>
|
||||
{#if bar.showDetails}
|
||||
<div class="show-hover absolute text-sm">
|
||||
<div class="text-{bar.textClass} text-xs font-semibold">
|
||||
{f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang, $page.data.localTz)}
|
||||
-
|
||||
{bar.summaryStatus}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if monitor.monitor_type === "GROUP" && !!!embed}
|
||||
<div class="-mt-4 flex justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
href="{base}?group={monitor.tag}"
|
||||
rel="external"
|
||||
class="bounce-right h-8 text-xs"
|
||||
on:click={scrollToRight}
|
||||
>
|
||||
{l(lang, "View in detail")}
|
||||
<ArrowRight class="arrow ml-1.5 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{f(
|
||||
new Date(bar.timestamp * 1000),
|
||||
"hh:mm a",
|
||||
selectedLang
|
||||
)}
|
||||
</p>
|
||||
{#if bar.status != "NO_DATA"}
|
||||
<p class="pl-2">
|
||||
{l(lang, bar.status)}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="pl-2">-</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if showDailyDataModal}
|
||||
<div
|
||||
transition:slide={{ direction: "bottom" }}
|
||||
class="absolute -left-2 top-10 z-10 mx-auto rounded-sm border bg-card px-[7px] py-[7px] shadow-lg md:w-[560px]"
|
||||
>
|
||||
<div class="mb-2 flex justify-between text-xs font-semibold">
|
||||
<span>{dateFetchedFor}</span>
|
||||
{#if !loadingDayData}
|
||||
<span>
|
||||
<TrendingUp class="mx-1 inline" size={12} />
|
||||
{dayUptime}%</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if dayIncidentsFull.length > 0}
|
||||
<div class="-mx-2 mb-4 grid grid-cols-1">
|
||||
<div class="col-span-1 px-2">
|
||||
<Badge variant="outline" class="border-0 pl-0">
|
||||
{l(lang, "Incident Updates")}
|
||||
</Badge>
|
||||
</div>
|
||||
{#each dayIncidentsFull as incident, index}
|
||||
<div class="col-span-1">
|
||||
<Incident {incident} {lang} index="incident-{index}" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-wrap">
|
||||
{#if loadingDayData}
|
||||
<LoaderBoxes />
|
||||
{:else}
|
||||
{#each Object.entries(_0Day) as [ts, bar]}
|
||||
<div data-index={bar.index} class="bg-{bar.cssClass} today-sq m-[1px] h-[10px] w-[10px]"></div>
|
||||
<div class="hiddenx relative">
|
||||
<div
|
||||
data-index={ts.index}
|
||||
class="message rounded border bg-black p-2 text-xs font-semibold text-white"
|
||||
>
|
||||
<p>
|
||||
<span class="text-{bar.cssClass}"> ● </span>
|
||||
|
||||
{f(new Date(bar.timestamp * 1000), "hh:mm a", selectedLang, $page.data.localTz)}
|
||||
</p>
|
||||
{#if bar.status != "NO_DATA"}
|
||||
<p class="pl-2">
|
||||
{l(lang, bar.status)}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="pl-2">-</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+114
-59
@@ -1,64 +1,119 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { Languages, Menu } from "lucide-svelte";
|
||||
import { base } from "$app/paths";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
export let data;
|
||||
let defaultPattern = data.site?.pattern || "squares";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import Languages from "lucide-svelte/icons/languages";
|
||||
import Menu from "lucide-svelte/icons/menu";
|
||||
import { base } from "$app/paths";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
export let data;
|
||||
let defaultPattern = data.site?.pattern || "squares";
|
||||
let allPets = [
|
||||
{
|
||||
url: base + "/chicken.gif",
|
||||
bottom: "-5"
|
||||
},
|
||||
{
|
||||
url: base + "/dog.gif",
|
||||
bottom: "-17"
|
||||
},
|
||||
{
|
||||
url: base + "/cockatiel.gif",
|
||||
bottom: "-10"
|
||||
},
|
||||
{
|
||||
url: base + "/crab.gif",
|
||||
bottom: "-20"
|
||||
},
|
||||
{
|
||||
url: base + "/fox.gif",
|
||||
bottom: "-9"
|
||||
},
|
||||
{
|
||||
url: base + "/horse.gif",
|
||||
bottom: "-11"
|
||||
},
|
||||
{
|
||||
url: base + "/panda.gif",
|
||||
bottom: "0"
|
||||
},
|
||||
{
|
||||
url: base + "/totoro.gif",
|
||||
bottom: "-27"
|
||||
},
|
||||
{
|
||||
url: base + "/rabbit.gif",
|
||||
bottom: "0"
|
||||
},
|
||||
{
|
||||
url: base + "/duck.gif",
|
||||
bottom: "-5"
|
||||
},
|
||||
{
|
||||
url: base + "/snake.gif",
|
||||
bottom: "0"
|
||||
}
|
||||
];
|
||||
let randomPet = allPets[Math.floor(Math.random() * allPets.length)];
|
||||
</script>
|
||||
|
||||
<div class="{defaultPattern}-pattern"></div>
|
||||
{#if defaultPattern == "pets" && !!randomPet}
|
||||
<div class="pets-pattern" style="background-image: url({randomPet.url});bottom: {randomPet.bottom}px"></div>
|
||||
{:else}
|
||||
<div class="{defaultPattern}-pattern"></div>
|
||||
{/if}
|
||||
|
||||
<header class="blurry-bg sticky top-0 z-50 mx-auto md:mt-2">
|
||||
<div class="container flex h-14 max-w-[820px] items-center border bg-card px-3 md:rounded-md">
|
||||
<a href={data.site.home ? data.site.home : base} class="mr-6 flex items-center space-x-2">
|
||||
{#if data.site.logo}
|
||||
<img src={base + data.site.logo} class="w-8" alt={data.site.title} srcset="" />
|
||||
{/if}
|
||||
{#if data.site.siteName}
|
||||
<span class=" inline-block text-[15px] font-bold lg:text-base">
|
||||
{data.site.siteName}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
<div class="flex w-full justify-end">
|
||||
{#if data.site.nav}
|
||||
<nav class=" hidden flex-wrap items-center text-sm font-medium md:flex">
|
||||
{#each data.site.nav as navItem}
|
||||
<a
|
||||
href={navItem.url}
|
||||
class="flex rounded-md px-3 py-2 text-card-foreground transition-all ease-linear hover:bg-background"
|
||||
on:click={() => analyticsEvent("nav", navItem.name)}
|
||||
>
|
||||
{#if navItem.iconURL}
|
||||
<img
|
||||
src={base + navItem.iconURL}
|
||||
class="mr-1.5 mt-0.5 inline h-4"
|
||||
alt={navItem.name}
|
||||
/>
|
||||
{/if}
|
||||
<span>{navItem.name}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
<DropdownMenu.Root class="">
|
||||
<DropdownMenu.Trigger class="mr-2 flex md:hidden">
|
||||
<Button variant="outline" size="sm">
|
||||
<Menu size={14} />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{#each data.site.nav as navItem}
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<a href={navItem.url}> {navItem.name} </a>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<header class="sticky top-0 z-50 mx-auto md:mt-2">
|
||||
<div class="container flex h-14 max-w-[820px] items-center border bg-card px-3 md:rounded-md">
|
||||
<a rel="external" href={data.site.home ? data.site.home : base} class="mr-6 flex items-center space-x-2">
|
||||
{#if data.site.logo}
|
||||
<GMI src={data.site.logo} classList="w-8" alt={data.site.title} srcset="" />
|
||||
{/if}
|
||||
{#if data.site.siteName}
|
||||
<span class=" inline-block text-[15px] font-bold lg:text-base">
|
||||
{data.site.siteName}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
<div class="flex w-full justify-end">
|
||||
{#if data.site.nav}
|
||||
<nav class=" hidden flex-wrap items-center text-sm font-medium md:flex">
|
||||
{#each data.site.nav as navItem}
|
||||
<a
|
||||
rel="external"
|
||||
href={navItem.url}
|
||||
class="flex rounded-md px-3 py-2 text-card-foreground transition-all ease-linear hover:bg-background"
|
||||
on:click={() =>
|
||||
analyticsEvent("navigation", {
|
||||
name: navItem.name
|
||||
})}
|
||||
>
|
||||
{#if navItem.iconURL}
|
||||
<GMI src={navItem.iconURL} classList="mr-1.5 mt-0.5 inline h-4" alt={navItem.name} />
|
||||
{/if}
|
||||
<span>{navItem.name}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="flex md:hidden">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="outline" size="sm">
|
||||
<Menu size={14} />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{#each data.site.nav as navItem}
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<a rel="external" href={navItem.url}> {navItem.name} </a>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
+267
-265
@@ -1,293 +1,295 @@
|
||||
<script>
|
||||
import { Share2, Link, CopyCheck, Code, TrendingUp, Percent, Loader } from "lucide-svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { base } from "$app/paths";
|
||||
import { onMount } from "svelte";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import { l, summaryTime } from "$lib/i18n/client";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import Share2 from "lucide-svelte/icons/share-2";
|
||||
import Link from "lucide-svelte/icons/link";
|
||||
import CopyCheck from "lucide-svelte/icons/copy-check";
|
||||
import Code from "lucide-svelte/icons/code";
|
||||
import TrendingUp from "lucide-svelte/icons/trending-up";
|
||||
import Percent from "lucide-svelte/icons/percent";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ExternalLink from "lucide-svelte/icons/external-link";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { base } from "$app/paths";
|
||||
import { onMount } from "svelte";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import { l, summaryTime } from "$lib/i18n/client";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
|
||||
export let monitor;
|
||||
export let lang;
|
||||
export let selectedLang = "en";
|
||||
let theme = "light";
|
||||
let copiedLink = false;
|
||||
let embedType = "js";
|
||||
let copiedEmbed = false;
|
||||
let copiedBadgeStatus = false;
|
||||
let copiedBadgeUptime = false;
|
||||
let copiedBadgeDotStandard = false;
|
||||
let copiedBadgeDotPing = false;
|
||||
export let monitor;
|
||||
export let lang;
|
||||
let theme = "light";
|
||||
let copiedLink = false;
|
||||
let embedType = "js";
|
||||
let copiedEmbed = false;
|
||||
let copiedBadgeStatus = false;
|
||||
let copiedBadgeUptime = false;
|
||||
let copiedBadgeDotStandard = false;
|
||||
let copiedBadgeDotPing = false;
|
||||
|
||||
let protocol;
|
||||
let domain;
|
||||
let protocol;
|
||||
let domain;
|
||||
|
||||
let pathMonitorLink;
|
||||
function copyLinkToClipboard() {
|
||||
analyticsEvent("monitor_link_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
navigator.clipboard.writeText(pathMonitorLink);
|
||||
copiedLink = true;
|
||||
setTimeout(function () {
|
||||
copiedLink = false;
|
||||
}, 1500);
|
||||
}
|
||||
let pathMonitorLink;
|
||||
function copyLinkToClipboard() {
|
||||
analyticsEvent("monitor_link_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
navigator.clipboard.writeText(pathMonitorLink);
|
||||
copiedLink = true;
|
||||
setTimeout(function () {
|
||||
copiedLink = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
let pathMonitorBadgeUptime;
|
||||
function copyUptimeBadge() {
|
||||
analyticsEvent("monitor_uptime_badge_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
navigator.clipboard.writeText(pathMonitorBadgeUptime);
|
||||
copiedBadgeUptime = true;
|
||||
setTimeout(function () {
|
||||
copiedBadgeUptime = false;
|
||||
}, 1500);
|
||||
}
|
||||
let pathMonitorBadgeUptime;
|
||||
function copyUptimeBadge() {
|
||||
analyticsEvent("monitor_uptime_badge_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
navigator.clipboard.writeText(pathMonitorBadgeUptime);
|
||||
copiedBadgeUptime = true;
|
||||
setTimeout(function () {
|
||||
copiedBadgeUptime = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
let pathMonitorBadgeStatus;
|
||||
function copyStatusBadge() {
|
||||
analyticsEvent("monitor_status_badge_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
navigator.clipboard.writeText(pathMonitorBadgeStatus);
|
||||
copiedBadgeStatus = true;
|
||||
setTimeout(function () {
|
||||
copiedBadgeStatus = false;
|
||||
}, 1500);
|
||||
}
|
||||
let pathMonitorBadgeStatus;
|
||||
function copyStatusBadge() {
|
||||
analyticsEvent("monitor_status_badge_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
navigator.clipboard.writeText(pathMonitorBadgeStatus);
|
||||
copiedBadgeStatus = true;
|
||||
setTimeout(function () {
|
||||
copiedBadgeStatus = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
let pathMonitorBadgeDot;
|
||||
function copyDotStandard() {
|
||||
analyticsEvent("monitor_svg_standard_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
let pathMonitorBadgeDot;
|
||||
function copyDotStandard() {
|
||||
analyticsEvent("monitor_svg_standard_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
|
||||
navigator.clipboard.writeText(pathMonitorBadgeDot);
|
||||
copiedBadgeDotStandard = true;
|
||||
setTimeout(function () {
|
||||
copiedBadgeDotStandard = false;
|
||||
}, 1500);
|
||||
}
|
||||
navigator.clipboard.writeText(pathMonitorBadgeDot);
|
||||
copiedBadgeDotStandard = true;
|
||||
setTimeout(function () {
|
||||
copiedBadgeDotStandard = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
let pathMonitorBadgeDotPing;
|
||||
function copyDotPing() {
|
||||
analyticsEvent("monitor_svg_pinging_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
navigator.clipboard.writeText(pathMonitorBadgeDotPing);
|
||||
copiedBadgeDotPing = true;
|
||||
setTimeout(function () {
|
||||
copiedBadgeDotPing = false;
|
||||
}, 1500);
|
||||
}
|
||||
let pathMonitorBadgeDotPing;
|
||||
function copyDotPing() {
|
||||
analyticsEvent("monitor_svg_pinging_copied", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
navigator.clipboard.writeText(pathMonitorBadgeDotPing);
|
||||
copiedBadgeDotPing = true;
|
||||
setTimeout(function () {
|
||||
copiedBadgeDotPing = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function copyScriptTagToClipboard() {
|
||||
//get domain with port number
|
||||
function copyScriptTagToClipboard() {
|
||||
//get domain with port number
|
||||
|
||||
analyticsEvent("monitor_embed_copied", {
|
||||
tag: monitor.tag,
|
||||
type: embedType
|
||||
});
|
||||
analyticsEvent("monitor_embed_copied", {
|
||||
tag: monitor.tag,
|
||||
type: embedType
|
||||
});
|
||||
|
||||
let path = `${base}/embed/monitor-${monitor.tag}`;
|
||||
let scriptTag =
|
||||
`<script async src="${protocol + "//" + domain + path}/js?theme=${theme}&monitor=${protocol + "//" + domain + path}"><` +
|
||||
"/script>";
|
||||
let path = `${base}/embed/monitor-${monitor.tag}`;
|
||||
let scriptTag =
|
||||
`<script async src="${protocol + "//" + domain + path}/js?theme=${theme}&monitor=${protocol + "//" + domain + path}"><` +
|
||||
"/script>";
|
||||
|
||||
if (embedType == "iframe") {
|
||||
scriptTag = `<iframe src="${protocol + "//" + domain + path}?theme=${theme}" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>`;
|
||||
}
|
||||
navigator.clipboard.writeText(scriptTag);
|
||||
copiedEmbed = true;
|
||||
setTimeout(function () {
|
||||
copiedEmbed = false;
|
||||
}, 1500);
|
||||
}
|
||||
if (embedType == "iframe") {
|
||||
scriptTag = `<iframe src="${protocol + "//" + domain + path}?theme=${theme}" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>`;
|
||||
}
|
||||
navigator.clipboard.writeText(scriptTag);
|
||||
copiedEmbed = true;
|
||||
setTimeout(function () {
|
||||
copiedEmbed = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
protocol = window.location.protocol;
|
||||
domain = window.location.host;
|
||||
pathMonitorLink = `${protocol}//${domain}${base}/?monitor=${monitor.tag}`;
|
||||
pathMonitorBadgeUptime = `${protocol}//${domain}${base}/badge/${monitor.tag}/uptime`;
|
||||
pathMonitorBadgeStatus = `${protocol}//${domain}${base}/badge/${monitor.tag}/status`;
|
||||
pathMonitorBadgeDot = `${protocol}//${domain}${base}/badge/${monitor.tag}/dot`;
|
||||
pathMonitorBadgeDotPing = `${protocol}//${domain}${base}/badge/${monitor.tag}/dot?animate=ping`;
|
||||
analyticsEvent("monitor_share_menu_open", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
});
|
||||
onMount(async () => {
|
||||
protocol = window.location.protocol;
|
||||
domain = window.location.host;
|
||||
pathMonitorLink = `${protocol}//${domain}${base}/?monitor=${monitor.tag}`;
|
||||
pathMonitorBadgeUptime = `${protocol}//${domain}${base}/badge/${monitor.tag}/uptime`;
|
||||
pathMonitorBadgeStatus = `${protocol}//${domain}${base}/badge/${monitor.tag}/status`;
|
||||
pathMonitorBadgeDot = `${protocol}//${domain}${base}/badge/${monitor.tag}/dot`;
|
||||
pathMonitorBadgeDotPing = `${protocol}//${domain}${base}/badge/${monitor.tag}/dot?animate=ping`;
|
||||
analyticsEvent("monitor_share_menu_open", {
|
||||
tag: monitor.tag
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative mb-2 scroll-m-20 px-4 pt-4 {!!monitor.image
|
||||
? 'pl-10'
|
||||
: ''} text-xl font-medium tracking-tight"
|
||||
>
|
||||
{#if !!monitor.image}
|
||||
<img
|
||||
src={monitor.image.startsWith("/") ? base + monitor.image : monitor.image}
|
||||
class="absolute left-4 top-5 inline h-5 w-5"
|
||||
alt={monitor.name}
|
||||
srcset=""
|
||||
/>
|
||||
{/if}
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{monitor.name}
|
||||
</p>
|
||||
<div class="relative mb-2 scroll-m-20 px-4 pt-4 {!!monitor.image ? 'pl-10' : ''} text-xl font-medium tracking-tight">
|
||||
{#if !!monitor.image}
|
||||
<GMI src={monitor.image} classList="absolute left-4 top-5 inline h-5 w-5" alt={monitor.name} srcset="" />
|
||||
{/if}
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{monitor.name}
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<h2 class="mb-1 text-sm font-semibold">
|
||||
{l(lang, "Share")}
|
||||
</h2>
|
||||
<p class="mb-2 text-xs text-muted-foreground">
|
||||
{l(lang, "Share this monitor using a link with others")}
|
||||
</p>
|
||||
<Button
|
||||
class="h-8 px-2 pr-4 text-xs font-semibold"
|
||||
variant="secondary"
|
||||
on:click={copyLinkToClipboard}
|
||||
>
|
||||
{#if !copiedLink}
|
||||
<Link class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Copy Link")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Link Copied")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<h2 class="mb-1 text-sm font-semibold">
|
||||
{l(lang, "Share")}
|
||||
</h2>
|
||||
<p class="mb-2 text-xs text-muted-foreground">
|
||||
{l(lang, "Share this monitor using a link with others")}
|
||||
</p>
|
||||
<Button class="h-8 px-2 pr-4 text-xs font-semibold" variant="secondary" on:click={copyLinkToClipboard}>
|
||||
{#if !copiedLink}
|
||||
<Link class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Copy Link")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Link Copied")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
href={pathMonitorLink}
|
||||
target="_blank"
|
||||
variant="link"
|
||||
class="h-8 px-3 text-xs font-semibold text-muted-foreground"
|
||||
>
|
||||
<ExternalLink class="inline" size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
<hr class="my-4" />
|
||||
<div class="px-4">
|
||||
<h2 class="mb-1 text-sm font-semibold">
|
||||
{l(lang, "Embed")}
|
||||
</h2>
|
||||
<p class="mb-1 text-xs text-muted-foreground">
|
||||
{l(lang, "Embed this monitor using <script> or <iframe> in your app.")}
|
||||
</p>
|
||||
<div class="mb-4 grid grid-cols-2 gap-2">
|
||||
<div class="col-span-1">
|
||||
<h3 class="mb-2 text-xs">
|
||||
{l(lang, "Theme")}
|
||||
</h3>
|
||||
<RadioGroup.Root bind:value={theme} class=" flex">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="light" id="light-theme" />
|
||||
<Label class="text-xs" for="light-theme">{l(lang, "Light")}</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="dark" id="dark-theme" />
|
||||
<Label class="text-xs" for="dark-theme">{l(lang, "Dark")}</Label>
|
||||
</div>
|
||||
<RadioGroup.Input name="theme" />
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<div class="col-span-1 pl-2">
|
||||
<h3 class="mb-2 text-xs">
|
||||
{l(lang, "Mode")}
|
||||
</h3>
|
||||
<RadioGroup.Root bind:value={embedType} class="flex">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="js" id="js-embed" />
|
||||
<Label class="text-xs" for="js-embed"><script></Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="iframe" id="iframe-embed" />
|
||||
<Label class="text-xs" for="iframe-embed"><iframe></Label>
|
||||
</div>
|
||||
<RadioGroup.Input name="embed" />
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
</div>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyScriptTagToClipboard}>
|
||||
{#if !copiedEmbed}
|
||||
<Code class="mr-2 inline" size={12} />
|
||||
<span class=" font-semibold">
|
||||
{l(lang, "Copy Code")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Code Copied")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<h2 class="mb-1 text-sm font-semibold">
|
||||
{l(lang, "Embed")}
|
||||
</h2>
|
||||
<p class="mb-1 text-xs text-muted-foreground">
|
||||
{l(lang, "Embed this monitor using <script> or <iframe> in your app.")}
|
||||
</p>
|
||||
<div class="mb-4 grid grid-cols-2 gap-2">
|
||||
<div class="col-span-1">
|
||||
<h3 class="mb-2 text-xs">
|
||||
{l(lang, "Theme")}
|
||||
</h3>
|
||||
<RadioGroup.Root bind:value={theme} class=" flex">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="light" id="light-theme" />
|
||||
<Label class="text-xs" for="light-theme">{l(lang, "Light")}</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="dark" id="dark-theme" />
|
||||
<Label class="text-xs" for="dark-theme">{l(lang, "Dark")}</Label>
|
||||
</div>
|
||||
<RadioGroup.Input name="theme" />
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<div class="col-span-1 pl-2">
|
||||
<h3 class="mb-2 text-xs">
|
||||
{l(lang, "Mode")}
|
||||
</h3>
|
||||
<RadioGroup.Root bind:value={embedType} class="flex">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="js" id="js-embed" />
|
||||
<Label class="text-xs" for="js-embed"><script></Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="iframe" id="iframe-embed" />
|
||||
<Label class="text-xs" for="iframe-embed"><iframe></Label>
|
||||
</div>
|
||||
<RadioGroup.Input name="embed" />
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
</div>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyScriptTagToClipboard}>
|
||||
{#if !copiedEmbed}
|
||||
<Code class="mr-2 inline" size={12} />
|
||||
<span class=" font-semibold">
|
||||
{l(lang, "Copy Code")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Code Copied")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
<hr class="my-4" />
|
||||
<div class="px-4">
|
||||
<h2 class="mb-1 text-sm font-semibold">
|
||||
{l(lang, "Badge")}
|
||||
</h2>
|
||||
<p class="mb-2 text-xs text-muted-foreground">
|
||||
{l(lang, "Get SVG badge for this monitor")}
|
||||
</p>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyStatusBadge}>
|
||||
{#if !copiedBadgeStatus}
|
||||
<TrendingUp class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Status")}
|
||||
{l(lang, "Badge")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Badge Copied")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyUptimeBadge}>
|
||||
{#if !copiedBadgeUptime}
|
||||
<Percent class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Uptime")}
|
||||
{l(lang, "Badge")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Badge Copied")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<h2 class="mb-1 text-sm font-semibold">
|
||||
{l(lang, "Badge")}
|
||||
</h2>
|
||||
<p class="mb-2 text-xs text-muted-foreground">
|
||||
{l(lang, "Get SVG badge for this monitor")}
|
||||
</p>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyStatusBadge}>
|
||||
{#if !copiedBadgeStatus}
|
||||
<TrendingUp class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Status")}
|
||||
{l(lang, "Badge")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Badge Copied")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyUptimeBadge}>
|
||||
{#if !copiedBadgeUptime}
|
||||
<Percent class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Uptime")}
|
||||
{l(lang, "Badge")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline" size={12} />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Badge Copied")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
<hr class="my-4" />
|
||||
<div class="mb-4 px-4">
|
||||
<h2 class="mb-1 text-sm font-semibold">
|
||||
{l(lang, "LIVE Status")}
|
||||
</h2>
|
||||
<p class="mb-2 text-xs text-muted-foreground">
|
||||
{l(lang, "Get a LIVE Status for this monitor")}
|
||||
</p>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyDotStandard}>
|
||||
{#if !copiedBadgeDotStandard}
|
||||
<img src={pathMonitorBadgeDot} class="mr-1 inline h-5" alt="status" />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Standard")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline h-5 w-5" />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Standard")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyDotPing}>
|
||||
{#if !copiedBadgeDotPing}
|
||||
<img src={pathMonitorBadgeDotPing} class="mr-1 inline h-5" alt="status" />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Pinging")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline h-5 w-5" />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Pinging")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<h2 class="mb-1 text-sm font-semibold">
|
||||
{l(lang, "LIVE Status")}
|
||||
</h2>
|
||||
<p class="mb-2 text-xs text-muted-foreground">
|
||||
{l(lang, "Get a LIVE Status for this monitor")}
|
||||
</p>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyDotStandard}>
|
||||
{#if !copiedBadgeDotStandard}
|
||||
<img src={pathMonitorBadgeDot} class="mr-1 inline h-5" alt="status" />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Standard")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline h-5 w-5" />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Standard")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyDotPing}>
|
||||
{#if !copiedBadgeDotPing}
|
||||
<img src={pathMonitorBadgeDotPing} class="mr-1 inline h-5" alt="status" />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Pinging")}
|
||||
</span>
|
||||
{:else}
|
||||
<CopyCheck class="mr-2 inline h-5 w-5" />
|
||||
<span class="font-semibold">
|
||||
{l(lang, "Pinging")}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<script>
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { page } from "$app/stores";
|
||||
import { base } from "$app/paths";
|
||||
import { onMount } from "svelte";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import SquareChartGantt from "lucide-svelte/icons/square-chart-gantt";
|
||||
import AlarmClockCheck from "lucide-svelte/icons/alarm-clock-check";
|
||||
import Logout from "lucide-svelte/icons/log-out";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import { l } from "$lib/i18n/client";
|
||||
|
||||
let apiError = "";
|
||||
let view = "loading";
|
||||
let localStorageKey = "user_subscription";
|
||||
|
||||
let login = {
|
||||
userEmail: "",
|
||||
verifyWithToken: "",
|
||||
verificationCode: ""
|
||||
};
|
||||
|
||||
let subscribedMonitors = [];
|
||||
|
||||
let subsInfo = {
|
||||
allMonitors: false,
|
||||
monitors: [],
|
||||
hasActiveSubscription: false
|
||||
};
|
||||
|
||||
let subscribableMonitors = $page.data.subscribableMonitors;
|
||||
let callingAPI = false;
|
||||
let alreadySubscribed = false;
|
||||
let subscriberData = null;
|
||||
|
||||
onMount(async () => {
|
||||
// Try to fetch stored subscription data from localStorage
|
||||
await fetchStoredSubscription();
|
||||
//
|
||||
});
|
||||
|
||||
async function createNewUserSubscription() {
|
||||
callingAPI = true;
|
||||
apiError = "";
|
||||
let action = "subscribe";
|
||||
|
||||
try {
|
||||
const response = await callAPI(action, {
|
||||
token: localStorage.getItem(localStorageKey),
|
||||
monitors: subsInfo.monitors,
|
||||
allMonitors: subsInfo.allMonitors
|
||||
});
|
||||
await fetchStoredSubscription();
|
||||
} catch (e) {
|
||||
apiError = e.message;
|
||||
} finally {
|
||||
callingAPI = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSubscription() {
|
||||
//use confirm
|
||||
if (!confirm("Are you sure you want to unsubscribe?")) {
|
||||
return;
|
||||
}
|
||||
callingAPI = true;
|
||||
apiError = "";
|
||||
let action = "unsubscribe";
|
||||
|
||||
try {
|
||||
await callAPI(action, {
|
||||
token: localStorage.getItem(localStorageKey)
|
||||
});
|
||||
await fetchStoredSubscription();
|
||||
} catch (e) {
|
||||
apiError = e.message;
|
||||
} finally {
|
||||
callingAPI = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function callAPI(action, data) {
|
||||
try {
|
||||
const response = await fetch(`${base}/api/subscriptions/${action}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
let error = await response.json();
|
||||
throw new Error(error.message || "An error occurred while processing your request.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error calling API:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function doLogin() {
|
||||
let action = "login";
|
||||
callingAPI = true;
|
||||
apiError = "";
|
||||
try {
|
||||
const response = await callAPI(action, {
|
||||
userEmail: login.userEmail
|
||||
});
|
||||
|
||||
if (response) {
|
||||
login.verifyWithToken = response.token;
|
||||
login.verificationCode = "";
|
||||
view = "verify_login";
|
||||
}
|
||||
} catch (e) {
|
||||
apiError = e.message;
|
||||
} finally {
|
||||
callingAPI = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyLogin() {
|
||||
let action = "verify";
|
||||
callingAPI = true;
|
||||
apiError = "";
|
||||
try {
|
||||
const response = await callAPI(action, {
|
||||
token: login.verifyWithToken,
|
||||
code: login.verificationCode
|
||||
});
|
||||
|
||||
if (response) {
|
||||
localStorage.setItem(localStorageKey, response.token);
|
||||
await fetchStoredSubscription();
|
||||
}
|
||||
} catch (e) {
|
||||
apiError = e.message;
|
||||
} finally {
|
||||
callingAPI = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStoredSubscription() {
|
||||
let action = "fetch";
|
||||
view = "loading";
|
||||
let token = localStorage.getItem(localStorageKey);
|
||||
if (!!!token) {
|
||||
view = "login";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await callAPI(action, {
|
||||
token: token
|
||||
});
|
||||
|
||||
if (response) {
|
||||
subsInfo.email = response.email;
|
||||
view = "subscribe";
|
||||
subsInfo.monitors = response.monitors;
|
||||
if (subsInfo.monitors && subsInfo.monitors.length > 0) {
|
||||
subsInfo.hasActiveSubscription = true;
|
||||
//if parsedData.monitors contains "_", set allMonitors to true
|
||||
const allMonitorsIncluded = subsInfo.monitors.includes("_");
|
||||
if (allMonitorsIncluded) {
|
||||
subsInfo.allMonitors = true;
|
||||
} else {
|
||||
subsInfo.allMonitors = false;
|
||||
subsInfo.monitors = subsInfo.monitors.filter((m) => m !== "_");
|
||||
}
|
||||
} else {
|
||||
subsInfo.allMonitors = false;
|
||||
subsInfo.monitors = [];
|
||||
subsInfo.hasActiveSubscription = false; // Updated to set hasActiveSubscription
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
view = "login";
|
||||
}
|
||||
}
|
||||
|
||||
function doLogout() {
|
||||
localStorage.removeItem(localStorageKey);
|
||||
login.userEmail = "";
|
||||
login.verificationCode = "";
|
||||
login.verifyWithToken = "";
|
||||
view = "login";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 pb-4" use:autoAnimate>
|
||||
<div class="relative mb-2 scroll-m-20 px-4 pt-4 text-xl font-medium tracking-tight">
|
||||
<p>
|
||||
{l($page.data.lang, "Manage Subscription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if view === "login"}
|
||||
<div class="flex flex-col gap-2 px-4">
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-semibold text-muted-foreground">
|
||||
{l(
|
||||
$page.data.lang,
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe."
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div class="relative">
|
||||
<Input type="email" placeholder={l($page.data.lang, "Enter your email")} bind:value={login.userEmail} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-col gap-2 px-4">
|
||||
{#if apiError}
|
||||
<p class="text-xs font-semibold text-destructive">{apiError}</p>
|
||||
{/if}
|
||||
<Button class="w-full" disabled={callingAPI || !login.userEmail} on:click={doLogin}>
|
||||
{l($page.data.lang, "Login")}
|
||||
{#if callingAPI}
|
||||
<Loader class="ml-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if view === "loading"}
|
||||
<div class="flex h-20 justify-center align-middle">
|
||||
<Loader class="mt-5 h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if view === "subscribe"}
|
||||
<div class="flex flex-col gap-2 px-4">
|
||||
{#if subsInfo.email}
|
||||
<div
|
||||
class="flex justify-between rounded-md stroke-secondary-foreground py-2 text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
<span class="mt-1 text-xs font-semibold text-muted-foreground">
|
||||
{l($page.data.lang, "You are logged in as %email", { email: subsInfo.email })}
|
||||
</span>
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class=" h-6 w-6 justify-center text-xs font-semibold text-muted-foreground"
|
||||
on:click={doLogout}
|
||||
>
|
||||
<Logout class=" h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-2">
|
||||
<label class="flex w-full cursor-pointer items-center justify-between">
|
||||
<span class="text-sm font-medium">{l($page.data.lang, "Subscribe to all monitors")}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
value=""
|
||||
class="peer sr-only"
|
||||
checked={subsInfo.allMonitors}
|
||||
on:change={(e) => {
|
||||
subsInfo.allMonitors = e.target.checked;
|
||||
if (e.target.checked) {
|
||||
subsInfo.monitors = [];
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{#if subscribableMonitors.length > 0 && !subsInfo.allMonitors}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="px-4">
|
||||
<span class="text-xs font-semibold text-muted-foreground">
|
||||
{l($page.data.lang, "Please select specific monitors to receive updates from.")}
|
||||
</span>
|
||||
</p>
|
||||
<div class="flex max-h-96 flex-col gap-2 overflow-y-auto px-4">
|
||||
{#each subscribableMonitors as monitor}
|
||||
<div class="flex items-center justify-between gap-2 border-b pb-2 last:border-0">
|
||||
<label for={monitor.tag} class="flex gap-x-2 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
{#if !!monitor.image}
|
||||
<GMI src={monitor.image} classList="mt-1 h-4 w-4" />
|
||||
{:else}
|
||||
<SquareChartGantt class="mt-1 h-4 w-4" />
|
||||
{/if}
|
||||
{monitor.name}
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
on:change={(e) => {
|
||||
if (e.target.checked) {
|
||||
subsInfo.monitors.push(monitor.tag);
|
||||
} else {
|
||||
subsInfo.monitors = subsInfo.monitors.filter((tag) => tag !== monitor.tag);
|
||||
}
|
||||
}}
|
||||
id={monitor.tag}
|
||||
value={monitor.tag}
|
||||
bind:group={subsInfo.monitors}
|
||||
class="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-2 flex flex-col gap-2 px-4">
|
||||
{#if apiError}
|
||||
<p class="text-xs font-semibold text-destructive">{apiError}</p>
|
||||
{/if}
|
||||
<Button class="w-full" disabled={callingAPI} on:click={createNewUserSubscription}>
|
||||
{#if subsInfo.monitors.length > 0}
|
||||
{l($page.data.lang, "Update Subscription")}
|
||||
{:else}
|
||||
{l($page.data.lang, "Subscribe")}
|
||||
{/if}
|
||||
|
||||
{#if callingAPI}
|
||||
<Loader class="ml-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if view === "verify_login"}
|
||||
<div class="flex flex-col gap-2 px-4">
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="bounce-left mb-2 h-8 justify-start pl-1.5 text-xs text-muted-foreground"
|
||||
on:click={() => (view = "login")}
|
||||
>
|
||||
<ChevronLeft class="arrow mr-1 h-5 w-5" />
|
||||
{l($page.data.lang, "Change Email")}
|
||||
</Button>
|
||||
<p class="mb-2 text-xs text-muted-foreground">
|
||||
{l($page.data.lang, "We have sent a code to your email. Please enter it below to confirm your login")}
|
||||
</p>
|
||||
|
||||
<Input type="text" placeholder={l($page.data.lang, "Enter the code")} bind:value={login.verificationCode} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-col gap-2 px-4">
|
||||
{#if apiError}
|
||||
<p class="text-xs font-semibold text-destructive">{apiError}</p>
|
||||
{/if}
|
||||
<Button class="w-full" disabled={callingAPI || !login.verificationCode} on:click={verifyLogin}>
|
||||
{l($page.data.lang, "Confirm Login")}
|
||||
|
||||
{#if callingAPI}
|
||||
<Loader class="ml-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import { ChevronDown } from "lucide-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = AccordionPrimitive.TriggerProps;
|
||||
type $$Events = AccordionPrimitive.TriggerEvents;
|
||||
type $$Props = AccordionPrimitive.TriggerProps;
|
||||
type $$Events = AccordionPrimitive.TriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let level: AccordionPrimitive.HeaderProps["level"] = 3;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let level: AccordionPrimitive.HeaderProps["level"] = 3;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Header {level} class="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
>
|
||||
<slot />
|
||||
<ChevronDown class="h-4 w-4 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
<AccordionPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
>
|
||||
<slot />
|
||||
<ChevronDown class="h-4 w-4 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Check } from "lucide-svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let checked: $$Props["checked"] = undefined;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let checked: $$Props["checked"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:checked
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
bind:checked
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Circle } from "lucide-svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import Circle from "lucide-svelte/icons/circle";
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerdown
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.RadioIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.RadioIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.RadioIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.RadioIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import { ChevronRight } from "lucide-svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
|
||||
|
||||
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||
inset?: boolean;
|
||||
};
|
||||
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let inset: $$Props["inset"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||
import { Circle } from "lucide-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||
import Circle from "lucide-svelte/icons/circle";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = RadioGroupPrimitive.ItemProps;
|
||||
type $$Events = RadioGroupPrimitive.ItemEvents;
|
||||
type $$Props = RadioGroupPrimitive.ItemProps;
|
||||
type $$Events = RadioGroupPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Item
|
||||
{value}
|
||||
class={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
{value}
|
||||
class={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<RadioGroupPrimitive.ItemIndicator>
|
||||
<Circle class="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.ItemIndicator>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<RadioGroupPrimitive.ItemIndicator>
|
||||
<Circle class="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.ItemIndicator>
|
||||
</div>
|
||||
</RadioGroupPrimitive.Item>
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { Check } from "lucide-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
|
||||
type $$Props = SelectPrimitive.ItemProps;
|
||||
type $$Events = SelectPrimitive.ItemEvents;
|
||||
type $$Props = SelectPrimitive.ItemProps;
|
||||
type $$Events = SelectPrimitive.ItemEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export let label: $$Props["label"] = undefined;
|
||||
export let disabled: $$Props["disabled"] = undefined;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"];
|
||||
export let label: $$Props["label"] = undefined;
|
||||
export let disabled: $$Props["disabled"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
{value}
|
||||
{disabled}
|
||||
{label}
|
||||
class={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
{value}
|
||||
{disabled}
|
||||
{label}
|
||||
class={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:keydown
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:pointerleave
|
||||
on:pointermove
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</SelectPrimitive.Item>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { ChevronDown } from "lucide-svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||
import { cn } from "$lib/utils";
|
||||
|
||||
type $$Props = SelectPrimitive.TriggerProps;
|
||||
type $$Events = SelectPrimitive.TriggerEvents;
|
||||
type $$Props = SelectPrimitive.TriggerProps;
|
||||
type $$Events = SelectPrimitive.TriggerEvents;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
let:builder
|
||||
on:click
|
||||
on:keydown
|
||||
class={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
let:builder
|
||||
on:click
|
||||
on:keydown
|
||||
>
|
||||
<slot {builder} />
|
||||
<div>
|
||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
<slot {builder} />
|
||||
<div>
|
||||
<ChevronDown class="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
</SelectPrimitive.Trigger>
|
||||
|
||||
+52
-46
@@ -1,70 +1,76 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr } from "date-fns/locale";
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr, nb, pl, es, it, faIR } from "date-fns/locale";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
|
||||
const locales = {
|
||||
ru,
|
||||
en: enUS,
|
||||
hi,
|
||||
de,
|
||||
"zh-CN": zhCN,
|
||||
vi,
|
||||
ja,
|
||||
nl,
|
||||
dk: da,
|
||||
fr,
|
||||
ko,
|
||||
"pt-BR": ptBR,
|
||||
tr
|
||||
ru,
|
||||
en: enUS,
|
||||
hi,
|
||||
de,
|
||||
"zh-CN": zhCN,
|
||||
vi,
|
||||
ja,
|
||||
nl,
|
||||
dk: da,
|
||||
fr,
|
||||
ko,
|
||||
"pt-BR": ptBR,
|
||||
tr,
|
||||
"nb-NO": nb,
|
||||
pl,
|
||||
es,
|
||||
it,
|
||||
fa: faIR,
|
||||
};
|
||||
|
||||
const f = function (date, formatStr, locale) {
|
||||
return format(date, formatStr, {
|
||||
locale: locales[locale] || enUS
|
||||
});
|
||||
const f = function (date, formatStr, locale, tz) {
|
||||
return formatInTimeZone(date, tz, formatStr, {
|
||||
locale: locales[locale] || enUS,
|
||||
});
|
||||
};
|
||||
|
||||
const fd = function (start, end, locale) {
|
||||
return formatDistance(start, end, { addSuffix: false, locale: locales[locale] });
|
||||
return formatDistance(start, end, { addSuffix: false, locale: locales[locale] });
|
||||
};
|
||||
const fdn = function (start, locale) {
|
||||
return formatDistanceToNow(start, { addSuffix: true, locale: locales[locale] });
|
||||
return formatDistanceToNow(start, { addSuffix: true, locale: locales[locale] });
|
||||
};
|
||||
const fdm = function (duration, locale) {
|
||||
return formatDuration(duration, {
|
||||
format: ["days", "hours", "minutes"],
|
||||
zero: false,
|
||||
delimiter: " ",
|
||||
locale: locales[locale]
|
||||
});
|
||||
return formatDuration(duration, {
|
||||
format: ["days", "hours", "minutes"],
|
||||
zero: false,
|
||||
delimiter: " ",
|
||||
locale: locales[locale],
|
||||
});
|
||||
};
|
||||
|
||||
const l = function (sessionLangMap, key, args = {}) {
|
||||
try {
|
||||
let obj = sessionLangMap[key];
|
||||
try {
|
||||
let obj = sessionLangMap[key];
|
||||
|
||||
// Replace placeholders in the string using the args object
|
||||
if (obj && typeof obj === "string") {
|
||||
obj = obj.replace(/%\w+/g, (placeholder) => {
|
||||
const argKey = placeholder.slice(1); // Remove the `%` to get the key
|
||||
return args[argKey] !== undefined ? args[argKey] : placeholder;
|
||||
});
|
||||
}
|
||||
return obj || key;
|
||||
} catch (e) {
|
||||
return key;
|
||||
}
|
||||
// Replace placeholders in the string using the args object
|
||||
if (obj && typeof obj === "string") {
|
||||
obj = obj.replace(/%\w+/g, (placeholder) => {
|
||||
const argKey = placeholder.slice(1); // Remove the `%` to get the key
|
||||
return args[argKey] !== undefined ? args[argKey] : placeholder;
|
||||
});
|
||||
}
|
||||
return obj || key;
|
||||
} catch (e) {
|
||||
return key;
|
||||
}
|
||||
};
|
||||
const summaryTime = function (summaryStatus) {
|
||||
if (summaryStatus == "No Data") {
|
||||
return "No Data";
|
||||
}
|
||||
if (summaryStatus == "UP") {
|
||||
return "Status OK";
|
||||
}
|
||||
if (summaryStatus == "No Data") {
|
||||
return "No Data";
|
||||
}
|
||||
if (summaryStatus == "UP") {
|
||||
return "Status OK";
|
||||
}
|
||||
|
||||
return "%status for %duration";
|
||||
return "%status for %duration";
|
||||
};
|
||||
|
||||
export { l, summaryTime, f, formatDistance, fd, fdn, fdm };
|
||||
|
||||
+90
-59
@@ -1,61 +1,92 @@
|
||||
{
|
||||
"%status for %duration": "%status für %duration",
|
||||
"14 Days": "14 Tage",
|
||||
"30 Days": "30 Tage",
|
||||
"60 Days": "60 Tage",
|
||||
"7 Days": "7 Tage",
|
||||
"90 Days": "90 Tage",
|
||||
"Availability per Component": "Verfügbarkeit pro Komponente",
|
||||
"Back": "Zurück",
|
||||
"Badge Copied": "Abzeichen kopiert",
|
||||
"Badge": "Abzeichen",
|
||||
"Browse Events": "Ereignisse durchsuchen",
|
||||
"Code Copied": "Code kopiert",
|
||||
"Copy Code": "Code kopieren",
|
||||
"Copy Link": "Link kopieren",
|
||||
"DEGRADED": "EINGESCHRÄNKT",
|
||||
"DOWN": "AUSGEFALLEN",
|
||||
"Dark": "Dunkel",
|
||||
"Days": "Tage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
|
||||
"Embed": "Einbetten",
|
||||
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
|
||||
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
|
||||
"IDENTIFIED": "IDENTIFIZIERT",
|
||||
"INVESTIGATING": "UNTERSUCHUNG",
|
||||
"Incident Updates": "Vorfall-Updates",
|
||||
"LIVE Status": "LIVE-Status",
|
||||
"Lasted for about %lastedFor": "Dauerte etwa %lastedFor",
|
||||
"Light": "Hell",
|
||||
"Link Copied": "Link kopiert",
|
||||
"MAINTENANCE": "WARTUNG",
|
||||
"MONITORING": "ÜBERWACHUNG",
|
||||
"Maintenance Completed": "Wartung abgeschlossen",
|
||||
"Maintenance in Progress": "Wartung läuft",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Keine Daten",
|
||||
"No Incident in %date": "Kein Vorfall am %date",
|
||||
"No Incidents": "Keine Vorfälle",
|
||||
"No Monitor Found": "Kein Monitor gefunden",
|
||||
"No Updates Yet": "Noch keine Updates",
|
||||
"Ongoing Incidents": "Laufende Vorfälle",
|
||||
"Pinging": "Pingen",
|
||||
"RESOLVED": "GELÖST",
|
||||
"Recent Incidents": "Kürzliche Vorfälle",
|
||||
"Recent Maintenances": "Kürzliche Wartungen",
|
||||
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
|
||||
"Share": "Teilen",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
|
||||
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
|
||||
"Starts %startedAt": "Beginnt in %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Thema",
|
||||
"Today": "Heute",
|
||||
"UP": "VERFÜGBAR",
|
||||
"Upcoming Maintenance": "Bevorstehende Wartung",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Verfügbarkeit"
|
||||
"%status for %duration": "%status für %duration",
|
||||
"120 Days": "120 Tage",
|
||||
"14 Days": "14 Tage",
|
||||
"150 Days": "150 Tage",
|
||||
"180 Days": "180 Tage",
|
||||
"30 Days": "30 Tage",
|
||||
"60 Days": "60 Tage",
|
||||
"7 Days": "7 Tage",
|
||||
"90 Days": "90 Tage",
|
||||
"Availability per Component": "Verfügbarkeit pro Komponente",
|
||||
"All Systems are Degraded": "Alle Systeme sind eingeschränkt",
|
||||
"All Systems are Down": "Alle Systeme sind ausgefallen",
|
||||
"All Systems are Operational": "Alle Systeme sind betriebsbereit",
|
||||
"All Systems are in Maintenance": "Alle Systeme sind in Wartung",
|
||||
"Back": "Zurück",
|
||||
"Badge Copied": "Abzeichen kopiert",
|
||||
"Badge": "Abzeichen",
|
||||
"Browse Events": "Ereignisse durchsuchen",
|
||||
"Change Email": "E-Mail ändern",
|
||||
"Code Copied": "Code kopiert",
|
||||
"Confirm Login": "Login bestätigen",
|
||||
"Copy Code": "Code kopieren",
|
||||
"Copy Link": "Link kopieren",
|
||||
"DEGRADED": "EINGESCHRÄNKT",
|
||||
"DOWN": "AUSGEFALLEN",
|
||||
"Dark": "Dunkel",
|
||||
"Days": "Tage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
|
||||
"Embed": "Einbetten",
|
||||
"Enter the code": "Code eingeben",
|
||||
"Enter your email": "E-Mail-Adresse eingeben",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Geben Sie Ihre E-Mail-Adresse ein, um sich anzumelden. Wenn Sie nicht abonniert haben, werden Sie zum Abonnieren aufgefordert.",
|
||||
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
|
||||
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
|
||||
"IDENTIFIED": "IDENTIFIZIERT",
|
||||
"INVESTIGATING": "UNTERSUCHUNG",
|
||||
"Incident Updates": "Vorfall-Updates",
|
||||
"LIVE Status": "LIVE-Status",
|
||||
"Lasted for about %lastedFor": "Dauerte etwa %lastedFor",
|
||||
"Light": "Hell",
|
||||
"Link Copied": "Link kopiert",
|
||||
"Login": "Anmelden",
|
||||
"MAINTENANCE": "WARTUNG",
|
||||
"MONITORING": "ÜBERWACHUNG",
|
||||
"Maintenance Completed": "Wartung abgeschlossen",
|
||||
"Maintenance in Progress": "Wartung läuft",
|
||||
"Manage Subscription": "Abonnement verwalten",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Keine Daten",
|
||||
"No Incident in %date": "Kein Vorfall am %date",
|
||||
"No Incidents": "Keine Vorfälle",
|
||||
"No Monitor Found": "Kein Monitor gefunden",
|
||||
"No Updates Yet": "Noch keine Updates",
|
||||
"Ongoing Incidents": "Laufende Vorfälle",
|
||||
"Pinging": "Pingen",
|
||||
"Please select specific monitors to receive updates from.": "Bitte wählen Sie bestimmte Monitore aus, von denen Sie Updates erhalten möchten.",
|
||||
"RESOLVED": "GELÖST",
|
||||
"Recent Incidents": "Kürzliche Vorfälle",
|
||||
"Recent Maintenances": "Kürzliche Wartungen",
|
||||
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
|
||||
"Share": "Teilen",
|
||||
"Some Systems are not working as expected": "Einige Systeme funktionieren nicht wie erwartet",
|
||||
"Some Systems Degraded": "Einige Systeme eingeschränkt",
|
||||
"Some Systems Down": "Einige Systeme ausgefallen",
|
||||
"Some Systems in Maintenance": "Einige Systeme sind in Wartung",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
|
||||
"Started %startedAt, will last for %lastedFor more": "Begann vor etwa %startedAt und wird noch etwa %lastedFor dauern",
|
||||
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
|
||||
"Starts %startedAt": "Beginnt in %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Subscribe": "Abonnieren",
|
||||
"Subscribe to Updates": "Updates abonnieren",
|
||||
"Subscribe to all monitors": "Alle Monitore abonnieren",
|
||||
"Switch to your timezone": "Wechseln Sie zu Ihrer Zeitzone",
|
||||
"Theme": "Thema",
|
||||
"There are no upcoming maintenances": "Es gibt keine bevorstehenden Wartungen",
|
||||
"Timezone set to %tz": "Zeitzone auf %tz gesetzt",
|
||||
"Today": "Heute",
|
||||
"UP": "VERFÜGBAR",
|
||||
"Update Subscription": "Abonnement aktualisieren",
|
||||
"Upcoming Maintenance": "Bevorstehende Wartung",
|
||||
"Upcoming Maintenances": "Bevorstehende Wartungen",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Verfügbarkeit",
|
||||
"View in detail": "Details anzeigen",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Wir haben einen Code an Ihre E-Mail-Adresse gesendet. Bitte geben Sie ihn unten ein, um Ihren Login zu bestätigen",
|
||||
"You are logged in as %email": "Sie sind angemeldet als %email"
|
||||
}
|
||||
|
||||
+90
-59
@@ -1,61 +1,92 @@
|
||||
{
|
||||
"%status for %duration": "%status i %duration",
|
||||
"14 Days": "14 Dage",
|
||||
"30 Days": "30 Dage",
|
||||
"60 Days": "60 Dage",
|
||||
"7 Days": "7 Dage",
|
||||
"90 Days": "90 Dage",
|
||||
"Availability per Component": "Tilgængelighed pr. Komponent",
|
||||
"Back": "Tilbage",
|
||||
"Badge Copied": "Mærke Kopieret",
|
||||
"Badge": "Mærke",
|
||||
"Browse Events": "Gennemse begivenheder",
|
||||
"Code Copied": "Kode Kopieret",
|
||||
"Copy Code": "Kopier Kode",
|
||||
"Copy Link": "Kopier Link",
|
||||
"DEGRADED": "FORRINGET",
|
||||
"DOWN": "NEDE",
|
||||
"Dark": "Mørk",
|
||||
"Days": "Dage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
|
||||
"Embed": "Integrer",
|
||||
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
|
||||
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
|
||||
"IDENTIFIED": "IDENTIFICERET",
|
||||
"INVESTIGATING": "UNDERSØGER",
|
||||
"Incident Updates": "Hændelsesopdateringer",
|
||||
"LIVE Status": "LIVE-status",
|
||||
"Lasted for about %lastedFor": "Varede i cirka %lastedFor",
|
||||
"Light": "Lys",
|
||||
"Link Copied": "Link Kopieret",
|
||||
"MAINTENANCE": "VEDLIGEHOLDELSE",
|
||||
"MONITORING": "OVERVÅGNING",
|
||||
"Maintenance Completed": "Vedligeholdelse afsluttet",
|
||||
"Maintenance in Progress": "Vedligeholdelse i gang",
|
||||
"Mode": "Tilstand",
|
||||
"No Data": "Ingen Data",
|
||||
"No Incident in %date": "Ingen Hændelse Den %date",
|
||||
"No Incidents": "Ingen Hændelser",
|
||||
"No Monitor Found": "Ingen Monitor Fundet",
|
||||
"No Updates Yet": "Ingen Opdateringer Endnu",
|
||||
"Ongoing Incidents": "Igangværende Hændelser",
|
||||
"Pinging": "Pinger",
|
||||
"RESOLVED": "LØST",
|
||||
"Recent Incidents": "Seneste Hændelser",
|
||||
"Recent Maintenances": "Seneste Vedligeholdelser",
|
||||
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
|
||||
"Share": "Del",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
|
||||
"Starts %startedAt": "Starter om %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Tema",
|
||||
"Today": "I dag",
|
||||
"UP": "OPPE",
|
||||
"Upcoming Maintenance": "Kommende vedligeholdelse",
|
||||
"Updates": "Opdateringer",
|
||||
"Uptime": "Oppetid"
|
||||
"%status for %duration": "%status i %duration",
|
||||
"14 Days": "14 Dage",
|
||||
"30 Days": "30 Dage",
|
||||
"60 Days": "60 Dage",
|
||||
"7 Days": "7 Dage",
|
||||
"90 Days": "90 Dage",
|
||||
"120 Days": "120 Dage",
|
||||
"150 Days": "150 Dage",
|
||||
"180 Days": "180 Dage",
|
||||
"Availability per Component": "Tilgængelighed pr. Komponent",
|
||||
"All Systems are Degraded": "Alle Systemer er Forringet",
|
||||
"All Systems are Down": "Alle Systemer er Nede",
|
||||
"All Systems are Operational": "Alle Systemer er Operationelle",
|
||||
"All Systems are in Maintenance": "Alle systemer er i vedligeholdelse",
|
||||
"Back": "Tilbage",
|
||||
"Badge Copied": "Mærke Kopieret",
|
||||
"Badge": "Mærke",
|
||||
"Browse Events": "Gennemse begivenheder",
|
||||
"Change Email": "Skift Email",
|
||||
"Code Copied": "Kode Kopieret",
|
||||
"Confirm Login": "Bekræft Login",
|
||||
"Copy Code": "Kopier Kode",
|
||||
"Copy Link": "Kopier Link",
|
||||
"DEGRADED": "FORRINGET",
|
||||
"DOWN": "NEDE",
|
||||
"Dark": "Mørk",
|
||||
"Days": "Dage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
|
||||
"Embed": "Integrer",
|
||||
"Enter the code": "Indtast koden",
|
||||
"Enter your email": "Indtast din email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Indtast din email for at logge ind. Hvis du ikke er abonnent, vil du blive bedt om at abonnere.",
|
||||
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
|
||||
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
|
||||
"IDENTIFIED": "IDENTIFICERET",
|
||||
"INVESTIGATING": "UNDERSØGER",
|
||||
"Incident Updates": "Hændelsesopdateringer",
|
||||
"LIVE Status": "LIVE-status",
|
||||
"Lasted for about %lastedFor": "Varede i cirka %lastedFor",
|
||||
"Light": "Lys",
|
||||
"Link Copied": "Link Kopieret",
|
||||
"Login": "Log ind",
|
||||
"MAINTENANCE": "VEDLIGEHOLDELSE",
|
||||
"MONITORING": "OVERVÅGNING",
|
||||
"Maintenance Completed": "Vedligeholdelse afsluttet",
|
||||
"Maintenance in Progress": "Vedligeholdelse i gang",
|
||||
"Manage Subscription": "Administrer Abonnement",
|
||||
"Mode": "Tilstand",
|
||||
"No Data": "Ingen Data",
|
||||
"No Incident in %date": "Ingen Hændelse Den %date",
|
||||
"No Incidents": "Ingen Hændelser",
|
||||
"No Monitor Found": "Ingen Monitor Fundet",
|
||||
"No Updates Yet": "Ingen Opdateringer Endnu",
|
||||
"Ongoing Incidents": "Igangværende Hændelser",
|
||||
"Pinging": "Pinger",
|
||||
"Please select specific monitors to receive updates from.": "Vælg venligst specifikke monitorer for at modtage opdateringer fra.",
|
||||
"RESOLVED": "LØST",
|
||||
"Recent Incidents": "Seneste Hændelser",
|
||||
"Recent Maintenances": "Seneste Vedligeholdelser",
|
||||
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
|
||||
"Share": "Del",
|
||||
"Some Systems are not working as expected": "Nogle Systemer fungerer ikke som forventet",
|
||||
"Some Systems Degraded": "Nogle Systemer er Forringet",
|
||||
"Some Systems Down": "Nogle Systemer er Nede",
|
||||
"Some Systems in Maintenance": "Nogle Systemer er i Vedligeholdelse",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
|
||||
"Started %startedAt, will last for %lastedFor more": "Startede for cirka %startedAt siden, vil vare i cirka %lastedFor mere",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
|
||||
"Starts %startedAt": "Starter om %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Subscribe": "Abonner",
|
||||
"Subscribe to Updates": "Abonner på opdateringer",
|
||||
"Subscribe to all monitors": "Abonner på alle monitorer",
|
||||
"Switch to your timezone": "Skift til din tidszone",
|
||||
"Theme": "Tema",
|
||||
"There are no upcoming maintenances": "Der er ingen kommende vedligeholdelser",
|
||||
"Timezone set to %tz": "Tidszone sat til %tz",
|
||||
"Today": "I dag",
|
||||
"UP": "OPPE",
|
||||
"Update Subscription": "Opdater Abonnement",
|
||||
"Upcoming Maintenance": "Kommende vedligeholdelse",
|
||||
"Upcoming Maintenances": "Kommende vedligeholdelser",
|
||||
"Updates": "Opdateringer",
|
||||
"Uptime": "Oppetid",
|
||||
"View in detail": "Vis i detaljer",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Vi har sendt en kode til din email. Indtast den venligst nedenfor for at bekræfte dit login",
|
||||
"You are logged in as %email": "Du er logget ind som %email"
|
||||
}
|
||||
|
||||
+90
-59
@@ -1,61 +1,92 @@
|
||||
{
|
||||
"%status for %duration": "%status for %duration",
|
||||
"14 Days": "14 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"7 Days": "7 Days",
|
||||
"90 Days": "90 Days",
|
||||
"Availability per Component": "Availability per Component",
|
||||
"Back": "Back",
|
||||
"Badge Copied": "Badge Copied",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Browse Events",
|
||||
"Code Copied": "Code Copied",
|
||||
"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",
|
||||
"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",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"MONITORING": "MONITORING",
|
||||
"Maintenance Completed": "Maintenance Completed",
|
||||
"Maintenance in Progress": "Maintenance in Progress",
|
||||
"Mode": "Mode",
|
||||
"No Data": "No Data",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
"Pinging": "Pinging",
|
||||
"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",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Theme",
|
||||
"Today": "Today",
|
||||
"UP": "UP",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"%status for %duration": "%status for %duration",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"%status for %duration": "%status durante %duration",
|
||||
"120 Days": "120 Días",
|
||||
"14 Days": "14 Días",
|
||||
"150 Days": "150 Días",
|
||||
"180 Days": "180 Días",
|
||||
"30 Days": "30 Días",
|
||||
"60 Days": "60 Días",
|
||||
"7 Days": "7 Días",
|
||||
"90 Days": "90 Días",
|
||||
"Availability per Component": "Disponibilidad por componente",
|
||||
"All Systems are Degraded": "Todos los sistemas están degradados",
|
||||
"All Systems are Down": "Todos los sistemas están caídos",
|
||||
"All Systems are Operational": "Todos los sistemas están operativos",
|
||||
"Back": "Volver",
|
||||
"Badge Copied": "Distintivo copiado",
|
||||
"Badge": "Distintivo",
|
||||
"Browse Events": "Ver eventos",
|
||||
"Change Email": "Cambiar correo electrónico",
|
||||
"Code Copied": "Código copiado",
|
||||
"Confirm Login": "Confirmar inicio de sesión",
|
||||
"Copy Code": "Copiar código",
|
||||
"Copy Link": "Copiar enlace",
|
||||
"DEGRADED": "DEGRADADO",
|
||||
"DOWN": "CAÍDO",
|
||||
"Dark": "Oscuro",
|
||||
"Days": "Días",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incrusta este monitor usando <script> o <iframe> en tu aplicación.",
|
||||
"Embed": "Incrustar",
|
||||
"Enter the code": "Ingresa el código",
|
||||
"Enter your email": "Ingresa tu correo electrónico",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Ingresa tu correo electrónico para iniciar sesión. Si no estás suscrito, se te pedirá que te suscribas.",
|
||||
"Get SVG badge for this monitor": "Obtener distintivo SVG para este monitor",
|
||||
"Get a LIVE Status for this monitor": "Obtener estado EN VIVO para este monitor",
|
||||
"IDENTIFIED": "IDENTIFICADO",
|
||||
"INVESTIGATING": "INVESTIGANDO",
|
||||
"Incident Updates": "Actualizaciones de incidentes",
|
||||
"LIVE Status": "Estado EN VIVO",
|
||||
"Lasted for about %lastedFor": "Duró aproximadamente %lastedFor",
|
||||
"Light": "Claro",
|
||||
"Link Copied": "Enlace copiado",
|
||||
"Login": "Iniciar sesión",
|
||||
"MAINTENANCE": "MANTENIMIENTO",
|
||||
"MONITORING": "MONITOREANDO",
|
||||
"Maintenance Completed": "Mantenimiento completado",
|
||||
"Maintenance in Progress": "Mantenimiento en progreso",
|
||||
"Manage Subscription": "Gestionar suscripción",
|
||||
"Mode": "Modo",
|
||||
"No Data": "Sin datos",
|
||||
"No Incident in %date": "Sin incidentes en %date",
|
||||
"No Incidents": "Sin incidentes",
|
||||
"No Monitor Found": "No se encontró monitor",
|
||||
"No Updates Yet": "Aún no hay actualizaciones",
|
||||
"Ongoing Incidents": "Incidentes en curso",
|
||||
"Pinging": "Haciendo ping",
|
||||
"Please select specific monitors to receive updates from.": "Por favor selecciona monitores específicos para recibir actualizaciones.",
|
||||
"RESOLVED": "RESUELTO",
|
||||
"Recent Incidents": "Incidentes recientes",
|
||||
"Recent Maintenances": "Mantenimientos recientes",
|
||||
"Share this monitor using a link with others": "Comparte este monitor mediante un enlace con otros",
|
||||
"Share": "Compartir",
|
||||
"Some Systems are not working as expected": "Algunos sistemas no funcionan como se espera",
|
||||
"Some Systems Degraded": "Algunos sistemas degradados",
|
||||
"Some Systems Down": "Algunos sistemas caídos",
|
||||
"Standard": "Estándar",
|
||||
"Started %startedAt, lasted for %lastedFor": "Comenzó el %startedAt, duró %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Comenzó el %startedAt, aún en curso",
|
||||
"Started %startedAt, will last for %lastedFor more": "Comenzó el %startedAt, durará %lastedFor más",
|
||||
"Starts %startedAt, will last for %lastedFor": "Empieza el %startedAt, durará %lastedFor",
|
||||
"Starts %startedAt": "Empieza el %startedAt",
|
||||
"Status OK": "Estado OK",
|
||||
"Status": "Estado",
|
||||
"Subscribe": "Suscribirse",
|
||||
"Subscribe to Updates": "Suscribirse a actualizaciones",
|
||||
"Subscribe to all monitors": "Suscribirse a todos los monitores",
|
||||
"Switch to your timezone": "Cambiar a tu zona horaria",
|
||||
"Theme": "Tema",
|
||||
"There are no upcoming maintenances": "No hay mantenimientos próximos",
|
||||
"Timezone set to %tz": "Zona horaria configurada a %tz",
|
||||
"Today": "Hoy",
|
||||
"UP": "ACTIVO",
|
||||
"Update Subscription": "Actualizar suscripción",
|
||||
"Upcoming Maintenance": "Mantenimiento próximo",
|
||||
"Upcoming Maintenances": "Mantenimientos próximos",
|
||||
"Updates": "Actualizaciones",
|
||||
"Uptime": "Tiempo en línea",
|
||||
"View in detail": "Ver en detalle",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Hemos enviado un código a tu correo. Por favor, ingrésalo abajo para confirmar tu inicio de sesión",
|
||||
"You are logged in as %email": "Has iniciado sesión como %email"
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"%status for %duration": "%status به مدت %duration",
|
||||
"120 Days": "۱۲۰ روزه",
|
||||
"14 Days": "۱۴ روزه",
|
||||
"150 Days": "۱۵۰ روزه",
|
||||
"180 Days": "۱۸۰ روزه",
|
||||
"30 Days": "۳۰ روزه",
|
||||
"60 Days": "۶۰ روزه",
|
||||
"7 Days": "۷ روزه",
|
||||
"90 Days": "۹۰ روزه",
|
||||
"Availability per Component": "دسترسپذیری هر کامپوننت",
|
||||
"All Systems are Degraded": "همه سامانهها دچار اختلالاند",
|
||||
"All Systems are Down": "همه سامانهها از کار افتادهاند",
|
||||
"All Systems are Operational": "همه سامانهها در حال کار هستند",
|
||||
"All Systems are in Maintenance": "همه سامانهها در حال تعمیر هستند",
|
||||
"Back": "بازگشت",
|
||||
"Badge Copied": "نشان کپی شد",
|
||||
"Badge": "نشان",
|
||||
"Browse Events": "مشاهده رویدادها",
|
||||
"Change Email": "تغییر ایمیل",
|
||||
"Code Copied": "کد کپی شد",
|
||||
"Confirm Login": "تأیید ورود",
|
||||
"Copy Code": "کپی کد",
|
||||
"Copy Link": "کپی لینک",
|
||||
"DEGRADED": "اختلال",
|
||||
"DOWN": "از کار افتاده",
|
||||
"Dark": "تاریک",
|
||||
"Days": "روزها",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "این مانیتور را با استفاده از <script> یا <iframe> در برنامهتان قرار دهید.",
|
||||
"Embed": "درج",
|
||||
"Enter the code": "کد را وارد کنید",
|
||||
"Enter your email": "ایمیل خود را وارد کنید",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "برای ورود، ایمیل خود را وارد کنید. اگر عضو نیستید، از شما خواسته میشود عضو شوید.",
|
||||
"Get SVG badge for this monitor": "دریافت نشان SVG برای این مانیتور",
|
||||
"Get a LIVE Status for this monitor": "دریافت وضعیت زنده این مانیتور",
|
||||
"IDENTIFIED": "شناسایی شده",
|
||||
"INVESTIGATING": "در حال بررسی",
|
||||
"Incident Updates": "بهروزرسانیهای حادثه",
|
||||
"LIVE Status": "وضعیت زنده",
|
||||
"Lasted for about %lastedFor": "حدوداً به مدت %lastedFor ادامه داشت",
|
||||
"Light": "روشن",
|
||||
"Link Copied": "لینک کپی شد",
|
||||
"Login": "ورود",
|
||||
"MAINTENANCE": "تعمیر و نگهداری",
|
||||
"MONITORING": "در حال پایش",
|
||||
"Maintenance Completed": "تعمیرات به پایان رسید",
|
||||
"Maintenance in Progress": "تعمیرات در حال انجام است",
|
||||
"Manage Subscription": "مدیریت اشتراک",
|
||||
"Mode": "حالت",
|
||||
"No Data": "بدون داده",
|
||||
"No Incident in %date": "هیچ حادثهای در %date وجود ندارد",
|
||||
"No Incidents": "هیچ حادثهای وجود ندارد",
|
||||
"No Monitor Found": "هیچ مانیتوری یافت نشد",
|
||||
"No Updates Yet": "هنوز بهروزرسانیای وجود ندارد",
|
||||
"Ongoing Incidents": "حوادث در حال وقوع",
|
||||
"Pinging": "در حال پینگ گرفتن",
|
||||
"Please select specific monitors to receive updates from.": "لطفاً مانیتورهای خاصی را برای دریافت بهروزرسانی انتخاب کنید.",
|
||||
"RESOLVED": "حل شده",
|
||||
"Recent Incidents": "حوادث اخیر",
|
||||
"Recent Maintenances": "تعمیرات اخیر",
|
||||
"Share this monitor using a link with others": "این مانیتور را با لینک با دیگران به اشتراک بگذارید",
|
||||
"Share": "اشتراکگذاری",
|
||||
"Some Systems are not working as expected": "برخی سامانهها بهدرستی کار نمیکنند",
|
||||
"Some Systems Degraded": "برخی سامانهها دچار اختلالاند",
|
||||
"Some Systems Down": "برخی سامانهها از کار افتادهاند",
|
||||
"Some Systems in Maintenance": "برخی سامانهها در حال تعمیر هستند",
|
||||
"Standard": "معمولی",
|
||||
"Started %startedAt, lasted for %lastedFor": "شروع در %startedAt، به مدت %lastedFor ادامه داشت",
|
||||
"Started %startedAt, still ongoing": "شروع در %startedAt، هنوز ادامه دارد",
|
||||
"Started %startedAt, will last for %lastedFor more": "شروع در %startedAt، تا %lastedFor دیگر ادامه خواهد داشت",
|
||||
"Starts %startedAt, will last for %lastedFor": "شروع در %startedAt، به مدت %lastedFor ادامه دارد",
|
||||
"Starts %startedAt": "شروع در %startedAt",
|
||||
"Status OK": "وضعیت خوب",
|
||||
"Status": "وضعیت",
|
||||
"Subscribe": "اشتراک",
|
||||
"Subscribe to Updates": "مشترک شدن در بهروزرسانیها",
|
||||
"Subscribe to all monitors": "اشتراک همه مانیتورها",
|
||||
"Switch to your timezone": "تغییر به منطقه زمانی شما",
|
||||
"Theme": "تم",
|
||||
"There are no upcoming maintenances": "هیچ تعمیر آیندهای وجود ندارد",
|
||||
"Timezone set to %tz": "منطقه زمانی روی %tz تنظیم شد",
|
||||
"Today": "امروز",
|
||||
"UP": "فعال",
|
||||
"Update Subscription": "بهروزرسانی اشتراک",
|
||||
"Upcoming Maintenance": "تعمیر آینده",
|
||||
"Upcoming Maintenances": "تعمیرات آینده",
|
||||
"Updates": "بهروزرسانیها",
|
||||
"Uptime": "زمان کارکرد",
|
||||
"View in detail": "مشاهده جزئیات",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "کدی به ایمیل شما ارسال کردیم. لطفاً برای تأیید ورود آن را وارد کنید.",
|
||||
"You are logged in as %email": "شما با ایمیل %email وارد شدهاید"
|
||||
}
|
||||
+90
-59
@@ -1,61 +1,92 @@
|
||||
{
|
||||
"%status for %duration": "%status pendant %duration",
|
||||
"14 Days": "14 jours",
|
||||
"30 Days": "30 jours",
|
||||
"60 Days": "60 jours",
|
||||
"7 Days": "7 jours",
|
||||
"90 Days": "90 jours",
|
||||
"Availability per Component": "Disponibilité par composant",
|
||||
"Back": "Retour",
|
||||
"Badge Copied": "Badge copié",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Parcourir les événements",
|
||||
"Code Copied": "Code copié",
|
||||
"Copy Code": "Copier le code",
|
||||
"Copy Link": "Copier le lien",
|
||||
"DEGRADED": "Dégradé",
|
||||
"DOWN": "Hors ligne",
|
||||
"Dark": "Sombre",
|
||||
"Days": "Jours",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
|
||||
"Embed": "Intégrer",
|
||||
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
|
||||
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
|
||||
"IDENTIFIED": "Identifié",
|
||||
"INVESTIGATING": "En cours d'investigation",
|
||||
"Incident Updates": "Mises à jour des incidents",
|
||||
"LIVE Status": "Statut LIVE",
|
||||
"Lasted for about %lastedFor": "A duré environ %lastedFor",
|
||||
"Light": "Clair",
|
||||
"Link Copied": "Lien copié",
|
||||
"MAINTENANCE": "Maintenance",
|
||||
"MONITORING": "En surveillance",
|
||||
"Maintenance Completed": "Maintenance terminée",
|
||||
"Maintenance in Progress": "Maintenance en cours",
|
||||
"Mode": "Mode",
|
||||
"No Data": "Pas de données",
|
||||
"No Incident in %date": "Pas d'incident le %date",
|
||||
"No Incidents": "Pas d'incidents",
|
||||
"No Monitor Found": "Aucun moniteur trouvé",
|
||||
"No Updates Yet": "Pas encore de mises à jour",
|
||||
"Ongoing Incidents": "Incidents en cours",
|
||||
"Pinging": "Ping en cours",
|
||||
"RESOLVED": "Résolu",
|
||||
"Recent Incidents": "Incidents récents",
|
||||
"Recent Maintenances": "Maintenances récentes",
|
||||
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
|
||||
"Share": "Partager",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
|
||||
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
|
||||
"Starts %startedAt": "Commence dans %startedAt",
|
||||
"Status OK": "Statut OK",
|
||||
"Status": "Statut",
|
||||
"Theme": "Thème",
|
||||
"Today": "Aujourd'hui",
|
||||
"UP": "En ligne",
|
||||
"Upcoming Maintenance": "Maintenance à venir",
|
||||
"Updates": "Mises à jour",
|
||||
"Uptime": "Temps de fonctionnement"
|
||||
"%status for %duration": "%status pendant %duration",
|
||||
"14 Days": "14 jours",
|
||||
"30 Days": "30 jours",
|
||||
"60 Days": "60 jours",
|
||||
"7 Days": "7 jours",
|
||||
"90 Days": "90 jours",
|
||||
"120 Days": "120 jours",
|
||||
"150 Days": "150 jours",
|
||||
"180 Days": "180 jours",
|
||||
"Availability per Component": "Disponibilité par composant",
|
||||
"All Systems are Degraded": "Tous les systèmes sont dégradés",
|
||||
"All Systems are Down": "Tous les systèmes sont hors ligne",
|
||||
"All Systems are Operational": "Tous les systèmes sont opérationnels",
|
||||
"All Systems are in Maintenance": "Tous les systèmes sont en maintenance",
|
||||
"Back": "Retour",
|
||||
"Badge Copied": "Badge copié",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Parcourir les événements",
|
||||
"Change Email": "Changer l'e-mail",
|
||||
"Code Copied": "Code copié",
|
||||
"Confirm Login": "Confirmer la connexion",
|
||||
"Copy Code": "Copier le code",
|
||||
"Copy Link": "Copier le lien",
|
||||
"DEGRADED": "Dégradé",
|
||||
"DOWN": "Hors ligne",
|
||||
"Dark": "Sombre",
|
||||
"Days": "Jours",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
|
||||
"Embed": "Intégrer",
|
||||
"Enter the code": "Entrez le code",
|
||||
"Enter your email": "Entrez votre e-mail",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Entrez votre e-mail pour vous connecter. Si vous n'êtes pas abonné, vous serez invité à vous abonner.",
|
||||
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
|
||||
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
|
||||
"IDENTIFIED": "Identifié",
|
||||
"INVESTIGATING": "En cours d'investigation",
|
||||
"Incident Updates": "Mises à jour des incidents",
|
||||
"LIVE Status": "Statut LIVE",
|
||||
"Lasted for about %lastedFor": "A duré environ %lastedFor",
|
||||
"Light": "Clair",
|
||||
"Link Copied": "Lien copié",
|
||||
"Login": "Connexion",
|
||||
"MAINTENANCE": "Maintenance",
|
||||
"MONITORING": "En surveillance",
|
||||
"Maintenance Completed": "Maintenance terminée",
|
||||
"Maintenance in Progress": "Maintenance en cours",
|
||||
"Manage Subscription": "Gérer l'abonnement",
|
||||
"Mode": "Mode",
|
||||
"No Data": "Pas de données",
|
||||
"No Incident in %date": "Pas d'incident le %date",
|
||||
"No Incidents": "Pas d'incidents",
|
||||
"No Monitor Found": "Aucun moniteur trouvé",
|
||||
"No Updates Yet": "Pas encore de mises à jour",
|
||||
"Ongoing Incidents": "Incidents en cours",
|
||||
"Pinging": "Ping en cours",
|
||||
"Please select specific monitors to receive updates from.": "Veuillez sélectionner les moniteurs spécifiques dont vous souhaitez recevoir des mises à jour.",
|
||||
"RESOLVED": "Résolu",
|
||||
"Recent Incidents": "Incidents récents",
|
||||
"Recent Maintenances": "Maintenances récentes",
|
||||
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
|
||||
"Share": "Partager",
|
||||
"Some Systems are not working as expected": "Certains systèmes ne fonctionnent pas comme prévu",
|
||||
"Some Systems Degraded": "Certains systèmes sont dégradés",
|
||||
"Some Systems Down": "Certains systèmes sont hors ligne",
|
||||
"Some Systems in Maintenance": "Certains systèmes sont en maintenance",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
|
||||
"Started %startedAt, will last for %lastedFor more": "Commencé il y a environ %startedAt, durera environ %lastedFor de plus",
|
||||
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
|
||||
"Starts %startedAt": "Commence dans %startedAt",
|
||||
"Status OK": "Statut OK",
|
||||
"Status": "Statut",
|
||||
"Subscribe": "S'abonner",
|
||||
"Subscribe to Updates": "S'abonner aux mises à jour",
|
||||
"Subscribe to all monitors": "S'abonner à tous les moniteurs",
|
||||
"Switch to your timezone": "Passer à votre fuseau horaire",
|
||||
"Theme": "Thème",
|
||||
"There are no upcoming maintenances": "Il n'y a pas de maintenances à venir",
|
||||
"Timezone set to %tz": "Fuseau horaire défini sur %tz",
|
||||
"Today": "Aujourd'hui",
|
||||
"UP": "En ligne",
|
||||
"Update Subscription": "Mettre à jour l'abonnement",
|
||||
"Upcoming Maintenance": "Maintenance à venir",
|
||||
"Upcoming Maintenances": "Maintenances à venir",
|
||||
"Updates": "Mises à jour",
|
||||
"Uptime": "Temps de fonctionnement",
|
||||
"View in detail": "Voir en détail",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Nous avons envoyé un code à votre e-mail. Veuillez l'entrer ci-dessous pour confirmer votre connexion",
|
||||
"You are logged in as %email": "Vous êtes connecté en tant que %email"
|
||||
}
|
||||
|
||||
+90
-59
@@ -1,61 +1,92 @@
|
||||
{
|
||||
"%status for %duration": "%duration के लिए %status",
|
||||
"14 Days": "14 दिन",
|
||||
"30 Days": "30 दिन",
|
||||
"60 Days": "60 दिन",
|
||||
"7 Days": "7 दिन",
|
||||
"90 Days": "90 दिन",
|
||||
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
|
||||
"Back": "वापस",
|
||||
"Badge Copied": "बैज कॉपी किया गया",
|
||||
"Badge": "बैज",
|
||||
"Browse Events": "ईवेंट ब्राउज़ करें",
|
||||
"Code Copied": "कोड कॉपी किया गया",
|
||||
"Copy Code": "कोड कॉपी करें",
|
||||
"Copy Link": "लिंक कॉपी करें",
|
||||
"DEGRADED": "क्षीण",
|
||||
"DOWN": "डाउन",
|
||||
"Dark": "डार्क",
|
||||
"Days": "दिन",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
|
||||
"Embed": "एम्बेड",
|
||||
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
|
||||
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
|
||||
"IDENTIFIED": "पहचाना गया",
|
||||
"INVESTIGATING": "जांच चल रही है",
|
||||
"Incident Updates": "घटना अपडेट",
|
||||
"LIVE Status": "लाइव स्टेटस",
|
||||
"Lasted for about %lastedFor": "%lastedFor तक चला",
|
||||
"Light": "लाइट",
|
||||
"Link Copied": "लिंक कॉपी किया गया",
|
||||
"MAINTENANCE": "मेंटेनेंस",
|
||||
"MONITORING": "निगरानी",
|
||||
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
|
||||
"Maintenance in Progress": "मेंटेनेंस जारी है",
|
||||
"Mode": "मोड",
|
||||
"No Data": "कोई डेटा नहीं",
|
||||
"No Incident in %date": "%date को कोई घटना नहीं",
|
||||
"No Incidents": "कोई घटना नहीं",
|
||||
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
|
||||
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
|
||||
"Ongoing Incidents": "चल रही घटनाएँ",
|
||||
"Pinging": "पिंगिंग",
|
||||
"RESOLVED": "सुलझाया गया",
|
||||
"Recent Incidents": "हाल की घटनाएँ",
|
||||
"Recent Maintenances": "हाल की मेंटेनेंस",
|
||||
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
|
||||
"Share": "साझा करें",
|
||||
"Standard": "मानक",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
|
||||
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
|
||||
"Starts %startedAt": "%startedAt में शुरू होगा",
|
||||
"Status OK": "स्थिति ठीक है",
|
||||
"Status": "स्थिति",
|
||||
"Theme": "थीम",
|
||||
"Today": "आज",
|
||||
"UP": "अप",
|
||||
"Upcoming Maintenance": "आगामी मेंटेनेंस",
|
||||
"Updates": "अपडेट्स",
|
||||
"Uptime": "अपटाइम"
|
||||
"%status for %duration": "%duration के लिए %status",
|
||||
"14 Days": "14 दिन",
|
||||
"30 Days": "30 दिन",
|
||||
"60 Days": "60 दिन",
|
||||
"7 Days": "7 दिन",
|
||||
"90 Days": "90 दिन",
|
||||
"120 Days": "120 दिन",
|
||||
"150 Days": "150 दिन",
|
||||
"180 Days": "180 दिन",
|
||||
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
|
||||
"All Systems are Degraded": "सभी सिस्टम क्षीण हैं",
|
||||
"All Systems are Down": "सभी सिस्टम डाउन हैं",
|
||||
"All Systems are Operational": "सभी सिस्टम कार्यरत हैं",
|
||||
"All Systems are in Maintenance": "सभी सिस्टम रखरखाव में हैं",
|
||||
"Back": "वापस",
|
||||
"Badge Copied": "बैज कॉपी किया गया",
|
||||
"Badge": "बैज",
|
||||
"Browse Events": "ईवेंट ब्राउज़ करें",
|
||||
"Change Email": "ईमेल बदलें",
|
||||
"Code Copied": "कोड कॉपी किया गया",
|
||||
"Confirm Login": "लॉगिन पुष्टि करें",
|
||||
"Copy Code": "कोड कॉपी करें",
|
||||
"Copy Link": "लिंक कॉपी करें",
|
||||
"DEGRADED": "क्षीण",
|
||||
"DOWN": "डाउन",
|
||||
"Dark": "डार्क",
|
||||
"Days": "दिन",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
|
||||
"Embed": "एम्बेड",
|
||||
"Enter the code": "कोड दर्ज करें",
|
||||
"Enter your email": "अपना ईमेल दर्ज करें",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "लॉगिन करने के लिए अपना ईमेल दर्ज करें। यदि आप सब्सक्राइब नहीं हैं, तो आपको सब्सक्राइब करने के लिए कहा जाएगा।",
|
||||
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
|
||||
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
|
||||
"IDENTIFIED": "पहचाना गया",
|
||||
"INVESTIGATING": "जांच चल रही है",
|
||||
"Incident Updates": "घटना अपडेट",
|
||||
"LIVE Status": "लाइव स्टेटस",
|
||||
"Lasted for about %lastedFor": "%lastedFor तक चला",
|
||||
"Light": "लाइट",
|
||||
"Link Copied": "लिंक कॉपी किया गया",
|
||||
"Login": "लॉगिन",
|
||||
"MAINTENANCE": "मेंटेनेंस",
|
||||
"MONITORING": "निगरानी",
|
||||
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
|
||||
"Maintenance in Progress": "मेंटेनेंस जारी है",
|
||||
"Manage Subscription": "सब्सक्रिप्शन प्रबंधित करें",
|
||||
"Mode": "मोड",
|
||||
"No Data": "कोई डेटा नहीं",
|
||||
"No Incident in %date": "%date को कोई घटना नहीं",
|
||||
"No Incidents": "कोई घटना नहीं",
|
||||
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
|
||||
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
|
||||
"Ongoing Incidents": "चल रही घटनाएँ",
|
||||
"Pinging": "पिंगिंग",
|
||||
"Please select specific monitors to receive updates from.": "अपडेट प्राप्त करने के लिए विशिष्ट मॉनिटर चुनें।",
|
||||
"RESOLVED": "सुलझाया गया",
|
||||
"Recent Incidents": "हाल की घटनाएँ",
|
||||
"Recent Maintenances": "हाल की मेंटेनेंस",
|
||||
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
|
||||
"Share": "साझा करें",
|
||||
"Some Systems are not working as expected": "कुछ सिस्टम अपेक्षित रूप से काम नहीं कर रहे हैं",
|
||||
"Some Systems Degraded": "कुछ सिस्टम क्षीण हैं",
|
||||
"Some Systems Down": "कुछ सिस्टम डाउन हैं",
|
||||
"Some Systems in Maintenance": "कुछ सिस्टम रखरखाव में हैं",
|
||||
"Standard": "मानक",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
|
||||
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
|
||||
"Started %startedAt, will last for %lastedFor more": "%startedAt शुरू होगा, %lastedFor और चलेगा",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
|
||||
"Starts %startedAt": "%startedAt में शुरू होगा",
|
||||
"Status OK": "स्थिति ठीक है",
|
||||
"Status": "स्थिति",
|
||||
"Subscribe": "सब्सक्राइब करें",
|
||||
"Subscribe to Updates": "अपडेट्स के लिए सब्सक्राइब करें",
|
||||
"Subscribe to all monitors": "सभी मॉनिटर को सब्सक्राइब करें",
|
||||
"Switch to your timezone": "अपने समय क्षेत्र में स्विच करें",
|
||||
"Theme": "थीम",
|
||||
"There are no upcoming maintenances": "कोई आगामी मेंटेनेंस नहीं है",
|
||||
"Timezone set to %tz": "समय क्षेत्र सेट किया गया %tz",
|
||||
"Today": "आज",
|
||||
"UP": "अप",
|
||||
"Update Subscription": "सब्सक्रिप्शन अपडेट करें",
|
||||
"Upcoming Maintenance": "आगामी मेंटेनेंस",
|
||||
"Upcoming Maintenances": "आगामी मेंटेनेंस सूची",
|
||||
"Updates": "अपडेट्स",
|
||||
"Uptime": "अपटाइम",
|
||||
"View in detail": "विस्तार में देखें",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "हमने आपके ईमेल पर एक कोड भेजा है। लॉगिन की पुष्टि करने के लिए इसे नीचे दर्ज करें",
|
||||
"You are logged in as %email": "आप %email के रूप में लॉग इन हैं"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"%status for %duration": "%status per %duration",
|
||||
"120 Days": "120 Giorni",
|
||||
"14 Days": "14 Giorni",
|
||||
"150 Days": "150 Giorni",
|
||||
"180 Days": "180 Giorni",
|
||||
"30 Days": "30 Giorni",
|
||||
"60 Days": "60 Giorni",
|
||||
"7 Days": "7 Giorni",
|
||||
"90 Days": "90 Giorni",
|
||||
"Availability per Component": "Disponibilità per componente",
|
||||
"All Systems are Degraded": "Tutti i sistemi sono degradati",
|
||||
"All Systems are Down": "Tutti i sistemi sono inattivi",
|
||||
"All Systems are Operational": "Tutti i sistemi sono operativi",
|
||||
"All Systems are in Maintenance": "Tutti i sistemi sono in manutenzione",
|
||||
"Back": "Indietro",
|
||||
"Badge Copied": "Badge copiato",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Sfoglia eventi",
|
||||
"Change Email": "Cambia email",
|
||||
"Code Copied": "Codice copiato",
|
||||
"Confirm Login": "Conferma accesso",
|
||||
"Copy Code": "Copia codice",
|
||||
"Copy Link": "Copia link",
|
||||
"DEGRADED": "DEGRADATO",
|
||||
"DOWN": "NON DISPONIBILE",
|
||||
"Dark": "Scuro",
|
||||
"Days": "Giorni",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incorpora questo monitor usando <script> o <iframe> nella tua app.",
|
||||
"Embed": "Incorpora",
|
||||
"Enter the code": "Inserisci il codice",
|
||||
"Enter your email": "Inserisci la tua email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Inserisci la tua email per accedere. Se non sei iscritto, ti verrà chiesto di iscriverti.",
|
||||
"Get SVG badge for this monitor": "Ottieni badge SVG per questo monitor",
|
||||
"Get a LIVE Status for this monitor": "Ottieni lo stato LIVE per questo monitor",
|
||||
"IDENTIFIED": "IDENTIFICATO",
|
||||
"INVESTIGATING": "IN ANALISI",
|
||||
"Incident Updates": "Aggiornamenti incidenti",
|
||||
"LIVE Status": "Stato LIVE",
|
||||
"Lasted for about %lastedFor": "Durato circa %lastedFor",
|
||||
"Light": "Chiaro",
|
||||
"Link Copied": "Link copiato",
|
||||
"Login": "Accedi",
|
||||
"MAINTENANCE": "MANUTENZIONE",
|
||||
"MONITORING": "MONITORAGGIO",
|
||||
"Maintenance Completed": "Manutenzione completata",
|
||||
"Maintenance in Progress": "Manutenzione in corso",
|
||||
"Manage Subscription": "Gestisci iscrizione",
|
||||
"Mode": "Modalità",
|
||||
"No Data": "Nessun dato",
|
||||
"No Incident in %date": "Nessun incidente in %date",
|
||||
"No Incidents": "Nessun incidente",
|
||||
"No Monitor Found": "Nessun monitor trovato",
|
||||
"No Updates Yet": "Nessun aggiornamento ancora",
|
||||
"Ongoing Incidents": "Incidenti in corso",
|
||||
"Pinging": "Ping in corso",
|
||||
"Please select specific monitors to receive updates from.": "Seleziona i monitor specifici da cui ricevere aggiornamenti.",
|
||||
"RESOLVED": "RISOLTO",
|
||||
"Recent Incidents": "Incidenti recenti",
|
||||
"Recent Maintenances": "Manutenzioni recenti",
|
||||
"Share this monitor using a link with others": "Condividi questo monitor tramite un link con altri",
|
||||
"Share": "Condividi",
|
||||
"Some Systems are not working as expected": "Alcuni sistemi non funzionano come previsto",
|
||||
"Some Systems Degraded": "Alcuni sistemi degradati",
|
||||
"Some Systems Down": "Alcuni sistemi inattivi",
|
||||
"Some Systems in Maintenance": "Alcuni sistemi in manutenzione",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Iniziato il %startedAt, durato %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Iniziato il %startedAt, ancora in corso",
|
||||
"Started %startedAt, will last for %lastedFor more": "Iniziato il %startedAt, durerà ancora %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor": "Inizia il %startedAt, durerà %lastedFor",
|
||||
"Starts %startedAt": "Inizia il %startedAt",
|
||||
"Status OK": "Stato OK",
|
||||
"Status": "Stato",
|
||||
"Subscribe": "Iscriviti",
|
||||
"Subscribe to Updates": "Iscriviti agli aggiornamenti",
|
||||
"Subscribe to all monitors": "Iscriviti a tutti i monitor",
|
||||
"Switch to your timezone": "Passa al tuo fuso orario",
|
||||
"Theme": "Tema",
|
||||
"There are no upcoming maintenances": "Non ci sono manutenzioni imminenti",
|
||||
"Timezone set to %tz": "Fuso orario impostato su %tz",
|
||||
"Today": "Oggi",
|
||||
"UP": "ATTIVO",
|
||||
"Update Subscription": "Aggiorna iscrizione",
|
||||
"Upcoming Maintenance": "Manutenzione imminente",
|
||||
"Upcoming Maintenances": "Manutenzioni imminenti",
|
||||
"Updates": "Aggiornamenti",
|
||||
"Uptime": "Tempo di attività",
|
||||
"View in detail": "Visualizza nel dettaglio",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Abbiamo inviato un codice alla tua email. Inseriscilo qui sotto per confermare l'accesso",
|
||||
"You are logged in as %email": "Sei connesso come %email"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user