mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d5382982c | |||
| a20b4ee570 | |||
| 909ff9e070 | |||
| 7d3e265ece | |||
| 78d8f311c0 | |||
| c52ba95fd0 | |||
| 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 | |||
| 1288b463f1 | |||
| 1aed6d0703 | |||
| 69f1e69140 | |||
| ab4ecd29c6 | |||
| 1d76c8a3d3 | |||
| b1c2cedbac | |||
| 8c95c94472 | |||
| 7dafb2eddc | |||
| 7c9f3eb87f | |||
| ab34dd81f8 | |||
| a8be878a87 | |||
| 2b1849f1a3 | |||
| 25a6590324 | |||
| fa251dc07f | |||
| 4de93e7e95 | |||
| a5e5f33dc8 | |||
| 4060094404 |
@@ -43,6 +43,7 @@ indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
print_width = 180
|
||||
|
||||
# Dockerfile
|
||||
[Dockerfile*]
|
||||
|
||||
@@ -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"
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20' # Adjust as needed
|
||||
node-version: '23' # Adjust as needed
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -153,4 +153,4 @@ jobs:
|
||||
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
|
||||
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
|
||||
@@ -9,6 +9,7 @@ config/server.yaml
|
||||
/src/lib/.kener
|
||||
/package
|
||||
.env
|
||||
.vscode
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@
|
||||
"semi": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
"printWidth": 180
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+22
-31
@@ -1,15 +1,15 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Global build arguments
|
||||
ARG ALPINE_VERSION=23.7.0-alpine3.21
|
||||
ARG DEBIAN_VERSION=23.7.0-bookworm-slim
|
||||
# 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} AS builder-debian
|
||||
FROM node:${DEBIAN_VERSION_TAG} AS builder-debian
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential=12.9 \
|
||||
python3=3.11.2-1+b1 \
|
||||
@@ -18,21 +18,21 @@ RUN apt-get update && apt-get install -y \
|
||||
make=4.3-4.1 \
|
||||
node-gyp=9.3.0-2 \
|
||||
g++=4:12.2.0-3 \
|
||||
tzdata=2024b-0+deb12u1 \
|
||||
tzdata \
|
||||
iputils-ping=3:20221126-1+deb12u1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM node:${ALPINE_VERSION} AS builder-alpine
|
||||
FROM node:${ALPINE_VERSION_TAG} AS builder-alpine
|
||||
RUN apk add --no-cache --update \
|
||||
build-base=0.5-r3 \
|
||||
python3=3.12.9-r0 \
|
||||
py3-pip=24.3.1-r0 \
|
||||
python3 \
|
||||
py3-pip \
|
||||
make=4.4.1-r2 \
|
||||
g++=14.2.0-r4 \
|
||||
sqlite=3.48.0-r0 \
|
||||
sqlite-dev=3.48.0-r0 \
|
||||
sqlite \
|
||||
sqlite-dev \
|
||||
tzdata \
|
||||
iputils=20240905-r0
|
||||
iputils
|
||||
|
||||
FROM builder-${VARIANT} AS builder
|
||||
|
||||
@@ -58,10 +58,9 @@ COPY . .
|
||||
# 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 && \
|
||||
# TODO: Consider changing below to `chmod -R u-rwX,g=rX,o= uploads database`
|
||||
static/documentation \
|
||||
static/fonts/lato/full && \
|
||||
mkdir -p uploads database && \
|
||||
chmod -R 750 uploads database
|
||||
|
||||
# Build the application and remove `devDependencies`
|
||||
@@ -72,21 +71,21 @@ RUN npm run build && \
|
||||
# STAGE 2: PRODUCTION/FINAL STAGE #
|
||||
#==========================================================#
|
||||
|
||||
FROM node:${DEBIAN_VERSION} AS final-debian
|
||||
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=2024b-0+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=7.88.1-10+deb12u8 && \
|
||||
curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM node:${ALPINE_VERSION} AS final-alpine
|
||||
FROM node:${ALPINE_VERSION_TAG} AS final-alpine
|
||||
RUN apk add --no-cache --update \
|
||||
iputils=20240905-r0 \
|
||||
sqlite=3.48.0-r0 \
|
||||
tzdata
|
||||
iputils \
|
||||
sqlite \
|
||||
tzdata
|
||||
|
||||
FROM final-${VARIANT} AS final
|
||||
|
||||
@@ -119,15 +118,7 @@ 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
|
||||
|
||||
# Set capabilities for ping (before changing to non-root user)
|
||||
RUN if [ -f "/etc/alpine-release" ]; then \
|
||||
# Alpine path
|
||||
setcap cap_net_raw+ep /bin/ping; \
|
||||
else \
|
||||
# Debian path
|
||||
setcap cap_net_raw+ep /usr/bin/ping; \
|
||||
fi
|
||||
COPY --chown=node:node --from=builder /app/package.json ./package.json
|
||||
|
||||
# Ensure necessary directories are writable
|
||||
VOLUME ["/uploads", "/database"]
|
||||
|
||||
@@ -79,7 +79,7 @@ npm run dev
|
||||
|
||||
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
|
||||
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.2-blue" alt="Kener latest stable version: 3.2.2" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.14"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.14-blue" alt="Kener latest stable version: 3.2.14" /></a>
|
||||
|
||||
#### Available Tags
|
||||
|
||||
@@ -93,38 +93,38 @@ Official Docker images for **Kener** are available on [Docker Hub](https://hub.d
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
|
||||
<td>Latest stable release (aka 3.2.2)</td>
|
||||
<td>Latest stable release (aka 3.2.14)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2" target="_blank"><code>3.2.2</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.14" target="_blank"><code>3.2.14</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.2) release within that minor version (3.2.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.14) 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.2) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.14) 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.2)</td>
|
||||
<td>Latest stable release (aka 3.2.14)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2-alpine" target="_blank"><code>3.2.2-alpine</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.14-alpine" target="_blank"><code>3.2.14-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.2) release within that minor version (3.2.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.14) 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.2) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.14) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
+3
-3
@@ -38,7 +38,7 @@ services:
|
||||
# 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
|
||||
name: kener_db
|
||||
container_name: postgres
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
# 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
|
||||
name: kener_db
|
||||
container_name: mysql
|
||||
environment:
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
|
||||
@@ -58,4 +58,4 @@ services:
|
||||
|
||||
volumes:
|
||||
data:
|
||||
name: kener_db
|
||||
name: kener_db
|
||||
|
||||
+62
-257
@@ -7,6 +7,68 @@ description: Changelogs for Kener
|
||||
|
||||
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
|
||||
|
||||
## v3.2.14
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **Gamedig Monitor Functionality:** Introduced a new monitor type using Gamedig to monitor over 320+ games and services. Includes options for host, port, timeout, game/service selection, and documentation updates. ([#389](https://github.com/rajnandan1/kener/pull/389) by ToxykAuBleu)
|
||||
- **Event Subscription:** Added functionality for event subscriptions, involving new database tables (`subscribers`, `subscriptions`, `subscription_triggers`), UI enhancements, a comprehensive game list, and package version update (3.2.13 to 3.2.14). ([#385](https://github.com/rajnandan1/kener/pull/385) by rajnandan1)
|
||||
- **CRUD API for Monitors:** Implemented API endpoints for creating, reading, updating, and deleting monitors (`GET /api/monitor`, `POST /api/monitor`, `GET /api/monitor/[monitor_id]`, `PUT /api/monitor/[monitor_id]`, `DELETE /api/monitor/[monitor_id]`). Includes OpenAPI spec updates. ([#384](https://github.com/rajnandan1/kener/pull/384) by jensvandenreyt)
|
||||
- **Modify Monitor Data:** Added the ability for users to modify historical monitoring data via a new UI option and backend API, useful for correcting past statuses (e.g., false positives). ([#380](https://github.com/rajnandan1/kener/pull/380) by mcorbin)
|
||||
- **Site Status Banner:** Now you can choose to show a site status banner in the main home page. The setting has to be turned on in the Home section of the kener portal.
|
||||
- **Upcoming Maintenance**: Dedicated Page for upcoming maintenance events.
|
||||
- **Event Page**: Dedicated page for each event (incident/maintenance)
|
||||
- **Admin Portal UI Update**: Revamped kener management portal for better accessibility.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **SMTP Secure Variable Fix:** Corrected the evaluation of the `SMTP_SECURE` environment variable to properly handle values like '0' or empty strings, preventing SSL errors with STARTTLS. ([#392](https://github.com/rajnandan1/kener/pull/392) by myned)
|
||||
- **Group Monitor Fix:** Fix group monitor using queues
|
||||
|
||||
### 🌍 Internationalization
|
||||
|
||||
- **Polish Translation:** Added Polish language support (`pl.json`) and integrated it into the application's localization framework. ([#386](https://github.com/rajnandan1/kener/pull/386) by lolen)
|
||||
|
||||
## v3.2.5
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
### Features
|
||||
|
||||
- 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
|
||||
|
||||
### Improvements
|
||||
|
||||
- Improved build time by migrating lucid-svelte to individual components
|
||||
|
||||
### 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>
|
||||
@@ -45,260 +107,3 @@ Here are the changelogs for Kener. Changelogs are only published when there are
|
||||
- **Documentation**
|
||||
- Added comprehensive documentation for Heartbeat Monitors
|
||||
- Updated API documentation with new endpoints
|
||||
|
||||
## v3.2.0
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
### Features
|
||||
|
||||
- **Improved Monitor Evaluation Functions**
|
||||
|
||||
- Replaced unsafe `eval()` with secure `Function()` constructors across all monitor types
|
||||
- Added support for using modules (like cheerio) directly in evaluation functions
|
||||
|
||||
- **Enhanced API Monitors**
|
||||
|
||||
- Raw response data is now passed directly to eval functions instead of base64 encoded
|
||||
- Added `modules` parameter with access to cheerio for HTML parsing
|
||||
- Updated default evaluation function to use the new parameter structure
|
||||
|
||||
- **Improved TCP & Ping Monitors**
|
||||
|
||||
- Simplified evaluation functions with direct access to ping/TCP data
|
||||
- Removed unnecessary base64 encoding/decoding steps
|
||||
- Better error handling for invalid evaluation functions
|
||||
|
||||
- **Documentation Updates**
|
||||
- Updated all examples and documentation for the new evaluation function signatures
|
||||
- Added more detailed explanations of input parameters
|
||||
- Improved examples showing usage with the new parameter structure
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **Monitor Evaluation Functions**
|
||||
- Custom evaluation functions will need to be updated to the new parameter structure
|
||||
- API monitors: `(statusCode, responseTime, responseRaw, modules)` instead of `(statusCode, responseTime, responseDataBase64)`
|
||||
- TCP/Ping monitors: `(arrayOfPings)` instead of `(responseDataBase64)`
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed "cheerio is undefined" errors in API monitor evaluations
|
||||
- Improved error handling and logging for monitor evaluation failures
|
||||
- Security enhancements by removing `eval()` usage
|
||||
|
||||
### Migration
|
||||
|
||||
If you're using custom evaluation functions in your monitors, you'll need to update them to the new format:
|
||||
|
||||
#### API Monitors
|
||||
|
||||
```javascript
|
||||
// Old format
|
||||
;(async function (statusCode, responseTime, responseDataBase64) {
|
||||
const resp = atob(responseDataBase64)
|
||||
// Your logic here
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
// New format
|
||||
;(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
// responseRaw is the direct response - no need to decode
|
||||
// Access cheerio with modules.cheerio
|
||||
// Your logic here
|
||||
})
|
||||
```
|
||||
|
||||
#### TCP/Ping Monitors
|
||||
|
||||
```javascript
|
||||
// Old format
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
// Your logic here
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
// New format
|
||||
;(async function (arrayOfPings) {
|
||||
// arrayOfPings is directly available - no need to decode
|
||||
// Your logic here
|
||||
})
|
||||
```
|
||||
|
||||
## v3.1.8
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
### Features
|
||||
|
||||
- **Timezone Support & UI Toggle**
|
||||
|
||||
- Added timezone support using `date-fns-tz` for improved date formatting.
|
||||
- Introduced a UI toggle in settings to switch between different timezones.
|
||||
|
||||
- **Enhanced Incident Management**
|
||||
|
||||
- Improved incident filtering to prevent duplicate auto incidents when creating manual incidents.
|
||||
- Added support for incident sources to refine incident handling.
|
||||
|
||||
- **Dynamic Cron Job Scheduling**
|
||||
|
||||
- Cron jobs are now dynamically added and removed based on active monitors.
|
||||
- Ensures jobs are triggered in the correct order and prevents duplicate incidents.
|
||||
|
||||
- **Monitor Component Improvements**
|
||||
- Refactored monitor component for better data display and interaction.
|
||||
- Improved uptime calculations.
|
||||
- Added a dropdown to select time ranges for better visibility.
|
||||
|
||||
### Fixes & Improvements
|
||||
|
||||
- **Refactored Incident Handling & Scheduling** for better reliability and performance.
|
||||
- **UI Responsiveness Fixes** to improve the experience on smaller screens.
|
||||
- **Dependency Updates** to support new timezone functionality and ensure stability.
|
||||
|
||||
## v3.0.10
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
### Features
|
||||
|
||||
- Added TCP monitors
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Ping monitors will break.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Bug fixes in the UI
|
||||
|
||||
## v3.0.9
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
### Features
|
||||
|
||||
- Support of SMTP for email notifications. Read more [here](/docs/triggers/#email-smtp)
|
||||
- Introduction of event type `MAINTENANCE` for incidents.
|
||||
- You can write eval function for ping now in monitors. Read more [here](/docs/monitors-ping/#eval)
|
||||
- Added category filter for monitor management.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Support longer TLD in `siteURL` example `https://example.network`
|
||||
- Remove googleapis preconnect and preload
|
||||
- Fixed wrong action url in webhook.
|
||||
|
||||
## v3.0.1
|
||||
|
||||
<picture>
|
||||
<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)
|
||||
|
||||
## v3.0.0
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
Here are the changes in this release
|
||||
|
||||
### Features
|
||||
|
||||
- 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
|
||||
|
||||
## v2.0.0
|
||||
|
||||
<picture>
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
|
||||
</picture>
|
||||
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+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
|
||||
|
||||
@@ -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).
|
||||
@@ -103,6 +103,10 @@ 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
|
||||
|
||||
+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.
|
||||
+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
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"title": "Databases",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
},
|
||||
{
|
||||
"title": "Access Control",
|
||||
"link": "/docs/rbac",
|
||||
"file": "/rbac.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -150,6 +155,11 @@
|
||||
"title": "Heartbeat Monitor",
|
||||
"link": "/docs/monitors-heartbeat",
|
||||
"file": "/monitors-heartbeat.md"
|
||||
},
|
||||
{
|
||||
"title": "Gamedig Monitor",
|
||||
"link": "/docs/monitors-gamedig",
|
||||
"file": "/monitors-gamedig.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+221
-45
@@ -3,7 +3,7 @@ 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -93,24 +119,65 @@ Body of the webhook will be sent as below:
|
||||
}
|
||||
```
|
||||
|
||||
| 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
|
||||
|
||||
@@ -149,6 +216,60 @@ The discord message when alert is `RESOLVED` will look like this
|
||||
|
||||

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

|
||||
|
||||
### Modify Slack Message
|
||||
|
||||
You can modify the slack message by using [mustache variables](#triggers-trigger-variables).
|
||||
|
||||
```json
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Hello Alert {{alert_name}}",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "{{description}}\n*Source:* {{source}}\n*Severity:* {{severity}}\n*Status:* {{status}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Metric:*\n{{metric}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Current Value:*\n{{current_value}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Threshold:*\n{{threshold}}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Environment:*\n$_NODE_ENV"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Timestamp:*\n<!date^{{timestamp_unix}}^{date} at {time}|{{timestamp}}>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "{{action_text}}"
|
||||
},
|
||||
"url": "{{{action_url}}}",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Email
|
||||
|
||||
Email triggers are used to send an email when a monitor goes down or up. Kener supports sending emails via [resend](https://resend.com) or over SMTP.
|
||||
@@ -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">
|
||||
|
||||
@@ -273,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>
|
||||
|
||||
+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
|
||||
//
|
||||
|
||||
@@ -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
+809
-49
File diff suppressed because it is too large
Load Diff
+30
-20
@@ -1,26 +1,26 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.2.3",
|
||||
"version": "3.2.15",
|
||||
"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.",
|
||||
"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": [
|
||||
"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"
|
||||
"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",
|
||||
@@ -52,7 +52,6 @@
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-picker-svelte": "^2.15.1",
|
||||
"mustache": "^4.2.0",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
@@ -68,9 +67,16 @@
|
||||
},
|
||||
"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",
|
||||
@@ -87,14 +93,16 @@
|
||||
"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.292.0",
|
||||
"lucide-svelte": "^0.483.0",
|
||||
"marked": "^11.1.1",
|
||||
"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",
|
||||
@@ -105,9 +113,11 @@
|
||||
"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"
|
||||
"tailwind-variants": "^0.1.18",
|
||||
"vite-plugin-package-version": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+129
-98
@@ -1,108 +1,10 @@
|
||||
/*one is the class for dotted background*/
|
||||
|
||||
.dots-pattern {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
height: 100svh;
|
||||
background: linear-gradient(177deg, rgba(255, 137, 131, 0.5) 0%, rgba(35, 136, 224, 0.05) 60%);
|
||||
clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%);
|
||||
}
|
||||
|
||||
.dots-pattern::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-image: radial-gradient(rgba(0, 0, 0, 0) 1.5px, var(--background-kener) 1px);
|
||||
background-size: 14px 14px;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
transform: blur(3px);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.squares-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
height: 100svh;
|
||||
|
||||
/* clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%); */
|
||||
}
|
||||
|
||||
.squares-pattern::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-image:
|
||||
linear-gradient(#444cf7 1px, transparent 1px),
|
||||
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0.8) 20%,
|
||||
rgba(0, 0, 0, 0.6) 40%,
|
||||
rgba(0, 0, 0, 0.4) 60%,
|
||||
rgba(0, 0, 0, 0.2) 80%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0.2) 0%,
|
||||
rgba(0, 0, 0, 0.1) 20%,
|
||||
rgba(0, 0, 0, 0.05) 40%,
|
||||
rgba(0, 0, 0, 0.025) 60%,
|
||||
rgba(0, 0, 0, 0.0125) 80%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
background-size: 32px 32px;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
|
||||
left: 0;
|
||||
}
|
||||
.dark .squares-pattern::after {
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0.5) 0%,
|
||||
rgba(0, 0, 0, 0.25) 20%,
|
||||
rgba(0, 0, 0, 0.125) 40%,
|
||||
rgba(0, 0, 0, 0.0625) 60%,
|
||||
rgba(0, 0, 0, 0.03125) 80%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0.5) 0%,
|
||||
rgba(0, 0, 0, 0.25) 20%,
|
||||
rgba(0, 0, 0, 0.125) 40%,
|
||||
rgba(0, 0, 0, 0.0625) 60%,
|
||||
rgba(0, 0, 0, 0.03125) 80%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
background-image:
|
||||
linear-gradient(#616fbf 1px, transparent 1px),
|
||||
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
|
||||
}
|
||||
|
||||
/*Needed to overlay content on top of dotted bg*/
|
||||
section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
/*Needed overlay content on top of dotted bg*/
|
||||
.blurry-bg {
|
||||
/* background-color: var(--background-kener-rgba);
|
||||
box-shadow: 0 0 64px 64px var(--background-kener-rgba); */
|
||||
}
|
||||
|
||||
:root {
|
||||
--up-color: #4ead94;
|
||||
@@ -905,3 +807,132 @@ textarea::placeholder {
|
||||
content: ".";
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
--cp-bg-color: hsl(var(--background) / var(--tw-bg-opacity, 1));
|
||||
--cp-border-color: hsl(var(--border) / var(--tw-border-opacity));
|
||||
--cp-text-color: hsl(var(--card-foreground) / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.color-picker .container {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.color-picker .color {
|
||||
border: 1px solid hsl(var(--border) / var(--tw-border-opacity));
|
||||
}
|
||||
.color-picker button {
|
||||
display: none;
|
||||
}
|
||||
.color-picker [aria-label="color picker"] input {
|
||||
color: hsl(var(--foreground) / var(--tw-border-opacity)) !important;
|
||||
background-color: hsl(var(--input) / var(--tw-text-opacity, 1)) !important;
|
||||
}
|
||||
|
||||
.dark .color-picker {
|
||||
--cp-bg-color: hsl(var(--background) / var(--tw-bg-opacity, 1));
|
||||
--cp-border-color: hsl(var(--border) / var(--tw-border-opacity));
|
||||
--cp-text-color: hsl(var(--card-foreground) / var(--tw-text-opacity, 1));
|
||||
--cp-input-color: hsl(var(--input) / var(--tw-text-opacity, 1));
|
||||
--cp-button-hover-color: hsl(var(--input) / var(--tw-text-opacity, 1));
|
||||
}
|
||||
.like-input {
|
||||
background-color: rgba(0, 0, 0, 0.02) !important;
|
||||
}
|
||||
.dark .like-input {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
.ͼo .cm-gutters {
|
||||
border-top-left-radius: var(--radius);
|
||||
border-bottom-left-radius: var(--radius);
|
||||
}
|
||||
.no-gutter .ͼo .cm-gutters {
|
||||
display: none;
|
||||
}
|
||||
.kener-md {
|
||||
@apply text-sm;
|
||||
}
|
||||
p code:not([class^="language-"]) {
|
||||
@apply rounded-sm bg-yellow-100 px-1 py-0.5 font-mono text-xs dark:bg-neutral-700 dark:text-neutral-100;
|
||||
}
|
||||
|
||||
.kener-theme-mono .section-hero .h1,
|
||||
.kener-theme-mono-hero-h1 {
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
.kener-theme-default .section-hero .h1,
|
||||
.kener-theme-default-hero-h1 {
|
||||
@apply bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-transparent;
|
||||
}
|
||||
.kener-theme-sunset .section-hero .h1,
|
||||
.kener-theme-sunset-hero-h1 {
|
||||
@apply bg-gradient-to-r from-yellow-500 via-red-500 to-pink-500 bg-clip-text text-transparent;
|
||||
}
|
||||
.kener-theme-forest .section-hero .h1,
|
||||
.kener-theme-forest-hero-h1 {
|
||||
@apply bg-gradient-to-l from-blue-500 via-teal-500 to-green-500 bg-clip-text text-transparent;
|
||||
}
|
||||
.kener-theme-ocean .section-hero .h1,
|
||||
.kener-theme-ocean-hero-h1 {
|
||||
@apply bg-gradient-to-l from-cyan-500 via-blue-500 to-indigo-500 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.prose :where(hr):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1.3em;
|
||||
margin-bottom: 1.3em;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:disabled + .peer {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Select any element whose class begins with "lucid-chevron" */
|
||||
.hide-chevron [data-bits-accordion-header] .lucide-chevron-down,
|
||||
.hide-chevron [data-bits-accordion-header] .lucide-chevron-up {
|
||||
/* add your styles here */
|
||||
display: none; /* Hides the chevron when the class 'hide-chevron' is applied */
|
||||
}
|
||||
|
||||
.hide-chevron [data-bits-accordion-header] button:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.copybtn .copy-btn {
|
||||
transform: scale(1);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.copybtn .check-btn {
|
||||
transform: scale(0);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.copybtn:focus .copy-btn {
|
||||
transform: scale(0);
|
||||
}
|
||||
.copybtn:focus .check-btn {
|
||||
transform: scale(1);
|
||||
}
|
||||
.dash-after::after {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
background-color: hsl(var(--accent) / var(--tw-bg-opacity, 1));
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
.dash-after:hover::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-color-bar {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.status-color-banner:hover .status-color-bar {
|
||||
bottom: -0.75rem;
|
||||
}
|
||||
.status-color-banner:hover > div {
|
||||
@apply bg-secondary;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -258,6 +258,10 @@ function isValidRange(value, min, max) {
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && num >= min && num <= max;
|
||||
}
|
||||
function IsValidPort(port) {
|
||||
return isValidRange(port, 1, 65535);
|
||||
}
|
||||
|
||||
function SortMonitor(monitorSort, resp) {
|
||||
let monitors = [];
|
||||
if (!!monitorSort && monitorSort.length > 0) {
|
||||
@@ -277,6 +281,15 @@ function RandomString(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,
|
||||
@@ -285,7 +298,9 @@ export {
|
||||
IsValidHost,
|
||||
IsValidNameServer,
|
||||
IsValidURL,
|
||||
IsValidPort,
|
||||
ValidateCronExpression,
|
||||
SortMonitor,
|
||||
RandomString,
|
||||
GetGameFromId,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script>
|
||||
import { formatDistanceToNow, formatDistance } from "date-fns";
|
||||
import { Settings, ArrowRight } from "lucide-svelte";
|
||||
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";
|
||||
@@ -8,9 +12,12 @@
|
||||
import { Tooltip } from "bits-ui";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { marked } from "marked";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let incident;
|
||||
export let index;
|
||||
export let lang;
|
||||
export let allowCollapse = true;
|
||||
let startTime = new Date(incident.start_date_time * 1000);
|
||||
let endTime = new Date();
|
||||
let nowTime = new Date();
|
||||
@@ -32,7 +39,7 @@
|
||||
}
|
||||
|
||||
let accordionValue = "incident-0";
|
||||
if ($page.data.site.incidentGroupView == "COLLAPSED") {
|
||||
if ($page.data.site.incidentGroupView == "COLLAPSED" && allowCollapse) {
|
||||
accordionValue = "incident-collapse";
|
||||
} else if ($page.data.site.incidentGroupView == "EXPANDED") {
|
||||
accordionValue = index;
|
||||
@@ -72,34 +79,66 @@
|
||||
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.Root
|
||||
bind:value={index}
|
||||
class="accor {allowCollapse === false ? 'hide-chevron' : ''}"
|
||||
disabled={!allowCollapse}
|
||||
>
|
||||
<Accordion.Item value={accordionValue}>
|
||||
<Accordion.Trigger class="px-4 hover:bg-muted hover:no-underline">
|
||||
<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">
|
||||
<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}
|
||||
{#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}
|
||||
</p>
|
||||
<div class="flex justify-start gap-x-2">
|
||||
<p class="flex gap-x-2 text-xs font-semibold">
|
||||
{#if incidentType == "INCIDENT"}
|
||||
<span class="badge-{incident.state}">
|
||||
{l($page.data.lang, incident.state)}
|
||||
</span>
|
||||
{:else if incidentType == "MAINTENANCE"}
|
||||
<span class="{maintenanceBadgeColor} ">
|
||||
{l($page.data.lang, maintenanceBadge)}
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
<div class="flex justify-end gap-x-2">
|
||||
{#if $page.data.isLoggedIn}
|
||||
<Button
|
||||
href="{base}/manage/app/events#{incident.id}"
|
||||
class="rotate-once h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
variant="link"
|
||||
>
|
||||
<Settings class="h-4 w-4 " />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !!pathMonitorLink}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="link"
|
||||
class="copybtn relative h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(pathMonitorLink);
|
||||
analyticsEvent("incident_copy_link", { incident_title: incident.title });
|
||||
}}
|
||||
>
|
||||
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
|
||||
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="font-medium">
|
||||
{incident.title}
|
||||
@@ -184,7 +223,11 @@
|
||||
</time>
|
||||
|
||||
<div class="mb-4 text-sm font-normal">
|
||||
{@html comment.comment}
|
||||
<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}
|
||||
@@ -202,9 +245,13 @@
|
||||
)}
|
||||
</time>
|
||||
|
||||
<p class="mb-2 text-sm font-normal">
|
||||
{comment.comment}
|
||||
</p>
|
||||
<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>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||
|
||||
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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -8,10 +8,20 @@
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import locales from "$lib/locales/locales.json?raw";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
|
||||
import { Loader, X, Plus, Info, Play } from "lucide-svelte";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import Play from "lucide-svelte/icons/play";
|
||||
import { Tooltip } from "bits-ui";
|
||||
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
export let data;
|
||||
|
||||
let formStateHero = "idle";
|
||||
@@ -27,9 +37,19 @@
|
||||
};
|
||||
let footerHTML = "";
|
||||
let tzToggle = "NO";
|
||||
let showSiteStatus = "NO";
|
||||
let homeIncidentCount = 10;
|
||||
let homeIncidentStartTimeWithin = 30;
|
||||
let incidentGroupView = "EXPAND_FIRST";
|
||||
let kenerTheme = "default";
|
||||
|
||||
let availableThemes = [
|
||||
{ name: "Default", value: "default" },
|
||||
{ name: "Mono", value: "mono" },
|
||||
{ name: "Sunset", value: "sunset" },
|
||||
{ name: "Forest", value: "forest" },
|
||||
{ name: "Ocean", value: "ocean" }
|
||||
];
|
||||
|
||||
let nav = [];
|
||||
let categories = [];
|
||||
@@ -70,9 +90,16 @@
|
||||
if (data.siteData.footerHTML) {
|
||||
footerHTML = data.siteData.footerHTML;
|
||||
}
|
||||
if (data.siteData.kenerTheme) {
|
||||
kenerTheme = data.siteData.kenerTheme;
|
||||
}
|
||||
if (data.siteData.tzToggle) {
|
||||
tzToggle = data.siteData.tzToggle;
|
||||
}
|
||||
|
||||
if (data.siteData.showSiteStatus) {
|
||||
showSiteStatus = data.siteData.showSiteStatus;
|
||||
}
|
||||
if (data.siteData.i18n) {
|
||||
i18n = data.siteData.i18n;
|
||||
//add locales that are not present in the data
|
||||
@@ -87,8 +114,9 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let heroError = "";
|
||||
async function formSubmitHero() {
|
||||
heroError = "";
|
||||
formStateHero = "loading";
|
||||
let resp = await storeSiteData({
|
||||
hero: JSON.stringify(hero)
|
||||
@@ -96,6 +124,20 @@
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formStateHero = "idle";
|
||||
if (data.error) {
|
||||
heroError = data.error;
|
||||
return;
|
||||
}
|
||||
saveTheme();
|
||||
}
|
||||
|
||||
async function saveTheme() {
|
||||
let resp = await storeSiteData({
|
||||
kenerTheme: kenerTheme,
|
||||
showSiteStatus: showSiteStatus
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
@@ -117,7 +159,10 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let categoriesErrorMessage = "";
|
||||
async function formSubmitCategories() {
|
||||
categoriesErrorMessage = "";
|
||||
formStateCategories = "loading";
|
||||
let resp = await storeSiteData({
|
||||
categories: JSON.stringify(categories)
|
||||
@@ -126,11 +171,14 @@
|
||||
let data = await resp.json();
|
||||
formStateCategories = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
categoriesErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let footerErrorMessage = "";
|
||||
async function formSubmitFooter() {
|
||||
footerErrorMessage = "";
|
||||
formStateFooter = "loading";
|
||||
let resp = await storeSiteData({
|
||||
footerHTML: footerHTML
|
||||
@@ -139,11 +187,14 @@
|
||||
let data = await resp.json();
|
||||
formStateFooter = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
footerErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let incidentErrorMessage = "";
|
||||
async function formSubmitIncident() {
|
||||
incidentErrorMessage = "";
|
||||
formStateIncident = "loading";
|
||||
let resp = await storeSiteData({
|
||||
homeIncidentCount: homeIncidentCount,
|
||||
@@ -154,7 +205,7 @@
|
||||
let data = await resp.json();
|
||||
formStateIncident = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
incidentErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -220,7 +271,9 @@
|
||||
i18n.defaultLocale = e.value;
|
||||
}
|
||||
|
||||
let i18nErrorMessage = "";
|
||||
async function formSubmiti18n() {
|
||||
i18nErrorMessage = "";
|
||||
formStatei18n = "loading";
|
||||
let resp = await storeSiteData({
|
||||
i18n: JSON.stringify(i18n),
|
||||
@@ -230,7 +283,7 @@
|
||||
let data = await resp.json();
|
||||
formStatei18n = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
i18nErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -242,44 +295,16 @@
|
||||
<Card.Description>Configure the hero section of your site.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitHero}>
|
||||
<form class="mx-auto mt-4 flex flex-col gap-y-4" on:submit|preventDefault={formSubmitHero}>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="hero_title">
|
||||
Title
|
||||
<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"
|
||||
>
|
||||
You can set the title of the hero section here. This will be displayed on the top of the page.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Label for="hero_title">Title</Label>
|
||||
<Input bind:value={hero.title} class="mt-2" type="text" id="hero_title" placeholder="Status Page for Kener" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="hero_subtitle">
|
||||
Subtitle
|
||||
<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"
|
||||
>
|
||||
You can set the subtitle of the hero section here. This will be displayed below the title.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Label for="hero_subtitle">Subtitle</Label>
|
||||
<Input
|
||||
bind:value={hero.subtitle}
|
||||
class="mt-2"
|
||||
@@ -289,9 +314,63 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div>
|
||||
<Label>Color</Label>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-2 rounded-md border p-2">
|
||||
<div class="w-3/4 pt-2">
|
||||
<span class="kener-theme-{kenerTheme}-hero-h1 font-medium">{hero.title}</span>
|
||||
</div>
|
||||
<div class="flex w-1/4 justify-end">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="outline">
|
||||
{availableThemes.find((el) => el.value === kenerTheme)?.name || "Mono"}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{#each availableThemes as theme}
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="h-6 w-full justify-start text-xs"
|
||||
on:click={() => {
|
||||
kenerTheme = theme.value;
|
||||
}}
|
||||
>
|
||||
{theme.name}
|
||||
</Button>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class=" font-medium">
|
||||
<label>
|
||||
<input
|
||||
on:change={(e) => {
|
||||
showSiteStatus = e.target.checked === true ? "YES" : "NO";
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={showSiteStatus === "YES"}
|
||||
/>
|
||||
Show Site Status Banner
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!heroError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{heroError}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStateHero === "loading"}>
|
||||
Save
|
||||
Save Hero Section
|
||||
{#if formStateHero === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -351,7 +430,7 @@
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<Button type="submit" disabled={formStateIncident === "loading"}>
|
||||
Save
|
||||
Save Incidents Settings
|
||||
{#if formStateIncident === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -362,6 +441,9 @@
|
||||
Translates to "Show {homeIncidentCount} incidents that have started in the last {homeIncidentStartTimeWithin}
|
||||
days in the past or going to start in {homeIncidentStartTimeWithin} days in the future"
|
||||
</p>
|
||||
{#if !!incidentErrorMessage}
|
||||
<p class="text-sm font-medium text-destructive">{incidentErrorMessage}</p>
|
||||
{/if}
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -470,10 +552,10 @@
|
||||
</div>
|
||||
<div class="flex w-full justify-end" style="margin-top:32px">
|
||||
<p class="px-4 pt-1">
|
||||
<span class="text-xs text-red-500"> {navErrorMessage} </span>
|
||||
<span class="text-sm font-medium text-destructive"> {navErrorMessage} </span>
|
||||
</p>
|
||||
<Button type="submit" disabled={formStateHero === "loading"}>
|
||||
Save
|
||||
Save Navigation
|
||||
{#if formStateHero === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -547,9 +629,12 @@
|
||||
Allow users to switch timezones
|
||||
</label>
|
||||
</p>
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!i18nErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{i18nErrorMessage}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStatei18n === "loading"}>
|
||||
Save
|
||||
Save Languages
|
||||
{#if formStatei18n === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -607,9 +692,12 @@
|
||||
<Plus class="h-4 w-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!categoriesErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{categoriesErrorMessage}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStateCategories === "loading"}>
|
||||
Save
|
||||
Save Categories
|
||||
{#if formStateCategories === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -628,21 +716,42 @@
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitFooter}>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<textarea
|
||||
bind:value={footerHTML}
|
||||
class="mt-2 h-48 w-full rounded-sm border p-2"
|
||||
placeholder="eg. <p>Powered by Kener</p>"
|
||||
></textarea>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={footerHTML}
|
||||
lang={html()}
|
||||
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>
|
||||
|
||||
<div class="flex w-full justify-end">
|
||||
<Button type="submit" disabled={formStateFooter === "loading"}>
|
||||
Save
|
||||
{#if formStateFooter === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
<div class="flex w-full justify-between gap-x-2">
|
||||
<div>
|
||||
<Button variant="outline" class="mt-2" on:click={() => (footerHTML = data.defaultFooterHTML)}>
|
||||
<span class="ml-2">Reset to default</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end gap-x-2">
|
||||
{#if !!footerErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
|
||||
{/if}
|
||||
</Button>
|
||||
<Button type="submit" disabled={formStateFooter === "loading"}>
|
||||
Save Custom Footer
|
||||
{#if formStateFooter === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Loader, Clipboard, Check } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { clickOutsideAction, slide } from "svelte-legos";
|
||||
import { ChevronRight, Trash } from "lucide-svelte";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Clipboard from "lucide-svelte/icons/clipboard";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import Trash from "lucide-svelte/icons/trash";
|
||||
import { base } from "$app/paths";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
import AllGamesListRaw from "$lib/all-games-list.json?raw";
|
||||
import {
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
ValidateCronExpression,
|
||||
IsValidHost,
|
||||
IsValidNameServer
|
||||
IsValidNameServer,
|
||||
GetGameFromId,
|
||||
IsValidPort
|
||||
} from "$lib/clientTools.js";
|
||||
import { GAMEDIG_SOCKET_TIMEOUT } from "$lib/anywhere";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let AllGamesList = JSON.parse(AllGamesListRaw);
|
||||
const defaultEval = `(async function (statusCode, responseTime, responseData) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
@@ -100,8 +113,8 @@
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// let evalResp = await eval(newMonitor.apiConfig.eval + `(200, 1000, "e30=")`);
|
||||
new Function(ev);
|
||||
|
||||
return true; // The code is valid
|
||||
} catch (error) {
|
||||
invalidFormMessage = error.message + " in eval.";
|
||||
@@ -179,6 +192,12 @@
|
||||
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
|
||||
return;
|
||||
}
|
||||
//validating timeout for ping
|
||||
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
|
||||
invalidFormMessage = "Timeout should be a number";
|
||||
return;
|
||||
}
|
||||
hosts[i].timeout = Number(hosts[i].timeout);
|
||||
//validating timeout
|
||||
if (hosts[i].timeout < 1) {
|
||||
invalidFormMessage = "Timeout should be greater than 0";
|
||||
@@ -213,13 +232,19 @@
|
||||
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
|
||||
return;
|
||||
}
|
||||
//validating timeout for tcp
|
||||
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
|
||||
invalidFormMessage = "Timeout should be a number";
|
||||
return;
|
||||
}
|
||||
hosts[i].timeout = Number(hosts[i].timeout);
|
||||
//validating timeout
|
||||
if (hosts[i].timeout < 1) {
|
||||
invalidFormMessage = "Timeout should be greater than 0";
|
||||
return;
|
||||
}
|
||||
//validating port
|
||||
if (hosts[i].port < 1 || hosts[i].port > 65535) {
|
||||
if (!IsValidPort(hosts[i].port)) {
|
||||
invalidFormMessage = "Port should be valid";
|
||||
return;
|
||||
}
|
||||
@@ -293,12 +318,7 @@
|
||||
return;
|
||||
}
|
||||
//validating port
|
||||
if (
|
||||
!!!newMonitor.sslConfig.port ||
|
||||
isNaN(newMonitor.sslConfig.port) ||
|
||||
newMonitor.sslConfig.port < 1 ||
|
||||
newMonitor.sslConfig.port > 65535
|
||||
) {
|
||||
if (!IsValidPort(newMonitor.sslConfig.port)) {
|
||||
invalidFormMessage = "Port should be valid";
|
||||
return;
|
||||
}
|
||||
@@ -381,6 +401,54 @@
|
||||
}
|
||||
|
||||
newMonitor.type_data = JSON.stringify(newMonitor.heartbeatConfig);
|
||||
} else if (newMonitor.monitor_type === "GAMEDIG") {
|
||||
// validating host
|
||||
if (!newMonitor.gamedigConfig.host) {
|
||||
invalidFormMessage = "Host is required";
|
||||
return;
|
||||
}
|
||||
if (ValidateIpAddress(newMonitor.gamedigConfig.host) === "Invalid") {
|
||||
invalidFormMessage = "Invalid host";
|
||||
return;
|
||||
}
|
||||
// validating port
|
||||
if (!IsValidPort(newMonitor.gamedigConfig.port)) {
|
||||
invalidFormMessage = "Port should be valid";
|
||||
return;
|
||||
}
|
||||
// validating game
|
||||
const game = GetGameFromId(AllGamesList, newMonitor.gamedigConfig.gameId);
|
||||
if (!game) {
|
||||
invalidFormMessage = "Invalid game/service";
|
||||
return;
|
||||
}
|
||||
if (!game.isValve && newMonitor.gamedigConfig.requestRules) {
|
||||
newMonitor.gamedigConfig.requestRules = false;
|
||||
}
|
||||
|
||||
//validating timeout
|
||||
if (!!newMonitor.gamedigConfig.timeout && isNaN(newMonitor.gamedigConfig.timeout)) {
|
||||
invalidFormMessage = "Timeout should be a number";
|
||||
return;
|
||||
}
|
||||
|
||||
newMonitor.gamedigConfig.timeout = Number(newMonitor.gamedigConfig.timeout);
|
||||
if (newMonitor.gamedigConfig.timeout < GAMEDIG_SOCKET_TIMEOUT) {
|
||||
invalidFormMessage = `Timeout should be greater than ${GAMEDIG_SOCKET_TIMEOUT}`;
|
||||
return;
|
||||
}
|
||||
|
||||
// validating eval
|
||||
if (!!newMonitor.gamedigConfig.eval) {
|
||||
newMonitor.gamedigConfig.eval = newMonitor.gamedigConfig.eval.trim();
|
||||
|
||||
if (!(await isValidEval(newMonitor.gamedigConfig.eval))) {
|
||||
invalidFormMessage = invalidFormMessage + ". Invalid eval";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
newMonitor.type_data = JSON.stringify(newMonitor.gamedigConfig);
|
||||
}
|
||||
formState = "loading";
|
||||
|
||||
@@ -410,6 +478,35 @@
|
||||
pg: "PostgreSQL",
|
||||
mysql2: "MySQL"
|
||||
};
|
||||
let deleteMonitorConfirmText = "";
|
||||
let deletingMonitor = false;
|
||||
|
||||
async function deleteMonitor() {
|
||||
if (deleteMonitorConfirmText != `delete ${newMonitor.tag}`) {
|
||||
invalidFormMessage = "Please type the correct text to delete the monitor";
|
||||
return;
|
||||
}
|
||||
deletingMonitor = true;
|
||||
try {
|
||||
let data = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ action: "deleteMonitor", data: { tag: newMonitor.tag } })
|
||||
});
|
||||
let resp = await data.json();
|
||||
if (resp.error) {
|
||||
invalidFormMessage = resp.error;
|
||||
} else {
|
||||
dispatch("closeModal", {});
|
||||
}
|
||||
} catch (error) {
|
||||
invalidFormMessage = "Error while deleting monitor";
|
||||
} finally {
|
||||
deletingMonitor = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
|
||||
@@ -634,6 +731,7 @@
|
||||
<Select.Item value="SSL" label="SSL" class="text-sm font-medium">SSL</Select.Item>
|
||||
<Select.Item value="SQL" label="SQL" class="text-sm font-medium">SQL</Select.Item>
|
||||
<Select.Item value="HEARTBEAT" label="HEARTBEAT" class="text-sm font-medium">HEARTBEAT</Select.Item>
|
||||
<Select.Item value="GAMEDIG" label="GAMEDIG" class="text-sm font-medium">GAMEDIG</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
@@ -718,6 +816,18 @@
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-span-6">
|
||||
<label class="cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
on:change={(e) => {
|
||||
newMonitor.apiConfig.allowSelfSignedCert = e.target.checked;
|
||||
}}
|
||||
checked={newMonitor.apiConfig.allowSelfSignedCert}
|
||||
/>
|
||||
<span class="ml-2 text-sm">Allow Self Signed Certificate</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-span-6">
|
||||
<Label for="eval">Eval</Label>
|
||||
<p class="my-1 text-xs text-muted-foreground">
|
||||
@@ -728,12 +838,23 @@
|
||||
href="https://kener.ing/docs/monitors-api#eval">Read the docs</a
|
||||
> to learn
|
||||
</p>
|
||||
<textarea
|
||||
bind:value={newMonitor.apiConfig.eval}
|
||||
id="eval"
|
||||
class="h-96 w-full rounded-sm border p-2"
|
||||
placeholder="Leave blank or write a custom eval function"
|
||||
></textarea>
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newMonitor.apiConfig.eval}
|
||||
lang={javascript()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "18rem",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if newMonitor.monitor_type == "PING"}
|
||||
@@ -823,12 +944,23 @@
|
||||
href="https://kener.ing/docs/monitors-ping#eval">Read the docs</a
|
||||
> to learn
|
||||
</p>
|
||||
<textarea
|
||||
bind:value={newMonitor.pingConfig.pingEval}
|
||||
id="pingEval"
|
||||
class="h-96 w-full rounded-sm border p-2"
|
||||
placeholder="Leave blank or write a custom eval function"
|
||||
></textarea>
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newMonitor.pingConfig.pingEval}
|
||||
lang={javascript()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "18rem",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -900,7 +1032,7 @@
|
||||
];
|
||||
}}
|
||||
>
|
||||
<Plus class="h-5 w-5" /> Add New
|
||||
<Plus class="mr-1 h-5 w-5" /> Add New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -914,12 +1046,22 @@
|
||||
href="https://kener.ing/docs/monitors-tcp#eval">Read the docs</a
|
||||
> to learn
|
||||
</p>
|
||||
<textarea
|
||||
bind:value={newMonitor.tcpConfig.tcpEval}
|
||||
id="tcpEval"
|
||||
class="h-96 w-full rounded-sm border p-2"
|
||||
placeholder="Leave blank or write a custom eval function"
|
||||
></textarea>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newMonitor.tcpConfig.tcpEval}
|
||||
lang={javascript()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "22rem",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1210,6 +1352,141 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if newMonitor.monitor_type == "GAMEDIG"}
|
||||
<div class="mt-4 grid grid-cols-6 gap-2">
|
||||
<div class="col-span-4">
|
||||
<Label for="gameId">
|
||||
Game <span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select.Root portal={null} onSelectedChange={(e) => (newMonitor.gamedigConfig.gameId = e.value)}>
|
||||
<Select.Trigger id="gameId">
|
||||
<Select.Value placeholder={GetGameFromId(AllGamesList, newMonitor.gamedigConfig.gameId).name || ""} />
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-56 overflow-y-auto">
|
||||
<Select.Group>
|
||||
<Select.Label>Game</Select.Label>
|
||||
{#each AllGamesList as game}
|
||||
<Select.Item value={game.id} label={game.name} class="text-sm font-medium">{game.name}</Select.Item>
|
||||
{/each}
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="col-span-2 col-start-1">
|
||||
<Label for="host">
|
||||
Host <span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input bind:value={newMonitor.gamedigConfig.host} id="host" placeholder="172.12.14.42" />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<Label for="port">
|
||||
Port <span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input bind:value={newMonitor.gamedigConfig.port} id="port" placeholder="port number ex 8080" />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<Label for="timeout">
|
||||
Timeout(ms)
|
||||
<span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input bind:value={newMonitor.gamedigConfig.timeout} id="timeout" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6">
|
||||
<label class="cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
on:change={(e) => {
|
||||
newMonitor.gamedigConfig.guessPort = e.target.checked;
|
||||
}}
|
||||
checked={newMonitor.gamedigConfig.guessPort}
|
||||
/>
|
||||
<span class="ml-2 text-sm">Guess port</span>
|
||||
<p class="my-1 text-xs text-muted-foreground">
|
||||
Used port can be different from client port, depending on queried game. Try this if you have
|
||||
unsuccessful responses.
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
{#if newMonitor.gamedigConfig.gameId && GetGameFromId(AllGamesList, newMonitor.gamedigConfig.gameId).isValve}
|
||||
<div class="col-span-6">
|
||||
<label class="cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
on:change={(e) => {
|
||||
newMonitor.gamedigConfig.requestRules = e.target.checked;
|
||||
}}
|
||||
checked={newMonitor.gamedigConfig.requestRules}
|
||||
/>
|
||||
<span class="ml-2 text-sm">Request additional rules</span>
|
||||
<p class="my-1 text-xs text-muted-foreground">
|
||||
Valve games can provide additional 'rules' to Gamedig monitors. If checked, they will be available in
|
||||
`reponseRaw.raw`, beware that it may increase query time.
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="col-span-6 mt-2">
|
||||
<Label for="eval">Eval</Label>
|
||||
<p class="my-1 text-xs text-muted-foreground">
|
||||
You can write a custom eval function to evaluate the response. The function should return a promise that
|
||||
resolves to an object with status and latency. <a
|
||||
target="_blank"
|
||||
class="font-medium text-primary"
|
||||
href="https://kener.ing/docs/monitors-gamedig#eval">Read the docs</a
|
||||
> to learn
|
||||
</p>
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newMonitor.gamedigConfig.eval}
|
||||
lang={javascript()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "18rem",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !!newMonitor.id}
|
||||
<div class="mt-4 flex">
|
||||
<div class="flex w-full flex-col rounded-md border border-destructive p-2">
|
||||
<div class="w-full">
|
||||
<h2 class="flex justify-between text-lg">
|
||||
<span class="text-destructive">Danger Zone</span>
|
||||
</h2>
|
||||
<p class="text-sm">Deleting a monitor is irreversible. Please be sure before deleting.</p>
|
||||
</div>
|
||||
<form class="flex gap-x-2" on:submit|preventDefault={deleteMonitor}>
|
||||
<div class="mt-1 flex flex-col gap-y-2">
|
||||
<Label for="deleteMonitor">
|
||||
Type <i class="text-destructive">delete {newMonitor.tag}</i> to confirm
|
||||
</Label>
|
||||
<Input bind:value={deleteMonitorConfirmText} id="deleteMonitor" required />
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="submit"
|
||||
class="mt-7"
|
||||
disabled={deleteMonitorConfirmText != `delete ${newMonitor.tag}`}
|
||||
>
|
||||
Delete
|
||||
{#if deletingMonitor}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Settings, Bell, Loader, ArrowDownUp, Grip, ExternalLink } from "lucide-svelte";
|
||||
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 DatabaseZap from "lucide-svelte/icons/database-zap";
|
||||
import ArrowDownUp from "lucide-svelte/icons/arrow-down-up";
|
||||
import Grip from "lucide-svelte/icons/grip";
|
||||
import ExternalLink from "lucide-svelte/icons/external-link";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { base } from "$app/paths";
|
||||
@@ -8,20 +16,32 @@
|
||||
import { onMount } from "svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import AllGamesListRaw from "$lib/all-games-list.json?raw";
|
||||
import { storeSiteData, SortMonitor, RandomString } from "$lib/clientTools.js";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { DateInput } from "date-picker-svelte";
|
||||
import { dndzone } from "svelte-dnd-action";
|
||||
import { flip } from "svelte/animate";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import {
|
||||
DefaultAPIEval,
|
||||
DefaultTCPEval,
|
||||
DefaultPingEval,
|
||||
DefaultGamedigEval,
|
||||
GAMEDIG_TIMEOUT
|
||||
} from "$lib/anywhere.js";
|
||||
|
||||
export let categories = [];
|
||||
export let colorDown = "#777";
|
||||
export let colorDegraded = "#777";
|
||||
export let monitorSort = [];
|
||||
let AllGamesList = JSON.parse(AllGamesListRaw);
|
||||
let monitors = [];
|
||||
let status = "ACTIVE";
|
||||
let showAddMonitor = false;
|
||||
let formState = "idle";
|
||||
let modifyMonitoringDataFormState = "idle";
|
||||
let loadingData = false;
|
||||
let triggers = [];
|
||||
let selectedCategory = "All Categories";
|
||||
@@ -70,16 +90,17 @@
|
||||
headers: [],
|
||||
body: "",
|
||||
timeout: 10000,
|
||||
eval: "",
|
||||
hideURLForGet: "NO"
|
||||
eval: DefaultAPIEval,
|
||||
hideURLForGet: "NO",
|
||||
allowSelfSignedCert: false
|
||||
},
|
||||
tcpConfig: {
|
||||
hosts: [], //{timeout: 1000, host: "", type:""}
|
||||
tcpEval: ""
|
||||
tcpEval: DefaultTCPEval
|
||||
},
|
||||
pingConfig: {
|
||||
hosts: [], //{timeout: 1000, host: "", count: "", type:""}
|
||||
pingEval: ""
|
||||
pingEval: DefaultPingEval
|
||||
},
|
||||
dnsConfig: {
|
||||
host: "",
|
||||
@@ -109,6 +130,15 @@
|
||||
degradedRemainingMinutes: 1,
|
||||
downRemainingMinutes: 2,
|
||||
secretString: RandomString(32)
|
||||
},
|
||||
gamedigConfig: {
|
||||
gameId: AllGamesList[0].id,
|
||||
host: "",
|
||||
port: "",
|
||||
timeout: GAMEDIG_TIMEOUT,
|
||||
guessPort: false,
|
||||
requestRules: false,
|
||||
eval: DefaultGamedigEval
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -147,6 +177,8 @@
|
||||
newMonitor.sqlConfig = JSON.parse(newMonitor.type_data);
|
||||
} else if (newMonitor.monitor_type == "HEARTBEAT") {
|
||||
newMonitor.heartbeatConfig = JSON.parse(newMonitor.type_data);
|
||||
} else if (newMonitor.monitor_type == "GAMEDIG") {
|
||||
newMonitor.gamedigConfig = JSON.parse(newMonitor.type_data);
|
||||
}
|
||||
showAddMonitor = true;
|
||||
}
|
||||
@@ -186,7 +218,12 @@
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ action: "getTriggers", data: {} })
|
||||
body: JSON.stringify({
|
||||
action: "getTriggers",
|
||||
data: {
|
||||
status: "ACTIVE"
|
||||
}
|
||||
})
|
||||
});
|
||||
triggers = await apiResp.json();
|
||||
} catch (error) {
|
||||
@@ -218,31 +255,108 @@
|
||||
};
|
||||
});
|
||||
let shareMenusToggle = false;
|
||||
|
||||
let monitorTriggers = {
|
||||
down_trigger: {
|
||||
failureThreshold: 1,
|
||||
trigger_type: "DOWN",
|
||||
successThreshold: 1,
|
||||
description: "The monitor is down",
|
||||
createIncident: "NO",
|
||||
active: false,
|
||||
triggers: [],
|
||||
severity: "critical"
|
||||
},
|
||||
degraded_trigger: {
|
||||
failureThreshold: 1,
|
||||
trigger_type: "DEGRADED",
|
||||
successThreshold: 1,
|
||||
active: false,
|
||||
description: "The monitor is degraded",
|
||||
createIncident: "NO",
|
||||
triggers: [],
|
||||
severity: "warning"
|
||||
}
|
||||
let monitoringDataMenusToggle = false;
|
||||
let monitorDataSelected;
|
||||
let monitoringDataRequest = {
|
||||
newStatus: "UP"
|
||||
};
|
||||
|
||||
function initMonitorTriggers() {
|
||||
monitorTriggers = {
|
||||
down_trigger: {
|
||||
failureThreshold: 1,
|
||||
trigger_type: "DOWN",
|
||||
successThreshold: 1,
|
||||
description: "The monitor is down",
|
||||
createIncident: "NO",
|
||||
active: false,
|
||||
triggers: [],
|
||||
severity: "critical"
|
||||
},
|
||||
degraded_trigger: {
|
||||
failureThreshold: 1,
|
||||
trigger_type: "DEGRADED",
|
||||
successThreshold: 1,
|
||||
active: false,
|
||||
description: "The monitor is degraded",
|
||||
createIncident: "NO",
|
||||
triggers: [],
|
||||
severity: "warning"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let modifyMonitoringDataError = "";
|
||||
async function ModifyMonitoringDataTriggers() {
|
||||
modifyMonitoringDataError = "";
|
||||
if (!monitoringDataRequest.start) {
|
||||
modifyMonitoringDataError = "Invalid start date";
|
||||
return;
|
||||
}
|
||||
if (!monitoringDataRequest.end) {
|
||||
modifyMonitoringDataError = "Invalid end date";
|
||||
return;
|
||||
}
|
||||
if (!monitorDataSelected.tag) {
|
||||
modifyMonitoringDataError = "Invalid monitor";
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!monitoringDataRequest.newStatus ||
|
||||
(monitoringDataRequest.newStatus != "UP" &&
|
||||
monitoringDataRequest.newStatus != "DEGRADED" &&
|
||||
monitoringDataRequest.newStatus != "DOWN")
|
||||
) {
|
||||
modifyMonitoringDataError = "Invalid status";
|
||||
return;
|
||||
}
|
||||
|
||||
let startTimestamp = parseInt(new Date(monitoringDataRequest.start).getTime() / 1000);
|
||||
let endTimestamp = parseInt(new Date(monitoringDataRequest.end).getTime() / 1000);
|
||||
if (startTimestamp >= endTimestamp) {
|
||||
modifyMonitoringDataError = "The start date must be before the end date";
|
||||
return;
|
||||
}
|
||||
let data = {
|
||||
monitor_tag: monitorDataSelected.tag,
|
||||
start: startTimestamp,
|
||||
end: endTimestamp,
|
||||
newStatus: monitoringDataRequest.newStatus
|
||||
};
|
||||
|
||||
modifyMonitoringDataFormState = "loading";
|
||||
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ action: "updateMonitoringData", data })
|
||||
});
|
||||
|
||||
let resp = await apiResp.json();
|
||||
if (resp.error) {
|
||||
modifyMonitoringDataError = resp.error;
|
||||
} else {
|
||||
monitoringDataMenusToggle = false;
|
||||
monitoringDataRequest = {
|
||||
newStatus: "UP"
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
modifyMonitoringDataError = "Error while modifying monitoring data";
|
||||
} finally {
|
||||
modifyMonitoringDataFormState = "idle";
|
||||
}
|
||||
}
|
||||
|
||||
let monitorTriggers;
|
||||
initMonitorTriggers();
|
||||
|
||||
let saveTriggerError = "";
|
||||
async function saveTriggers() {
|
||||
saveTriggerError = "";
|
||||
let data = {
|
||||
id: currentAlertMonitor.id,
|
||||
down_trigger: JSON.stringify(monitorTriggers.down_trigger),
|
||||
@@ -258,16 +372,23 @@
|
||||
},
|
||||
body: JSON.stringify({ action: "updateMonitorTriggers", data })
|
||||
});
|
||||
|
||||
let resp = await apiResp.json();
|
||||
if (resp.error) {
|
||||
saveTriggerError = resp.error;
|
||||
} else {
|
||||
shareMenusToggle = false;
|
||||
loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
saveTriggerError = "Error while saving triggers";
|
||||
} finally {
|
||||
formState = "idle";
|
||||
loadData();
|
||||
shareMenusToggle = false;
|
||||
}
|
||||
}
|
||||
let currentAlertMonitor;
|
||||
function openAlertMenu(m) {
|
||||
initMonitorTriggers();
|
||||
currentAlertMonitor = m;
|
||||
if (m.down_trigger) {
|
||||
monitorTriggers.down_trigger = m.down_trigger;
|
||||
@@ -278,15 +399,30 @@
|
||||
shareMenusToggle = true;
|
||||
}
|
||||
const flipDurationMs = 200;
|
||||
function openMonitoringDataMenu(m) {
|
||||
monitoringDataMenusToggle = true;
|
||||
monitorDataSelected = m;
|
||||
}
|
||||
let orderErrorMessage = "";
|
||||
function handleSort(e) {
|
||||
dropTargetStyle = {
|
||||
border: "1px solid transparent"
|
||||
};
|
||||
monitors = e.detail.items;
|
||||
monitorSort = monitors.map((m) => m.id);
|
||||
|
||||
storeSiteData({
|
||||
monitorSort: JSON.stringify(monitorSort)
|
||||
});
|
||||
})
|
||||
.then(async (resp) => {
|
||||
let data = await resp.json();
|
||||
if (data.error) {
|
||||
orderErrorMessage = data.error;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
orderErrorMessage = "Error while saving order";
|
||||
});
|
||||
}
|
||||
let dropTargetStyle;
|
||||
let draggableMenu = false;
|
||||
@@ -360,6 +496,9 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !!orderErrorMessage}
|
||||
<p class="py-2 text-sm font-medium text-destructive">{orderErrorMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,17 +562,19 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{#if status == "ACTIVE"}
|
||||
<Button size="icon" variant="secondary" on:click={() => (draggableMenu = !draggableMenu)}>
|
||||
<ArrowDownUp class=" " />
|
||||
{#if $page.data.user.role != "member"}
|
||||
<div>
|
||||
{#if status == "ACTIVE"}
|
||||
<Button size="icon" variant="secondary" on:click={() => (draggableMenu = !draggableMenu)}>
|
||||
<ArrowDownUp class=" " />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={showAddMonitorSheet}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
Add Monitor
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={showAddMonitorSheet}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
Add Monitor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -452,7 +593,7 @@
|
||||
<div class="absolute right-2 top-0.5 flex gap-x-1">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="secondary" class="h-8 p-2 text-xs" on:click={() => testMonitor(i)}>TEST</Button>
|
||||
<Button variant="secondary" class="h-8 p-2 text-xs" on:click={() => testMonitor(i)}>Test Monitor</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="max-w-md">
|
||||
<DropdownMenu.Group>
|
||||
@@ -468,7 +609,7 @@
|
||||
<div class="text-red-500">
|
||||
{monitor.testResult.error}
|
||||
</div>
|
||||
{:else if !!monitor.testResult.status && !!monitor.testResult.latency}
|
||||
{:else if monitor.testResult.status != undefined && monitor.testResult.latency != undefined}
|
||||
<p class="text-muted-foreground">
|
||||
Status: <span class="text-api-{monitor.testResult.status.toLowerCase()}"
|
||||
>{monitor.testResult.status}</span
|
||||
@@ -482,22 +623,28 @@
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="h-8 w-8 p-2 {monitor.down_trigger?.active || monitor.degraded_trigger?.active
|
||||
? 'text-yellow-500'
|
||||
: ''}"
|
||||
on:click={() => openAlertMenu(monitor)}
|
||||
>
|
||||
<Bell class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button
|
||||
variant="secondary"
|
||||
class="h-8 w-8 p-2 {monitor.down_trigger?.active || monitor.degraded_trigger?.active
|
||||
? 'text-yellow-500'
|
||||
: ''}"
|
||||
on:click={() => openAlertMenu(monitor)}
|
||||
>
|
||||
<Bell class="inline h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" on:click={() => openMonitoringDataMenu(monitor)}>
|
||||
<DatabaseZap class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" rel="external" href="{base}/?monitor={monitor.tag}">
|
||||
<ExternalLink class="inline h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" href="#{monitor.tag}">
|
||||
<Settings class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button variant="secondary" class="h-8 w-8 p-2" href="#{monitor.tag}">
|
||||
<Settings class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
@@ -532,6 +679,88 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if monitoringDataMenusToggle}
|
||||
<div class="moldal-container fixed left-0 top-0 z-50 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-2xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
monitoringDataMenusToggle = 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">Modify monitors data</h2>
|
||||
<p class="text-xs text-muted-foreground">You can modify the monitor results for a given time range.</p>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="col-span-1">
|
||||
<Label for="start_date_time">
|
||||
Start date
|
||||
<span class="text-red-500">*</span>
|
||||
</Label>
|
||||
|
||||
<DateInput
|
||||
bind:value={monitoringDataRequest.start}
|
||||
id="start_date_time"
|
||||
timePrecision="minute"
|
||||
class=" text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Label for="end_date_time">
|
||||
End date
|
||||
<span class="text-red-500">*</span>
|
||||
</Label>
|
||||
<DateInput
|
||||
bind:value={monitoringDataRequest.end}
|
||||
id="end_date_time"
|
||||
timePrecision="minute"
|
||||
class="text-sm"
|
||||
min={monitoringDataRequest.start}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Label for="monitorDataNewStatus">New status</Label>
|
||||
<Select.Root portal={null} value="UP" onSelectedChange={(e) => (monitoringDataRequest.newStatus = e.value)}>
|
||||
<Select.Trigger id="monitorDataNewStatus">
|
||||
<Select.Value bind:value={monitoringDataRequest.newStatus} placeholder="UP" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Item value="UP" label="UP" class="text-sm font-medium">UP</Select.Item>
|
||||
<Select.Item value="DEGRADED" label="DEGRADED" class="text-sm font-medium">DEGRADED</Select.Item>
|
||||
<Select.Item value="DOWN" label="DOWN" class="text-sm font-medium">DOWN</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-x-2">
|
||||
{#if !!modifyMonitoringDataError}
|
||||
<div class="overflow-hidden text-ellipsis whitespace-nowrap py-2 text-sm font-medium text-destructive">
|
||||
{modifyMonitoringDataError}
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
class=""
|
||||
on:click={ModifyMonitoringDataTriggers}
|
||||
disabled={modifyMonitoringDataFormState === "loading"}
|
||||
>
|
||||
Save
|
||||
{#if modifyMonitoringDataFormState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if shareMenusToggle}
|
||||
<div class="moldal-container fixed left-0 top-0 z-50 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm">
|
||||
<div
|
||||
@@ -677,9 +906,12 @@
|
||||
<hr class="my-4" />
|
||||
{/each}
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button class="w-full" on:click={saveTriggers} disabled={formState === "loading"}>
|
||||
Save
|
||||
<div class="flex justify-end gap-x-2">
|
||||
{#if !!saveTriggerError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{saveTriggerError}</div>
|
||||
{/if}
|
||||
<Button class="" on:click={saveTriggers} disabled={formState === "loading"}>
|
||||
Save Alerts
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
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 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";
|
||||
@@ -431,11 +433,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<p class="px-4 pt-3 text-sm">
|
||||
<span class="text-red-500">
|
||||
{formErrorMessage}
|
||||
</span>
|
||||
<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
|
||||
|
||||
@@ -3,18 +3,26 @@
|
||||
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 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 { 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 CodeMirror from "svelte-codemirror-editor";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
export let data;
|
||||
|
||||
let formState = "idle";
|
||||
let themeError = "";
|
||||
|
||||
let themeData = {
|
||||
pattern: "none",
|
||||
@@ -23,6 +31,7 @@
|
||||
barStyle: "PARTIAL",
|
||||
barRoundness: "ROUNDED",
|
||||
summaryStyle: "CURRENT",
|
||||
kenerTheme: "default",
|
||||
colorsJ: {
|
||||
UP: "#4ead94",
|
||||
DOWN: "#ca3038",
|
||||
@@ -49,6 +58,7 @@
|
||||
|
||||
async function formSubmit() {
|
||||
formState = "loading";
|
||||
themeError = "";
|
||||
let themeDataAPI = { ...themeData };
|
||||
themeDataAPI.colors = JSON.stringify(themeDataAPI.colorsJ);
|
||||
themeDataAPI.font = JSON.stringify(themeDataAPI.fontJ);
|
||||
@@ -59,7 +69,7 @@
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
themeError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -91,16 +101,64 @@
|
||||
</p>
|
||||
|
||||
<div class="flex">
|
||||
<RadioGroup.Root bind:value={themeData.pattern} onValueChange={(e) => (themeData.pattern = e)}>
|
||||
<div class="flex items-center space-x-2">
|
||||
<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 items-center space-x-2">
|
||||
<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 items-center space-x-2">
|
||||
<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>
|
||||
@@ -235,53 +293,51 @@
|
||||
</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">
|
||||
<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, and DOWN.
|
||||
</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"
|
||||
--cp-input-color="#777"
|
||||
--cp-button-hover-color="#767676"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.UP = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full">
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.DEGRADED}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
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">
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.DOWN}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
label="DOWN"
|
||||
--cp-input-color="#777"
|
||||
--cp-button-hover-color="#767676"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.DOWN = event.detail.hex;
|
||||
}}
|
||||
@@ -334,20 +390,37 @@
|
||||
<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.
|
||||
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>
|
||||
<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 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">
|
||||
<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
|
||||
Save Theme Settings
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Loader, Settings, Check, ChevronRight } from "lucide-svelte";
|
||||
import { page } from "$app/stores";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { base } from "$app/paths";
|
||||
@@ -9,8 +15,14 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { IsValidURL } from "$lib/clientTools.js";
|
||||
import { DiscordJSONTemplate, WebhookJSONTemplate, SlackJSONTemplate } from "$lib/anywhere.js";
|
||||
import { clickOutsideAction, slide } from "svelte-legos";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let status = "ACTIVE";
|
||||
let formState = "idle";
|
||||
let showAddTrigger = false;
|
||||
@@ -29,8 +41,12 @@
|
||||
headers: [],
|
||||
to: "",
|
||||
from: data.fromEmail,
|
||||
webhook_body: "",
|
||||
webhook_body: WebhookJSONTemplate,
|
||||
has_webhook_body: false,
|
||||
has_discord_body: false,
|
||||
discord_body: DiscordJSONTemplate,
|
||||
has_slack_body: false,
|
||||
slack_body: SlackJSONTemplate,
|
||||
email_type: data.preferredModeEmail,
|
||||
smtp_host: data.smtp?.smtp_host ? data.smtp.smtp_host : "",
|
||||
smtp_port: data.smtp?.smtp_port ? data.smtp.smtp_port : "",
|
||||
@@ -52,8 +68,12 @@
|
||||
headers: [],
|
||||
to: "",
|
||||
from: data.fromEmail,
|
||||
webhook_body: "",
|
||||
webhook_body: WebhookJSONTemplate,
|
||||
has_webhook_body: false,
|
||||
has_discord_body: false,
|
||||
discord_body: DiscordJSONTemplate,
|
||||
has_slack_body: false,
|
||||
slack_body: SlackJSONTemplate,
|
||||
email_type: data.preferredModeEmail,
|
||||
smtp_host: data.smtp?.smtp_host ? data.smtp.smtp_host : "",
|
||||
smtp_port: data.smtp?.smtp_port ? data.smtp.smtp_port : "",
|
||||
@@ -91,13 +111,35 @@
|
||||
|
||||
async function addNewTrigger() {
|
||||
invalidFormMessage = "";
|
||||
formState = "loading";
|
||||
// formState = "loading";
|
||||
//newTrigger.trigger_type present not empty
|
||||
if (newTrigger.trigger_type == "") {
|
||||
invalidFormMessage = "Trigger Type is required";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
if (newTrigger.trigger_type == "discord") {
|
||||
if (newTrigger.trigger_meta.has_discord_body) {
|
||||
try {
|
||||
JSON.parse(newTrigger.trigger_meta.discord_body);
|
||||
} catch (e) {
|
||||
invalidFormMessage = "Invalid JSON in Discord Body";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newTrigger.trigger_type == "slack") {
|
||||
if (newTrigger.trigger_meta.has_slack_body) {
|
||||
try {
|
||||
JSON.parse(newTrigger.trigger_meta.slack_body);
|
||||
} catch (e) {
|
||||
invalidFormMessage = "Invalid JSON in Slack Body";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newTrigger.trigger_type == "email") {
|
||||
newTrigger.trigger_meta.url = "";
|
||||
@@ -346,10 +388,12 @@
|
||||
<Loader class="ml-2 mt-2 inline h-6 w-6 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<Button on:click={addNewTriggerFn}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
New Trigger
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button on:click={addNewTriggerFn}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
New Trigger
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -382,12 +426,13 @@
|
||||
{#if trigger.testLoaders == "success"}
|
||||
<Check class="mr-1 inline h-3 w-3 text-green-500 " />
|
||||
{/if}
|
||||
<span class="h-4 text-xs font-medium"> TEST </span>
|
||||
</Button>
|
||||
|
||||
<Button variant="secondary" class=" h-8 w-8 p-2" href={"#" + trigger.id}>
|
||||
<Settings class="inline h-4 w-4" />
|
||||
<span class="h-4 text-xs font-medium"> Test Trigger </span>
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button variant="secondary" class=" h-8 w-8 p-2" href={"#" + trigger.id}>
|
||||
<Settings class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
@@ -551,7 +596,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<label class="text-sm font-medium">
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_webhook_body = e.target.checked;
|
||||
@@ -567,21 +612,116 @@
|
||||
set. The default is JSON. There are <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#webhook-body">variables</a
|
||||
href="https://kener.ing/docs/triggers#triggers-trigger-variables">mustache variables</a
|
||||
>
|
||||
that you can use for the webhook body. To use a variable wrap it like
|
||||
<code>${variable}</code>
|
||||
<code>{{variable}}</code>. There are some examples
|
||||
<a target="_blank" class="text-blue-500" href="https://kener.ing/docs/triggers#webhook-examples"
|
||||
>here</a
|
||||
>
|
||||
</p>
|
||||
<div class="w-full">
|
||||
<textarea
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newTrigger.trigger_meta.webhook_body}
|
||||
class="mt-2 h-[500px] w-full rounded-sm border p-2"
|
||||
placeholder={placeholderWebhookBody}
|
||||
></textarea>
|
||||
lang={json()}
|
||||
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>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "slack"}
|
||||
<div class="mt-4 flex w-full flex-col gap-y-2">
|
||||
<div>
|
||||
<label class="text-sm font-medium">
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_slack_body = e.target.checked;
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={newTrigger.trigger_meta.has_slack_body}
|
||||
/>
|
||||
Use Custom Slack Payload
|
||||
</label>
|
||||
</div>
|
||||
{#if !!newTrigger.trigger_meta.has_slack_body}
|
||||
<p class="my-2 text-xs text-muted-foreground">
|
||||
You can use a custom slack body. You can use mustache variable to generate a body. See the <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#triggers-trigger-variables">variables</a
|
||||
>
|
||||
that you can use for the slack body.
|
||||
</p>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newTrigger.trigger_meta.slack_body}
|
||||
lang={json()}
|
||||
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>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "discord"}
|
||||
<div class="mt-4 flex w-full flex-col gap-y-2">
|
||||
<div>
|
||||
<label class="text-sm font-medium">
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_discord_body = e.target.checked;
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={newTrigger.trigger_meta.has_discord_body}
|
||||
/>
|
||||
Use Custom Discord Payload
|
||||
</label>
|
||||
</div>
|
||||
{#if !!newTrigger.trigger_meta.has_discord_body}
|
||||
<p class="my-2 text-xs text-muted-foreground">
|
||||
You can use a custom discord body. You can use mustache variable to generate a body. See the <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#triggers-trigger-variables">variables</a
|
||||
>
|
||||
that you can use for the discord body.
|
||||
</p>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newTrigger.trigger_meta.discord_body}
|
||||
lang={json()}
|
||||
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>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "email"}
|
||||
<div class="mt-4 w-full">
|
||||
<RadioGroup.Root class="my-4 flex" bind:value={newTrigger.trigger_meta.email_type}>
|
||||
@@ -675,11 +815,11 @@
|
||||
</div>
|
||||
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
|
||||
<div class="col-span-5 py-2.5">
|
||||
<p class="text-right text-xs font-medium text-red-500">{invalidFormMessage}</p>
|
||||
<p class="text-right text-sm font-medium text-destructive">{invalidFormMessage}</p>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Button class="col-span-1 w-full" disabled={formState === "loading"} on:click={addNewTrigger}>
|
||||
Save
|
||||
Save Trigger
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
import { sub, startOfDay, getUnixTime } from "date-fns";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
import { Share2, ArrowRight, Settings, TrendingUp, Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||
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";
|
||||
@@ -88,33 +94,33 @@
|
||||
let uptimesRollers = [
|
||||
{
|
||||
text: `${l(lang, "90 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 90 }))),
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 89,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp,
|
||||
value: uptime90Day
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "60 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 59 }))),
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 59,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "30 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 29 }))),
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 29,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "14 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 13 }))),
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 13,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "7 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 }))),
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 6,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: l(lang, "Today"),
|
||||
startTs: getUnixTime(startOfDay(new Date())),
|
||||
startTs: monitor.pageData.startOfTheDay,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
}
|
||||
];
|
||||
@@ -124,6 +130,10 @@
|
||||
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;
|
||||
@@ -173,12 +183,18 @@
|
||||
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);
|
||||
@@ -207,7 +223,7 @@
|
||||
{@html monitor.description}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="absolute right-14 top-5 flex gap-x-2">
|
||||
<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}"
|
||||
@@ -229,16 +245,6 @@
|
||||
>
|
||||
<Share2 class="h-4 w-4 " />
|
||||
</Button>
|
||||
{#if monitor.monitor_type === "GROUP"}
|
||||
<Button
|
||||
class="bounce-right h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
variant="link"
|
||||
href="{base}?group={monitor.tag}"
|
||||
rel="external"
|
||||
>
|
||||
<ArrowRight class="arrow h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,7 +325,6 @@
|
||||
></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"
|
||||
@@ -337,6 +342,21 @@
|
||||
{/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}
|
||||
|
||||
{#if showDailyDataModal}
|
||||
<div
|
||||
transition:slide={{ direction: "bottom" }}
|
||||
|
||||
@@ -1,17 +1,69 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { Languages, Menu } from "lucide-svelte";
|
||||
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">
|
||||
<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}
|
||||
@@ -31,7 +83,10 @@
|
||||
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("nav", navItem.name)}
|
||||
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} />
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script>
|
||||
import { Share2, Link, CopyCheck, Code, TrendingUp, Percent, Loader, ExternalLink } from "lucide-svelte";
|
||||
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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr, nb } from "date-fns/locale";
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr, nb, pl } from "date-fns/locale";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
|
||||
const locales = {
|
||||
@@ -19,6 +19,7 @@ const locales = {
|
||||
"pt-BR": ptBR,
|
||||
tr,
|
||||
"nb-NO": nb,
|
||||
pl,
|
||||
};
|
||||
|
||||
const f = function (date, formatStr, locale, tz) {
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -19,6 +24,9 @@
|
||||
"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",
|
||||
@@ -28,10 +36,12 @@
|
||||
"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",
|
||||
@@ -40,11 +50,15 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -53,12 +67,21 @@
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 Dage",
|
||||
"90 Days": "90 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",
|
||||
"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",
|
||||
@@ -19,6 +24,9 @@
|
||||
"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",
|
||||
@@ -28,10 +36,12 @@
|
||||
"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",
|
||||
@@ -40,11 +50,15 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -53,12 +67,21 @@
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -19,6 +24,9 @@
|
||||
"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",
|
||||
@@ -28,10 +36,12 @@
|
||||
"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",
|
||||
@@ -40,11 +50,15 @@
|
||||
"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",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
@@ -53,12 +67,21 @@
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 jours",
|
||||
"90 Days": "90 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",
|
||||
"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é",
|
||||
@@ -19,6 +24,9 @@
|
||||
"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é",
|
||||
@@ -28,10 +36,12 @@
|
||||
"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",
|
||||
@@ -40,11 +50,15 @@
|
||||
"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",
|
||||
"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",
|
||||
@@ -53,12 +67,21 @@
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 दिन",
|
||||
"90 Days": "90 दिन",
|
||||
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
|
||||
"All Systems are Degraded": "सभी सिस्टम क्षीण हैं",
|
||||
"All Systems are Down": "सभी सिस्टम डाउन हैं",
|
||||
"All Systems are Operational": "सभी सिस्टम कार्यरत हैं",
|
||||
"Back": "वापस",
|
||||
"Badge Copied": "बैज कॉपी किया गया",
|
||||
"Badge": "बैज",
|
||||
"Browse Events": "ईवेंट ब्राउज़ करें",
|
||||
"Change Email": "ईमेल बदलें",
|
||||
"Code Copied": "कोड कॉपी किया गया",
|
||||
"Confirm Login": "लॉगिन पुष्टि करें",
|
||||
"Copy Code": "कोड कॉपी करें",
|
||||
"Copy Link": "लिंक कॉपी करें",
|
||||
"DEGRADED": "क्षीण",
|
||||
@@ -19,6 +24,9 @@
|
||||
"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": "पहचाना गया",
|
||||
@@ -28,10 +36,12 @@
|
||||
"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 को कोई घटना नहीं",
|
||||
@@ -40,11 +50,15 @@
|
||||
"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": "कुछ सिस्टम डाउन हैं",
|
||||
"Standard": "मानक",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
|
||||
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
|
||||
@@ -53,12 +67,21 @@
|
||||
"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": "अपटाइम"
|
||||
"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 के रूप में लॉग इन हैं"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7日間",
|
||||
"90 Days": "90日間",
|
||||
"Availability per Component": "コンポーネントごとの可用性",
|
||||
"All Systems are Degraded": "すべてのシステムが劣化しています",
|
||||
"All Systems are Down": "すべてのシステムがダウンしています",
|
||||
"All Systems are Operational": "すべてのシステムが正常に稼働しています",
|
||||
"Back": "戻る",
|
||||
"Badge Copied": "バッジがコピーされました",
|
||||
"Badge": "バッジ",
|
||||
"Browse Events": "イベントを閲覧",
|
||||
"Change Email": "メールを変更",
|
||||
"Code Copied": "コードがコピーされました",
|
||||
"Confirm Login": "ログインを確認",
|
||||
"Copy Code": "コードをコピー",
|
||||
"Copy Link": "リンクをコピー",
|
||||
"DEGRADED": "劣化",
|
||||
@@ -19,6 +24,9 @@
|
||||
"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": "識別済み",
|
||||
@@ -28,10 +36,12 @@
|
||||
"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 にインシデントなし",
|
||||
@@ -40,11 +50,15 @@
|
||||
"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": "一部のシステムがダウンしています",
|
||||
"Standard": "標準",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
|
||||
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
|
||||
@@ -53,12 +67,21 @@
|
||||
"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": "稼働時間"
|
||||
"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 としてログインしています"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 일",
|
||||
"90 Days": "90 일",
|
||||
"Availability per Component": "항목당 가용성",
|
||||
"All Systems are Degraded": "모든 시스템이 성능 저하 상태입니다",
|
||||
"All Systems are Down": "모든 시스템이 중단되었습니다",
|
||||
"All Systems are Operational": "모든 시스템이 정상 작동 중입니다",
|
||||
"Back": "뒤로",
|
||||
"Badge Copied": "배지 복사됨",
|
||||
"Badge": "배지",
|
||||
"Browse Events": "이벤트 찾아보기",
|
||||
"Change Email": "이메일 변경",
|
||||
"Code Copied": "소스코드 복사됨",
|
||||
"Confirm Login": "로그인 확인",
|
||||
"Copy Code": "소스코드 복사",
|
||||
"Copy Link": "링크 복사",
|
||||
"DEGRADED": "성능 저하됨",
|
||||
@@ -19,6 +24,9 @@
|
||||
"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": "확인됨",
|
||||
@@ -28,10 +36,12 @@
|
||||
"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 에는 아무 사건도 없었음",
|
||||
@@ -40,11 +50,15 @@
|
||||
"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": "일부 시스템이 중단되었습니다",
|
||||
"Standard": "기본",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
|
||||
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
|
||||
@@ -53,12 +67,21 @@
|
||||
"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": "업타임"
|
||||
"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로 로그인했습니다"
|
||||
}
|
||||
|
||||
@@ -54,5 +54,9 @@
|
||||
{
|
||||
"code": "nb-NO",
|
||||
"name": "Norsk Bokmål"
|
||||
},
|
||||
{
|
||||
"code": "pl",
|
||||
"name": "Polski"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 dager",
|
||||
"90 Days": "90 dager",
|
||||
"Availability per Component": "Tilgjengelighet per komponent",
|
||||
"All Systems are Degraded": "Alle systemer har nedsatt ytelse",
|
||||
"All Systems are Down": "Alle systemer er nede",
|
||||
"All Systems are Operational": "Alle systemer er operative",
|
||||
"Back": "Tilbake",
|
||||
"Badge Copied": "Merke kopiert",
|
||||
"Badge": "Merke",
|
||||
"Browse Events": "Bla gjennom hendelser",
|
||||
"Change Email": "Endre e-post",
|
||||
"Code Copied": "Kode kopiert",
|
||||
"Confirm Login": "Bekreft pålogging",
|
||||
"Copy Code": "Kopier kode",
|
||||
"Copy Link": "Kopier lenke",
|
||||
"DEGRADED": "NEDSATT",
|
||||
@@ -19,6 +24,9 @@
|
||||
"Days": "Dager",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Bygg inn denne monitoren ved å bruke <script> eller <iframe> i appen din.",
|
||||
"Embed": "Bygg inn",
|
||||
"Enter the code": "Skriv inn koden",
|
||||
"Enter your email": "Skriv inn din e-post",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Skriv inn din e-post for å logge inn. Hvis du ikke er abonnent, vil du bli bedt om å abonnere.",
|
||||
"Get SVG badge for this monitor": "Hent SVG-merke for denne monitoren",
|
||||
"Get a LIVE Status for this monitor": "Få en LIVE-status for denne monitoren",
|
||||
"IDENTIFIED": "IDENTIFISERT",
|
||||
@@ -28,10 +36,12 @@
|
||||
"Lasted for about %lastedFor": "Varte i omtrent %lastedFor",
|
||||
"Light": "Lys",
|
||||
"Link Copied": "Lenke kopiert",
|
||||
"Login": "Logg inn",
|
||||
"MAINTENANCE": "VEDLIKEHOLD",
|
||||
"MONITORING": "OVERVÅKER",
|
||||
"Maintenance Completed": "Vedlikehold fullført",
|
||||
"Maintenance in Progress": "Vedlikehold pågår",
|
||||
"Manage Subscription": "Administrer abonnement",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Ingen data",
|
||||
"No Incident in %date": "Ingen hendelser på %date",
|
||||
@@ -40,11 +50,15 @@
|
||||
"No Updates Yet": "Ingen oppdateringer ennå",
|
||||
"Ongoing Incidents": "Pågående hendelser",
|
||||
"Pinging": "Pinger",
|
||||
"Please select specific monitors to receive updates from.": "Vennligst velg spesifikke monitorer å motta oppdateringer fra.",
|
||||
"RESOLVED": "LØST",
|
||||
"Recent Incidents": "Nylige hendelser",
|
||||
"Recent Maintenances": "Nylige vedlikehold",
|
||||
"Share this monitor using a link with others": "Del denne monitoren med andre via lenke",
|
||||
"Share": "Del",
|
||||
"Some Systems are not working as expected": "Noen systemer fungerer ikke som forventet",
|
||||
"Some Systems Degraded": "Noen systemer har nedsatt ytelse",
|
||||
"Some Systems Down": "Noen systemer er nede",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
|
||||
@@ -53,12 +67,21 @@
|
||||
"Starts %startedAt": "Starter %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Subscribe": "Abonner",
|
||||
"Subscribe to Updates": "Abonner på oppdateringer",
|
||||
"Subscribe to all monitors": "Abonner på alle monitorer",
|
||||
"Switch to your timezone": "Bytt til din tidssone",
|
||||
"Theme": "Tema",
|
||||
"There are no upcoming maintenances": "Det er ingen kommende vedlikeholdsperioder",
|
||||
"Timezone set to %tz": "Tidssone satt til %tz",
|
||||
"Today": "I dag",
|
||||
"UP": "OPPE",
|
||||
"Update Subscription": "Oppdater abonnement",
|
||||
"Upcoming Maintenance": "Kommende vedlikehold",
|
||||
"Upcoming Maintenances": "Kommende vedlikeholdsperioder",
|
||||
"Updates": "Oppdateringer",
|
||||
"Uptime": "Oppetid"
|
||||
"Uptime": "Oppetid",
|
||||
"View in detail": "Vis detaljer",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Vi har sendt en kode til din e-post. Vennligst skriv den inn nedenfor for å bekrefte påloggingen",
|
||||
"You are logged in as %email": "Du er logget inn som %email"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 dagen",
|
||||
"90 Days": "90 dagen",
|
||||
"Availability per Component": "Beschikbaarheid per component",
|
||||
"All Systems are Degraded": "Alle systemen werken met beperkte functionaliteit",
|
||||
"All Systems are Down": "Alle systemen zijn niet bereikbaar",
|
||||
"All Systems are Operational": "Alle systemen zijn operationeel",
|
||||
"Back": "Terug",
|
||||
"Badge Copied": "Badge gekopieerd",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Evenementen bekijken",
|
||||
"Change Email": "E-mail wijzigen",
|
||||
"Code Copied": "Code gekopieerd",
|
||||
"Confirm Login": "Inloggen bevestigen",
|
||||
"Copy Code": "Kopieer code",
|
||||
"Copy Link": "Kopieer link",
|
||||
"DEGRADED": "BEPERKT",
|
||||
@@ -19,6 +24,9 @@
|
||||
"Days": "Dagen",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Sluit deze monitor in met behulp van <script> of <iframe> in je app.",
|
||||
"Embed": "Insluiten",
|
||||
"Enter the code": "Voer de code in",
|
||||
"Enter your email": "Voer je e-mailadres in",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Voer je e-mailadres in om in te loggen. Als je nog niet geabonneerd bent, word je gevraagd om je te abonneren.",
|
||||
"Get SVG badge for this monitor": "SVG-badge ophalen voor deze monitor",
|
||||
"Get a LIVE Status for this monitor": "LIVE-status ophalen voor deze monitor",
|
||||
"IDENTIFIED": "GEIDENTIFICEERD",
|
||||
@@ -28,10 +36,12 @@
|
||||
"Lasted for about %lastedFor": "Duurde ongeveer %lastedFor",
|
||||
"Light": "Licht",
|
||||
"Link Copied": "Link gekopieerd",
|
||||
"Login": "Inloggen",
|
||||
"MAINTENANCE": "ONDERHOUD",
|
||||
"MONITORING": "MONITOREN",
|
||||
"Maintenance Completed": "Onderhoud voltooid",
|
||||
"Maintenance in Progress": "Onderhoud bezig",
|
||||
"Manage Subscription": "Abonnement beheren",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Geen gegevens",
|
||||
"No Incident in %date": "Geen incident op %date",
|
||||
@@ -40,11 +50,15 @@
|
||||
"No Updates Yet": "Nog geen updates",
|
||||
"Ongoing Incidents": "Lopende incidenten",
|
||||
"Pinging": "Pingen",
|
||||
"Please select specific monitors to receive updates from.": "Selecteer specifieke monitoren waarvan je updates wilt ontvangen.",
|
||||
"RESOLVED": "OPGELOST",
|
||||
"Recent Incidents": "Recente incidenten",
|
||||
"Recent Maintenances": "Recente onderhoudsbeurten",
|
||||
"Share this monitor using a link with others": "Deel deze monitor met anderen via een link",
|
||||
"Share": "Delen",
|
||||
"Some Systems are not working as expected": "Sommige systemen werken niet zoals verwacht",
|
||||
"Some Systems Degraded": "Sommige systemen werken met beperkte functionaliteit",
|
||||
"Some Systems Down": "Sommige systemen zijn niet bereikbaar",
|
||||
"Standard": "Standaard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
|
||||
@@ -53,12 +67,21 @@
|
||||
"Starts %startedAt": "Begint over %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Subscribe": "Abonneren",
|
||||
"Subscribe to Updates": "Abonneren op updates",
|
||||
"Subscribe to all monitors": "Abonneren op alle monitoren",
|
||||
"Switch to your timezone": "Schakel over naar je tijdzone",
|
||||
"Theme": "Thema",
|
||||
"There are no upcoming maintenances": "Er zijn geen geplande onderhoudsperiodes",
|
||||
"Timezone set to %tz": "Tijdzone ingesteld op %tz",
|
||||
"Today": "Vandaag",
|
||||
"UP": "BEREIKBAAR",
|
||||
"Update Subscription": "Abonnement bijwerken",
|
||||
"Upcoming Maintenance": "Aankomend onderhoud",
|
||||
"Upcoming Maintenances": "Aankomende onderhoudsperiodes",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"Uptime": "Uptime",
|
||||
"View in detail": "Bekijk in detail",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "We hebben een code naar je e-mailadres gestuurd. Voer deze hieronder in om je login te bevestigen",
|
||||
"You are logged in as %email": "Je bent ingelogd als %email"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"%status for %duration": "%status przez %duration",
|
||||
"14 Days": "14 dni",
|
||||
"30 Days": "30 dni",
|
||||
"60 Days": "60 dni",
|
||||
"7 Days": "7 dni",
|
||||
"90 Days": "90 dni",
|
||||
"Availability per Component": "Dostępność według komponentu",
|
||||
"All Systems are Degraded": "Wszystkie systemy działają z ograniczeniami",
|
||||
"All Systems are Down": "Wszystkie systemy są niedostępne",
|
||||
"All Systems are Operational": "Wszystkie systemy działają poprawnie",
|
||||
"Back": "Wstecz",
|
||||
"Badge Copied": "Odznaka skopiowana",
|
||||
"Badge": "Odznaka",
|
||||
"Browse Events": "Przeglądaj zdarzenia",
|
||||
"Change Email": "Zmień email",
|
||||
"Code Copied": "Kod skopiowany",
|
||||
"Confirm Login": "Potwierdź logowanie",
|
||||
"Copy Code": "Skopiuj kod",
|
||||
"Copy Link": "Skopiuj link",
|
||||
"DEGRADED": "OGRANICZONA",
|
||||
"DOWN": "NIEDOSTĘPNY",
|
||||
"Dark": "Ciemny",
|
||||
"Days": "Dni",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Osadź ten monitor za pomocą <script> lub <iframe> w swojej aplikacji.",
|
||||
"Embed": "Osadź",
|
||||
"Enter the code": "Wprowadź kod",
|
||||
"Enter your email": "Wprowadź swój email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Wprowadź swój email, aby się zalogować. Jeśli nie masz subskrypcji, zostaniesz poproszony o jej utworzenie.",
|
||||
"Get SVG badge for this monitor": "Pobierz odznakę SVG dla tego monitora",
|
||||
"Get a LIVE Status for this monitor": "Uzyskaj STATUS NA ŻYWO tego monitora",
|
||||
"IDENTIFIED": "ZIDENTYFIKOWANE",
|
||||
"INVESTIGATING": "BADANE",
|
||||
"Incident Updates": "Aktualizacje incydentu",
|
||||
"LIVE Status": "Status NA ŻYWO",
|
||||
"Lasted for about %lastedFor": "Trwało około %lastedFor",
|
||||
"Light": "Jasny",
|
||||
"Link Copied": "Link skopiowany",
|
||||
"Login": "Zaloguj się",
|
||||
"MAINTENANCE": "KONSERWACJA",
|
||||
"MONITORING": "MONITOROWANE",
|
||||
"Maintenance Completed": "Konserwacja zakończona",
|
||||
"Maintenance in Progress": "Trwa konserwacja",
|
||||
"Manage Subscription": "Zarządzaj subskrypcją",
|
||||
"Mode": "Tryb",
|
||||
"No Data": "Brak danych",
|
||||
"No Incident in %date": "Brak incydentów w dniu %date",
|
||||
"No Incidents": "Brak incydentów",
|
||||
"No Monitor Found": "Nie znaleziono monitora",
|
||||
"No Updates Yet": "Brak aktualizacji",
|
||||
"Ongoing Incidents": "Trwające incydenty",
|
||||
"Pinging": "Pingowanie",
|
||||
"Please select specific monitors to receive updates from.": "Wybierz konkretne monitory, z których chcesz otrzymywać aktualizacje.",
|
||||
"RESOLVED": "ROZWIĄZANE",
|
||||
"Recent Incidents": "Ostatnie incydenty",
|
||||
"Recent Maintenances": "Ostatnie konserwacje",
|
||||
"Share this monitor using a link with others": "Udostępnij ten monitor za pomocą linku innym osobom",
|
||||
"Share": "Udostępnij",
|
||||
"Some Systems are not working as expected": "Niektóre systemy nie działają zgodnie z oczekiwaniami",
|
||||
"Some Systems Degraded": "Niektóre systemy działają z ograniczeniami",
|
||||
"Some Systems Down": "Niektóre systemy są niedostępne",
|
||||
"Standard": "Standardowy",
|
||||
"Started %startedAt, lasted for %lastedFor": "Rozpoczęto %startedAt, trwało %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Rozpoczęto %startedAt, nadal trwa",
|
||||
"Started %startedAt, will last for %lastedFor more": "Rozpoczęto %startedAt, potrwa jeszcze %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor": "Rozpocznie się %startedAt, potrwa %lastedFor",
|
||||
"Starts %startedAt": "Rozpocznie się %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Subscribe": "Subskrybuj",
|
||||
"Subscribe to Updates": "Subskrybuj aktualizacje",
|
||||
"Subscribe to all monitors": "Subskrybuj wszystkie monitory",
|
||||
"Switch to your timezone": "Przełącz na swoją strefę czasową",
|
||||
"Theme": "Motyw",
|
||||
"There are no upcoming maintenances": "Nie ma nadchodzących konserwacji",
|
||||
"Timezone set to %tz": "Strefa czasowa ustawiona na %tz",
|
||||
"Today": "Dzisiaj",
|
||||
"UP": "DOSTĘPNY",
|
||||
"Update Subscription": "Aktualizuj subskrypcję",
|
||||
"Upcoming Maintenance": "Nadchodząca konserwacja",
|
||||
"Upcoming Maintenances": "Nadchodzące konserwacje",
|
||||
"Updates": "Aktualizacje",
|
||||
"Uptime": "Czas działania",
|
||||
"View in detail": "Zobacz szczegóły",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Wysłaliśmy kod na Twój adres email. Wprowadź go poniżej, aby potwierdzić logowanie",
|
||||
"You are logged in as %email": "Jesteś zalogowany jako %email"
|
||||
}
|
||||
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 Dias",
|
||||
"90 Days": "90 Dias",
|
||||
"Availability per Component": "Disponibilidade por Componente",
|
||||
"All Systems are Degraded": "Todos os Sistemas estão com Falhas",
|
||||
"All Systems are Down": "Todos os Sistemas estão Indisponíveis",
|
||||
"All Systems are Operational": "Todos os Sistemas estão Operacionais",
|
||||
"Back": "Voltar",
|
||||
"Badge Copied": "Emblema Copiado",
|
||||
"Badge": "Emblema",
|
||||
"Browse Events": "Navegar Eventos",
|
||||
"Change Email": "Alterar E-mail",
|
||||
"Code Copied": "Código Copiado",
|
||||
"Confirm Login": "Confirmar Login",
|
||||
"Copy Code": "Copiar Código",
|
||||
"Copy Link": "Copiar Link",
|
||||
"DEGRADED": "FALHA",
|
||||
@@ -19,6 +24,9 @@
|
||||
"Days": "Dias",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
|
||||
"Embed": "Incorporar",
|
||||
"Enter the code": "Digite o código",
|
||||
"Enter your email": "Digite seu e-mail",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Digite seu e-mail para fazer login. Se você não estiver inscrito, você será solicitado a se inscrever.",
|
||||
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
|
||||
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
|
||||
"IDENTIFIED": "IDENTIFICADO",
|
||||
@@ -28,10 +36,12 @@
|
||||
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
|
||||
"Light": "Claro",
|
||||
"Link Copied": "Link Copiado",
|
||||
"Login": "Entrar",
|
||||
"MAINTENANCE": "MANUTENÇÃO",
|
||||
"MONITORING": "MONITORANDO",
|
||||
"Maintenance Completed": "Manutenção Concluída",
|
||||
"Maintenance in Progress": "Manutenção em Progresso",
|
||||
"Manage Subscription": "Gerenciar Inscrição",
|
||||
"Mode": "Modo",
|
||||
"No Data": "Sem dados",
|
||||
"No Incident in %date": "Nenhum incidente em %date",
|
||||
@@ -40,11 +50,15 @@
|
||||
"No Updates Yet": "Nenhuma atualização ainda",
|
||||
"Ongoing Incidents": "Incidentes em Andamento",
|
||||
"Pinging": "Animado",
|
||||
"Please select specific monitors to receive updates from.": "Por favor, selecione monitores específicos para receber atualizações.",
|
||||
"RESOLVED": "RESOLVIDO",
|
||||
"Recent Incidents": "Incidentes Recentes",
|
||||
"Recent Maintenances": "Manutenções Recentes",
|
||||
"Share this monitor using a link with others": "Compartilhar este monitor através do link",
|
||||
"Share": "Compartilhar",
|
||||
"Some Systems are not working as expected": "Alguns Sistemas não estão funcionando como esperado",
|
||||
"Some Systems Degraded": "Alguns Sistemas estão com Falhas",
|
||||
"Some Systems Down": "Alguns Sistemas estão Indisponíveis",
|
||||
"Standard": "Padrão",
|
||||
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
|
||||
@@ -53,12 +67,21 @@
|
||||
"Starts %startedAt": "Inicia em %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Subscribe": "Inscrever-se",
|
||||
"Subscribe to Updates": "Inscrever-se para atualizações",
|
||||
"Subscribe to all monitors": "Inscrever-se em todos os monitores",
|
||||
"Switch to your timezone": "Mudar para o seu fuso horário",
|
||||
"Theme": "Tema",
|
||||
"There are no upcoming maintenances": "Não há manutenções agendadas",
|
||||
"Timezone set to %tz": "Fuso horário definido para %tz",
|
||||
"Today": "Hoje",
|
||||
"UP": "OPERACIONAL",
|
||||
"Update Subscription": "Atualizar Inscrição",
|
||||
"Upcoming Maintenance": "Manutenção Agendada",
|
||||
"Upcoming Maintenances": "Manutenções Agendadas",
|
||||
"Updates": "Atualizações",
|
||||
"Uptime": "Tempo de Atividade"
|
||||
"Uptime": "Tempo de Atividade",
|
||||
"View in detail": "Ver em detalhes",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Enviamos um código para seu e-mail. Por favor, digite-o abaixo para confirmar seu login",
|
||||
"You are logged in as %email": "Você está logado como %email"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 Дней",
|
||||
"90 Days": "90 Дней",
|
||||
"Availability per Component": "Доступность по компонентам",
|
||||
"All Systems are Degraded": "Все системы работают с ухудшенной производительностью",
|
||||
"All Systems are Down": "Все системы недоступны",
|
||||
"All Systems are Operational": "Все системы работают нормально",
|
||||
"Back": "Назад",
|
||||
"Badge Copied": "Значок скопирован",
|
||||
"Badge": "Значок",
|
||||
"Browse Events": "Просмотр событий",
|
||||
"Change Email": "Изменить email",
|
||||
"Code Copied": "Код скопирован",
|
||||
"Confirm Login": "Подтвердить вход",
|
||||
"Copy Code": "Копировать код",
|
||||
"Copy Link": "Копировать ссылку",
|
||||
"DEGRADED": "УХУДШЕНО",
|
||||
@@ -19,6 +24,9 @@
|
||||
"Days": "Дней",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Встройте этот монитор используя <script> или <iframe> в ваше приложение.",
|
||||
"Embed": "Встроить",
|
||||
"Enter the code": "Введите код",
|
||||
"Enter your email": "Введите ваш email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Введите ваш email для входа. Если вы не подписаны, вам будет предложено подписаться.",
|
||||
"Get SVG badge for this monitor": "Получить SVG значок для этого монитора",
|
||||
"Get a LIVE Status for this monitor": "Получить ЖИВОЙ статус для этого монитора",
|
||||
"IDENTIFIED": "ВЫЯВЛЕНО",
|
||||
@@ -28,10 +36,12 @@
|
||||
"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",
|
||||
@@ -40,11 +50,15 @@
|
||||
"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": "Некоторые системы недоступны",
|
||||
"Standard": "Стандартная",
|
||||
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
|
||||
@@ -53,12 +67,21 @@
|
||||
"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": "Время работы"
|
||||
"Uptime": "Время работы",
|
||||
"View in detail": "Подробный просмотр",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Мы отправили код на ваш email. Пожалуйста, введите его ниже для подтверждения входа",
|
||||
"You are logged in as %email": "Вы вошли как %email"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 Gün",
|
||||
"90 Days": "90 Gün",
|
||||
"Availability per Component": "Bileşen Kullanılabilirliği",
|
||||
"All Systems are Degraded": "Tüm Sistemler Bozuk Durumda",
|
||||
"All Systems are Down": "Tüm Sistemler Çalışmıyor",
|
||||
"All Systems are Operational": "Tüm Sistemler Çalışıyor",
|
||||
"Back": "Geri",
|
||||
"Badge Copied": "Rozet Kopyalandı",
|
||||
"Badge": "Rozet",
|
||||
"Browse Events": "Etkinliklere Göz At",
|
||||
"Change Email": "E-postayı Değiştir",
|
||||
"Code Copied": "Kod Kopyalandı",
|
||||
"Confirm Login": "Girişi Onayla",
|
||||
"Copy Code": "Kodu Kopyala",
|
||||
"Copy Link": "Bağlantıyı Kopyala",
|
||||
"DEGRADED": "BOZUK",
|
||||
@@ -19,6 +24,9 @@
|
||||
"Days": "Günler",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
|
||||
"Embed": "Yerleştir",
|
||||
"Enter the code": "Kodu girin",
|
||||
"Enter your email": "E-posta adresinizi girin",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Giriş yapmak için e-posta adresinizi girin. Abone değilseniz, abone olmanız istenecektir.",
|
||||
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
|
||||
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
|
||||
"IDENTIFIED": "TANIMLANDI",
|
||||
@@ -28,10 +36,12 @@
|
||||
"Lasted for about %lastedFor": "%lastedFor sürdü",
|
||||
"Light": "Açık",
|
||||
"Link Copied": "Bağlantı Kopyalandı",
|
||||
"Login": "Giriş Yap",
|
||||
"MAINTENANCE": "BAKIM",
|
||||
"MONITORING": "İZLEME",
|
||||
"Maintenance Completed": "Bakım Tamamlandı",
|
||||
"Maintenance in Progress": "Bakım Devam Ediyor",
|
||||
"Manage Subscription": "Aboneliği Yönet",
|
||||
"Mode": "Mod",
|
||||
"No Data": "Veri Yok",
|
||||
"No Incident in %date": "%date Tarihinde Olay Yok",
|
||||
@@ -40,11 +50,15 @@
|
||||
"No Updates Yet": "Henüz Güncelleme Yok",
|
||||
"Ongoing Incidents": "Devam Eden Olaylar",
|
||||
"Pinging": "Pinging",
|
||||
"Please select specific monitors to receive updates from.": "Lütfen güncellemeleri almak istediğiniz belirli servisleri seçin.",
|
||||
"RESOLVED": "ÇÖZÜLDÜ",
|
||||
"Recent Incidents": "Son Olaylar",
|
||||
"Recent Maintenances": "Son Bakımlar",
|
||||
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
|
||||
"Share": "Paylaş",
|
||||
"Some Systems are not working as expected": "Bazı Sistemler Beklendiği Gibi Çalışmıyor",
|
||||
"Some Systems Degraded": "Bazı Sistemler Bozuk Durumda",
|
||||
"Some Systems Down": "Bazı Sistemler Çalışmıyor",
|
||||
"Standard": "Standart",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
|
||||
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
|
||||
@@ -53,12 +67,21 @@
|
||||
"Starts %startedAt": "%startedAt başlıyor",
|
||||
"Status OK": "Durum UYGUN",
|
||||
"Status": "Durum",
|
||||
"Subscribe": "Abone Ol",
|
||||
"Subscribe to Updates": "Güncellemelere Abone Ol",
|
||||
"Subscribe to all monitors": "Tüm servislere abone ol",
|
||||
"Switch to your timezone": "Saat diliminize geçin",
|
||||
"Theme": "Tema",
|
||||
"There are no upcoming maintenances": "Yaklaşan bakım bulunmamaktadır",
|
||||
"Timezone set to %tz": "Saat dilimi %tz olarak ayarlandı",
|
||||
"Today": "Bugün",
|
||||
"UP": "ÇALIŞIYOR",
|
||||
"Update Subscription": "Aboneliği Güncelle",
|
||||
"Upcoming Maintenance": "Yaklaşan Bakım",
|
||||
"Upcoming Maintenances": "Yaklaşan Bakımlar",
|
||||
"Updates": "Güncellemeler",
|
||||
"Uptime": "Çalışma Süresi"
|
||||
"Uptime": "Çalışma Süresi",
|
||||
"View in detail": "Detaylı Görüntüle",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "E-posta adresinize bir kod gönderdik. Girişinizi onaylamak için lütfen aşağıya girin",
|
||||
"You are logged in as %email": "%email olarak giriş yaptınız"
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7 ngày",
|
||||
"90 Days": "90 ngày",
|
||||
"Availability per Component": "Tính khả dụng theo thành phần",
|
||||
"All Systems are Degraded": "Tất cả hệ thống đang suy giảm hiệu suất",
|
||||
"All Systems are Down": "Tất cả hệ thống đang ngừng hoạt động",
|
||||
"All Systems are Operational": "Tất cả hệ thống đang hoạt động bình thường",
|
||||
"Back": "Quay lại",
|
||||
"Badge Copied": "Huy hiệu đã được sao chép",
|
||||
"Badge": "Huy hiệu",
|
||||
"Browse Events": "Duyệt sự kiện",
|
||||
"Change Email": "Thay đổi email",
|
||||
"Code Copied": "Mã đã được sao chép",
|
||||
"Confirm Login": "Xác nhận đăng nhập",
|
||||
"Copy Code": "Sao chép mã",
|
||||
"Copy Link": "Sao chép liên kết",
|
||||
"DEGRADED": "Suy giảm",
|
||||
@@ -19,6 +24,9 @@
|
||||
"Days": "Ngày",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Nhúng trình theo dõi này vào ứng dụng của bạn bằng <script> hoặc <iframe>.",
|
||||
"Embed": "Nhúng",
|
||||
"Enter the code": "Nhập mã",
|
||||
"Enter your email": "Nhập email của bạn",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Nhập email của bạn để đăng nhập. Nếu bạn chưa đăng ký, bạn sẽ được nhắc đăng ký.",
|
||||
"Get SVG badge for this monitor": "Lấy huy hiệu SVG cho trình theo dõi này",
|
||||
"Get a LIVE Status for this monitor": "Lấy trạng thái TRỰC TIẾP cho trình theo dõi này",
|
||||
"IDENTIFIED": "Đã xác định",
|
||||
@@ -28,10 +36,12 @@
|
||||
"Lasted for about %lastedFor": "Kéo dài khoảng %lastedFor",
|
||||
"Light": "Sáng",
|
||||
"Link Copied": "Liên kết đã được sao chép",
|
||||
"Login": "Đăng nhập",
|
||||
"MAINTENANCE": "Bảo trì",
|
||||
"MONITORING": "Đang giám sát",
|
||||
"Maintenance Completed": "Bảo trì đã hoàn thành",
|
||||
"Maintenance in Progress": "Bảo trì đang diễn ra",
|
||||
"Manage Subscription": "Quản lý đăng ký",
|
||||
"Mode": "Chế độ",
|
||||
"No Data": "Không có dữ liệu",
|
||||
"No Incident in %date": "Không có sự cố vào %date",
|
||||
@@ -40,11 +50,15 @@
|
||||
"No Updates Yet": "Chưa có cập nhật",
|
||||
"Ongoing Incidents": "Sự cố đang diễn ra",
|
||||
"Pinging": "Đang kiểm tra",
|
||||
"Please select specific monitors to receive updates from.": "Vui lòng chọn các trình theo dõi cụ thể để nhận cập nhật.",
|
||||
"RESOLVED": "Đã giải quyết",
|
||||
"Recent Incidents": "Sự cố gần đây",
|
||||
"Recent Maintenances": "Bảo trì gần đây",
|
||||
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
|
||||
"Share": "Chia sẻ",
|
||||
"Some Systems are not working as expected": "Một số hệ thống không hoạt động như mong đợi",
|
||||
"Some Systems Degraded": "Một số hệ thống đang suy giảm hiệu suất",
|
||||
"Some Systems Down": "Một số hệ thống đang ngừng hoạt động",
|
||||
"Standard": "Tiêu chuẩn",
|
||||
"Started %startedAt, lasted for %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
|
||||
@@ -53,12 +67,21 @@
|
||||
"Starts %startedAt": "Bắt đầu trong %startedAt",
|
||||
"Status OK": "Trạng thái OK",
|
||||
"Status": "Trạng thái",
|
||||
"Subscribe": "Đăng ký",
|
||||
"Subscribe to Updates": "Đăng ký nhận cập nhật",
|
||||
"Subscribe to all monitors": "Đăng ký tất cả trình theo dõi",
|
||||
"Switch to your timezone": "Chuyển sang múi giờ của bạn",
|
||||
"Theme": "Giao diện",
|
||||
"There are no upcoming maintenances": "Không có bảo trì sắp tới nào",
|
||||
"Timezone set to %tz": "Đã đặt múi giờ %tz",
|
||||
"Today": "Hôm nay",
|
||||
"UP": "Hoạt động",
|
||||
"Update Subscription": "Cập nhật đăng ký",
|
||||
"Upcoming Maintenance": "Bảo trì sắp tới",
|
||||
"Upcoming Maintenances": "Các bảo trì sắp tới",
|
||||
"Updates": "Cập nhật",
|
||||
"Uptime": "Thời gian hoạt động"
|
||||
"Uptime": "Thời gian hoạt động",
|
||||
"View in detail": "Xem chi tiết",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Chúng tôi đã gửi một mã đến email của bạn. Vui lòng nhập mã đó dưới đây để xác nhận đăng nhập",
|
||||
"You are logged in as %email": "Bạn đã đăng nhập với tư cách %email"
|
||||
}
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
"7 Days": "7天",
|
||||
"90 Days": "90天",
|
||||
"Availability per Component": "每个组件的可用性",
|
||||
"All Systems are Degraded": "所有系统性能下降",
|
||||
"All Systems are Down": "所有系统不可用",
|
||||
"All Systems are Operational": "所有系统运行正常",
|
||||
"Back": "返回",
|
||||
"Badge Copied": "徽章已复制",
|
||||
"Badge": "徽章",
|
||||
"Browse Events": "浏览事件",
|
||||
"Change Email": "更改邮箱",
|
||||
"Code Copied": "代码已复制",
|
||||
"Confirm Login": "确认登录",
|
||||
"Copy Code": "复制代码",
|
||||
"Copy Link": "复制链接",
|
||||
"DEGRADED": "性能下降",
|
||||
@@ -19,6 +24,9 @@
|
||||
"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": "已识别",
|
||||
@@ -28,10 +36,12 @@
|
||||
"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 无事件",
|
||||
@@ -40,11 +50,15 @@
|
||||
"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": "部分系统不可用",
|
||||
"Standard": "标准",
|
||||
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
|
||||
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
|
||||
@@ -53,12 +67,21 @@
|
||||
"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": "正常运行时间"
|
||||
"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 登录"
|
||||
}
|
||||
|
||||
+29
-13
@@ -8,6 +8,7 @@ import {
|
||||
CreateIncident,
|
||||
AddIncidentComment,
|
||||
AddIncidentMonitor,
|
||||
GetIncidentByIDDashboard,
|
||||
InsertNewAlert,
|
||||
} from "./controllers/controller.js";
|
||||
|
||||
@@ -15,6 +16,7 @@ import db from "./db/db.js";
|
||||
|
||||
const TRIGGERED = "TRIGGERED";
|
||||
const RESOLVED = "RESOLVED";
|
||||
const CLOSED = "CLOSED";
|
||||
|
||||
async function createJSONCommonAlert(monitor, config, alert, severity) {
|
||||
let siteData = await GetAllSiteData();
|
||||
@@ -58,17 +60,22 @@ async function createNewIncident(monitor, alert, commonData) {
|
||||
};
|
||||
|
||||
let update = commonData.description;
|
||||
update =
|
||||
update +
|
||||
`<div style="line-height: 1.5;">
|
||||
<p><strong>Monitor Name:</strong> ${monitor.name}</p>
|
||||
<p><strong>Incident Status:</strong> ${commonData.status}</p>
|
||||
<p><strong>Severity:</strong> ${commonData.severity}</p>
|
||||
<p><strong>Monitor Status:</strong> ${alert.monitor_status}</p>
|
||||
<p><strong>Monitor Health Checks:</strong> ${alert.health_checks}</p>
|
||||
<p><strong>Monitor Failure Threshold:</strong> ${commonData.details.threshold}</p>
|
||||
<p><strong>Visit:</strong> <a href="${commonData.actions[0].url}">${commonData.actions[0].url}</a></p>
|
||||
</div>`;
|
||||
update = `#### ${update}
|
||||
---
|
||||
**Monitor Name:** ${monitor.name}
|
||||
|
||||
**Incident Status:** ${commonData.status}
|
||||
|
||||
**Severity:** ${commonData.severity}
|
||||
|
||||
**Monitor Status:** ${alert.monitor_status}
|
||||
|
||||
**Monitor Health Checks:** ${alert.health_checks}
|
||||
|
||||
**Monitor Failure Threshold:** ${commonData.details.threshold}
|
||||
|
||||
**Visit:** [${commonData.actions[0].url}](${commonData.actions[0].url})
|
||||
`;
|
||||
|
||||
let newIncident = await CreateIncident(payload);
|
||||
|
||||
@@ -82,13 +89,22 @@ async function createNewIncident(monitor, alert, commonData) {
|
||||
}
|
||||
|
||||
async function closeIncident(alert, comment) {
|
||||
//check if incident is already resolved
|
||||
let incident = await GetIncidentByIDDashboard({
|
||||
incident_id: alert.incident_number,
|
||||
});
|
||||
if (incident.status === CLOSED || incident.state === RESOLVED) {
|
||||
console.log("Incident is already resolved");
|
||||
return;
|
||||
}
|
||||
|
||||
let incident_id = alert.incident_number;
|
||||
return await AddIncidentComment(incident_id, comment, "RESOLVED", moment(alert.updated_at).unix());
|
||||
return await AddIncidentComment(incident_id, comment, RESOLVED, moment(alert.updated_at).unix());
|
||||
}
|
||||
|
||||
function createClosureComment(alert, commonJSON) {
|
||||
let comment = "The incident has been auto resolved";
|
||||
let downtimeDuration = moment(alert.updated_at).diff(moment(alert.created_at), "minutes");
|
||||
let downtimeDuration = Math.round(moment(alert.updated_at).diff(moment(alert.created_at), "seconds") / 60);
|
||||
comment = comment + `, Total downtime: ` + downtimeDuration + ` minutes`;
|
||||
return comment;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ const MANUAL = "manual";
|
||||
const WEBHOOK = "webhook";
|
||||
const DEFAULT_STATUS = "default_status";
|
||||
const SIGNAL = "signal";
|
||||
const INVITE_VERIFY_EMAIL = "invite_verify_email";
|
||||
|
||||
export {
|
||||
MONITOR,
|
||||
@@ -123,4 +124,5 @@ export {
|
||||
WEBHOOK,
|
||||
DEFAULT_STATUS,
|
||||
SIGNAL,
|
||||
INVITE_VERIFY_EMAIL,
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,214 @@
|
||||
// @ts-nocheck
|
||||
import {
|
||||
IsValidAnalytics,
|
||||
IsValidColors,
|
||||
IsValidHero,
|
||||
IsValidI18n,
|
||||
IsValidJSONArray,
|
||||
IsValidJSONString,
|
||||
IsValidNav,
|
||||
IsValidURL,
|
||||
} from "./validators.js";
|
||||
|
||||
export const siteDataKeys = [
|
||||
{
|
||||
key: "title",
|
||||
isValid: (value) => typeof value === "string" && value.trim().length > 0,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "siteName",
|
||||
isValid: (value) => typeof value === "string" && value.trim().length > 0,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "siteURL",
|
||||
isValid: IsValidURL,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "home",
|
||||
isValid: (value) => typeof value === "string" && value.trim().length > 0,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "favicon",
|
||||
isValid: (value) => typeof value === "string" && value.trim().length > 0,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "logo",
|
||||
isValid: (value) => typeof value === "string" && value.trim().length > 0,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "metaTags",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "nav",
|
||||
isValid: IsValidNav,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "hero",
|
||||
isValid: IsValidHero,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "footerHTML",
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "kenerTheme",
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "customCSS",
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "i18n",
|
||||
isValid: IsValidI18n,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "pattern",
|
||||
//string dots or squares or circle
|
||||
isValid: (value) =>
|
||||
typeof value === "string" &&
|
||||
[
|
||||
"dots",
|
||||
"squares",
|
||||
"tiles",
|
||||
"none",
|
||||
"radial-blue",
|
||||
"radial-mono",
|
||||
"radial-midnight",
|
||||
"circle-mono",
|
||||
"carbon-fibre",
|
||||
"texture-sky",
|
||||
"angular-mono",
|
||||
"angular-spring",
|
||||
"angular-bloom",
|
||||
"pets",
|
||||
].includes(value),
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "analytics",
|
||||
isValid: IsValidAnalytics,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "theme",
|
||||
//light dark system none
|
||||
isValid: (value) => typeof value === "string" && ["light", "dark", "system", "none"].includes(value),
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "themeToggle",
|
||||
//boolean
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "tzToggle",
|
||||
//boolean
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "showSiteStatus",
|
||||
//boolean
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "barStyle",
|
||||
//PARTIAL or FULL
|
||||
isValid: (value) => typeof value === "string" && ["PARTIAL", "FULL"].includes(value),
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "barRoundness",
|
||||
//SHARP or ROUNDED
|
||||
isValid: (value) => typeof value === "string" && ["SHARP", "ROUNDED"].includes(value),
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "summaryStyle",
|
||||
//CURRENT or DAY
|
||||
isValid: (value) => typeof value === "string" && ["CURRENT", "DAY"].includes(value),
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "colors",
|
||||
isValid: IsValidColors,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "font",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "monitorSort",
|
||||
isValid: IsValidJSONArray,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "categories",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "homeIncidentCount",
|
||||
isValid: (value) => parseInt(value) >= 0,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "homeIncidentStartTimeWithin",
|
||||
isValid: (value) => parseInt(value) >= 1,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "incidentGroupView",
|
||||
isValid: (value) => typeof value === "string" && value.trim().length > 0,
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "analytics.googleTagManager",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "analytics.plausible",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "analytics.mixpanel",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "analytics.amplitude",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "analytics.clarity",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "analytics.umami",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
];
|
||||
@@ -12,6 +12,7 @@ import path from "path";
|
||||
import db from "./db/db.js";
|
||||
import notification from "./notification/notif.js";
|
||||
import DNSResolver from "./dns.js";
|
||||
import { InsertMonitoringData } from "./controllers/controller.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -66,90 +67,101 @@ async function manualIncident(monitor) {
|
||||
}
|
||||
|
||||
const Minuter = async (monitor) => {
|
||||
let realTimeData = {};
|
||||
let manualData = {};
|
||||
try {
|
||||
let realTimeData = {};
|
||||
let manualData = {};
|
||||
|
||||
const startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
const serviceClient = new Service(monitor);
|
||||
if (monitor.monitor_type === "API") {
|
||||
let apiResponse = await serviceClient.execute();
|
||||
const startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
const serviceClient = new Service(monitor);
|
||||
if (monitor.monitor_type === "API") {
|
||||
let apiResponse = await serviceClient.execute();
|
||||
|
||||
realTimeData[startOfMinute] = apiResponse;
|
||||
realTimeData[startOfMinute] = apiResponse;
|
||||
|
||||
//if timeout, retry after 500ms
|
||||
if (apiResponse.type === TIMEOUT) {
|
||||
apiQueue.push(async (cb) => {
|
||||
await Wait(500); //wait for 500ms
|
||||
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
|
||||
serviceClient.execute().then(async (data) => {
|
||||
await db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: startOfMinute,
|
||||
status: data.status,
|
||||
latency: data.latency,
|
||||
type: data.type,
|
||||
//if timeout, retry after 500ms
|
||||
if (apiResponse.type === TIMEOUT) {
|
||||
apiQueue.push(async (cb) => {
|
||||
await Wait(500); //wait for 500ms
|
||||
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
|
||||
serviceClient.execute().then(async (data) => {
|
||||
await InsertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: startOfMinute,
|
||||
status: data.status,
|
||||
latency: data.latency,
|
||||
type: data.type,
|
||||
});
|
||||
cb();
|
||||
});
|
||||
cb();
|
||||
});
|
||||
}
|
||||
} else if (monitor.monitor_type === "PING") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "TCP") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "DNS") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "GROUP") {
|
||||
// realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
|
||||
realTimeData[startOfMinute] = null;
|
||||
} else if (monitor.monitor_type === "SSL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "SQL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "HEARTBEAT") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
}
|
||||
|
||||
manualData = await manualIncident(monitor);
|
||||
|
||||
//merge noData, apiData, webhookData, dayData
|
||||
let mergedData = {};
|
||||
|
||||
if (monitor.default_status !== undefined && monitor.default_status !== null) {
|
||||
if ([UP, DOWN, DEGRADED].indexOf(monitor.default_status) !== -1) {
|
||||
mergedData[startOfMinute] = {
|
||||
status: monitor.default_status,
|
||||
latency: 0,
|
||||
type: DEFAULT_STATUS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in realTimeData) {
|
||||
if (!!realTimeData[timestamp] && !!realTimeData[timestamp].status) {
|
||||
mergedData[timestamp] = realTimeData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in manualData) {
|
||||
if (!!manualData[timestamp] && !!manualData[timestamp].status) {
|
||||
mergedData[timestamp] = manualData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in mergedData) {
|
||||
const element = mergedData[timestamp];
|
||||
await InsertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: parseInt(timestamp),
|
||||
status: element.status,
|
||||
latency: element.latency,
|
||||
type: element.type,
|
||||
});
|
||||
}
|
||||
} else if (monitor.monitor_type === "PING") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "TCP") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "DNS") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "GROUP") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
|
||||
} else if (monitor.monitor_type === "SSL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "SQL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "HEARTBEAT") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} catch (e) {
|
||||
console.log(`[Error] in Running Monitor name: ${monitor.name}, tag: ${monitor.tag} error:`, e);
|
||||
}
|
||||
|
||||
manualData = await manualIncident(monitor);
|
||||
|
||||
//merge noData, apiData, webhookData, dayData
|
||||
let mergedData = {};
|
||||
|
||||
if (monitor.default_status !== undefined && monitor.default_status !== null) {
|
||||
if ([UP, DOWN, DEGRADED].indexOf(monitor.default_status) !== -1) {
|
||||
mergedData[startOfMinute] = {
|
||||
status: monitor.default_status,
|
||||
latency: 0,
|
||||
type: DEFAULT_STATUS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in realTimeData) {
|
||||
if (!!realTimeData[timestamp] && !!realTimeData[timestamp].status) {
|
||||
mergedData[timestamp] = realTimeData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in manualData) {
|
||||
if (!!manualData[timestamp] && !!manualData[timestamp].status) {
|
||||
mergedData[timestamp] = manualData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in mergedData) {
|
||||
const element = mergedData[timestamp];
|
||||
db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: parseInt(timestamp),
|
||||
status: element.status,
|
||||
latency: element.latency,
|
||||
type: element.type,
|
||||
});
|
||||
}
|
||||
alertingQueue.push(async (cb) => {
|
||||
setTimeout(async () => {
|
||||
await alerting(monitor);
|
||||
cb();
|
||||
try {
|
||||
await alerting(monitor);
|
||||
cb();
|
||||
} catch (e) {
|
||||
console.log(`[Error] in Running Alerting name: ${monitor.name}, tag: ${monitor.tag} error:`, e);
|
||||
cb();
|
||||
}
|
||||
}, 1042);
|
||||
});
|
||||
};
|
||||
|
||||
+534
-4
@@ -30,6 +30,14 @@ class DbImpl {
|
||||
.where("timestamp", "<=", end)
|
||||
.orderBy("timestamp", "asc");
|
||||
}
|
||||
//given monitor_tags array of string, start and end timestamp in utc seconds return data
|
||||
async getMonitoringDataAll(monitor_tags, start, end) {
|
||||
return await this.knex("monitoring_data")
|
||||
.whereIn("monitor_tag", monitor_tags)
|
||||
.where("timestamp", ">=", start)
|
||||
.where("timestamp", "<=", end)
|
||||
.orderBy("timestamp", "asc");
|
||||
}
|
||||
|
||||
//get latest data for a monitor_tag
|
||||
async getLatestMonitoringData(monitor_tag) {
|
||||
@@ -40,6 +48,48 @@ class DbImpl {
|
||||
.first();
|
||||
}
|
||||
|
||||
//get data at time stamp
|
||||
async getMonitoringDataAt(monitor_tag, timestamp) {
|
||||
return await this.knex("monitoring_data")
|
||||
.where("monitor_tag", monitor_tag)
|
||||
.where("timestamp", timestamp)
|
||||
.orderBy("timestamp", "desc")
|
||||
.limit(1)
|
||||
.first();
|
||||
}
|
||||
|
||||
//get latest data for all active monitors
|
||||
async getLatestMonitoringDataAllActive(monitor_tags) {
|
||||
// Find the latest timestamp for each provided monitor tag
|
||||
const latestTimestamps = await this.knex("monitoring_data")
|
||||
.select("monitor_tag")
|
||||
.select(this.knex.raw("MAX(timestamp) as max_timestamp"))
|
||||
.whereIn("monitor_tag", monitor_tags)
|
||||
.groupBy("monitor_tag");
|
||||
|
||||
// Early exit if no results
|
||||
if (!latestTimestamps || latestTimestamps.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Then fetch the complete records using the timestamp pairs
|
||||
const conditions = latestTimestamps.map((item) => {
|
||||
return function () {
|
||||
this.where(function () {
|
||||
this.where("monitor_tag", item.monitor_tag).andWhere("timestamp", item.max_timestamp);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Build query with OR conditions
|
||||
let query = this.knex("monitoring_data").where(conditions[0]);
|
||||
for (let i = 1; i < conditions.length; i++) {
|
||||
query = query.orWhere(conditions[i]);
|
||||
}
|
||||
|
||||
return await query;
|
||||
}
|
||||
|
||||
async getLastHeartbeat(monitor_tag) {
|
||||
return await this.knex("monitoring_data")
|
||||
.where("monitor_tag", monitor_tag)
|
||||
@@ -75,6 +125,15 @@ class DbImpl {
|
||||
.limit(1)
|
||||
.first();
|
||||
}
|
||||
//get the last status before the timestamp given monitor_tag and start timestamp
|
||||
async getLastStatusBeforeAll(monitor_tags, timestamp) {
|
||||
return await this.knex("monitoring_data")
|
||||
.whereIn("monitor_tag", monitor_tags)
|
||||
.where("timestamp", "<", timestamp)
|
||||
.orderBy("timestamp", "desc")
|
||||
.limit(1)
|
||||
.first();
|
||||
}
|
||||
|
||||
async getDataGroupByDayAlternative(monitor_tag, start, end) {
|
||||
// Fetch all raw data
|
||||
@@ -89,9 +148,9 @@ class DbImpl {
|
||||
return data;
|
||||
}
|
||||
|
||||
async getLastStatusBeforeCombined(monitor_tags_arr, timestamp) {
|
||||
async getLastStatusBeforeCombined(monitor_tags_arr, timestamp, minTimestamp) {
|
||||
//monitor_tag_arr is an array of string
|
||||
return await this.knex("monitoring_data")
|
||||
let query = this.knex("monitoring_data")
|
||||
.select(
|
||||
"timestamp",
|
||||
this.knex.raw("COUNT(*) as total_entries"),
|
||||
@@ -104,8 +163,15 @@ class DbImpl {
|
||||
END as status
|
||||
`),
|
||||
)
|
||||
.whereIn("monitor_tag", monitor_tags_arr)
|
||||
.where("timestamp", "=", timestamp)
|
||||
.whereIn("monitor_tag", monitor_tags_arr);
|
||||
|
||||
if (!!minTimestamp) {
|
||||
query = query.whereBetween("timestamp", [minTimestamp, timestamp]);
|
||||
} else {
|
||||
query = query.where("timestamp", "=", timestamp);
|
||||
}
|
||||
|
||||
return await query
|
||||
.groupBy("timestamp")
|
||||
.havingRaw("COUNT(*) = ?", [monitor_tags_arr.length])
|
||||
.orderBy("timestamp", "desc")
|
||||
@@ -222,6 +288,61 @@ class DbImpl {
|
||||
return await this.knex("monitor_alerts").count("* as count").first();
|
||||
}
|
||||
|
||||
//getMonitorsByTags array of string
|
||||
async getMonitorsByTags(tags) {
|
||||
return await this.knex("monitors").whereIn("tag", tags);
|
||||
}
|
||||
|
||||
async getMonitorsByTag(tag) {
|
||||
return await this.knex("monitors").where("tag", tag).first();
|
||||
}
|
||||
|
||||
//get incidents paginated and direction, given a start timestamp, also might have filter on incident_type, incident_source
|
||||
async getIncidentsPaginated(page, limit, filter, direction = "after") {
|
||||
let query = this.knex("incidents").select("*").whereRaw("1=1");
|
||||
if (filter && filter.status) {
|
||||
query = query.andWhere("status", filter.status);
|
||||
}
|
||||
if (filter && filter.start && direction === "after") {
|
||||
query = query.andWhere("start_date_time", ">=", filter.start);
|
||||
}
|
||||
if (filter && filter.start && direction === "before") {
|
||||
query = query.andWhere("start_date_time", "<=", filter.start);
|
||||
}
|
||||
if (filter && filter.end && direction === "after") {
|
||||
query = query.andWhere("start_date_time", ">=", filter.end);
|
||||
}
|
||||
if (filter && filter.end && direction === "before") {
|
||||
query = query.andWhere("start_date_time", "<=", filter.end);
|
||||
}
|
||||
|
||||
if (filter && filter.state) {
|
||||
query = query.andWhere("state", filter.state);
|
||||
}
|
||||
if (filter && filter.id) {
|
||||
query = query.andWhere("id", filter.id);
|
||||
}
|
||||
if (filter && filter.incident_type) {
|
||||
query = query.andWhere("incident_type", filter.incident_type);
|
||||
}
|
||||
if (filter && filter.incident_source) {
|
||||
query = query.andWhere("incident_source", filter.incident_source);
|
||||
}
|
||||
if (direction === "after") {
|
||||
query = query
|
||||
.orderBy("start_date_time", "asc")
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit);
|
||||
} else {
|
||||
query = query
|
||||
.orderBy("start_date_time", "desc")
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit);
|
||||
}
|
||||
|
||||
return await query;
|
||||
}
|
||||
|
||||
//update alert to inactive given monitor_tag, monitor_status, given id
|
||||
async updateAlertStatus(id, alert_status) {
|
||||
return await this.knex("monitor_alerts").where({ id }).update({
|
||||
@@ -268,6 +389,11 @@ class DbImpl {
|
||||
return await this.knex("site_data").where("key", key).first();
|
||||
}
|
||||
|
||||
//get site data that starts with analytics.
|
||||
async getAllSiteDataAnalytics() {
|
||||
return await this.knex("site_data").where("key", "like", "analytics.%");
|
||||
}
|
||||
|
||||
//insert into monitors
|
||||
async insertMonitor(data) {
|
||||
return await this.knex("monitors").insert({
|
||||
@@ -309,6 +435,39 @@ class DbImpl {
|
||||
});
|
||||
}
|
||||
|
||||
//given monitor_tag, start and end timestamp and a status, update all monitoring data with this status
|
||||
async updateMonitoringData(monitor_tag, start, end, newStatus, type) {
|
||||
const count = Math.floor((end - start) / 60) + 1;
|
||||
const timestamps = Array.from({ length: count }, (_, i) => start + i * 60);
|
||||
|
||||
const records = timestamps.map((ts) => ({
|
||||
monitor_tag,
|
||||
timestamp: ts,
|
||||
status: newStatus,
|
||||
type,
|
||||
latency: 0,
|
||||
}));
|
||||
|
||||
// Use transaction for better performance with batches
|
||||
const batchSize = 500;
|
||||
|
||||
return await this.knex.transaction(async (trx) => {
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < records.length; i += batchSize) {
|
||||
const batch = records.slice(i, i + batchSize);
|
||||
const result = await trx("monitoring_data")
|
||||
.insert(batch)
|
||||
.onConflict(["monitor_tag", "timestamp"])
|
||||
.merge({ status: newStatus });
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
async updateMonitorTrigger(data) {
|
||||
return await this.knex("monitors").where({ id: data.id }).update({
|
||||
down_trigger: data.down_trigger,
|
||||
@@ -329,6 +488,10 @@ class DbImpl {
|
||||
if (!!data.id) {
|
||||
query = query.andWhere("id", data.id);
|
||||
}
|
||||
//monitor_type
|
||||
if (!!data.monitor_type) {
|
||||
query = query.andWhere("monitor_type", data.monitor_type);
|
||||
}
|
||||
if (!!data.tag) {
|
||||
query = query.andWhere("tag", data.tag);
|
||||
}
|
||||
@@ -403,6 +566,14 @@ class DbImpl {
|
||||
return await this.knex("users").select("password_hash").where("id", id).first();
|
||||
}
|
||||
|
||||
//get user name, email from users given id
|
||||
async getUserById(id) {
|
||||
return await this.knex("users")
|
||||
.select("id", "email", "name", "is_active", "is_verified", "role", "created_at", "updated_at")
|
||||
.where("id", id)
|
||||
.first();
|
||||
}
|
||||
|
||||
//insert user
|
||||
async insertUser(data) {
|
||||
return await this.knex("users").insert({
|
||||
@@ -423,6 +594,27 @@ class DbImpl {
|
||||
});
|
||||
}
|
||||
|
||||
//get all columns of users except password_hash order by create at
|
||||
async getAllUsers() {
|
||||
return await this.knex("users")
|
||||
.select("id", "email", "name", "role", "is_active", "is_verified", "created_at", "updated_at")
|
||||
.orderBy("created_at", "desc");
|
||||
}
|
||||
|
||||
//get all users paginated
|
||||
async getUsersPaginated(page, limit) {
|
||||
return await this.knex("users")
|
||||
.select("id", "email", "name", "role", "is_active", "is_verified", "created_at", "updated_at")
|
||||
.orderBy("created_at", "desc")
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit);
|
||||
}
|
||||
|
||||
//getTotalUsers
|
||||
async getTotalUsers() {
|
||||
return await this.knex("users").count("* as count").first();
|
||||
}
|
||||
|
||||
//new api key
|
||||
async createNewApiKey(data) {
|
||||
return await this.knex("api_keys").insert({
|
||||
@@ -513,6 +705,10 @@ class DbImpl {
|
||||
query = query.andWhere("state", filter.state);
|
||||
}
|
||||
|
||||
if (filter && filter.id) {
|
||||
query = query.andWhere("id", filter.id);
|
||||
}
|
||||
|
||||
query = query
|
||||
.orderBy("id", "desc")
|
||||
.limit(limit)
|
||||
@@ -742,6 +938,340 @@ class DbImpl {
|
||||
async getIncidentCommentByID(id) {
|
||||
return await this.knex("incident_comments").where({ id }).first();
|
||||
}
|
||||
|
||||
//update users.name by id
|
||||
async updateUserName(id, name) {
|
||||
return await this.knex("users").where({ id }).update({
|
||||
name,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
//updateUserRole
|
||||
async updateUserRole(id, role) {
|
||||
return await this.knex("users").where({ id }).update({
|
||||
role,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//update is_active
|
||||
async updateUserIsActive(id, is_active) {
|
||||
return await this.knex("users").where({ id }).update({
|
||||
is_active,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//update password
|
||||
async updateUserPassword(data) {
|
||||
return await this.knex("users").where({ id: data.id }).update({
|
||||
password_hash: data.password_hash,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
async updateIsVerified(id, is_verified) {
|
||||
return await this.knex("users").where({ id }).update({
|
||||
is_verified: is_verified,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//insert into invitations
|
||||
async insertInvitation(data) {
|
||||
return await this.knex("invitations").insert({
|
||||
invitation_token: data.invitation_token,
|
||||
invitation_type: data.invitation_type,
|
||||
invited_user_id: data.invited_user_id,
|
||||
invited_by_user_id: data.invited_by_user_id,
|
||||
invitation_meta: data.invitation_meta,
|
||||
invitation_expiry: data.invitation_expiry,
|
||||
invitation_status: data.invitation_status,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//update invitation_status of all invitations for invited_user_id, given invitation_type to VOID
|
||||
async updateInvitationStatusToVoid(invited_user_id, invitation_type) {
|
||||
return await this.knex("invitations").where({ invited_user_id, invitation_type }).update({
|
||||
invitation_status: "VOID",
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//check if there is a row for given invited_user_id,invitation_type and invitation_status = PENDING
|
||||
async invitationExists(invited_user_id, invitation_type) {
|
||||
const result = await this.knex("invitations")
|
||||
.count("* as count")
|
||||
.where({
|
||||
invited_user_id,
|
||||
invitation_type,
|
||||
invitation_status: "PENDING",
|
||||
})
|
||||
.first();
|
||||
return result.count > 0;
|
||||
}
|
||||
|
||||
//update invitation_status of invitation given invitation_token to ACCEPTED
|
||||
async updateInvitationStatusToAccepted(invitation_token) {
|
||||
return await this.knex("invitations").where({ invitation_token }).update({
|
||||
invitation_status: "ACCEPTED",
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//get invitations by token, the invitation should be PENDING, and it should not be expired
|
||||
async getActiveInvitationByToken(invitation_token) {
|
||||
return await this.knex("invitations")
|
||||
.where("invitation_token", invitation_token)
|
||||
.andWhere("invitation_status", "PENDING")
|
||||
.andWhere("invitation_expiry", ">", this.knex.fn.now())
|
||||
.first();
|
||||
}
|
||||
|
||||
//delete from monitor_data using tag
|
||||
async deleteMonitorDataByTag(tag) {
|
||||
return await this.knex("monitoring_data").where("monitor_tag", tag).del();
|
||||
}
|
||||
//delete from incident monitors using tag
|
||||
async deleteIncidentMonitorsByTag(tag) {
|
||||
return await this.knex("incident_monitors").where("monitor_tag", tag).del();
|
||||
}
|
||||
//delete from monitor alerts using tag
|
||||
async deleteMonitorAlertsByTag(tag) {
|
||||
return await this.knex("monitor_alerts").where("monitor_tag", tag).del();
|
||||
}
|
||||
//delete from monitors using tag
|
||||
async deleteMonitorsByTag(tag) {
|
||||
return await this.knex("monitors").where("tag", tag).del();
|
||||
}
|
||||
|
||||
//insert into subscribers
|
||||
async insertSubscriber(data) {
|
||||
return await this.knex("subscribers").insert({
|
||||
subscriber_send: data.subscriber_send,
|
||||
subscriber_meta: data.subscriber_meta,
|
||||
subscriber_type: data.subscriber_type,
|
||||
subscriber_status: data.subscriber_status,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//update subscriber_meta given id
|
||||
async updateSubscriberMeta(id, subscriber_meta) {
|
||||
return await this.knex("subscribers").where({ id }).update({
|
||||
subscriber_meta,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//update subscriber_status given id
|
||||
async updateSubscriberStatus(id, subscriber_status) {
|
||||
return await this.knex("subscribers").where({ id }).update({
|
||||
subscriber_status,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//delete subscriber by id
|
||||
async deleteSubscriberById(id) {
|
||||
return await this.knex("subscribers").where({ id }).del();
|
||||
}
|
||||
|
||||
//insert into subscriptions table
|
||||
async insertSubscription(data) {
|
||||
return await this.knex("subscriptions").insert({
|
||||
subscriber_id: data.subscriber_id,
|
||||
subscriptions_status: data.subscriptions_status,
|
||||
subscriptions_monitors: data.subscriptions_monitors,
|
||||
subscriptions_meta: data.subscriptions_meta,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//given subscriber_id remove all data from subscriptions
|
||||
async removeAllDataFromSubscriptions(subscriber_id) {
|
||||
return await this.knex("subscriptions").where({ subscriber_id }).del();
|
||||
}
|
||||
|
||||
//get subscriptions by subscriber_id
|
||||
async getSubscriptionsBySubscriberId(subscriber_id) {
|
||||
return await this.knex("subscriptions").where("subscriber_id", subscriber_id).orderBy("id", "desc");
|
||||
}
|
||||
|
||||
//update subscription status
|
||||
async updateSubscriptionStatus(id, subscriptions_status) {
|
||||
return await this.knex("subscriptions").where({ id }).update({
|
||||
subscriptions_status,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//get all subscribers with active status
|
||||
async getAllActiveSubscribers() {
|
||||
return await this.knex("subscribers").where("subscriber_status", "ACTIVE");
|
||||
}
|
||||
|
||||
//get subscriptions for a monitor tag
|
||||
async getSubscriptionsForMonitor(monitor_tag) {
|
||||
return await this.knex("subscriptions as s")
|
||||
.join("subscribers as sub", "s.subscriber_id", "sub.id")
|
||||
.where("s.subscriptions_status", "ACTIVE")
|
||||
.where("sub.subscriber_status", "ACTIVE")
|
||||
.whereRaw("s.subscriptions_monitors = ? OR s.subscriptions_monitors = 'ALL'", [monitor_tag])
|
||||
.select("sub.subscriber_send", "sub.subscriber_type", "sub.subscriber_meta", "s.subscriptions_meta");
|
||||
}
|
||||
|
||||
//get subscriber by subscriber_send and subscriber_type
|
||||
async getSubscriberByDetails(subscriber_send, subscriber_type) {
|
||||
return await this.knex("subscribers")
|
||||
.where({
|
||||
subscriber_send,
|
||||
subscriber_type,
|
||||
})
|
||||
.first();
|
||||
}
|
||||
|
||||
//get all subscribers by type
|
||||
async getSubscribersByType(subscriber_type) {
|
||||
return await this.knex("subscribers").where("subscriber_type", subscriber_type).orderBy("id", "desc");
|
||||
}
|
||||
|
||||
//get subscriptions paginated
|
||||
async getSubscriptionsPaginated(page, limit) {
|
||||
return await this.knex("subscriptions")
|
||||
.select(
|
||||
"id",
|
||||
"subscriber_id",
|
||||
"subscriptions_status",
|
||||
"subscriptions_monitors",
|
||||
"subscriptions_meta",
|
||||
"created_at",
|
||||
)
|
||||
.orderBy("id", "desc")
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit);
|
||||
}
|
||||
|
||||
//get total subscription count
|
||||
|
||||
async getTotalSubscriptionCount() {
|
||||
const result = await this.knex("subscriptions").count("* as count").first();
|
||||
return result.count || 0;
|
||||
}
|
||||
|
||||
//get all subscriber emails from subscriptions table that are of given tag or = _
|
||||
async getSubscriberEmails(monitor_tags) {
|
||||
return await this.knex("subscriptions")
|
||||
.join("subscribers", "subscribers.id", "subscriptions.subscriber_id")
|
||||
.distinct("subscribers.subscriber_send as subscriber_send")
|
||||
.where("subscriptions.subscriptions_status", "ACTIVE")
|
||||
.whereIn("subscriptions.subscriptions_monitors", monitor_tags)
|
||||
.orderBy("subscriber_send");
|
||||
}
|
||||
|
||||
//getSubscriberById
|
||||
async getSubscriberById(id) {
|
||||
return await this.knex("subscribers")
|
||||
.select("id", "subscriber_send", "subscriber_meta", "subscriber_type", "subscriber_status", "created_at")
|
||||
.where("id", id)
|
||||
.first();
|
||||
}
|
||||
|
||||
// Get subscribers paginated
|
||||
async getSubscribersPaginated(page, limit) {
|
||||
// Use a subquery for pagination to apply limit/offset before joining
|
||||
const subquery = this.knex("subscribers")
|
||||
.select("id")
|
||||
.orderBy("created_at", "desc")
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit)
|
||||
.as("paginated_subscribers");
|
||||
|
||||
const dbType = GetDbType(); // Get the database type
|
||||
const aggregationFunction = dbType === "postgresql" ? "STRING_AGG" : "GROUP_CONCAT";
|
||||
|
||||
const results = await this.knex("subscribers as s")
|
||||
.select(
|
||||
"s.id",
|
||||
"s.subscriber_send",
|
||||
"s.subscriber_status",
|
||||
"s.created_at",
|
||||
// Aggregate monitor tags based on DB type
|
||||
this.knex.raw(`${aggregationFunction}(sub.subscriptions_monitors, ',') as monitors_agg`),
|
||||
)
|
||||
.innerJoin(subquery, "s.id", "paginated_subscribers.id") // Join with paginated IDs
|
||||
.leftJoin("subscriptions as sub", "s.id", "sub.subscriber_id") // Left join to include subscribers with no subscriptions
|
||||
.groupBy("s.id", "s.subscriber_send", "s.subscriber_status", "s.created_at") // Group by subscriber details
|
||||
.orderBy("s.created_at", "desc"); // Ensure final order
|
||||
|
||||
// Process results to split the aggregated string into an array
|
||||
return results.map((row) => ({
|
||||
...row,
|
||||
subscriptions_monitors: row.monitors_agg ? row.monitors_agg.split(",") : [], // Split string into array, handle null
|
||||
monitors_agg: undefined, // Remove the temporary aggregation field
|
||||
}));
|
||||
}
|
||||
|
||||
// Get total count of subscribers
|
||||
async getSubscribersCount() {
|
||||
const result = await this.knex("subscribers").count("* as count").first();
|
||||
return result.count || 0; // Return count or 0 if no subscribers
|
||||
}
|
||||
|
||||
// CRUD operations for subscription_triggers
|
||||
async insertSubscriptionTrigger(data) {
|
||||
return await this.knex("subscription_triggers").insert({
|
||||
subscription_trigger_type: data.subscription_trigger_type,
|
||||
subscription_trigger_status: data.subscription_trigger_status,
|
||||
config: data.config,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
async getSubscriptionTriggerById(id) {
|
||||
return await this.knex("subscription_triggers").where({ id }).first();
|
||||
}
|
||||
|
||||
async getAllSubscriptionTriggers() {
|
||||
return await this.knex("subscription_triggers").orderBy("id", "desc");
|
||||
}
|
||||
|
||||
// get a single subscription_trigger by type
|
||||
async getSubscriptionTriggerByType(subscription_trigger_type) {
|
||||
return await this.knex("subscription_triggers")
|
||||
.where("subscription_trigger_type", subscription_trigger_type)
|
||||
.first();
|
||||
}
|
||||
|
||||
async updateSubscriptionTrigger(data) {
|
||||
return await this.knex("subscription_triggers").where({ id: data.id }).update({
|
||||
subscription_trigger_type: data.subscription_trigger_type,
|
||||
subscription_trigger_status: data.subscription_trigger_status,
|
||||
config: data.config,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
async updateSubscriptionTriggerStatus(id, subscription_trigger_status) {
|
||||
return await this.knex("subscription_triggers").where({ id }).update({
|
||||
subscription_trigger_status,
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
//deleteSubscriptionTriggerByType
|
||||
async deleteSubscriptionTriggerByType(subscription_trigger_type) {
|
||||
return await this.knex("subscription_triggers").where({ subscription_trigger_type }).del();
|
||||
}
|
||||
|
||||
async deleteSubscriptionTriggerById(id) {
|
||||
return await this.knex("subscription_triggers").where({ id }).del();
|
||||
}
|
||||
}
|
||||
|
||||
export default DbImpl;
|
||||
|
||||
@@ -43,8 +43,20 @@ const seedSiteData = {
|
||||
title: "Build stunning status pages in minutes",
|
||||
subtitle: "Let your users know what's going on.",
|
||||
},
|
||||
footerHTML:
|
||||
'<p class="text-center">\nMade using \n<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">\n Kener\n</a>\nan open source status page system built with Svelte and TailwindCSS.<br/>\nCreated with ❤️ by <a href="https://rajnandan.com" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.\n</p>\n',
|
||||
footerHTML: `<div
|
||||
class="container relative mt-4 max-w-[655px]"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
|
||||
<p class="text-center text-sm leading-loose text-muted-foreground ">
|
||||
Made using
|
||||
<a href="https://github.com/rajnandan1/kener" target="_blank" class="font-medium underline underline-offset-4">
|
||||
Kener
|
||||
</a>
|
||||
an open source status page system built with Svelte and TailwindCSS.<br/>
|
||||
Created with ❤️ by <a href="https://rajnandan.com" target="_blank" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>`,
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: [{ code: "en", name: "English", selected: true, disabled: false }],
|
||||
@@ -88,6 +100,8 @@ const seedSiteData = {
|
||||
categories: [{ name: "Home", description: "Monitors for Home Page" }],
|
||||
homeIncidentCount: 5,
|
||||
homeIncidentStartTimeWithin: 30,
|
||||
kenerTheme: "default",
|
||||
showSiteStatus: "NO",
|
||||
};
|
||||
|
||||
export default seedSiteData;
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import Mustache from "mustache";
|
||||
import { DiscordJSONTemplate } from "../../anywhere.js";
|
||||
import variables from "./variables.js";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Discord {
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
trigger_meta;
|
||||
|
||||
constructor(url, siteData, monitorData) {
|
||||
constructor(url, siteData, monitorData, trigger_meta) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
|
||||
this.url = url;
|
||||
@@ -17,77 +24,43 @@ class Discord {
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
this.trigger_meta = trigger_meta;
|
||||
|
||||
if (!!this.trigger_meta.has_discord_body && !!this.trigger_meta.discord_body) {
|
||||
let envSecrets = GetRequiredSecrets(this.trigger_meta.discord_body);
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.trigger_meta.discord_body = ReplaceAllOccurrences(
|
||||
this.trigger_meta.discord_body,
|
||||
secret.find,
|
||||
secret.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
let siteURL = this.siteData.siteURL;
|
||||
let view = variables(this.siteData, data);
|
||||
|
||||
//remove / if there at the end of the url
|
||||
if (siteURL.endsWith("/")) {
|
||||
siteURL = siteURL.slice(0, -1);
|
||||
let discordTemplate = DiscordJSONTemplate;
|
||||
if (!!this.trigger_meta.has_discord_body && !!this.trigger_meta.discord_body) {
|
||||
discordTemplate = this.trigger_meta.discord_body;
|
||||
}
|
||||
|
||||
if (!this.siteData.logo.startsWith("http"))
|
||||
this.siteData.logo = `${siteURL}${!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : ""}${this.siteData.logo}`;
|
||||
|
||||
let logo = this.siteData.logo;
|
||||
|
||||
let color = 13250616; //down;
|
||||
if (data.severity === "warning") {
|
||||
color = 15125089;
|
||||
}
|
||||
if (data.status === "RESOLVED") {
|
||||
color = 5156244;
|
||||
}
|
||||
return {
|
||||
username: this.siteData.siteName,
|
||||
avatar_url: logo,
|
||||
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
|
||||
embeds: [
|
||||
{
|
||||
title: data.alert_name,
|
||||
description: data.description,
|
||||
url: data.actions[0].url,
|
||||
color: color,
|
||||
fields: [
|
||||
{
|
||||
name: "Monitor",
|
||||
value: data.details.metric,
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Alert ID",
|
||||
value: data.id,
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Current Value",
|
||||
value: data.details.current_value,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Threshold",
|
||||
value: data.details.threshold,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: "Kener",
|
||||
icon_url: logo,
|
||||
},
|
||||
timestamp: data.timestamp,
|
||||
},
|
||||
],
|
||||
};
|
||||
return Mustache.render(discordTemplate, view);
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const toSend = this.transformData(data);
|
||||
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(this.transformData(data)),
|
||||
body: toSend,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from discord`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
|
||||
@@ -3,56 +3,57 @@ import { Resend } from "resend";
|
||||
import nodemailer from "nodemailer";
|
||||
import getSMTPTransport from "./smtps.js";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Email {
|
||||
to;
|
||||
from;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
meta;
|
||||
to;
|
||||
from;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
meta;
|
||||
|
||||
constructor(meta, siteData, monitorData) {
|
||||
this.to = meta.to;
|
||||
this.from = meta.from;
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
constructor(meta, siteData, monitorData) {
|
||||
this.to = meta.to;
|
||||
this.from = meta.from;
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
|
||||
let metaString = JSON.stringify(meta);
|
||||
let metaString = JSON.stringify(meta);
|
||||
|
||||
let envSecrets = GetRequiredSecrets(`${JSON.stringify(meta)}`);
|
||||
let envSecrets = GetRequiredSecrets(`${JSON.stringify(meta)}`);
|
||||
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
metaString = ReplaceAllOccurrences(metaString, secret.find, secret.replace);
|
||||
}
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
metaString = ReplaceAllOccurrences(metaString, secret.find, secret.replace);
|
||||
}
|
||||
|
||||
this.meta = JSON.parse(metaString);
|
||||
}
|
||||
this.meta = JSON.parse(metaString);
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
let ctaLink = data.actions[0].url;
|
||||
let ctaText = data.actions[0].text;
|
||||
let emailApiRequest = {
|
||||
from: this.from,
|
||||
to: this.to.split(",").map((email) => email.trim()),
|
||||
subject: `[${data.status}] ${data.alert_name} at ${data.timestamp}`,
|
||||
text: `Alert ${data.alert_name} has been ${data.status} at ${data.timestamp}. Click here to view the alert: ${ctaLink}`,
|
||||
html: ""
|
||||
};
|
||||
transformData(data) {
|
||||
let ctaLink = data.actions[0].url;
|
||||
let ctaText = data.actions[0].text;
|
||||
let emailApiRequest = {
|
||||
from: this.from,
|
||||
to: this.to.split(",").map((email) => email.trim()),
|
||||
subject: `[${data.status}] ${data.alert_name} at ${data.timestamp}`,
|
||||
text: `Alert ${data.alert_name} has been ${data.status} at ${data.timestamp}. Click here to view the alert: ${ctaLink}`,
|
||||
html: "",
|
||||
};
|
||||
|
||||
let bgColor = "#f4f4f4";
|
||||
if (data.severity == "critical") {
|
||||
bgColor = this.siteData.colors.DOWN;
|
||||
} else if (data.severity == "warning") {
|
||||
bgColor = this.siteData.colors.DEGRADED;
|
||||
}
|
||||
let bgColor = "#f4f4f4";
|
||||
if (data.severity == "critical") {
|
||||
bgColor = this.siteData.colors.DOWN;
|
||||
} else if (data.severity == "warning") {
|
||||
bgColor = this.siteData.colors.DEGRADED;
|
||||
}
|
||||
|
||||
if (data.status === "RESOLVED") {
|
||||
bgColor = this.siteData.colors.UP;
|
||||
}
|
||||
if (data.status === "RESOLVED") {
|
||||
bgColor = this.siteData.colors.UP;
|
||||
}
|
||||
|
||||
let html = `
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -177,53 +178,53 @@ class Email {
|
||||
</table>
|
||||
|
||||
<div class="footer">
|
||||
This is an automated alert notification from ${this.siteData.siteName} monitoring system.
|
||||
This is an automated alert notification from ${this.siteData.siteName} monitoring system. It is being powered by <a href='https://kener.ing'>Kener</a> v${version()}.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
emailApiRequest.html = html;
|
||||
emailApiRequest.html = html;
|
||||
|
||||
return emailApiRequest;
|
||||
}
|
||||
return emailApiRequest;
|
||||
}
|
||||
|
||||
type() {
|
||||
return "email";
|
||||
}
|
||||
type() {
|
||||
return "email";
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
let emailBody = this.transformData(data); // object containing email data (to, subject, text, html, etc)
|
||||
if (!this.meta.email_type || this.meta.email_type === "resend") {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
try {
|
||||
return await resend.emails.send(emailBody);
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
if (this.meta.email_type === "smtp") {
|
||||
// Configure the SMTP transporter using environment variables
|
||||
const transporter = getSMTPTransport(this.meta);
|
||||
async send(data) {
|
||||
let emailBody = this.transformData(data); // object containing email data (to, subject, text, html, etc)
|
||||
if (!this.meta.email_type || this.meta.email_type === "resend") {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
try {
|
||||
return await resend.emails.send(emailBody);
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
if (this.meta.email_type === "smtp") {
|
||||
// Configure the SMTP transporter using environment variables
|
||||
const transporter = getSMTPTransport(this.meta);
|
||||
|
||||
const mailOptions = {
|
||||
from: emailBody.from, // sender address
|
||||
to: Array.isArray(emailBody.to) ? emailBody.to.join(",") : emailBody.to, // recipient address(es)
|
||||
subject: emailBody.subject, // email subject
|
||||
text: emailBody.text, // plain text body
|
||||
html: emailBody.html // HTML body (if any)
|
||||
};
|
||||
const mailOptions = {
|
||||
from: emailBody.from, // sender address
|
||||
to: Array.isArray(emailBody.to) ? emailBody.to.join(",") : emailBody.to, // recipient address(es)
|
||||
subject: emailBody.subject, // email subject
|
||||
text: emailBody.text, // plain text body
|
||||
html: emailBody.html, // HTML body (if any)
|
||||
};
|
||||
|
||||
try {
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Email;
|
||||
|
||||
@@ -5,26 +5,26 @@ import Slack from "./slack.js";
|
||||
import Email from "./email.js";
|
||||
|
||||
class Notification {
|
||||
client;
|
||||
client;
|
||||
|
||||
constructor(trigger, siteData, monitorData) {
|
||||
let trigger_meta = JSON.parse(trigger.trigger_meta);
|
||||
if (trigger.trigger_type === "webhook") {
|
||||
this.client = new Webhook(trigger_meta, "POST", siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "discord") {
|
||||
this.client = new Discord(trigger_meta.url, siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "slack") {
|
||||
this.client = new Slack(trigger_meta.url, siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "email") {
|
||||
this.client = new Email(trigger_meta, siteData, monitorData);
|
||||
} else {
|
||||
console.log("Invalid Notification");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
constructor(trigger, siteData, monitorData) {
|
||||
let trigger_meta = JSON.parse(trigger.trigger_meta);
|
||||
if (trigger.trigger_type === "webhook") {
|
||||
this.client = new Webhook(trigger_meta, "POST", siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "discord") {
|
||||
this.client = new Discord(trigger_meta.url, siteData, monitorData, trigger_meta);
|
||||
} else if (trigger.trigger_type === "slack") {
|
||||
this.client = new Slack(trigger_meta.url, siteData, monitorData, trigger_meta);
|
||||
} else if (trigger.trigger_type === "email") {
|
||||
this.client = new Email(trigger_meta, siteData, monitorData);
|
||||
} else {
|
||||
console.log("Invalid Notification");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
return await this.client.send(data);
|
||||
}
|
||||
async send(data) {
|
||||
return await this.client.send(data);
|
||||
}
|
||||
}
|
||||
export default Notification;
|
||||
|
||||
@@ -1,100 +1,66 @@
|
||||
// @ts-nocheck
|
||||
import variables from "./variables.js";
|
||||
import { SlackJSONTemplate } from "../../anywhere.js";
|
||||
import Mustache from "mustache";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Slack {
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
trigger_meta;
|
||||
|
||||
constructor(url, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener"
|
||||
};
|
||||
constructor(url, siteData, monitorData, trigger_meta) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
|
||||
this.url = url;
|
||||
this.headers = Object.assign(kenerHeader, {});
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
}
|
||||
this.url = url;
|
||||
this.headers = Object.assign(kenerHeader, {});
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
this.trigger_meta = trigger_meta;
|
||||
|
||||
transformData(alert) {
|
||||
return {
|
||||
blocks: [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: alert.alert_name,
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: alert.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved",
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${alert.description}\n*Source:* ${alert.source}\n*Severity:* ${alert.severity}\n*Status:* ${alert.status}`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
fields: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Metric:*\n${alert.details.metric}`
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Current Value:*\n${alert.details.current_value}`
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Threshold:*\n${alert.details.threshold}`
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Timestamp:*\n<!date^${Math.floor(new Date(alert.timestamp).getTime() / 1000)}^{date} at {time}|${alert.timestamp}>`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: alert.actions.map((action) => ({
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: action.text
|
||||
},
|
||||
url: action.url,
|
||||
style: "primary"
|
||||
}))
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
if (!!this.trigger_meta.has_slack_body && !!this.trigger_meta.slack_body) {
|
||||
let envSecrets = GetRequiredSecrets(this.trigger_meta.slack_body);
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.trigger_meta.slack_body = ReplaceAllOccurrences(this.trigger_meta.slack_body, secret.find, secret.replace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(this.transformData(data))
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
transformData(alert) {
|
||||
let view = variables(this.siteData, alert);
|
||||
|
||||
let slackTemplate = SlackJSONTemplate;
|
||||
if (!!this.trigger_meta.has_slack_body && !!this.trigger_meta.slack_body) {
|
||||
slackTemplate = this.trigger_meta.slack_body;
|
||||
}
|
||||
return Mustache.render(slackTemplate, view);
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: this.transformData(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from slack`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Slack;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import nodemailer from "nodemailer";
|
||||
import { HashString } from "../controllers/controller.js";
|
||||
import { HashString } from "../tool.js";
|
||||
const transports = {};
|
||||
export default function getSMTPTransport(meta) {
|
||||
//convert meta to string and generate has id
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// @ts-nocheck
|
||||
export default function variables(siteData, data) {
|
||||
let siteURL = siteData.siteURL;
|
||||
let siteName = siteData.siteName;
|
||||
if (siteURL.endsWith("/")) {
|
||||
siteURL = siteURL.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!siteData.logo.startsWith("http")) {
|
||||
siteData.logo = `${siteURL}${!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : ""}${siteData.logo}`;
|
||||
}
|
||||
let view = {
|
||||
site_name: siteName,
|
||||
logo_url: siteData.logo,
|
||||
site_url: siteURL,
|
||||
alert_name: data.alert_name,
|
||||
status: data.status,
|
||||
is_resolved: data.status === "RESOLVED",
|
||||
is_triggered: data.status === "TRIGGERED",
|
||||
description: data.description,
|
||||
action_text: data.actions[0].text,
|
||||
action_url: data.actions[0].url,
|
||||
metric: data.details.metric,
|
||||
severity: data.severity,
|
||||
id: data.id,
|
||||
current_value: data.details.current_value,
|
||||
threshold: data.details.threshold,
|
||||
source: data.source,
|
||||
timestamp: data.timestamp,
|
||||
};
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import Mustache from "mustache";
|
||||
import { WebhookJSONTemplate } from "../../anywhere.js";
|
||||
import variables from "./variables.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Webhook {
|
||||
url;
|
||||
@@ -13,7 +17,7 @@ class Webhook {
|
||||
constructor(trigger_meta, method, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener/3.2.3",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
let headers = trigger_meta.headers;
|
||||
this.trigger_meta = trigger_meta;
|
||||
@@ -52,8 +56,13 @@ class Webhook {
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
|
||||
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
|
||||
let view = variables(this.siteData, data);
|
||||
|
||||
if (!!!this.trigger_meta.has_webhook_body || !!!this.trigger_meta.webhook_body) {
|
||||
return Mustache.render(WebhookJSONTemplate, view);
|
||||
}
|
||||
|
||||
//have to keep this for backward compatibility
|
||||
let body = this.trigger_meta.webhook_body;
|
||||
body = ReplaceAllOccurrences(body, "${id}", data.id);
|
||||
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
|
||||
@@ -67,7 +76,9 @@ class Webhook {
|
||||
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
|
||||
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
|
||||
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
|
||||
return body;
|
||||
//have to keep this for backward compatibility
|
||||
|
||||
return Mustache.render(body, view);
|
||||
}
|
||||
|
||||
type() {
|
||||
@@ -81,6 +92,9 @@ class Webhook {
|
||||
headers: this.headers,
|
||||
body: this.transformData(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from webhook`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
|
||||
@@ -197,6 +197,7 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
barRoundness: site.barRoundness,
|
||||
midnight90DaysAgo: midnight90DaysAgoTz,
|
||||
maxDateTodayTimestamp: maxDateTodayTimestampTz,
|
||||
startOfTheDay: midnightTz,
|
||||
};
|
||||
};
|
||||
export { FetchData };
|
||||
|
||||
+64
-63
@@ -3,85 +3,86 @@ import net from "net"; // Use import instead of require
|
||||
import ping from "ping";
|
||||
|
||||
const TCP = function (type, host, port, timeout) {
|
||||
port = parseInt(port, 10);
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
const start = process.hrtime.bigint(); // High-precision timestamp
|
||||
let resolved = false;
|
||||
timeout = Number(timeout);
|
||||
port = parseInt(port, 10);
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
const start = process.hrtime.bigint(); // High-precision timestamp
|
||||
let resolved = false;
|
||||
|
||||
const onFinish = (status) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
const end = process.hrtime.bigint();
|
||||
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
|
||||
socket.destroy();
|
||||
resolve({ status, latency, host, port, type });
|
||||
}
|
||||
};
|
||||
const onFinish = (status) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
const end = process.hrtime.bigint();
|
||||
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
|
||||
socket.destroy();
|
||||
resolve({ status, latency, host, port, type });
|
||||
}
|
||||
};
|
||||
|
||||
socket.setTimeout(timeout);
|
||||
socket.once("connect", () => onFinish("open"));
|
||||
socket.once("timeout", () => onFinish("timeout"));
|
||||
socket.once("error", () => onFinish("error"));
|
||||
socket.setTimeout(timeout);
|
||||
socket.once("connect", () => onFinish("open"));
|
||||
socket.once("timeout", () => onFinish("timeout"));
|
||||
socket.once("error", () => onFinish("error"));
|
||||
|
||||
// Check if it's an IPv6 address (contains ':')
|
||||
const options = {
|
||||
host,
|
||||
port,
|
||||
family: type === "IP6" ? 6 : 4
|
||||
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
|
||||
socket.connect(options);
|
||||
});
|
||||
// Check if it's an IPv6 address (contains ':')
|
||||
const options = {
|
||||
host,
|
||||
port,
|
||||
family: type === "IP6" ? 6 : 4,
|
||||
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
|
||||
socket.connect(options);
|
||||
});
|
||||
};
|
||||
|
||||
const Ping = async function (type, host, timeout, count) {
|
||||
let output = {
|
||||
alive: false,
|
||||
min: null,
|
||||
max: null,
|
||||
avg: null,
|
||||
latencies: [],
|
||||
latency: null,
|
||||
host: host,
|
||||
type: type
|
||||
};
|
||||
let output = {
|
||||
alive: false,
|
||||
min: null,
|
||||
max: null,
|
||||
avg: null,
|
||||
latencies: [],
|
||||
latency: null,
|
||||
host: host,
|
||||
type: type,
|
||||
};
|
||||
|
||||
try {
|
||||
let res = await ping.promise.probe(host, {
|
||||
v6: type === "IP6",
|
||||
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
|
||||
min_reply: count
|
||||
});
|
||||
output.alive = res.alive;
|
||||
output.min = res.min;
|
||||
output.max = res.max;
|
||||
output.avg = res.avg;
|
||||
output.latencies = res.times;
|
||||
output.latency = res.time;
|
||||
} catch (error) {
|
||||
console.log(`Error in pingCall IP4 for ${host}`, error);
|
||||
}
|
||||
return output;
|
||||
try {
|
||||
let res = await ping.promise.probe(host, {
|
||||
v6: type === "IP6",
|
||||
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
|
||||
min_reply: count,
|
||||
});
|
||||
output.alive = res.alive;
|
||||
output.min = res.min;
|
||||
output.max = res.max;
|
||||
output.avg = res.avg;
|
||||
output.latencies = res.times;
|
||||
output.latency = res.time;
|
||||
} catch (error) {
|
||||
console.log(`Error in pingCall IP4 for ${host}`, error);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
*/
|
||||
function ExtractIPv6HostAndPort(input) {
|
||||
const parts = input.split(":"); // Split by colons
|
||||
const parts = input.split(":"); // Split by colons
|
||||
|
||||
// If there's a valid port at the end, extract it
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
|
||||
// If there's a valid port at the end, extract it
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
|
||||
|
||||
// Reconstruct the IPv6 address
|
||||
const host = parts.join(":");
|
||||
// Reconstruct the IPv6 address
|
||||
const host = parts.join(":");
|
||||
|
||||
// Ensure it's a valid IPv6 format
|
||||
if (host.includes(":")) {
|
||||
return { host, port }; // Port may be null if not present
|
||||
}
|
||||
return null; // Return null if the format is incorrect
|
||||
// Ensure it's a valid IPv6 format
|
||||
if (host.includes(":")) {
|
||||
return { host, port }; // Port may be null if not present
|
||||
}
|
||||
return null; // Return null if the format is incorrect
|
||||
}
|
||||
|
||||
export { TCP, ExtractIPv6HostAndPort, Ping };
|
||||
|
||||
@@ -3,20 +3,9 @@ import axios from "axios";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
const defaultEval = `(async function (statusCode, responseTime, 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,
|
||||
}
|
||||
})`;
|
||||
import { DefaultAPIEval } from "../../anywhere.js";
|
||||
import version from "../../version.js";
|
||||
import https from "https";
|
||||
|
||||
class ApiCall {
|
||||
monitor;
|
||||
@@ -31,7 +20,7 @@ class ApiCall {
|
||||
|
||||
async execute() {
|
||||
let axiosHeaders = {};
|
||||
axiosHeaders["User-Agent"] = "Kener/" + "3.1.0";
|
||||
axiosHeaders["User-Agent"] = `Kener/${version()}`;
|
||||
axiosHeaders["Accept"] = "*/*";
|
||||
|
||||
let body = this.monitor.type_data.body;
|
||||
@@ -44,10 +33,10 @@ class ApiCall {
|
||||
}
|
||||
|
||||
let method = this.monitor.type_data.method;
|
||||
let timeout = this.monitor.type_data.timeout || 5000;
|
||||
let timeout = this.monitor.type_data.timeout || 10000;
|
||||
let tag = this.monitor.tag;
|
||||
|
||||
let monitorEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : defaultEval;
|
||||
let monitorEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : DefaultAPIEval;
|
||||
|
||||
for (let i = 0; i < this.envSecrets.length; i++) {
|
||||
const secret = this.envSecrets[i];
|
||||
@@ -82,6 +71,10 @@ class ApiCall {
|
||||
transformResponse: (r) => r,
|
||||
};
|
||||
|
||||
if (!!this.monitor.type_data.allowSelfSignedCert) {
|
||||
options.httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||
}
|
||||
|
||||
if (!!body) {
|
||||
options.data = body;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// @ts-nocheck
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import { GameDig } from 'gamedig';
|
||||
import { DefaultGamedigEval, GAMEDIG_SOCKET_TIMEOUT, GAMEDIG_TIMEOUT } from "../../anywhere.js";
|
||||
|
||||
class GamedigCall {
|
||||
monitor;
|
||||
|
||||
constructor(monitor) {
|
||||
this.monitor = monitor;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const tag = this.monitor.tag;
|
||||
const host = this.monitor.type_data.host;
|
||||
const port = this.monitor.type_data.port;
|
||||
const timeout = !!this.monitor.type_data.timeout ? this.monitor.type_data.timeout : GAMEDIG_TIMEOUT;
|
||||
const gamedigEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : DefaultGamedigEval;
|
||||
|
||||
// Query
|
||||
let responseTime, responseRaw;
|
||||
try {
|
||||
const response = await GameDig.query({
|
||||
type: this.monitor.type_data.gameId,
|
||||
host: host,
|
||||
port: port,
|
||||
socketTimeout: GAMEDIG_SOCKET_TIMEOUT,
|
||||
attemptTimeout: timeout,
|
||||
givenPortOnly: !this.monitor.type_data.guessPort,
|
||||
requestRules: this.monitor.type_data.requestRules,
|
||||
});
|
||||
|
||||
responseTime = response.ping;
|
||||
responseRaw = response.raw;
|
||||
} catch (error) {
|
||||
console.log(`Error while requesting game's information for ${tag}: `, error.message);
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
type: ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
// Eval
|
||||
let evalResp = undefined;
|
||||
const evalFunction = new Function("responseTime", "responseRaw", `return (${gamedigEval})(responseTime, responseRaw);`);
|
||||
try {
|
||||
evalResp = await evalFunction(responseTime, responseRaw);
|
||||
} catch (error) {
|
||||
console.log(`Error in gamedigEval for ${tag}: `, error.message);
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
type: ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
// Result
|
||||
return {
|
||||
status: evalResp.status,
|
||||
latency: evalResp.latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default GamedigCall;
|
||||
@@ -1,24 +1,19 @@
|
||||
// @ts-nocheck
|
||||
import axios from "axios";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences, Wait } from "../tool.js";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences, Wait, GetMinuteStartNowTimestampUTC } from "../tool.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import db from "../db/db.js";
|
||||
|
||||
async function waitForDataAndReturn(arr, ts, d, maxTime) {
|
||||
await Wait(d);
|
||||
let data = await db.getLastStatusBeforeCombined(arr, ts);
|
||||
if (data) {
|
||||
data.type = REALTIME;
|
||||
return data;
|
||||
} else if (d > maxTime) {
|
||||
async function waitForDataAndReturn(tag) {
|
||||
let res = await db.getLatestMonitoringData(tag);
|
||||
if (!!res) {
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
type: TIMEOUT,
|
||||
status: res.status,
|
||||
latency: res.latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
} else {
|
||||
return await waitForDataAndReturn(arr, ts, d * 2, maxTime);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class GroupCall {
|
||||
@@ -29,8 +24,11 @@ class GroupCall {
|
||||
}
|
||||
|
||||
async execute(startOfMinute) {
|
||||
if (!!!startOfMinute) {
|
||||
startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
}
|
||||
let tagArr = this.monitor.type_data.monitors.map((m) => m.tag);
|
||||
return await waitForDataAndReturn(tagArr, startOfMinute, 500, this.monitor.type_data.timeout);
|
||||
return await waitForDataAndReturn(this.monitor.tag);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,7 @@
|
||||
import axios from "axios";
|
||||
import { Ping } from "../ping.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
|
||||
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,
|
||||
}
|
||||
})`;
|
||||
import { DefaultPingEval } from "../../anywhere.js";
|
||||
|
||||
class PingCall {
|
||||
monitor;
|
||||
@@ -27,7 +13,7 @@ class PingCall {
|
||||
|
||||
async execute() {
|
||||
let hosts = this.monitor.type_data.hosts;
|
||||
let pingEval = !!this.monitor.type_data.pingEval ? this.monitor.type_data.pingEval : defaultPingEval;
|
||||
let pingEval = !!this.monitor.type_data.pingEval ? this.monitor.type_data.pingEval : DefaultPingEval;
|
||||
let tag = this.monitor.tag;
|
||||
if (hosts === undefined) {
|
||||
console.log(
|
||||
|
||||
@@ -7,6 +7,7 @@ import GroupCall from "./groupCall.js";
|
||||
import SSLCall from "./sslCall.js";
|
||||
import SqlCall from "./sqlCall.js";
|
||||
import HeartbeatCall from "./heartbeatCall.js";
|
||||
import GamedigCall from "./gamedigCall.js";
|
||||
|
||||
class Service {
|
||||
service;
|
||||
@@ -30,6 +31,8 @@ class Service {
|
||||
this.service = new SqlCall(monitor);
|
||||
} else if (monitor.monitor_type === "HEARTBEAT") {
|
||||
this.service = new HeartbeatCall(monitor);
|
||||
} else if (monitor.monitor_type === "GAMEDIG") {
|
||||
this.service = new GamedigCall(monitor);
|
||||
} else {
|
||||
console.log("Invalid monitor.monitor_type ", monitor.monitor_type);
|
||||
process.exit(1);
|
||||
|
||||
@@ -2,25 +2,7 @@
|
||||
import axios from "axios";
|
||||
import { TCP } from "../ping.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
|
||||
const defaultTcpEval = `(async function (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,
|
||||
}
|
||||
})`;
|
||||
import { DefaultTCPEval } from "../../anywhere.js";
|
||||
|
||||
class TcpCall {
|
||||
monitor;
|
||||
@@ -31,7 +13,7 @@ class TcpCall {
|
||||
|
||||
async execute() {
|
||||
let hosts = this.monitor.type_data.hosts;
|
||||
let tcpEval = !!this.monitor.type_data.tcpEval ? this.monitor.type_data.tcpEval : defaultTcpEval;
|
||||
let tcpEval = !!this.monitor.type_data.tcpEval ? this.monitor.type_data.tcpEval : DefaultTCPEval;
|
||||
let tag = this.monitor.tag;
|
||||
|
||||
if (hosts === undefined) {
|
||||
|
||||
@@ -4,29 +4,17 @@ import figlet from "figlet";
|
||||
import { Cron } from "croner";
|
||||
import { Minuter } from "./cron-minute.js";
|
||||
import db from "./db/db.js";
|
||||
import { GetAllSiteData, GetMonitorsParsed, HashString } from "./controllers/controller.js";
|
||||
import { GetAllSiteData, GetMonitorsParsed } from "./controllers/controller.js";
|
||||
import { HashString } from "./tool.js";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import fs from "fs";
|
||||
import version from "../version.js";
|
||||
|
||||
const jobs = [];
|
||||
process.env.TZ = "UTC";
|
||||
let isStartUP = true;
|
||||
|
||||
// Get the version from package.json
|
||||
const getVersion = () => {
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const packagePath = resolve(__dirname, "../../../package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
return packageJson.version;
|
||||
} catch (error) {
|
||||
console.error("Error reading version:", error);
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleCronJobs = async () => {
|
||||
// Fetch and map all active monitors, creating a unique hash for each
|
||||
const activeMonitors = (await GetMonitorsParsed({ status: "ACTIVE" })).map((monitor) => ({
|
||||
@@ -85,15 +73,13 @@ async function Startup() {
|
||||
|
||||
mainJob.trigger();
|
||||
|
||||
const version = getVersion();
|
||||
|
||||
figlet("Kener v" + version, function (err, data) {
|
||||
figlet("Kener v" + version(), function (err, data) {
|
||||
if (err) {
|
||||
console.log("Something went wrong...");
|
||||
return;
|
||||
}
|
||||
console.log(data);
|
||||
console.log(`Kener version ${version} is running!`);
|
||||
console.log(`Kener version ${version()} is running!`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<link rel="preload" as="image" href="{{logo_url}}" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
</head>
|
||||
<body
|
||||
style="
|
||||
background-color: rgb(243, 244, 246);
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
"
|
||||
>
|
||||
<!--$-->
|
||||
<div
|
||||
style="
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
line-height: 1px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
"
|
||||
>
|
||||
Your verification code: {{email_code}}
|
||||
</div>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 8px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
padding: 24px;
|
||||
max-width: 600px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width: 100%">
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="margin-top: 8px; margin-bottom: 32px; text-align: center"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
alt="{{brand_name}}"
|
||||
height="40"
|
||||
src="{{logo_url}}"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
"
|
||||
width="120"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h1
|
||||
style="
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: rgb(31, 41, 55);
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
Verification Code
|
||||
</h1>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
To complete your {{action}} for {{brand_name}} updates, please use the
|
||||
verification code below:
|
||||
</p>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background-color: rgb(249, 250, 251);
|
||||
border-width: 1px;
|
||||
border-color: rgb(229, 231, 235);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
color: rgb(31, 41, 55);
|
||||
margin: 0px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
"
|
||||
>
|
||||
{{email_code}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
This code will expire in 5 minutes. If you didn't request this code, you
|
||||
can safely ignore this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--7--><!--/$-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,248 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<link rel="preload" as="image" href="{{logo_url}}" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
</head>
|
||||
<body
|
||||
style="
|
||||
background-color: rgb(243, 244, 246);
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
"
|
||||
>
|
||||
<!--$-->
|
||||
<div
|
||||
style="
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
line-height: 1px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
"
|
||||
>
|
||||
{{title}}
|
||||
</div>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width: 100%">
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background-color: #e4e5ec;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tbody style="width: 100%">
|
||||
<tr style="width: 100%">
|
||||
<td data-id="__react-email-column" style="text-align: center">
|
||||
<img
|
||||
alt="{{brand_name}}"
|
||||
src="{{logo_url}}"
|
||||
style="
|
||||
height: auto;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
"
|
||||
width="80"
|
||||
/>
|
||||
<p
|
||||
style="
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #191919;
|
||||
margin: 8px 0 0 0;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
{{brand_name}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
padding-top: 32px;
|
||||
padding-bottom: 32px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h1
|
||||
style="
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: rgb(31, 41, 55);
|
||||
margin-bottom: 16px;
|
||||
"
|
||||
>
|
||||
{{title}}
|
||||
</h1>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(55, 65, 81);
|
||||
margin-bottom: 16px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
Dear Valued Customer,
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(55, 65, 81);
|
||||
margin-bottom: 16px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
We would like to provide you with an update regarding the current system
|
||||
status.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(55, 65, 81);
|
||||
margin-bottom: 16px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
<strong>Update:</strong>
|
||||
{{update_message}}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="{{incident_url}}"
|
||||
style="
|
||||
background-color: rgb(22, 163, 74);
|
||||
color: rgb(255, 255, 255);
|
||||
font-weight: 700;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
text-decoration-line: none;
|
||||
text-align: center;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
max-width: 100%;
|
||||
mso-padding-alt: 0px;
|
||||
padding: 12px 24px 12px 24px;
|
||||
"
|
||||
target="_blank"
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%; mso-text-raise: 18" hidden
|
||||
>   </i
|
||||
><!
|
||||
[endif]--></span
|
||||
><span
|
||||
style="
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
line-height: 120%;
|
||||
mso-padding-alt: 0px;
|
||||
mso-text-raise: 9px;
|
||||
"
|
||||
>View Incident Details</span
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%" hidden
|
||||
>   ​</i
|
||||
><!
|
||||
[endif]--></span
|
||||
></a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr
|
||||
style="
|
||||
border-top-width: 1px;
|
||||
border-color: rgb(209, 213, 219);
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-top: 1px solid #eaeaea;
|
||||
"
|
||||
/>
|
||||
<footer>
|
||||
<p style="text-align: center; color: #6b7280; font-size: 16px; margin: 24px 0 0 0">
|
||||
Thank you,<br />The {{brand_name}} Team
|
||||
</p>
|
||||
</footer>
|
||||
<div style="height: 32px"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--7--><!--/$-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,217 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<link rel="preload" as="image" href="{{logo_url}}" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<!--$-->
|
||||
</head>
|
||||
<div
|
||||
style="
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
line-height: 1px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
"
|
||||
>
|
||||
Reset your {{brand_name}} password
|
||||
</div>
|
||||
|
||||
<body
|
||||
style="
|
||||
background-color: rgb(243, 244, 246);
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
max-width: 600px;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgb(255, 255, 255);
|
||||
padding: 20px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width: 100%">
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="text-align: center"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
alt="{{brand_name}} Logo"
|
||||
height="50"
|
||||
src="{{logo_url}}"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
"
|
||||
width="200"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h1
|
||||
style="
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: rgb(31, 41, 55);
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
"
|
||||
>
|
||||
Password Reset Request
|
||||
</h1>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
Hello,
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
We received a request to reset your password for your {{brand_name}} account. If you
|
||||
didn't make this request, you can safely ignore this email.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 32px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
To reset your password, please click the button below:
|
||||
</p>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="text-align: center; margin-bottom: 32px"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="{{link}}"
|
||||
style="
|
||||
background-color: rgb(37, 99, 235);
|
||||
color: rgb(255, 255, 255);
|
||||
font-weight: 700;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
border-radius: 0.25rem;
|
||||
box-sizing: border-box;
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
mso-padding-alt: 0px;
|
||||
padding: 12px 24px 12px 24px;
|
||||
"
|
||||
target="_blank"
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%; mso-text-raise: 18" hidden
|
||||
>   </i
|
||||
><!
|
||||
[endif]--></span
|
||||
><span
|
||||
style="
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
line-height: 120%;
|
||||
mso-padding-alt: 0px;
|
||||
mso-text-raise: 9px;
|
||||
"
|
||||
>Reset Password</span
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%" hidden
|
||||
>   ​</i
|
||||
><!
|
||||
[endif]--></span
|
||||
></a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
This password reset link will expire in 24 hours. If you need assistance, please
|
||||
contact our support team.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 32px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
Thank you,<br />The {{brand_name}} Team
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--/$-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,221 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<link rel="preload" as="image" href="{{logo_url}}" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<!--$-->
|
||||
</head>
|
||||
<div
|
||||
style="
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
line-height: 1px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
"
|
||||
>
|
||||
Verify your email address for {{brand_name}}
|
||||
</div>
|
||||
|
||||
<body
|
||||
style="
|
||||
background-color: rgb(243, 244, 246);
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
var(--tw-ring-offset-shadow, 0 0 #0000),
|
||||
var(--tw-ring-shadow, 0 0 #0000),
|
||||
0 1px 2px 0 rgb(0, 0, 0, 0.05);
|
||||
max-width: 37.5em;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width: 100%">
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="margin-top: 32px"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
alt="{{brand_name}}"
|
||||
height="auto"
|
||||
src="{{logo_url}}"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
"
|
||||
width="120"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="margin-top: 32px"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h1
|
||||
style="
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: rgb(31, 41, 55);
|
||||
"
|
||||
>
|
||||
Verify Your Email Address
|
||||
</h1>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
Thank you for signing up with
|
||||
<!-- -->{{brand_name}}<!-- -->! To complete your registration and get started,
|
||||
please verify your email address.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 32px;
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
Click the button below to verify your email address and activate your account:
|
||||
</p>
|
||||
<a
|
||||
href="{{verification_url}}"
|
||||
style="
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
text-decoration-line: none;
|
||||
text-align: center;
|
||||
color: rgb(255, 255, 255);
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
mso-padding-alt: 0px;
|
||||
background-color: #ed702d;
|
||||
padding: 12px 24px 12px 24px;
|
||||
"
|
||||
target="_blank"
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%; mso-text-raise: 18" hidden
|
||||
>   </i
|
||||
><!
|
||||
[endif]--></span
|
||||
><span
|
||||
style="
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
line-height: 120%;
|
||||
mso-padding-alt: 0px;
|
||||
mso-text-raise: 9px;
|
||||
"
|
||||
>Verify Email Address</span
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%" hidden
|
||||
>   ​</i
|
||||
><!
|
||||
[endif]--></span
|
||||
></a
|
||||
>
|
||||
<p
|
||||
style="
|
||||
font-size: 14px;
|
||||
color: rgb(107, 114, 128);
|
||||
margin-top: 32px;
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
If you didn't create an account with
|
||||
<!-- -->{{brand_name}}<!-- -->, you can safely ignore this email.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 14px;
|
||||
color: rgb(107, 114, 128);
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
If the button above doesn't work, copy and paste the following link into
|
||||
your browser:
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 14px;
|
||||
color: rgb(107, 114, 128);
|
||||
word-break: break-all;
|
||||
line-height: 24px;
|
||||
margin: 16px 0;
|
||||
"
|
||||
>
|
||||
<a
|
||||
href="{{verification_url}}"
|
||||
style="color: rgb(37, 99, 235); text-decoration-line: underline"
|
||||
target="_blank"
|
||||
>{{verification_url}}</a
|
||||
>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--/$-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { AllRecordTypes } from "./constants.js";
|
||||
import knexOb from "../../../knexfile.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
@@ -279,6 +280,9 @@ function Wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
function MaskString(str) {
|
||||
if (str.length <= 4) {
|
||||
return "*".repeat(str.length);
|
||||
}
|
||||
return "*".repeat(str.length - 4) + str.slice(-4);
|
||||
}
|
||||
|
||||
@@ -286,6 +290,25 @@ function GetDbType() {
|
||||
//sqlite, postgresql, mysql
|
||||
return knexOb.databaseType;
|
||||
}
|
||||
function HashString(str) {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(str);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
//function to validate email address
|
||||
function ValidateEmail(email) {
|
||||
const regex =
|
||||
/^(?:(?:[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+\.)*[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+)@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\[(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?:\.(?!$)|$)){4}\]))$/;
|
||||
return regex.test(email);
|
||||
}
|
||||
|
||||
//function to generate 6 digit random number, number of digits as argument
|
||||
function GenerateRandomNumber(digits) {
|
||||
const min = Math.pow(10, digits - 1);
|
||||
const max = Math.pow(10, digits) - 1;
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
export {
|
||||
IsValidURL,
|
||||
@@ -315,4 +338,7 @@ export {
|
||||
Wait,
|
||||
MaskString,
|
||||
GetDbType,
|
||||
HashString,
|
||||
ValidateEmail,
|
||||
GenerateRandomNumber,
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user