mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 07:14:56 +00:00
Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0cf568aa9 | |||
| c12d7f8345 | |||
| 9ba115d5ab | |||
| 3756bbfd06 | |||
| 9096bcc641 | |||
| c4d04854e5 | |||
| d95fc0005b | |||
| 2b9093184a | |||
| 831376054b | |||
| 885ee0b6d6 | |||
| 511d0f26dd | |||
| 5263698625 | |||
| 7859f9564e | |||
| 149456682d | |||
| 8c040997da | |||
| 868fed0176 | |||
| c7f44a48da | |||
| 6300bff4e7 | |||
| 3e6b1ffdbb | |||
| 408fd5318b | |||
| cfe419f6df | |||
| 5403726099 | |||
| 057c14909f | |||
| 37b18c06bb | |||
| 3ad1eb9a03 | |||
| a1064771f8 | |||
| 463ee286bf | |||
| a7ae6e72ef | |||
| 36c67f60dd | |||
| bb718b04a6 | |||
| b6ea060054 | |||
| 5dba54c048 | |||
| 415276163f | |||
| 017b9e2e84 | |||
| 5740991ee1 | |||
| 06008bd719 | |||
| b53cd485e5 | |||
| 7c1fd39158 | |||
| f7888f1ba4 | |||
| ece5ac37ad | |||
| a51183fb23 | |||
| 7875192fb0 | |||
| eb128ad431 | |||
| ba61ed87ab | |||
| 8b5348081d | |||
| bb7e69ace5 | |||
| 3b8bb60423 | |||
| 93339ce9df | |||
| d30d99d8c2 | |||
| 3468df16e1 | |||
| ccdeff98dc | |||
| 7e182b1df2 | |||
| ff652ad435 | |||
| 1fdafa7966 | |||
| 1f5e683af5 | |||
| db8c881571 | |||
| 14f49c5b3b | |||
| 52286f26a5 | |||
| 45e7567cb6 | |||
| c0edb59884 | |||
| 1e86c429ca | |||
| fbaebdcbcf | |||
| 3f9716b0a4 | |||
| 57d32197cf | |||
| 9e6a3f26c2 | |||
| 23314beb15 | |||
| 64b94da585 | |||
| 1609b4fc50 | |||
| dfffffb101 | |||
| 81250a117a | |||
| 72bd0241e9 | |||
| 63c24f147f | |||
| 88fb7df3f5 | |||
| 320b1a0cc5 | |||
| 08b168ee6e | |||
| d6a87ac81a | |||
| 615dba42b8 | |||
| 3ffec4f1fe | |||
| 60b4b6f207 | |||
| b9f5eb56c5 | |||
| 7b9ae73044 | |||
| 87f2c33eba | |||
| 0a73a8b10a | |||
| af65404fd3 | |||
| 398f891dac | |||
| 6a2375a774 | |||
| 43bbcf4015 | |||
| fadb563337 | |||
| 1ae54b3906 | |||
| 1c069e2ee2 | |||
| ed1099ac11 | |||
| 99d3a7e046 | |||
| 2144acf34d | |||
| 7a8ad8e833 | |||
| 3b45f33692 | |||
| b4a2340ec7 | |||
| 5910e3b930 | |||
| fd58beaa69 | |||
| 5449e422a5 | |||
| dfb1784b6f | |||
| d956c60b48 | |||
| 69e1f2af6e | |||
| 7cc2f5ed4b | |||
| ce2b17a5c9 | |||
| 0fcc60bf65 | |||
| 45ac25055b | |||
| d3e201f2e4 | |||
| 693735dc2e | |||
| a4fa85dd79 | |||
| 29f70860ef | |||
| ae882f6afc | |||
| 57941f67ce | |||
| ab356d516b | |||
| d11b3f1a99 | |||
| 83ba08dd6b | |||
| 34cc00a1f6 | |||
| 7b86429056 | |||
| e67c1b1539 | |||
| e501f8e05d | |||
| 9e40d89317 | |||
| 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
|
||||
@@ -96,19 +96,17 @@ jobs:
|
||||
${{ env.DOCKERHUB_IMAGE_NAME }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }}
|
||||
tags: |
|
||||
# Tag non-main branches (...for Alpine variant, add suffix)
|
||||
type=ref,event=branch,suffix=-${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' && github.ref != 'refs/heads/main' }}
|
||||
type=ref,event=branch,enable=${{ matrix.variant == 'debian' && github.ref != 'refs/heads/main' }}
|
||||
# Tag matrix.variant (Alpine) releases as version (1.0.0), major.minor (1.0), major (1), and matrix.variant (alpine)
|
||||
type=semver,pattern={{version}},suffix=-${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' }}
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' }}
|
||||
type=semver,pattern={{major}},suffix=-${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' }}
|
||||
type=raw,value=${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' && github.ref == 'refs/heads/main' }}
|
||||
# Tag default (Debian) releases as version (1.0.0), major.minor (1.0), major (1), and latest
|
||||
# Debian Variant Tags
|
||||
type=semver,pattern={{version}},enable=${{ matrix.variant == 'debian' }}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ matrix.variant == 'debian' }}
|
||||
type=semver,pattern={{major}},enable=${{ matrix.variant == 'debian' }}
|
||||
type=raw,value=latest,enable=${{ matrix.variant == 'debian' && github.ref == 'refs/heads/main' }}
|
||||
type=raw,value=latest,enable=${{ matrix.variant == 'debian' }}
|
||||
|
||||
# Alpine Variant Tags
|
||||
type=semver,pattern={{version}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
type=semver,pattern={{major}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
type=raw,value=alpine,enable=${{ matrix.variant == 'alpine' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
@@ -143,15 +141,16 @@ jobs:
|
||||
- name: Save Build Version to Repository Variable
|
||||
if: matrix.variant == 'debian' && github.run_attempt == 1
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
# VERSION="${{ steps.meta.outputs.version }}"
|
||||
VERSION=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "")
|
||||
|
||||
# Check if VERSION is empty and set a fallback value
|
||||
if [ -z "$VERSION" ]; then
|
||||
# Fetch the latest release using Git
|
||||
VERSION=$(git tag -l --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || echo "3.1.0")
|
||||
VERSION=$(git tag -l --sort=-version:refname | grep -E '^(v)?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || echo "3.1.0")
|
||||
fi
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
echo "Setting BUILD_VERSION to $VERSION"
|
||||
gh variable set BUILD_VERSION --body "$VERSION"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
|
||||
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
-22
@@ -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,11 +18,11 @@ 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 \
|
||||
@@ -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,17 +71,17 @@ 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 \
|
||||
# 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 && \
|
||||
tzdata \
|
||||
# TODO: Is it ok to change to `curl` here so that we don't have to maintain `wget` version mismatch between Debian architectures? (`curl` is only used for the container healthcheck and because there is an Alpine variant (best!) we probably don't care if the Debian image ends up building bigger due to `curl`.)
|
||||
curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM node:${ALPINE_VERSION} 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 \
|
||||
@@ -119,6 +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
|
||||
COPY --chown=node:node --from=builder /app/package.json ./package.json
|
||||
|
||||
# Ensure necessary directories are writable
|
||||
VOLUME ["/uploads", "/database"]
|
||||
@@ -126,19 +126,19 @@ VOLUME ["/uploads", "/database"]
|
||||
# Set container timezone and make entrypoint script executable
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
|
||||
chmod +x ./entrypoint.sh
|
||||
# TODO: To improve security, consider dropping unnecessary capabilities instead of granting image all network capabilities of host. (Maybe `setcap cap_net_raw+p /usr/bin/ping`, etc.) Could also drop all and then grant only the capabilities that are explicitly needed. Some examples are commented out below...
|
||||
# setcap cap_net_bind_service=+ep /usr/local/bin/node
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/ping
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/ping6
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/tracepath
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/clockdiff
|
||||
# TODO: To improve security, consider dropping unnecessary capabilities instead of granting image all network capabilities of host. (Maybe `setcap cap_net_raw+p /usr/bin/ping`, etc.) Could also drop all and then grant only the capabilities that are explicitly needed. Some examples are commented out below...
|
||||
# setcap cap_net_bind_service=+ep /usr/local/bin/node
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/ping
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/ping6
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/tracepath
|
||||
# setcap cap_net_bind_service=+ep /usr/bin/clockdiff
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE $PORT
|
||||
|
||||
# Add a healthcheck to the container; `wget` vs. `curl` depending on base image. Using this approach because `wget` does not actually maintain versioning across architectures, so we cannot pin a `wget` version (in above `final-debian` base, `apt-get install`) between differing architectures (e.g. arm64, amd64)
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||
CMD sh -c 'if [ -f "/etc/alpine-release" ]; then wget --quiet --spider http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; else curl --silent --head --fail http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; fi'
|
||||
CMD sh -c 'if [ -f "/etc/alpine-release" ]; then wget --quiet --spider http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; else curl --silent --head --fail http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; fi'
|
||||
|
||||
# TODO: Revisit letting user define $PUID & $PGID overrides (e.g. `addgroup -g $PGID newgroup && adduser -D -G newgroup -u $PUID node`) as well as potentially ensure no root user exists. (Make sure no processes are running as root, first!)
|
||||
# Use a non-root user (recommended for security)
|
||||
|
||||
@@ -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.1.6"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.1.6-blue" alt="Kener latest stable version: 3.1.6" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.10"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.10-blue" alt="Kener latest stable version: 3.2.10" /></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.1.6)</td>
|
||||
<td>Latest stable release (aka 3.2.10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.6" target="_blank"><code>3.1.6</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.10" target="_blank"><code>3.2.10</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.1" target="_blank"><code>3.1</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.1.6) release within that minor version (3.1.x)</td>
|
||||
<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.10) 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.1.6) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.10) 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.1.6)</td>
|
||||
<td>Latest stable release (aka 3.2.10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.6-alpine" target="_blank"><code>3.1.6-alpine</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.10-alpine" target="_blank"><code>3.2.10-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.1-alpine" target="_blank"><code>3.1-alpine</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.1.6) release within that minor version (3.1.x)</td>
|
||||
<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.10) 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.1.6) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.10) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
+247
-48
@@ -7,6 +7,205 @@ description: Changelogs for Kener
|
||||
|
||||
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
|
||||
|
||||
## 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>
|
||||
<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
|
||||
|
||||
- **New Heartbeat Monitors**
|
||||
|
||||
- Added support for push-based monitoring via heartbeats
|
||||
- Monitors systems that send periodic signals to confirm they're running
|
||||
- Configurable thresholds for degraded and down states
|
||||
- Secured with secret token authentication
|
||||
- Accessible via simple HTTP API endpoints
|
||||
|
||||
- **Data Interpolation Improvements**
|
||||
- Fixed data interpolation issues for more accurate uptime calculations
|
||||
- Improved handling of timestamp boundaries
|
||||
|
||||
### Fixes & Improvements
|
||||
|
||||
- **UI Enhancements**
|
||||
|
||||
- Better mobile responsiveness for theme toggles and controls
|
||||
- Fixed layout issues with bottom navigation buttons
|
||||
- Improved timezone selector display
|
||||
|
||||
- **Data Processing**
|
||||
|
||||
- Fixed edge case issues with uptime calculations
|
||||
- Improved handling of "No Data" status in summaries
|
||||
- More accurate time range display across timezones
|
||||
|
||||
- **Documentation**
|
||||
- Added comprehensive documentation for Heartbeat Monitors
|
||||
- Updated API documentation with new endpoints
|
||||
|
||||
## 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>
|
||||
@@ -16,15 +215,15 @@ Here are the changelogs for Kener. Changelogs are only published when there are
|
||||
|
||||
### Features
|
||||
|
||||
- Added TCP monitors
|
||||
- Added TCP monitors
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Ping monitors will break.
|
||||
- Ping monitors will break.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Bug fixes in the UI
|
||||
- Bug fixes in the UI
|
||||
|
||||
## v3.0.9
|
||||
|
||||
@@ -35,16 +234,16 @@ Here are the changelogs for Kener. Changelogs are only published when there are
|
||||
|
||||
### Features
|
||||
|
||||
- Support of SMTP for email notifications. Read more [here](/docs/triggers/#email-smtp)
|
||||
- Introduction of event type `MAINTENANCE` for incidents.
|
||||
- You can write eval function for ping now in monitors. Read more [here](/docs/monitors-ping/#eval)
|
||||
- Added category filter for monitor management.
|
||||
- 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.
|
||||
- Support longer TLD in `siteURL` example `https://example.network`
|
||||
- Remove googleapis preconnect and preload
|
||||
- Fixed wrong action url in webhook.
|
||||
|
||||
## v3.0.1
|
||||
|
||||
@@ -57,8 +256,8 @@ 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)
|
||||
- Support for i18n in dates.
|
||||
- Support of i18n in monitor embeds. Read more [here](/docs/embed#javascript-parameters)
|
||||
|
||||
## v3.0.0
|
||||
|
||||
@@ -71,18 +270,18 @@ 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
|
||||
- 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
|
||||
|
||||
@@ -95,14 +294,14 @@ Here are the changes in this release
|
||||
|
||||
### Features
|
||||
|
||||
- Added support for sqlite3 and removed dependency on file system
|
||||
- Added support for postgres database. Read more [here](/docs/database)
|
||||
- Added support for alerting. Read more [here](/docs/alerting)
|
||||
- Added color customization. Read more [here](/docs/customize-site#color)
|
||||
- Added three new customizations for home page. Read more [here](/docs/customize-site#barstyle)
|
||||
- `barStyle`
|
||||
- `barRoundness`
|
||||
- `summaryStyle`
|
||||
- Added 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
|
||||
|
||||
@@ -119,27 +318,27 @@ 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)
|
||||
- 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
|
||||
- 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
|
||||
- Use `-v $(pwd)/database:/app/database` and `-v $(pwd)/config:/app/config` in your docker run command
|
||||
|
||||
+30
-75
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Kener Documentation
|
||||
title: Kener Documentation | Open Source Status Page
|
||||
description: Kener is a feature-rich and modern status page system built with SvelteKit and NodeJS. It is open-source and free to use.
|
||||
---
|
||||
|
||||
@@ -36,86 +36,41 @@ description: Kener is a feature-rich and modern status page system built with Sv
|
||||
</picture>
|
||||
</div>
|
||||
<div class="flex gap-2 kener-home-links">
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing">Live Demo</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/quick-start">Quick Start</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://github.com/rajnandan1/kener">Clone</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/deployment">Deploy</a>
|
||||
</div>
|
||||
<div class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
<a href="https://kener.ing/docs/kener-apis">APIs</a>
|
||||
</div>
|
||||
<a href="https://kener.ing" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Live Demo
|
||||
</a>
|
||||
<a href="https://kener.ing/docs/quick-start" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Quick Start
|
||||
</a>
|
||||
<a href="https://github.com/rajnandan1/kener" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Clone
|
||||
</a>
|
||||
<a href="https://kener.ing/docs/deployment" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
Deploy
|
||||
</a>
|
||||
<a href="https://kener.ing/docs/kener-apis" class="flex-1 border rounded-md py-4 px-2 text-center">
|
||||
APIs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## What is Kener?
|
||||
## Forget Boring Status Pages. Hello Kener!
|
||||
|
||||
Kener is status page system built with Sveltekit and NodeJS. It does not try to replace the Datadogs and Atlassian of the world. It tries to help some who wants to come up with a status page that looks nice and minimum overhead, in a modern way.
|
||||
**Kener** isn't just another status page – it's a beautifully crafted, lightning-fast monitoring system that makes your services look good even when they're down. Built with modern tech (**SvelteKit** + **NodeJS**), it delivers enterprise-grade reliability monitoring without the enterprise price tag (it's completely free!).
|
||||
|
||||
It comes with all the basic asks for a status page. It is open-source and free to use.
|
||||
### Why Teams Are Switching to Kener
|
||||
|
||||
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a
|
||||
cheaply available domain.
|
||||
- **📈 Up and running in minutes**, not days – from Docker to deployment in less time than it takes to make coffee
|
||||
- **🎭 Dark mode that actually looks good** – because your 3 AM incident responses shouldn't burn your retinas
|
||||
- **🧩 Ridiculously customizable** – make it match your brand or go wild with a unique design
|
||||
- **⚡ Blazing fast performance** – server-rendered for speed that both users and search engines love
|
||||
- **🌐 Works everywhere** – auto-adjusts to visitors' time zones and looks great on any device
|
||||
- **🔄 Real-time data** with gorgeous visualizations that make your uptime metrics pop
|
||||
- **🛠️ Modern tooling** without the bloat – everything you need, nothing you don't
|
||||
|
||||
## Quick Deployment
|
||||
### Used by Companies That Value Simplicity and Power
|
||||
|
||||
[](https://railway.com/template/spSvic?referralCode=1Pn7vs)
|
||||
From indie developers to growing startups, teams are choosing Kener when they need reliability monitoring that doesn't require a dedicated DevOps team to maintain.
|
||||
|
||||
## Features
|
||||
"_My team set up Kener in less than an hour, and our customers love the transparency it provides. The embedded widgets on our pricing page increased conversions by showing our reliability upfront._" – A happy user
|
||||
|
||||
### Monitoring and Tracking
|
||||
|
||||
- Advanced application performance monitoring tools
|
||||
- Types of Monitors: HTTP, TCP, DNS, ICMP etc
|
||||
- Real-time network monitor software capabilities
|
||||
- Polls HTTP endpoint or Push data to monitor using Rest APIs
|
||||
- Handles Timezones for visitors
|
||||
- Categorize Monitors into different Sections
|
||||
- Cron-based scheduling for monitors. Minimum per minute
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
|
||||
### Customization and Branding
|
||||
|
||||
- Customizable status page
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
- Beautifully Crafted Status Page
|
||||
|
||||
### Incident Management
|
||||
|
||||
- Incident Management
|
||||
- Incident Communication
|
||||
- Comprehensive APIs for Incident Management
|
||||
|
||||
### User Experience and Design
|
||||
|
||||
- 100% Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
- Server Side Rendering
|
||||
|
||||
## Technologies used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Support Me
|
||||
|
||||
If you are using Kener and want to support me, you can do so by sponsoring me on GitHub or buying me a coffee.
|
||||
|
||||
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
|
||||
|
||||
[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
## Ready to Transform Your Status Page?
|
||||
|
||||
+93
-90
@@ -28,65 +28,68 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
|
||||
```json
|
||||
{
|
||||
"%status for %duration": "%status for %duration",
|
||||
"14 Days": "14 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"7 Days": "7 Days",
|
||||
"90 Days": "90 Days",
|
||||
"Availability per Component": "Availability per Component",
|
||||
"Back": "Back",
|
||||
"Badge Copied": "Badge Copied",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Browse Events",
|
||||
"Code Copied": "Code Copied",
|
||||
"Copy Code": "Copy Code",
|
||||
"Copy Link": "Copy Link",
|
||||
"DEGRADED": "DEGRADED",
|
||||
"DOWN": "DOWN",
|
||||
"Dark": "Dark",
|
||||
"Days": "Days",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"Embed": "Embed",
|
||||
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
|
||||
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Incident Updates": "Incident Updates",
|
||||
"LIVE Status": "LIVE Status",
|
||||
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
|
||||
"Light": "Light",
|
||||
"Link Copied": "Link Copied",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"MONITORING": "MONITORING",
|
||||
"Maintenance Completed": "Maintenance Completed",
|
||||
"Maintenance in Progress": "Maintenance in Progress",
|
||||
"Mode": "Mode",
|
||||
"No Data": "No Data",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
"Pinging": "Pinging",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"Recent Incidents": "Recent Incidents",
|
||||
"Recent Maintenances": "Recent Maintenances",
|
||||
"Share this monitor using a link with others": "Share this monitor using a link with others",
|
||||
"Share": "Share",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Theme",
|
||||
"Today": "Today",
|
||||
"UP": "UP",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"%status for %duration": "%status for %duration",
|
||||
"14 Days": "14 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"7 Days": "7 Days",
|
||||
"90 Days": "90 Days",
|
||||
"Availability per Component": "Availability per Component",
|
||||
"Back": "Back",
|
||||
"Badge Copied": "Badge Copied",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Browse Events",
|
||||
"Code Copied": "Code Copied",
|
||||
"Copy Code": "Copy Code",
|
||||
"Copy Link": "Copy Link",
|
||||
"DEGRADED": "DEGRADED",
|
||||
"DOWN": "DOWN",
|
||||
"Dark": "Dark",
|
||||
"Days": "Days",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"Embed": "Embed",
|
||||
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
|
||||
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Incident Updates": "Incident Updates",
|
||||
"LIVE Status": "LIVE Status",
|
||||
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
|
||||
"Light": "Light",
|
||||
"Link Copied": "Link Copied",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"MONITORING": "MONITORING",
|
||||
"Maintenance Completed": "Maintenance Completed",
|
||||
"Maintenance in Progress": "Maintenance in Progress",
|
||||
"Mode": "Mode",
|
||||
"No Data": "No Data",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
"Pinging": "Pinging",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"Recent Incidents": "Recent Incidents",
|
||||
"Recent Maintenances": "Recent Maintenances",
|
||||
"Share this monitor using a link with others": "Share this monitor using a link with others",
|
||||
"Share": "Share",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
"Started %startedAt, will last for %lastedFor more": "Started %startedAt, will last for %lastedFor more",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Switch to your timezone": "Switch to your timezone",
|
||||
"Theme": "Theme",
|
||||
"Timezone set to %tz": "Timezone set to %tz",
|
||||
"Today": "Today",
|
||||
"UP": "UP",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -100,30 +103,30 @@ Open `src/lib/locales/locales.json` and add the locale code and the name of the
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"code": "en",
|
||||
"name": "English"
|
||||
},
|
||||
{
|
||||
"code": "hi",
|
||||
"name": "हिन्दी"
|
||||
},
|
||||
{
|
||||
"code": "zh-CN",
|
||||
"name": "中文"
|
||||
},
|
||||
{
|
||||
"code": "ja",
|
||||
"name": "日本語"
|
||||
},
|
||||
{
|
||||
"code": "vi",
|
||||
"name": "Tiếng Việt"
|
||||
},
|
||||
{
|
||||
"code": "fr",
|
||||
"name": "Français"
|
||||
}
|
||||
{
|
||||
"code": "en",
|
||||
"name": "English"
|
||||
},
|
||||
{
|
||||
"code": "hi",
|
||||
"name": "हिन्दी"
|
||||
},
|
||||
{
|
||||
"code": "zh-CN",
|
||||
"name": "中文"
|
||||
},
|
||||
{
|
||||
"code": "ja",
|
||||
"name": "日本語"
|
||||
},
|
||||
{
|
||||
"code": "vi",
|
||||
"name": "Tiếng Việt"
|
||||
},
|
||||
{
|
||||
"code": "fr",
|
||||
"name": "Français"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -132,7 +135,7 @@ Open `src/lib/locales/locales.json` and add the locale code and the name of the
|
||||
Open `src/lib/i18n/client.js` and add the locale code to the `locales` array.
|
||||
|
||||
```js
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale";
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale"
|
||||
```
|
||||
|
||||
This should be valid for the date-fns library. You can find the list of locales [here](https://github.com/date-fns/date-fns/blob/9bb51691f201c3ec05ab832acbc5d478f2e5c47a/docs/i18nLocales.md).
|
||||
@@ -140,7 +143,7 @@ This should be valid for the date-fns library. You can find the list of locales
|
||||
Next add a mapping in the `locales` object.
|
||||
|
||||
```js
|
||||
const locales = { ru, en: enUS, hi, dk: de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko };
|
||||
const locales = { ru, en: enUS, hi, dk: de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko }
|
||||
```
|
||||
|
||||
### Step 4
|
||||
@@ -155,11 +158,11 @@ DO NOT forget to raise a PR with the new translation file so that others can use
|
||||
|
||||
There are few variables that you you should not change,
|
||||
|
||||
- %status : This will be replaced by the status
|
||||
- %lastedFor : This will be replaced by the duration the incident lasted
|
||||
- %startedAt : This will be replaced by the time the incident started
|
||||
- %date : This will be replaced by the date
|
||||
- %duration : This will be replaced by the duration
|
||||
- %status : This will be replaced by the status
|
||||
- %lastedFor : This will be replaced by the duration the incident lasted
|
||||
- %startedAt : This will be replaced by the time the incident started
|
||||
- %date : This will be replaced by the date
|
||||
- %duration : This will be replaced by the duration
|
||||
|
||||
## Dates
|
||||
|
||||
|
||||
+38
-16
@@ -51,7 +51,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
(async function (statusCode, responseTime, responseDataBase64) {
|
||||
(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
let status = 'DOWN'
|
||||
if(statusCodeShort >=2 && statusCodeShort <= 3) {
|
||||
@@ -64,23 +64,19 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
})
|
||||
```
|
||||
|
||||
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
|
||||
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it
|
||||
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
|
||||
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
|
||||
- `responseRaw` **REQUIRED** is the raw response of the API
|
||||
- `modules` is an object that has [`cheerio`](https://www.npmjs.com/package/cheerio) that you can use to parse HTML. `const $ = modules.cheerio.load(responseRaw)`
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64)
|
||||
//if the response is a json object
|
||||
//let jsonResp = JSON.parse(decodedResp)
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
### Example 1
|
||||
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the status code is 2XX then the status is UP, if the status code is 5XX then the status is DOWN. If the response contains the word `Unknown Error` then the status is DOWN. If the response time is greater than 2000 then the status is DEGRADED.
|
||||
|
||||
```javascript
|
||||
;(async function (statusCode, responseTime, responseDataBase64) {
|
||||
const resp = atob(responseDataBase64) //convert base64 to string
|
||||
(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
|
||||
let status = "DOWN"
|
||||
|
||||
@@ -95,7 +91,7 @@ The following example shows how to use the eval function to evaluate the respons
|
||||
//if the status code is 5XX then the status is DOWN
|
||||
if (/^[5]\d{2}$/.test(statusCode)) status = "DOWN"
|
||||
|
||||
if (resp.includes("Unknown Error")) {
|
||||
if (responseRaw.includes("Unknown Error")) {
|
||||
status = "DOWN"
|
||||
}
|
||||
|
||||
@@ -106,11 +102,13 @@ The following example shows how to use the eval function to evaluate the respons
|
||||
})
|
||||
```
|
||||
|
||||
### Example 2
|
||||
|
||||
This next example shows how to call another API withing eval. It is scrapping the second last script tag from the response and checking if the heading is "No recent issues" then the status is UP else it is DOWN.
|
||||
|
||||
```javascript
|
||||
;(async function raj(statusCode, responseTime, responseDataBase64) {
|
||||
let htmlString = atob(responseDataBase64)
|
||||
```js
|
||||
(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let htmlString = responseRaw;
|
||||
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g)
|
||||
if (scriptTags && scriptTags.length >= 2) {
|
||||
// Extract the second last script tag's src attribute
|
||||
@@ -136,6 +134,30 @@ This next example shows how to call another API withing eval. It is scrapping th
|
||||
})
|
||||
```
|
||||
|
||||
### Example 3
|
||||
|
||||
The next example shows how to use cheerio to parse bitbucket status page and check if all the components are operational. If all the components are operational then the status is UP else it is DOWN.
|
||||
|
||||
```js
|
||||
(async function (statusCode, responseTime, responseDataBase64, modules) {
|
||||
let html = atob(responseDataBase64)
|
||||
const $ = modules.cheerio.load(html)
|
||||
const components = $(".components-section .components-container .component-container")
|
||||
let status = true
|
||||
components.each((index, element) => {
|
||||
const name = $(element).find(".component-name").text().trim()
|
||||
const statusText = $(element).find(".component-status").text().trim()
|
||||
if (statusText !== "Operational") {
|
||||
status = false
|
||||
}
|
||||
})
|
||||
return {
|
||||
status: status ? "UP" : "DOWN",
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Website Monitor
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Heartbeat Monitors | Kener
|
||||
description: Heartbeat monitors allow you to monitor systems using a "push" based approach
|
||||
---
|
||||
|
||||
# Heartbeat Monitors
|
||||
|
||||
Heartbeat monitors allow you to monitor systems using a "push" based approach, where your services send signals (or "heartbeats") to Kener on a regular basis. If Kener doesn't receive a heartbeat within the configured time period, it will mark the system as degraded or down.
|
||||
|
||||
This is useful for monitoring services that:
|
||||
|
||||
- Run on a schedule
|
||||
- Cannot be directly accessed for pull-based monitoring
|
||||
- Need to report their own health status
|
||||
|
||||
## How It Works
|
||||
|
||||
1. You configure a heartbeat monitor in Kener
|
||||
2. Kener provides a unique URL endpoint for your service to ping
|
||||
3. Your service sends HTTP requests to this endpoint at regular intervals
|
||||
4. If Kener doesn't receive a ping within the configured time window, it will mark the service as degraded or down
|
||||
|
||||
## Setting Up a Heartbeat Monitor
|
||||
|
||||
To create a heartbeat monitor:
|
||||
|
||||
1. Navigate to the Monitors page in the dashboard
|
||||
2. Click "Add Monitor"
|
||||
3. Select "Heartbeat" as the monitor type
|
||||
4. Configure the following settings:
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------- |
|
||||
| Secret String | A unique secret that authenticates your heartbeat requests |
|
||||
| Degraded After (Minutes) | Number of minutes after which the service is considered degraded if no heartbeat is received |
|
||||
| Down After (Minutes) | Number of minutes after which the service is considered down if no heartbeat is received |
|
||||
|
||||
## Sending Heartbeats
|
||||
|
||||
Your service needs to send HTTP requests to the Kener heartbeat URL. Both GET and POST requests are supported.
|
||||
|
||||
### Endpoint Format
|
||||
|
||||
```
|
||||
https://<your-kener-instance>/api/heartbeat/<tag>:<secretString>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<tag>` is the tag you configured for your monitor
|
||||
- `<secretString>` is the secret you configured during setup
|
||||
|
||||
### Example
|
||||
|
||||
If your monitor has:
|
||||
|
||||
- Tag: `nightly-backup`
|
||||
- Secret String: `super-secret-key`
|
||||
|
||||
Your service should send requests to:
|
||||
|
||||
```
|
||||
https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
|
||||
```
|
||||
|
||||
You can use any HTTP client to send these requests, such as curl:
|
||||
|
||||
```bash
|
||||
curl https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
|
||||
```
|
||||
|
||||
Or in a scheduled script:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Run your task here
|
||||
./perform-backup.sh
|
||||
|
||||
# Send heartbeat to Kener
|
||||
curl https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
|
||||
```
|
||||
|
||||
## Monitoring State
|
||||
|
||||
The monitor status works as follows:
|
||||
|
||||
- **UP**: Heartbeats are being received within the configured time windows
|
||||
- **DEGRADED**: No heartbeat has been received for longer than the "Degraded After" time period
|
||||
- **DOWN**: No heartbeat has been received for longer than the "Down After" time period
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Keep your secret string confidential, as anyone with knowledge of it can send heartbeats
|
||||
- Consider using HTTPS for your Kener instance to encrypt heartbeat communications
|
||||
- Rotate the secret string periodically by updating the monitor configuration
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Set the "Degraded After" time slightly longer than your expected interval between heartbeats
|
||||
- Set the "Down After" time based on your SLA requirements
|
||||
- For critical systems, consider sending heartbeats more frequently than required
|
||||
- Add logging in your service to track successful and failed heartbeat attempts
|
||||
|
||||
Heartbeat monitors provide a simple but effective way to ensure your scheduled tasks and services are running properly, without requiring external polling.
|
||||
+5
-10
@@ -32,8 +32,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
@@ -49,12 +48,9 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
})
|
||||
```
|
||||
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
|
||||
- `arrayOfPings` **REQUIRED** is an array of Ping Response Objects as shown below.
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64)
|
||||
let jsonResp = JSON.parse(decodedResp)
|
||||
console.log(jsonResp)
|
||||
```json
|
||||
/*
|
||||
[
|
||||
{
|
||||
@@ -93,7 +89,7 @@ console.log(jsonResp)
|
||||
|
||||
### Understanding the Input
|
||||
|
||||
The input to the eval function is a base64 encoded string. You will have to decode it and then parse it to get the array of objects. Each object in the array represents the ping response of a host.
|
||||
Each object in the array represents the ping response of a host.
|
||||
|
||||
- `alive` is a boolean. It is true if the host is alive and false if the host is down.
|
||||
- `min` is a string. It is the minimum latency of the pings.
|
||||
@@ -109,8 +105,7 @@ The input to the eval function is a base64 encoded string. You will have to deco
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
|
||||
|
||||
```javascript
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: SQL Monitors | Kener
|
||||
description: Monitor a SQL database
|
||||
---
|
||||
|
||||
# SQL Monitors
|
||||
|
||||
SQL monitors are used to monitor a SQL database. You can monitor a SQL database by adding the database connection details to the monitor. The SQL monitor will check the database connection and notify you if the database connection is down.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

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

|
||||
|
||||
</div>
|
||||
|
||||
## Host
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The host is the domain name of the website you want to monitor. The host should be a valid domain name.
|
||||
|
||||
## Port
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The port is the ssl port of the website you want to monitor. The default ssl port is 443.
|
||||
|
||||
## Degraded If
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
Set this to the number of hours that still remains for the certificate to expire. If the certificate is about to expire in the given number of hours, the monitor status will be set to degraded. Example if you send the value to be 24, the monitor status will be set to degraded if the certificate is about to expire in 24 hours.
|
||||
|
||||
## Down If
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
Set this to the number of hours that still remains for the certificate to expire. If the certificate is about to expire in the given number of hours, the monitor status will be set to down. Example if you send the value to be 12, the monitor status will be set to down if the certificate is about to expire in 12 hours.
|
||||
|
||||
<div class="note">
|
||||
|
||||
`Degraded If` should be more than `Down If`.
|
||||
|
||||
</div>
|
||||
+18
-23
@@ -5,7 +5,7 @@ description: Learn how to set up and work with TCP monitors in kener.
|
||||
|
||||
# TCP Monitors
|
||||
|
||||
TCP monitors are used to monitor the livenees of your servers. You can use TCP monitors to monitor the uptime of your servers and get notified when they are down.
|
||||
TCP monitors are used to monitor the liveness of your servers. You can use TCP monitors to monitor the uptime of your servers and get notified when they are down.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||
@@ -17,10 +17,10 @@ TCP monitors are used to monitor the livenees of your servers. You can use TCP m
|
||||
|
||||
You can add as many hosts as you want to monitor. The host can be an IP(IP4 and IP6) address or a domain name.
|
||||
|
||||
- Type: Choose the type of host you want to monitor. It can be either `IP4` or `IP6` or `DOMAIN`.
|
||||
- Host: Enter the IP address or domain name of the host you want to monitor.
|
||||
- Port: Enter the port number of the host you want to monitor.
|
||||
- Timeout: Enter the timeout in milliseconds for each ping request of each host
|
||||
- Type: Choose the type of host you want to monitor. It can be either `IP4` or `IP6` or `DOMAIN`.
|
||||
- Host: Enter the IP address or domain name of the host you want to monitor.
|
||||
- Port: Enter the port number of the host you want to monitor.
|
||||
- Timeout: Enter the timeout in milliseconds for each ping request of each host
|
||||
|
||||
## Eval
|
||||
|
||||
@@ -32,8 +32,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
@@ -53,12 +52,9 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
})
|
||||
```
|
||||
|
||||
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
|
||||
- `arrayOfPings` **REQUIRED** is an array of TCP Response Objects as shown below.
|
||||
|
||||
```js
|
||||
let decodedResp = atob(responseDataBase64)
|
||||
let jsonResp = JSON.parse(decodedResp)
|
||||
console.log(jsonResp)
|
||||
```json
|
||||
/*
|
||||
[
|
||||
{
|
||||
@@ -88,24 +84,23 @@ console.log(jsonResp)
|
||||
|
||||
### Understanding the Input
|
||||
|
||||
The input to the eval function is a base64 encoded string. You will have to decode it and then parse it to get the array of objects. Each object in the array represents the ping response of a host.
|
||||
Each object in the array represents the tcp response of a host.
|
||||
|
||||
- `host`: The host that was pinged.
|
||||
- `port`: The port that was pinged. Defaults to 80 if not provided.
|
||||
- `type`: The type of IP address. Can be `IP4` or `IP6`.
|
||||
- `status`: The status of the ping. Can be `open` , `error` or `timeout`.
|
||||
- `open`: The host is reachable.
|
||||
- `error`: There was an error while pinging the host.
|
||||
- `timeout`: The host did not respond in time.
|
||||
- `latency`: The time taken to ping the host. This is in milliseconds.
|
||||
- `host`: The host that was pinged.
|
||||
- `port`: The port that was pinged. Defaults to 80 if not provided.
|
||||
- `type`: The type of IP address. Can be `IP4` or `IP6`.
|
||||
- `status`: The status of the ping. Can be `open` , `error` or `timeout`.
|
||||
- `open`: The host is reachable.
|
||||
- `error`: There was an error while pinging the host.
|
||||
- `timeout`: The host did not respond in time.
|
||||
- `latency`: The time taken to ping the host. This is in milliseconds.
|
||||
|
||||
### Example
|
||||
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
|
||||
|
||||
```javascript
|
||||
;(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64))
|
||||
(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
|
||||
+21
-10
@@ -109,15 +109,15 @@ To monitor the ping, you need to provide the IP address or the domain name. If t
|
||||
|
||||
To add a trigger to a monitor make sure you have created a trigger. You can read more about triggers [here](/docs/triggers).
|
||||
|
||||
- Click on the 🔔 icon on the top right corner of the monitor.
|
||||
- Add details for either DOWN or DEGRADED.
|
||||
- Failure Threshold(Required): The number of consecutive failures before the trigger is activated.
|
||||
- Success Threshold(Required): The number of consecutive successes before the trigger is deactivated.
|
||||
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in Github. So make sure you have set up the Github token in the environment variables. The incident will be closed when the monitor is back to UP.
|
||||
- Severity (Required): The severity of the incident. It can be `Critcal` or `Warning`.
|
||||
- Custom Message (Required): Add your owner alert message.
|
||||
- Choose Triggers: Choose the triggers you want to activate the trigger. You can choose multiple triggers.
|
||||
- It will take upto 1 minute for the trigger to be activated.
|
||||
- Click on the 🔔 icon on the top right corner of the monitor.
|
||||
- Add details for either DOWN or DEGRADED.
|
||||
- Failure Threshold(Required): The number of consecutive failures before the trigger is activated.
|
||||
- Success Threshold(Required): The number of consecutive successes before the trigger is deactivated.
|
||||
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in Github. So make sure you have set up the Github token in the environment variables. The incident will be closed when the monitor is back to UP.
|
||||
- Severity (Required): The severity of the incident. It can be `Critcal` or `Warning`.
|
||||
- Custom Message (Required): Add your owner alert message.
|
||||
- Choose Triggers: Choose the triggers you want to activate the trigger. You can choose multiple triggers.
|
||||
- It will take upto 1 minute for the trigger to be activated.
|
||||
|
||||
---
|
||||
|
||||
@@ -135,6 +135,17 @@ You can activate the monitor by switching the toggle to on. Activation a monitor
|
||||
|
||||
<div class="note info">
|
||||
|
||||
Any changes in the a live monitor will take one minute to reflect.
|
||||
Any changes in the a live monitor will take 2 minutes to reflect.
|
||||
|
||||
</div>
|
||||
|
||||
## Supported Monitors
|
||||
|
||||
- [API Monitors](/docs/monitors-api)
|
||||
- [DNS Monitors](/docs/monitors-dns)
|
||||
- [PING Monitors](/docs/monitors-ping)
|
||||
- [TCP Monitors](/docs/monitors-tcp)
|
||||
- [SQL Monitors](/docs/monitors-sql)
|
||||
- [SSL Monitors](/docs/monitors-ssl)
|
||||
- [HEARTBEAT Monitors](/docs/monitors-heartbeat)
|
||||
- [GROUP Monitors](/docs/monitors-group)
|
||||
|
||||
+18
-12
@@ -9,9 +9,9 @@ Here is a demonstration of how to get started with Kener in seconds
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js Minimum version required is `v18`.
|
||||
- Git
|
||||
- sqlite3
|
||||
- Node.js Minimum version required is `v18`.
|
||||
- Git
|
||||
- sqlite3
|
||||
|
||||
## Clone the repository
|
||||
|
||||
@@ -54,9 +54,9 @@ Status Page Manage Portal will be at [http://localhost:3000/manage/app/site](htt
|
||||
|
||||
If this is the first time your are launching kener then you would be redirected to the [set up page](/manage/setup). Fill in the details and click on `Let's Go` button.
|
||||
|
||||
- **Name**: Your Name
|
||||
- **Email**: Your Email
|
||||
- **Password**: Your Password
|
||||
- **Name**: Your Name
|
||||
- **Email**: Your Email
|
||||
- **Password**: Your Password
|
||||
|
||||
Please note that the email should be a valid email address and password should be atleast 8 characters long with uppercase lowercase and numbers.
|
||||
|
||||
@@ -64,13 +64,19 @@ Please note that the email should be a valid email address and password should b
|
||||
|
||||
Once you have created the user, you can login with the credentials you have provided by going to the [login page](/manage/signin)
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
You can watch the video tutorial on how to get started with Kener
|
||||
|
||||
[](https://www.youtube.com/watch?v=4L9PnWPqMPw)
|
||||
|
||||
## Next Steps
|
||||
|
||||
Learn how to configure kener by going through one of the topics
|
||||
|
||||
- [Monitors](/docs/monitors): Learn how to set up and work with monitors in kener.
|
||||
- [Triggers](/docs/triggers): Learn how to set up and work with triggers in kener.
|
||||
- [Environment Variables](/docs/environment-vars): Learn how to set up and work with environment variables in kener.
|
||||
- [API](/docs/kener-apis): Learn how to use the API in kener.
|
||||
- [Databases](/docs/database): Learn how to set up and work with databases in kener.
|
||||
- [Theme](/docs/theme): Learn how to set up and work with theme in kener.
|
||||
- [Monitors](/docs/monitors): Learn how to set up and work with monitors in kener.
|
||||
- [Triggers](/docs/triggers): Learn how to set up and work with triggers in kener.
|
||||
- [Environment Variables](/docs/environment-vars): Learn how to set up and work with environment variables in kener.
|
||||
- [API](/docs/kener-apis): Learn how to use the API in kener.
|
||||
- [Databases](/docs/database): Learn how to set up and work with databases in kener.
|
||||
- [Theme](/docs/theme): Learn how to set up and work with theme in kener.
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Kener Role Based Access Control (RBAC)
|
||||
description: Role-Based Access Control (RBAC) in Kener
|
||||
---
|
||||
|
||||
# Role-Based Access Control (RBAC) in Kener
|
||||
|
||||
Kener includes a comprehensive role-based access control system that allows you to manage user permissions and access to various features. This document explains the available roles, their permissions, and how to manage users effectively.
|
||||
|
||||
## Available Roles
|
||||
|
||||
Kener offers three different roles with varying levels of permissions:
|
||||
|
||||
| Role | Description |
|
||||
| ------ | ------------------------------------------------------------------------------ |
|
||||
| Admin | Full access to all features including user management and system configuration |
|
||||
| Editor | Can create and edit monitors, triggers, incidents, and other operational data |
|
||||
| Member | Read-only access with limited interaction capabilities |
|
||||
|
||||
## Role Permissions
|
||||
|
||||
### Admin
|
||||
|
||||
Admins have unrestricted access to the entire system:
|
||||
|
||||
- **User Management**:
|
||||
|
||||
- Create, update, and deactivate users
|
||||
- Change user roles
|
||||
- Reset passwords for other users
|
||||
- Send verification emails
|
||||
|
||||
- **System Configuration**:
|
||||
|
||||
- Configure all site settings
|
||||
- Manage API keys
|
||||
- Set up triggers and integrations
|
||||
|
||||
- **Operational Access**:
|
||||
- Full access to create and manage monitors
|
||||
- Create and update incidents
|
||||
- Run tests on monitors and triggers
|
||||
- Configure all notification channels
|
||||
|
||||
### Editor
|
||||
|
||||
Editors can manage most operational aspects but cannot administer users:
|
||||
|
||||
- **Content Management**:
|
||||
|
||||
- Create and edit monitors
|
||||
- Configure monitor settings
|
||||
- Create and manage triggers
|
||||
- Create and update incidents
|
||||
- Add incident updates and comments
|
||||
|
||||
- **Limited Access**:
|
||||
- Cannot manage users
|
||||
- Cannot change role assignments
|
||||
- Cannot access certain system-level configurations
|
||||
|
||||
### Member
|
||||
|
||||
Members have read-only access with minimal interaction capabilities:
|
||||
|
||||
- **View Access**:
|
||||
|
||||
- View all monitors and their status
|
||||
- View incidents and their history
|
||||
- See system configuration (but cannot modify)
|
||||
|
||||
- **Limited Interactions**:
|
||||
- Can test existing triggers but cannot create or edit them
|
||||
- Cannot create or update incidents
|
||||
- Cannot modify any system configuration
|
||||
|
||||
## Managing Users
|
||||
|
||||
### Adding New Users
|
||||
|
||||
Only Admins and Editors can add new users to the system:
|
||||
|
||||
1. Navigate to the **Users** page in the management dashboard
|
||||
2. Click the **Add New User** button
|
||||
3. Fill in the required information:
|
||||
- Name
|
||||
- Email
|
||||
- Password
|
||||
- Role (Member or Editor)
|
||||
4. Click **Add User** to create the account
|
||||
|
||||
When a new user is added, a verification email can be sent to confirm their email address if email sending is configured.
|
||||
|
||||
### User Settings
|
||||
|
||||
Admins can manage user accounts through the user settings page:
|
||||
|
||||
- **Email Verification**: Send verification emails to users
|
||||
- **Password Reset**: Reset a user's password
|
||||
- **Role Management**: Change a user's role between Member and Editor
|
||||
- **Account Status**: Activate or deactivate user accounts
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Follow the Principle of Least Privilege**:
|
||||
|
||||
- Assign the minimum necessary permissions for users to perform their job
|
||||
- Start users with the Member role and elevate as needed
|
||||
|
||||
2. **Regular Access Reviews**:
|
||||
|
||||
- Periodically review user access and roles
|
||||
- Remove access for users who no longer need it
|
||||
|
||||
3. **Admin Accounts**:
|
||||
- Limit the number of Admin accounts
|
||||
- Use strong passwords for Admin accounts
|
||||
- Consider using email verification for all users
|
||||
|
||||
## Role Limitations
|
||||
|
||||
- The Admin role can only be assigned during initial setup
|
||||
- Members cannot create or modify content
|
||||
- Users cannot modify their own role (only an Admin can change roles)
|
||||
|
||||
## Email Verification
|
||||
|
||||
When email sending is configured, users can verify their email addresses:
|
||||
|
||||
1. Admins can send verification emails from the user management interface
|
||||
2. Users receive an email with a verification link
|
||||
3. After clicking the link, the user's email is marked as verified
|
||||
|
||||
Email verification improves security and ensures that users have provided valid email addresses for notifications.
|
||||
+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
|
||||
|
||||
|
||||
+51
-25
@@ -28,6 +28,11 @@
|
||||
"title": "Databases",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
},
|
||||
{
|
||||
"title": "Access Control",
|
||||
"link": "/docs/rbac",
|
||||
"file": "/rbac.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -49,31 +54,7 @@
|
||||
"link": "/docs/monitors",
|
||||
"file": "/monitors.md"
|
||||
},
|
||||
{
|
||||
"title": "API/Website Monitor",
|
||||
"link": "/docs/monitors-api",
|
||||
"file": "/monitors-api.md"
|
||||
},
|
||||
{
|
||||
"title": "Ping Monitor",
|
||||
"link": "/docs/monitors-ping",
|
||||
"file": "/monitors-ping.md"
|
||||
},
|
||||
{
|
||||
"title": "TCP Monitor",
|
||||
"link": "/docs/monitors-tcp",
|
||||
"file": "/monitors-tcp.md"
|
||||
},
|
||||
{
|
||||
"title": "DNS Monitor",
|
||||
"link": "/docs/monitors-dns",
|
||||
"file": "/monitors-dns.md"
|
||||
},
|
||||
{
|
||||
"title": "Group Monitor",
|
||||
"link": "/docs/monitors-group",
|
||||
"file": "/monitors-group.md"
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Setup Triggers",
|
||||
"link": "/docs/triggers",
|
||||
@@ -132,6 +113,51 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "Monitors",
|
||||
"children": [
|
||||
{
|
||||
"title": "API/Website Monitor",
|
||||
"link": "/docs/monitors-api",
|
||||
"file": "/monitors-api.md"
|
||||
},
|
||||
{
|
||||
"title": "Ping Monitor",
|
||||
"link": "/docs/monitors-ping",
|
||||
"file": "/monitors-ping.md"
|
||||
},
|
||||
{
|
||||
"title": "TCP Monitor",
|
||||
"link": "/docs/monitors-tcp",
|
||||
"file": "/monitors-tcp.md"
|
||||
},
|
||||
{
|
||||
"title": "DNS Monitor",
|
||||
"link": "/docs/monitors-dns",
|
||||
"file": "/monitors-dns.md"
|
||||
},
|
||||
{
|
||||
"title": "Group Monitor",
|
||||
"link": "/docs/monitors-group",
|
||||
"file": "/monitors-group.md"
|
||||
},
|
||||
{
|
||||
"title": "SSL Monitor",
|
||||
"link": "/docs/monitors-ssl",
|
||||
"file": "/monitors-ssl.md"
|
||||
},
|
||||
{
|
||||
"title": "SQL Monitor",
|
||||
"link": "/docs/monitors-sql",
|
||||
"file": "/monitors-sql.md"
|
||||
},
|
||||
{
|
||||
"title": "Heartbeat Monitor",
|
||||
"link": "/docs/monitors-heartbeat",
|
||||
"file": "/monitors-heartbeat.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sectionTitle": "API Reference",
|
||||
"children": [
|
||||
|
||||
+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-bitbuckettcp/js?bgc=212226&locale=en&theme=dark&monitor=http://localhost:3000/embed/monitor-bitbuckettcp"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+18
-17
@@ -9,28 +9,29 @@ const databaseType = databaseURLParts[0];
|
||||
const databasePath = databaseURLParts[1];
|
||||
|
||||
const knexOb = {
|
||||
migrations: {
|
||||
directory: "./migrations"
|
||||
},
|
||||
seeds: {
|
||||
directory: "./seeds"
|
||||
}
|
||||
migrations: {
|
||||
directory: "./migrations",
|
||||
},
|
||||
seeds: {
|
||||
directory: "./seeds",
|
||||
},
|
||||
databaseType,
|
||||
};
|
||||
if (databaseType === "sqlite") {
|
||||
knexOb.client = "better-sqlite3";
|
||||
knexOb.connection = {
|
||||
filename: databasePath
|
||||
};
|
||||
knexOb.useNullAsDefault = true;
|
||||
knexOb.client = "better-sqlite3";
|
||||
knexOb.connection = {
|
||||
filename: databasePath,
|
||||
};
|
||||
knexOb.useNullAsDefault = true;
|
||||
} else if (databaseType === "postgresql") {
|
||||
knexOb.client = "pg";
|
||||
knexOb.connection = databaseURL;
|
||||
knexOb.client = "pg";
|
||||
knexOb.connection = databaseURL;
|
||||
} else if (databaseType === "mysql") {
|
||||
knexOb.client = "mysql2";
|
||||
knexOb.connection = databaseURL;
|
||||
knexOb.client = "mysql2";
|
||||
knexOb.connection = databaseURL;
|
||||
} else {
|
||||
console.error("Invalid database type");
|
||||
process.exit(1);
|
||||
console.error("Invalid database type");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export default knexOb;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export function up(knex) {
|
||||
return knex.schema.alterTable("incidents", function (table) {
|
||||
table.text("incident_source").defaultTo("DASHBOARD");
|
||||
});
|
||||
}
|
||||
|
||||
export function down(knex) {
|
||||
return knex.schema.alterTable("incidents", function (table) {
|
||||
table.dropColumn("incident_source");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
export function up(knex) {
|
||||
return knex.schema.createTable("invitations", (table) => {
|
||||
// Primary key
|
||||
table.increments("id").primary();
|
||||
|
||||
// Core invitation fields
|
||||
table.string("invitation_token").unique().notNullable();
|
||||
table.string("invitation_type").notNullable();
|
||||
table.integer("invited_user_id").nullable();
|
||||
table.integer("invited_by_user_id").notNullable();
|
||||
|
||||
// Additional data fields
|
||||
table.text("invitation_meta").nullable(); // For storing JSON or other metadata
|
||||
table.timestamp("invitation_expiry").notNullable();
|
||||
table.string("invitation_status").notNullable().defaultTo("PENDING");
|
||||
|
||||
// Timestamps
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Indexes
|
||||
table.index("invitation_status");
|
||||
table.index("invitation_expiry");
|
||||
table.index(["invited_by_user_id", "invitation_status"]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
export function down(knex) {
|
||||
return knex.schema.dropTableIfExists("invitations");
|
||||
}
|
||||
Generated
+567
-46
@@ -1,26 +1,33 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.0.12",
|
||||
"version": "3.2.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kener",
|
||||
"version": "3.0.12",
|
||||
"version": "3.2.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@number-flow/svelte": "^0.2.1",
|
||||
"@scalar/express-api-reference": "^0.4.167",
|
||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||
"analytics": "^0.8.14",
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"bits-ui": "^0.9.9",
|
||||
"cheerio": "^1.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"croner": "^7.0.5",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dns2": "^2.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
@@ -29,11 +36,12 @@
|
||||
"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",
|
||||
@@ -44,9 +52,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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
@@ -58,7 +68,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",
|
||||
@@ -174,10 +183,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
|
||||
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
|
||||
"dev": true,
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
|
||||
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
@@ -186,6 +194,158 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
|
||||
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz",
|
||||
"integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/css": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
|
||||
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/css": "^1.1.0",
|
||||
"@lezer/html": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz",
|
||||
"integrity": "sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
|
||||
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.2.tgz",
|
||||
"integrity": "sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz",
|
||||
"integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.1.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz",
|
||||
"integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz",
|
||||
"integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.36.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz",
|
||||
"integrity": "sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
@@ -193,7 +353,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -210,7 +369,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -227,7 +385,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -244,7 +401,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -261,7 +417,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -278,7 +433,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -295,7 +449,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -312,7 +465,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -329,7 +481,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -346,7 +497,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -363,7 +513,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -380,7 +529,6 @@
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -397,7 +545,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -414,7 +561,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -431,7 +577,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -448,7 +593,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -465,7 +609,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -482,7 +625,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -499,7 +641,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -516,7 +657,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -533,7 +673,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -550,7 +689,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -754,6 +892,84 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
|
||||
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.11.tgz",
|
||||
"integrity": "sha512-FuAnusbLBl1SEAtfN8NdShxYJiESKw9LAFysfea1T96jD3ydBn12oYjaSG1a04BQRIUd93/0D8e5CV1cUMkmQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
|
||||
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
|
||||
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.4.21",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
|
||||
"integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
|
||||
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.2.tgz",
|
||||
"integrity": "sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
@@ -774,6 +990,12 @@
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@melt-ui/svelte": {
|
||||
"version": "0.61.2",
|
||||
"resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.61.2.tgz",
|
||||
@@ -1202,6 +1424,37 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@uiw/codemirror-theme-github": {
|
||||
"version": "4.23.10",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.23.10.tgz",
|
||||
"integrity": "sha512-jTg2sHAcU1d+8x0O+EBDI71rtJ8PWKIW8gzy+SW4wShQTAdsqGHk5y1ynt3KIeoaUkqngLqAK4SkhPaUKlqZqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@uiw/codemirror-themes": "4.23.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-themes": {
|
||||
"version": "4.23.10",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.23.10.tgz",
|
||||
"integrity": "sha512-dU0UgEEgEXCAYpxuVDQ6fovE82XsqgHZckTJOH6Bs8xCi3Z7dwBKO4pXuiA8qGDwTOXOMjSzfi+pRViDm7OfWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/language": ">=6.0.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@unhead/schema": {
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.18.tgz",
|
||||
@@ -1523,9 +1776,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -1700,6 +1953,12 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
@@ -1929,6 +2188,76 @@
|
||||
"binary-search": "^1.3.5"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
|
||||
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cheerio-select": "^2.1.0",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"encoding-sniffer": "^0.2.0",
|
||||
"htmlparser2": "^9.1.0",
|
||||
"parse5": "^7.1.2",
|
||||
"parse5-htmlparser2-tree-adapter": "^7.0.0",
|
||||
"parse5-parser-stream": "^7.1.2",
|
||||
"undici": "^6.19.5",
|
||||
"whatwg-mimetype": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/htmlparser2": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"entities": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/undici": {
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -2017,6 +2346,22 @@
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
|
||||
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2189,6 +2534,12 @@
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/croner": {
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/croner/-/croner-7.0.8.tgz",
|
||||
@@ -2247,6 +2598,22 @@
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
|
||||
"integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling=="
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
@@ -2266,6 +2633,18 @@
|
||||
"integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -2339,6 +2718,15 @@
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns-tz": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
|
||||
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"date-fns": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-picker-svelte": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/date-picker-svelte/-/date-picker-svelte-2.15.1.tgz",
|
||||
@@ -2686,6 +3074,31 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding-sniffer": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
|
||||
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.3",
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding-sniffer/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
@@ -2854,7 +3267,6 @@
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -4639,12 +5051,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-svelte": {
|
||||
"version": "0.292.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.292.0.tgz",
|
||||
"integrity": "sha512-bnTpg9pbm6pQDc+YiLK2yxtRFk2Cc+hbzwjAPaV85k56x10CJ9LsXjon6wRrlNTSdxJR7GOsRjz0A5ZNu3Z7dg==",
|
||||
"version": "0.483.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.483.0.tgz",
|
||||
"integrity": "sha512-MyMgEVLlFfPbyodGpkB+KCpyPkpjI7EKiFw1crA92B1ZXRK5hq5vTsGWAm9Nt3GAKHunoNc5MVsq3EOCz0DZSQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"svelte": ">=3 <5"
|
||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
@@ -4966,7 +5378,6 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
@@ -5376,6 +5787,18 @@
|
||||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/number-flow": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/number-flow/-/number-flow-0.4.2.tgz",
|
||||
@@ -5501,6 +5924,43 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
|
||||
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^4.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
|
||||
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.3",
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-parser-stream": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
|
||||
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseley": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||
@@ -6148,9 +6608,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -6435,7 +6895,6 @@
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
@@ -6582,7 +7041,6 @@
|
||||
"version": "3.29.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
|
||||
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@@ -7395,6 +7853,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
|
||||
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sucrase": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||
@@ -7605,6 +8069,16 @@
|
||||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-codemirror-editor": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-codemirror-editor/-/svelte-codemirror-editor-1.4.1.tgz",
|
||||
"integrity": "sha512-Pv350iro0Y/AZTT/y2OLaonheQqAwl50Hdfipa2Jv1Z04TSP5kPUyxQnRjqxeRW7DXOX9s5Nd11tHdBl9iYSzw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"codemirror": "^6.0.0",
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-dnd-action": {
|
||||
"version": "0.9.57",
|
||||
"resolved": "https://registry.npmjs.org/svelte-dnd-action/-/svelte-dnd-action-0.9.57.tgz",
|
||||
@@ -8230,7 +8704,6 @@
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz",
|
||||
"integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
@@ -8282,6 +8755,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-package-version": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.1.0.tgz",
|
||||
"integrity": "sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vite": ">=2.0.0-beta.69"
|
||||
}
|
||||
},
|
||||
"node_modules/vitefu": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
|
||||
@@ -8297,12 +8779,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
|
||||
+13
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.1.7",
|
||||
"version": "3.2.11",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
|
||||
@@ -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,18 +67,25 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@number-flow/svelte": "^0.2.1",
|
||||
"@scalar/express-api-reference": "^0.4.167",
|
||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||
"analytics": "^0.8.14",
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"bits-ui": "^0.9.9",
|
||||
"cheerio": "^1.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"croner": "^7.0.5",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dns2": "^2.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
@@ -88,11 +94,12 @@
|
||||
"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",
|
||||
@@ -103,9 +110,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"
|
||||
|
||||
+3
-2
@@ -18,9 +18,10 @@ main {
|
||||
text-decoration: none;
|
||||
background-color: var(--bg-background);
|
||||
}
|
||||
.kener-home-links > div:hover {
|
||||
.kener-home-links > a:hover {
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 0 8px 1.5px #3e9a4b;
|
||||
border-color: #ed702d;
|
||||
background-color: #ed702d;
|
||||
}
|
||||
|
||||
.accm input:checked ~ div {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+85
-5
@@ -41,7 +41,8 @@
|
||||
.squares-pattern::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-image: linear-gradient(#444cf7 1px, transparent 1px),
|
||||
background-image:
|
||||
linear-gradient(#444cf7 1px, transparent 1px),
|
||||
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
180deg,
|
||||
@@ -87,7 +88,8 @@
|
||||
rgba(0, 0, 0, 0.03125) 80%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
background-image: linear-gradient(#616fbf 1px, transparent 1px),
|
||||
background-image:
|
||||
linear-gradient(#616fbf 1px, transparent 1px),
|
||||
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
|
||||
}
|
||||
|
||||
@@ -192,6 +194,12 @@ section {
|
||||
border-bottom: 1px solid;
|
||||
border-color: hsl(var(--border) / var(--tw-border-opacity));
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.monitors-card .monitor {
|
||||
padding: 1.2em 1em 0.875em 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.monitors-card .monitor:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -470,7 +478,8 @@ textarea::placeholder {
|
||||
}
|
||||
|
||||
.mesh {
|
||||
background-image: linear-gradient(
|
||||
background-image:
|
||||
linear-gradient(
|
||||
323deg,
|
||||
rgba(255, 255, 255, 0.01) 0%,
|
||||
rgba(255, 255, 255, 0.01) 16.667%,
|
||||
@@ -601,7 +610,8 @@ textarea::placeholder {
|
||||
}
|
||||
|
||||
.dark .mesh {
|
||||
background-image: linear-gradient(
|
||||
background-image:
|
||||
linear-gradient(
|
||||
158deg,
|
||||
rgba(84, 84, 84, 0.03) 0%,
|
||||
rgba(84, 84, 84, 0.03) 20%,
|
||||
@@ -815,7 +825,8 @@ textarea::placeholder {
|
||||
}
|
||||
|
||||
.button-77:active:not(:disabled):after {
|
||||
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
|
||||
background-image:
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
|
||||
linear-gradient(92.83deg, #ff7426 0, #f93a13 100%);
|
||||
bottom: 4px;
|
||||
left: 4px;
|
||||
@@ -866,3 +877,72 @@ textarea::placeholder {
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
}
|
||||
.dots-animation::after {
|
||||
content: ".";
|
||||
animation: dots 1.5s steps(4) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0% {
|
||||
content: ".";
|
||||
}
|
||||
16% {
|
||||
content: "..";
|
||||
}
|
||||
32% {
|
||||
content: "...";
|
||||
}
|
||||
48% {
|
||||
content: "....";
|
||||
}
|
||||
64% {
|
||||
content: "...";
|
||||
}
|
||||
80% {
|
||||
content: "..";
|
||||
}
|
||||
100% {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,194 @@
|
||||
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 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>`;
|
||||
+13
-10
@@ -1,13 +1,16 @@
|
||||
// @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
|
||||
event = "kener:" + event;
|
||||
if (typeof document !== "undefined") {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("analyticsEvent", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
event,
|
||||
data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+248
-245
@@ -1,288 +1,291 @@
|
||||
// @ts-nocheck
|
||||
import { base } from "$app/paths";
|
||||
function siteDataExtractFromDb(data, obj) {
|
||||
let requestedObject = { ...obj };
|
||||
for (const key in requestedObject) {
|
||||
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
|
||||
const element = data[key];
|
||||
if (data[key]) {
|
||||
requestedObject[key] = data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
//remove any keys that are still null or empty
|
||||
for (const key in requestedObject) {
|
||||
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
|
||||
const element = requestedObject[key];
|
||||
if (element === null || element === "") {
|
||||
delete requestedObject[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return requestedObject;
|
||||
let requestedObject = { ...obj };
|
||||
for (const key in requestedObject) {
|
||||
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
|
||||
const element = data[key];
|
||||
if (data[key]) {
|
||||
requestedObject[key] = data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
//remove any keys that are still null or empty
|
||||
for (const key in requestedObject) {
|
||||
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
|
||||
const element = requestedObject[key];
|
||||
if (element === null || element === "") {
|
||||
delete requestedObject[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return requestedObject;
|
||||
}
|
||||
|
||||
//a function to make an api call to /manage/api/ to store site data
|
||||
function storeSiteData(data) {
|
||||
return fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ action: "storeSiteData", data })
|
||||
});
|
||||
return fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ action: "storeSiteData", data }),
|
||||
});
|
||||
}
|
||||
const allRecordTypes = {
|
||||
A: 1,
|
||||
NS: 2,
|
||||
MD: 3,
|
||||
MF: 4,
|
||||
CNAME: 5,
|
||||
SOA: 6,
|
||||
MB: 7,
|
||||
MG: 8,
|
||||
MR: 9,
|
||||
NULL: 10,
|
||||
WKS: 11,
|
||||
PTR: 12,
|
||||
HINFO: 13,
|
||||
MINFO: 14,
|
||||
MX: 15,
|
||||
TXT: 16,
|
||||
RP: 17,
|
||||
AFSDB: 18,
|
||||
X25: 19,
|
||||
ISDN: 20,
|
||||
RT: 21,
|
||||
NSAP: 22,
|
||||
NSAP_PTR: 23,
|
||||
SIG: 24,
|
||||
KEY: 25,
|
||||
PX: 26,
|
||||
GPOS: 27,
|
||||
AAAA: 28,
|
||||
LOC: 29,
|
||||
NXT: 30,
|
||||
EID: 31,
|
||||
NIMLOC: 32,
|
||||
SRV: 33,
|
||||
ATMA: 34,
|
||||
NAPTR: 35,
|
||||
KX: 36,
|
||||
CERT: 37,
|
||||
A6: 38,
|
||||
DNAME: 39,
|
||||
SINK: 40,
|
||||
OPT: 41,
|
||||
APL: 42,
|
||||
DS: 43,
|
||||
SSHFP: 44,
|
||||
IPSECKEY: 45,
|
||||
RRSIG: 46,
|
||||
NSEC: 47,
|
||||
DNSKEY: 48,
|
||||
DHCID: 49,
|
||||
NSEC3: 50,
|
||||
NSEC3PARAM: 51,
|
||||
TLSA: 52,
|
||||
SMIMEA: 53,
|
||||
HIP: 55,
|
||||
NINFO: 56,
|
||||
RKEY: 57,
|
||||
TALINK: 58,
|
||||
CDS: 59,
|
||||
CDNSKEY: 60,
|
||||
OPENPGPKEY: 61,
|
||||
CSYNC: 62,
|
||||
SPF: 99,
|
||||
UINFO: 100,
|
||||
UID: 101,
|
||||
GID: 102,
|
||||
UNSPEC: 103,
|
||||
NID: 104,
|
||||
L32: 105,
|
||||
L64: 106,
|
||||
LP: 107,
|
||||
EUI48: 108,
|
||||
EUI64: 109,
|
||||
TKEY: 249,
|
||||
TSIG: 250,
|
||||
IXFR: 251,
|
||||
AXFR: 252,
|
||||
MAILB: 253,
|
||||
MAILA: 254,
|
||||
ANY: 255
|
||||
A: 1,
|
||||
NS: 2,
|
||||
MD: 3,
|
||||
MF: 4,
|
||||
CNAME: 5,
|
||||
SOA: 6,
|
||||
MB: 7,
|
||||
MG: 8,
|
||||
MR: 9,
|
||||
NULL: 10,
|
||||
WKS: 11,
|
||||
PTR: 12,
|
||||
HINFO: 13,
|
||||
MINFO: 14,
|
||||
MX: 15,
|
||||
TXT: 16,
|
||||
RP: 17,
|
||||
AFSDB: 18,
|
||||
X25: 19,
|
||||
ISDN: 20,
|
||||
RT: 21,
|
||||
NSAP: 22,
|
||||
NSAP_PTR: 23,
|
||||
SIG: 24,
|
||||
KEY: 25,
|
||||
PX: 26,
|
||||
GPOS: 27,
|
||||
AAAA: 28,
|
||||
LOC: 29,
|
||||
NXT: 30,
|
||||
EID: 31,
|
||||
NIMLOC: 32,
|
||||
SRV: 33,
|
||||
ATMA: 34,
|
||||
NAPTR: 35,
|
||||
KX: 36,
|
||||
CERT: 37,
|
||||
A6: 38,
|
||||
DNAME: 39,
|
||||
SINK: 40,
|
||||
OPT: 41,
|
||||
APL: 42,
|
||||
DS: 43,
|
||||
SSHFP: 44,
|
||||
IPSECKEY: 45,
|
||||
RRSIG: 46,
|
||||
NSEC: 47,
|
||||
DNSKEY: 48,
|
||||
DHCID: 49,
|
||||
NSEC3: 50,
|
||||
NSEC3PARAM: 51,
|
||||
TLSA: 52,
|
||||
SMIMEA: 53,
|
||||
HIP: 55,
|
||||
NINFO: 56,
|
||||
RKEY: 57,
|
||||
TALINK: 58,
|
||||
CDS: 59,
|
||||
CDNSKEY: 60,
|
||||
OPENPGPKEY: 61,
|
||||
CSYNC: 62,
|
||||
SPF: 99,
|
||||
UINFO: 100,
|
||||
UID: 101,
|
||||
GID: 102,
|
||||
UNSPEC: 103,
|
||||
NID: 104,
|
||||
L32: 105,
|
||||
L64: 106,
|
||||
LP: 107,
|
||||
EUI48: 108,
|
||||
EUI64: 109,
|
||||
TKEY: 249,
|
||||
TSIG: 250,
|
||||
IXFR: 251,
|
||||
AXFR: 252,
|
||||
MAILB: 253,
|
||||
MAILA: 254,
|
||||
ANY: 255,
|
||||
};
|
||||
const ValidateIpAddress = function (input) {
|
||||
// Check if input is a valid IPv4 address with an optional port
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(input)) {
|
||||
return "IP4";
|
||||
}
|
||||
// Check if input is a valid IPv4 address with an optional port
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(input)) {
|
||||
return "IP4";
|
||||
}
|
||||
|
||||
// Improved IPv6 regex that better handles compressed notation
|
||||
const ipv6Regex =
|
||||
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){0,7}:|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){6}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|(?:[0-9a-fA-F]{1,4}:){1,7}(?::|:[0-9a-fA-F]{1,4})|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$/;
|
||||
if (ipv6Regex.test(input)) {
|
||||
return "IP6";
|
||||
}
|
||||
// Improved IPv6 regex that better handles compressed notation
|
||||
const ipv6Regex =
|
||||
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){0,7}:|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){6}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|(?:[0-9a-fA-F]{1,4}:){1,7}(?::|:[0-9a-fA-F]{1,4})|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$/;
|
||||
if (ipv6Regex.test(input)) {
|
||||
return "IP6";
|
||||
}
|
||||
|
||||
// Check if input is a valid domain name with an optional port
|
||||
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
if (domainRegex.test(input)) {
|
||||
return "DOMAIN";
|
||||
}
|
||||
// Check if input is a valid domain name with an optional port
|
||||
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
if (domainRegex.test(input)) {
|
||||
return "DOMAIN";
|
||||
}
|
||||
|
||||
// If none of the above conditions match, the input is invalid
|
||||
return "Invalid";
|
||||
// If none of the above conditions match, the input is invalid
|
||||
return "Invalid";
|
||||
};
|
||||
|
||||
function IsValidHost(domain) {
|
||||
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
return regex.test(domain);
|
||||
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
return regex.test(domain);
|
||||
}
|
||||
function IsValidNameServer(nameServer) {
|
||||
//8.8.8.8 example
|
||||
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
return regex.test(nameServer);
|
||||
//8.8.8.8 example
|
||||
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
return regex.test(nameServer);
|
||||
}
|
||||
const IsValidURL = function (url) {
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
};
|
||||
function ValidateCronExpression(cronExp) {
|
||||
// Check if expression is provided and is a string
|
||||
if (!cronExp || typeof cronExp !== "string") {
|
||||
return { isValid: false, message: "Cron expression must be a non-empty string" };
|
||||
}
|
||||
// Check if expression is provided and is a string
|
||||
if (!cronExp || typeof cronExp !== "string") {
|
||||
return { isValid: false, message: "Cron expression must be a non-empty string" };
|
||||
}
|
||||
|
||||
// Split the expression into its components
|
||||
const fields = cronExp.trim().split(/\s+/);
|
||||
// Split the expression into its components
|
||||
const fields = cronExp.trim().split(/\s+/);
|
||||
|
||||
// Standard cron should have 5 or 6 fields
|
||||
// minute hour day-of-month month day-of-week [year]
|
||||
if (fields.length < 5 || fields.length > 6) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "Cron expression must have 5 or 6 fields"
|
||||
};
|
||||
}
|
||||
// Standard cron should have 5 or 6 fields
|
||||
// minute hour day-of-month month day-of-week [year]
|
||||
if (fields.length < 5 || fields.length > 6) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "Cron expression must have 5 or 6 fields",
|
||||
};
|
||||
}
|
||||
|
||||
// Define field constraints
|
||||
const fieldConstraints = [
|
||||
{ name: "minute", min: 0, max: 59 },
|
||||
{ name: "hour", min: 0, max: 23 },
|
||||
{ name: "day", min: 1, max: 31 },
|
||||
{ name: "month", min: 1, max: 12 },
|
||||
{ name: "weekday", min: 0, max: 6 },
|
||||
{ name: "year", min: 1970, max: 2099 } // Optional field
|
||||
];
|
||||
// Define field constraints
|
||||
const fieldConstraints = [
|
||||
{ name: "minute", min: 0, max: 59 },
|
||||
{ name: "hour", min: 0, max: 23 },
|
||||
{ name: "day", min: 1, max: 31 },
|
||||
{ name: "month", min: 1, max: 12 },
|
||||
{ name: "weekday", min: 0, max: 6 },
|
||||
{ name: "year", min: 1970, max: 2099 }, // Optional field
|
||||
];
|
||||
|
||||
// Valid characters in cron expressions
|
||||
const validChars = /^[\d/*,\-]+$/;
|
||||
// Valid characters in cron expressions
|
||||
const validChars = /^[\d/*,\-]+$/;
|
||||
|
||||
// Validate each field
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i];
|
||||
const constraint = fieldConstraints[i];
|
||||
// Validate each field
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i];
|
||||
const constraint = fieldConstraints[i];
|
||||
|
||||
// Check for valid characters
|
||||
if (!validChars.test(field)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid characters in ${constraint.name} field`
|
||||
};
|
||||
}
|
||||
// Check for valid characters
|
||||
if (!validChars.test(field)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid characters in ${constraint.name} field`,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle special characters
|
||||
if (field === "*") {
|
||||
continue; // Asterisk is valid for all fields
|
||||
}
|
||||
// Handle special characters
|
||||
if (field === "*") {
|
||||
continue; // Asterisk is valid for all fields
|
||||
}
|
||||
|
||||
// Handle lists (comma-separated values)
|
||||
if (field.includes(",")) {
|
||||
const values = field.split(",");
|
||||
for (const value of values) {
|
||||
if (!isValidRange(value, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${value}`
|
||||
};
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Handle lists (comma-separated values)
|
||||
if (field.includes(",")) {
|
||||
const values = field.split(",");
|
||||
for (const value of values) {
|
||||
if (!isValidRange(value, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${value}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle ranges (with hyphens)
|
||||
if (field.includes("-")) {
|
||||
const [start, end] = field.split("-").map(Number);
|
||||
if (
|
||||
start == null ||
|
||||
end == null ||
|
||||
start < constraint.min ||
|
||||
end > constraint.max ||
|
||||
start > end
|
||||
) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${field}`
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Handle ranges (with hyphens)
|
||||
if (field.includes("-")) {
|
||||
const [start, end] = field.split("-").map(Number);
|
||||
if (start == null || end == null || start < constraint.min || end > constraint.max || start > end) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${field}`,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle steps (with forward slash)
|
||||
if (field.includes("/")) {
|
||||
const [range, step] = field.split("/");
|
||||
if (range !== "*" && !isValidRange(range, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${range}`
|
||||
};
|
||||
}
|
||||
if (!isValidRange(step, 1, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid step value in ${constraint.name} field: ${step}`
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Handle steps (with forward slash)
|
||||
if (field.includes("/")) {
|
||||
const [range, step] = field.split("/");
|
||||
if (range !== "*" && !isValidRange(range, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${range}`,
|
||||
};
|
||||
}
|
||||
if (!isValidRange(step, 1, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid step value in ${constraint.name} field: ${step}`,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle plain numbers
|
||||
if (!isValidRange(field, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${field}`
|
||||
};
|
||||
}
|
||||
}
|
||||
// Handle plain numbers
|
||||
if (!isValidRange(field, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${field}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, message: "Valid cron expression" };
|
||||
return { isValid: true, message: "Valid cron expression" };
|
||||
}
|
||||
|
||||
function isValidRange(value, min, max) {
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && num >= min && num <= max;
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && num >= min && num <= max;
|
||||
}
|
||||
function SortMonitor(monitorSort, resp) {
|
||||
let monitors = [];
|
||||
if (!!monitorSort && monitorSort.length > 0) {
|
||||
monitors = monitorSort.map((id) => resp.find((m) => m.id == id)).filter((m) => !!m);
|
||||
//append any new monitors
|
||||
monitors = [...monitors, ...resp.filter((m) => !monitorSort.includes(m.id))];
|
||||
} else {
|
||||
monitors = resp;
|
||||
}
|
||||
return monitors;
|
||||
let monitors = [];
|
||||
if (!!monitorSort && monitorSort.length > 0) {
|
||||
monitors = monitorSort.map((id) => resp.find((m) => m.id == id)).filter((m) => !!m);
|
||||
//append any new monitors
|
||||
monitors = [...monitors, ...resp.filter((m) => !monitorSort.includes(m.id))];
|
||||
} else {
|
||||
monitors = resp;
|
||||
}
|
||||
return monitors;
|
||||
}
|
||||
//js function to generate 32 character random string
|
||||
function RandomString(length) {
|
||||
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let result = "";
|
||||
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
|
||||
return result;
|
||||
}
|
||||
|
||||
export {
|
||||
siteDataExtractFromDb,
|
||||
storeSiteData,
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
IsValidHost,
|
||||
IsValidNameServer,
|
||||
IsValidURL,
|
||||
ValidateCronExpression,
|
||||
SortMonitor
|
||||
siteDataExtractFromDb,
|
||||
storeSiteData,
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
IsValidHost,
|
||||
IsValidNameServer,
|
||||
IsValidURL,
|
||||
ValidateCronExpression,
|
||||
SortMonitor,
|
||||
RandomString,
|
||||
};
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<script>
|
||||
import { formatDistanceToNow, formatDistance } from "date-fns";
|
||||
import { Settings } from "lucide-svelte";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import * as Accordion from "$lib/components/ui/accordion";
|
||||
import { l, f, fd, fdn } from "$lib/i18n/client";
|
||||
import { base } from "$app/paths";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { marked } from "marked";
|
||||
export let incident;
|
||||
export let index;
|
||||
export let lang;
|
||||
export let selectedLang = "en";
|
||||
let startTime = new Date(incident.start_date_time * 1000);
|
||||
let endTime = new Date();
|
||||
let nowTime = new Date();
|
||||
@@ -18,13 +21,17 @@
|
||||
endTime = new Date(incident.end_date_time * 1000);
|
||||
}
|
||||
let incidentType = incident.incident_type;
|
||||
const lastedFor = fd(startTime, endTime, selectedLang);
|
||||
const startedAt = fdn(startTime, selectedLang);
|
||||
const lastedFor = fd(startTime, endTime, $page.data.selectedLang);
|
||||
const remainingTime = fd(nowTime, endTime, $page.data.selectedLang);
|
||||
const startedAt = fdn(startTime, $page.data.selectedLang);
|
||||
|
||||
let isFuture = false;
|
||||
//is future incident
|
||||
let incidentTimeStatus = "";
|
||||
if (nowTime < startTime) {
|
||||
isFuture = true;
|
||||
incidentTimeStatus = "YET_TO_START";
|
||||
} else if (nowTime >= startTime && nowTime <= endTime) {
|
||||
incidentTimeStatus = "ONGOING";
|
||||
} else if (nowTime > endTime) {
|
||||
incidentTimeStatus = "COMPLETED";
|
||||
}
|
||||
|
||||
let accordionValue = "incident-0";
|
||||
@@ -37,30 +44,36 @@
|
||||
let incidentDateSummary = "";
|
||||
let maintenanceBadge = "";
|
||||
let maintenanceBadgeColor = "";
|
||||
if (!isFuture && incident.state != "RESOLVED") {
|
||||
incidentDateSummary = l(lang, "Started %startedAt, still ongoing", {
|
||||
|
||||
if (incidentTimeStatus == "YET_TO_START") {
|
||||
incidentDateSummary = l($page.data.lang, "Starts %startedAt", { startedAt });
|
||||
if (incidentType === "MAINTENANCE") {
|
||||
incidentDateSummary = l($page.data.lang, "Starts %startedAt, will last for %lastedFor", {
|
||||
startedAt,
|
||||
lastedFor
|
||||
});
|
||||
maintenanceBadge = "Upcoming Maintenance";
|
||||
maintenanceBadgeColor = "text-upcoming-maintenance";
|
||||
}
|
||||
} else if (incidentTimeStatus == "ONGOING") {
|
||||
incidentDateSummary = l($page.data.lang, "Started %startedAt, still ongoing", {
|
||||
startedAt
|
||||
});
|
||||
maintenanceBadge = "Maintenance in Progress";
|
||||
maintenanceBadgeColor = "text-maintenance-in-progress";
|
||||
} else if (!isFuture && incident.state == "RESOLVED") {
|
||||
incidentDateSummary = l(lang, "Started %startedAt, lasted for %lastedFor", {
|
||||
if (incidentType === "MAINTENANCE") {
|
||||
incidentDateSummary = l($page.data.lang, "Started %startedAt, will last for %lastedFor more", {
|
||||
startedAt,
|
||||
lastedFor: remainingTime
|
||||
});
|
||||
maintenanceBadge = "Maintenance in Progress";
|
||||
maintenanceBadgeColor = "text-maintenance-in-progress";
|
||||
}
|
||||
} else if (incidentTimeStatus == "COMPLETED") {
|
||||
incidentDateSummary = l($page.data.lang, "Started %startedAt, lasted for %lastedFor", {
|
||||
startedAt,
|
||||
lastedFor
|
||||
});
|
||||
maintenanceBadge = "Maintenance Completed";
|
||||
maintenanceBadgeColor = "text-maintenance-completed";
|
||||
} else if (isFuture && incident.state != "RESOLVED") {
|
||||
incidentDateSummary = l(lang, "Starts %startedAt", { startedAt });
|
||||
maintenanceBadge = "Upcoming Maintenance";
|
||||
maintenanceBadgeColor = "text-upcoming-maintenance";
|
||||
} else if (isFuture && incident.state == "RESOLVED") {
|
||||
incidentDateSummary = l(lang, "Starts %startedAt, will last for %lastedFor", {
|
||||
startedAt,
|
||||
lastedFor
|
||||
});
|
||||
maintenanceBadge = "Upcoming Maintenance";
|
||||
maintenanceBadgeColor = "text-upcoming-maintenance";
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -68,16 +81,19 @@
|
||||
<div class="col-span-12">
|
||||
<Accordion.Root bind:value={index} class="accor">
|
||||
<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(lang, incident.state)}
|
||||
{l($page.data.lang, incident.state)}
|
||||
</span>
|
||||
{:else if incidentType == "MAINTENANCE"}
|
||||
<span class="{maintenanceBadgeColor} ">
|
||||
{l(lang, maintenanceBadge)}
|
||||
{l($page.data.lang, maintenanceBadge)}
|
||||
</span>
|
||||
{/if}
|
||||
{#if $page.data.isLoggedIn}
|
||||
@@ -91,20 +107,48 @@
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<p class="scroll-m-20 text-lg font-medium tracking-tight">
|
||||
<p class="font-medium">
|
||||
{incident.title}
|
||||
</p>
|
||||
{#if !!incidentDateSummary}
|
||||
<p class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground">
|
||||
{incidentDateSummary}
|
||||
</p>
|
||||
<Tooltip.Root openDelay={100} side="bottom" align="end">
|
||||
<Tooltip.Trigger
|
||||
class=" text-ellipsis whitespace-nowrap text-xs font-medium tracking-normal text-muted-foreground"
|
||||
>
|
||||
{incidentDateSummary}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class=" z-20 mt-11" side="bottom" align="end">
|
||||
<div
|
||||
class="items-center justify-center rounded border bg-primary px-1.5 py-1 text-xs font-medium text-primary-foreground shadow-popover"
|
||||
>
|
||||
{f(
|
||||
new Date(incident.start_date_time * 1000),
|
||||
"MMMM do yyyy, h:mm:ss a",
|
||||
$page.data.selectedLang,
|
||||
$page.data.localTz
|
||||
)}
|
||||
{#if incident.end_date_time}
|
||||
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
|
||||
{f(
|
||||
new Date(incident.end_date_time * 1000),
|
||||
"MMMM do yyyy, h:mm:ss a",
|
||||
$page.data.selectedLang,
|
||||
$page.data.localTz
|
||||
)}
|
||||
{:else}
|
||||
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
|
||||
<span class="dots-animation inline-block w-6 text-left"></span>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
<div class="px-4 pt-2">
|
||||
{#if incident.monitors.length > 0}
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each incident.monitors as monitor}
|
||||
<div class="tag-affected-text flex gap-x-2 rounded-md bg-secondary px-1 py-1 pr-2">
|
||||
<div
|
||||
@@ -123,7 +167,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<p class="my-3 text-xs font-semibold uppercase text-muted-foreground">
|
||||
{l(lang, "Updates")}
|
||||
{l($page.data.lang, "Updates")}
|
||||
</p>
|
||||
{#if incident.comments.length > 0}
|
||||
{#if incidentType == "INCIDENT"}
|
||||
@@ -133,15 +177,24 @@
|
||||
<div
|
||||
class="absolute top-0 w-28 -translate-x-32 rounded border bg-secondary px-1.5 py-1 text-center text-xs font-semibold"
|
||||
>
|
||||
{l(lang, comment.state)}
|
||||
{l($page.data.lang, comment.state)}
|
||||
</div>
|
||||
|
||||
<time class=" mb-1 text-sm font-medium leading-none text-muted-foreground">
|
||||
{f(new Date(comment.commented_at * 1000), "MMMM do yyyy, h:mm:ss a", selectedLang)}
|
||||
{f(
|
||||
new Date(comment.commented_at * 1000),
|
||||
"MMMM do yyyy, h:mm:ss a",
|
||||
$page.data.selectedLang,
|
||||
$page.data.localTz
|
||||
)}
|
||||
</time>
|
||||
|
||||
<div class="mb-4 text-sm font-normal">
|
||||
{@html comment.comment}
|
||||
<div
|
||||
class="prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
|
||||
>
|
||||
{@html marked.parse(comment.comment)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -151,7 +204,12 @@
|
||||
{#each incident.comments as comment}
|
||||
<li class="relative pb-2 last:border-0">
|
||||
<time class=" mb-1 text-sm font-medium leading-none text-muted-foreground">
|
||||
{f(new Date(comment.commented_at * 1000), "MMMM do yyyy, h:mm:ss a", selectedLang)}
|
||||
{f(
|
||||
new Date(comment.commented_at * 1000),
|
||||
"MMMM do yyyy, h:mm:ss a",
|
||||
$page.data.selectedLang,
|
||||
$page.data.localTz
|
||||
)}
|
||||
</time>
|
||||
|
||||
<p class="mb-2 text-sm font-normal">
|
||||
@@ -163,7 +221,7 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-sm font-medium">
|
||||
{l(lang, "No Updates Yet")}
|
||||
{l($page.data.lang, "No Updates Yet")}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
import locales from "$lib/locales/locales.json?raw";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
|
||||
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";
|
||||
|
||||
export let data;
|
||||
@@ -26,6 +30,7 @@
|
||||
subtitle: ""
|
||||
};
|
||||
let footerHTML = "";
|
||||
let tzToggle = "NO";
|
||||
let homeIncidentCount = 10;
|
||||
let homeIncidentStartTimeWithin = 30;
|
||||
let incidentGroupView = "EXPAND_FIRST";
|
||||
@@ -69,6 +74,9 @@
|
||||
if (data.siteData.footerHTML) {
|
||||
footerHTML = data.siteData.footerHTML;
|
||||
}
|
||||
if (data.siteData.tzToggle) {
|
||||
tzToggle = data.siteData.tzToggle;
|
||||
}
|
||||
if (data.siteData.i18n) {
|
||||
i18n = data.siteData.i18n;
|
||||
//add locales that are not present in the data
|
||||
@@ -83,8 +91,9 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let heroError = "";
|
||||
async function formSubmitHero() {
|
||||
heroError = "";
|
||||
formStateHero = "loading";
|
||||
let resp = await storeSiteData({
|
||||
hero: JSON.stringify(hero)
|
||||
@@ -93,7 +102,7 @@
|
||||
let data = await resp.json();
|
||||
formStateHero = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
heroError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -113,7 +122,10 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let categoriesErrorMessage = "";
|
||||
async function formSubmitCategories() {
|
||||
categoriesErrorMessage = "";
|
||||
formStateCategories = "loading";
|
||||
let resp = await storeSiteData({
|
||||
categories: JSON.stringify(categories)
|
||||
@@ -122,11 +134,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
|
||||
@@ -135,11 +150,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,
|
||||
@@ -150,7 +168,7 @@
|
||||
let data = await resp.json();
|
||||
formStateIncident = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
incidentErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -216,16 +234,19 @@
|
||||
i18n.defaultLocale = e.value;
|
||||
}
|
||||
|
||||
let i18nErrorMessage = "";
|
||||
async function formSubmiti18n() {
|
||||
i18nErrorMessage = "";
|
||||
formStatei18n = "loading";
|
||||
let resp = await storeSiteData({
|
||||
i18n: JSON.stringify(i18n)
|
||||
i18n: JSON.stringify(i18n),
|
||||
tzToggle: tzToggle
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formStatei18n = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
i18nErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -234,9 +255,7 @@
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Hero Section</Card.Title>
|
||||
<Card.Description>
|
||||
Configure the hero section of your site. This is the first section that appears on your site.
|
||||
</Card.Description>
|
||||
<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}>
|
||||
@@ -286,7 +305,10 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<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
|
||||
{#if formStateHero === "loading"}
|
||||
@@ -359,6 +381,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>
|
||||
@@ -366,9 +391,7 @@
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Navigation</Card.Title>
|
||||
<Card.Description>
|
||||
Configure the navigation of your site. This is the first section that appears on your site.
|
||||
</Card.Description>
|
||||
<Card.Description>Configure the navigation of your site.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitNav}>
|
||||
@@ -469,7 +492,7 @@
|
||||
</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
|
||||
@@ -485,9 +508,7 @@
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Internationalization</Card.Title>
|
||||
<Card.Description>
|
||||
Configure the languages of your site. This is the first section that appears on your site.
|
||||
</Card.Description>
|
||||
<Card.Description>Configure the languages of your site.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmiti18n}>
|
||||
@@ -530,7 +551,28 @@
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<div class="flex w-full justify-end">
|
||||
<p class="pt-4 text-lg font-medium">Timezone Switching</p>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
Kener will automatically detect the user's timezone and show the status page in their timezone. You can let
|
||||
users switch between different timezones if you want.
|
||||
</p>
|
||||
|
||||
<p class="text-sm font-medium">
|
||||
<label>
|
||||
<input
|
||||
on:change={(e) => {
|
||||
tzToggle = e.target.checked === true ? "YES" : "NO";
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={tzToggle === "YES"}
|
||||
/>
|
||||
Allow users to switch timezones
|
||||
</label>
|
||||
</p>
|
||||
<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
|
||||
{#if formStatei18n === "loading"}
|
||||
@@ -545,9 +587,7 @@
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Site Categories</Card.Title>
|
||||
<Card.Description>
|
||||
Configure the categories of your site. This is the first section that appears on your site.
|
||||
</Card.Description>
|
||||
<Card.Description>Configure the categories of your site.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitCategories}>
|
||||
@@ -592,7 +632,10 @@
|
||||
<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
|
||||
{#if formStateCategories === "loading"}
|
||||
@@ -607,9 +650,7 @@
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Site Footer</Card.Title>
|
||||
<Card.Description>
|
||||
Configure the footer of your site. This is the first section that appears on your site.
|
||||
</Card.Description>
|
||||
<Card.Description>Configure the footer of your site.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitFooter}>
|
||||
@@ -623,7 +664,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!footerErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStateFooter === "loading"}>
|
||||
Save
|
||||
{#if formStateFooter === "loading"}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Loader } 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 {
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
@@ -84,6 +94,10 @@
|
||||
|
||||
let invalidFormMessage = "";
|
||||
|
||||
function copyToClipboard(t) {
|
||||
navigator.clipboard.writeText(t);
|
||||
}
|
||||
|
||||
async function isValidEval(ev) {
|
||||
if (ev.endsWith(";")) {
|
||||
invalidFormMessage = "Eval should not end with semicolon";
|
||||
@@ -95,8 +109,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.";
|
||||
@@ -174,6 +188,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";
|
||||
@@ -208,6 +228,12 @@
|
||||
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";
|
||||
@@ -277,8 +303,106 @@
|
||||
timeout: parseInt(newMonitor.groupConfig.timeout),
|
||||
hideMonitors: newMonitor.groupConfig.hideMonitors
|
||||
});
|
||||
}
|
||||
} else if (newMonitor.monitor_type === "SSL") {
|
||||
//validating host
|
||||
if (!newMonitor.sslConfig.host) {
|
||||
invalidFormMessage = "Host is required";
|
||||
return;
|
||||
}
|
||||
if (!IsValidHost(newMonitor.sslConfig.host)) {
|
||||
invalidFormMessage = "Invalid Host";
|
||||
return;
|
||||
}
|
||||
//validating port
|
||||
if (
|
||||
!!!newMonitor.sslConfig.port ||
|
||||
isNaN(newMonitor.sslConfig.port) ||
|
||||
newMonitor.sslConfig.port < 1 ||
|
||||
newMonitor.sslConfig.port > 65535
|
||||
) {
|
||||
invalidFormMessage = "Port should be valid";
|
||||
return;
|
||||
}
|
||||
//check if degradedRemainingHours > 0
|
||||
if (
|
||||
!!!newMonitor.sslConfig.degradedRemainingHours ||
|
||||
isNaN(newMonitor.sslConfig.degradedRemainingHours) ||
|
||||
newMonitor.sslConfig.degradedRemainingHours < 0
|
||||
) {
|
||||
invalidFormMessage = "Degraded Remaining Hours should be greater than 0";
|
||||
return;
|
||||
}
|
||||
//check if downRemainingHours > 0
|
||||
if (
|
||||
!!!newMonitor.sslConfig.downRemainingHours ||
|
||||
isNaN(newMonitor.sslConfig.downRemainingHours) ||
|
||||
newMonitor.sslConfig.downRemainingHours < 0
|
||||
) {
|
||||
invalidFormMessage = "Down Remaining Hours should be greater than 0";
|
||||
return;
|
||||
}
|
||||
|
||||
newMonitor.sslConfig.degradedRemainingHours = Number(newMonitor.sslConfig.degradedRemainingHours);
|
||||
newMonitor.sslConfig.downRemainingHours = Number(newMonitor.sslConfig.downRemainingHours);
|
||||
|
||||
//check if degradedRemainingHours > downRemainingHours
|
||||
if (newMonitor.sslConfig.degradedRemainingHours <= newMonitor.sslConfig.downRemainingHours) {
|
||||
invalidFormMessage = "Degraded Remaining Hours should be greater than Down Remaining Hours";
|
||||
return;
|
||||
}
|
||||
newMonitor.type_data = JSON.stringify(newMonitor.sslConfig);
|
||||
} else if (newMonitor.monitor_type === "SQL") {
|
||||
//connectionString cannot be empty
|
||||
if (!!!newMonitor.sqlConfig.connectionString) {
|
||||
invalidFormMessage = "Connection String is required";
|
||||
return;
|
||||
}
|
||||
|
||||
//connection string has to start with postgresql or mysql
|
||||
if (
|
||||
!newMonitor.sqlConfig.connectionString.startsWith("postgresql://") &&
|
||||
!newMonitor.sqlConfig.connectionString.startsWith("mysql://")
|
||||
) {
|
||||
invalidFormMessage = "Connection string should start with postgresql:// or mysql2://";
|
||||
return;
|
||||
}
|
||||
|
||||
//timeout should be positive number
|
||||
if (newMonitor.sqlConfig.timeout < 1) {
|
||||
invalidFormMessage = "Timeout should be greater than 0";
|
||||
return;
|
||||
}
|
||||
|
||||
//query cannot be empty
|
||||
if (!!!newMonitor.sqlConfig.query) {
|
||||
invalidFormMessage = "SQL Query is required";
|
||||
return;
|
||||
}
|
||||
|
||||
newMonitor.type_data = JSON.stringify(newMonitor.sqlConfig);
|
||||
} else if (newMonitor.monitor_type === "HEARTBEAT") {
|
||||
//newMonitor.heartbeatConfig.degradedRemainingMinutes should be a number and greater than equal to 1
|
||||
if (
|
||||
!!!newMonitor.heartbeatConfig.degradedRemainingMinutes ||
|
||||
isNaN(newMonitor.heartbeatConfig.degradedRemainingMinutes) ||
|
||||
newMonitor.heartbeatConfig.degradedRemainingMinutes < 1
|
||||
) {
|
||||
invalidFormMessage = "Degraded or grace Period should at least be 1 minute";
|
||||
return;
|
||||
}
|
||||
|
||||
//newMonitor.heartbeatConfig.downRemainingMinutes should be a number and greater than degradedRemainingMinutes
|
||||
if (
|
||||
!!!newMonitor.heartbeatConfig.downRemainingMinutes ||
|
||||
isNaN(newMonitor.heartbeatConfig.downRemainingMinutes) ||
|
||||
newMonitor.heartbeatConfig.downRemainingMinutes <= newMonitor.heartbeatConfig.degradedRemainingMinutes
|
||||
) {
|
||||
invalidFormMessage = "Down or impacted Period should be greater than Degraded or grace Period";
|
||||
return;
|
||||
}
|
||||
|
||||
newMonitor.type_data = JSON.stringify(newMonitor.heartbeatConfig);
|
||||
}
|
||||
formState = "loading";
|
||||
|
||||
try {
|
||||
@@ -302,6 +426,40 @@
|
||||
}
|
||||
}
|
||||
let typeOfLogoUpload = newMonitor.image.startsWith("http") ? "URL" : "FILE";
|
||||
|
||||
let databaseTypes = {
|
||||
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">
|
||||
@@ -341,7 +499,7 @@
|
||||
}}
|
||||
/>
|
||||
<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"
|
||||
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>
|
||||
@@ -523,6 +681,9 @@
|
||||
<Select.Item value="DNS" label="DNS" class="text-sm font-medium">DNS</Select.Item>
|
||||
<Select.Item value="TCP" label="TCP" class="text-sm font-medium">TCP</Select.Item>
|
||||
<Select.Item value="GROUP" label="GROUP" class="text-sm font-medium">GROUP</Select.Item>
|
||||
<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.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
@@ -607,6 +768,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">
|
||||
@@ -617,12 +790,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"}
|
||||
@@ -712,12 +896,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>
|
||||
@@ -789,7 +984,7 @@
|
||||
];
|
||||
}}
|
||||
>
|
||||
<Plus class="h-5 w-5" /> Add New
|
||||
<Plus class="mr-1 h-5 w-5" /> Add New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -803,12 +998,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>
|
||||
@@ -896,7 +1101,10 @@
|
||||
</Label>
|
||||
<Input bind:value={newMonitor.groupConfig.timeout} class="w-40" id="timeout" />
|
||||
<p class="my-1 text-xs text-muted-foreground">
|
||||
Maximum Time in milliseconds it will wait for all the monitors to resolved in that particular timestamp
|
||||
Maximum Time in milliseconds it will wait for all the monitors to resolved in that particular timestamp.
|
||||
Read the <a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-group">
|
||||
docs
|
||||
</a> to learn more
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-span-6">
|
||||
@@ -935,7 +1143,195 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{:else if newMonitor.monitor_type == "SSL"}
|
||||
<div class="mt-4 grid grid-cols-7 gap-2">
|
||||
<div class="col-span-2">
|
||||
<Label for="sslHost">Host</Label>
|
||||
<Input placeholder="example.com" bind:value={newMonitor.sslConfig.host} id="sslHost" />
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Label for="sslHost">Port</Label>
|
||||
<Input placeholder="443" bind:value={newMonitor.sslConfig.port} id="sslPort" />
|
||||
</div>
|
||||
<div class="relative col-span-2">
|
||||
<Label for="degradedRemainingHours">Degraded If (in hours)</Label>
|
||||
<Input
|
||||
class="pl-20"
|
||||
type="number"
|
||||
placeholder="x hours"
|
||||
bind:value={newMonitor.sslConfig.degradedRemainingHours}
|
||||
id="degradedRemainingHours"
|
||||
/>
|
||||
<span class="absolute left-2 top-9 -mt-0.5 text-sm font-medium text-muted-foreground">Expires In</span>
|
||||
</div>
|
||||
<div class="relative col-span-2">
|
||||
<Label for="downRemainingHours">Down If (in hours)</Label>
|
||||
<Input
|
||||
class="pl-20"
|
||||
placeholder="x hours"
|
||||
type="number"
|
||||
bind:value={newMonitor.sslConfig.downRemainingHours}
|
||||
id="downRemainingHours"
|
||||
/>
|
||||
<span class="absolute left-2 top-9 -mt-0.5 text-sm font-medium text-muted-foreground">Expires In</span>
|
||||
</div>
|
||||
<div class="col-span-7">
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Degraded Remaining Hours should be greater than Down Remaining Hours. Refer to the
|
||||
<a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-ssl"
|
||||
>documentation</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if newMonitor.monitor_type == "SQL"}
|
||||
<div>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<div class="w-36">
|
||||
<Label for="sqlConfigdbType">Database Type</Label>
|
||||
<Select.Root
|
||||
portal={null}
|
||||
onSelectedChange={(e) => {
|
||||
newMonitor.sqlConfig.dbType = e.value;
|
||||
}}
|
||||
selected={{
|
||||
value: newMonitor.sqlConfig.dbType,
|
||||
label: databaseTypes[newMonitor.sqlConfig.dbType]
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="sqlConfigdbType">
|
||||
<Select.Value bind:value={newMonitor.sqlConfig.dbType} placeholder="DB" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Select DB</Select.Label>
|
||||
<Select.Item value="pg" label={databaseTypes.pg} class="text-sm font-medium">
|
||||
{databaseTypes.pg}
|
||||
</Select.Item>
|
||||
<Select.Item value="mysql2" label={databaseTypes.mysql2} class="text-sm font-medium">
|
||||
{databaseTypes.mysql2}
|
||||
</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="w-36">
|
||||
<Label for="dbtimeout">Timeout(ms)</Label>
|
||||
<Input
|
||||
placeholder="in milliseconds"
|
||||
type="number"
|
||||
bind:value={newMonitor.sqlConfig.timeout}
|
||||
id="dbtimeout"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-1">
|
||||
<div class="col-span-1">
|
||||
<Label for="connectionString">Connection String</Label>
|
||||
<Input
|
||||
bind:value={newMonitor.sqlConfig.connectionString}
|
||||
id="connectionString"
|
||||
placeholder="Enter the connection string"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-1">
|
||||
<div class="col-span-1">
|
||||
<Label for="sqlQuery">SQL Query</Label>
|
||||
<Input bind:value={newMonitor.sqlConfig.query} id="sqlQuery" placeholder="SELECT 1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Refer to the
|
||||
<a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-sql">
|
||||
documentation
|
||||
</a> for more details
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if newMonitor.monitor_type == "HEARTBEAT"}
|
||||
<div>
|
||||
<div class="relative mt-4 justify-start gap-x-4">
|
||||
<p class="truncate rounded-md border bg-popover p-2 pr-8 text-sm font-medium">
|
||||
Heart beat url: <span class="text-muted-foreground">
|
||||
{$page.data.siteData.siteURL +
|
||||
`${base}/api/heartbeat/${newMonitor.tag}:${newMonitor.heartbeatConfig.secretString}`}</span
|
||||
>
|
||||
</p>
|
||||
<Button
|
||||
class="copybtn absolute right-2 top-2 flex h-6 w-6 justify-center p-1"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
on:click={() =>
|
||||
copyToClipboard(
|
||||
$page.data.siteData.siteURL +
|
||||
`${base}/api/heartbeat/${newMonitor.tag}:${newMonitor.heartbeatConfig.secretString}`
|
||||
)}
|
||||
>
|
||||
<Check class="check-btn absolute h-4 w-4 text-green-500" />
|
||||
<Clipboard class="copy-btn absolute h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-row justify-start gap-x-4">
|
||||
<div>
|
||||
<Label for="degradedRemainingMinutes">Degraded or grace Period (in minutes)</Label>
|
||||
<Input
|
||||
placeholder="in minutes"
|
||||
class="w-full"
|
||||
type="number"
|
||||
bind:value={newMonitor.heartbeatConfig.degradedRemainingMinutes}
|
||||
id="degradedRemainingMinutes"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="downRemainingMinutes">Down or impacted Period (in minutes)</Label>
|
||||
<Input
|
||||
placeholder="in minutes"
|
||||
type="number"
|
||||
bind:value={newMonitor.heartbeatConfig.downRemainingMinutes}
|
||||
id="downRemainingMinutes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Refer to the
|
||||
<a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-heartbeat">
|
||||
documentation
|
||||
</a> for more details
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<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>
|
||||
</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">
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<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 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,11 +15,13 @@
|
||||
import { onMount } from "svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { storeSiteData, SortMonitor } from "$lib/clientTools.js";
|
||||
import { storeSiteData, SortMonitor, RandomString } from "$lib/clientTools.js";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
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 } from "$lib/anywhere.js";
|
||||
|
||||
export let categories = [];
|
||||
export let colorDown = "#777";
|
||||
@@ -70,16 +79,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: "",
|
||||
@@ -92,7 +102,24 @@
|
||||
monitors: [],
|
||||
timeout: 10000,
|
||||
hideMonitors: false
|
||||
})
|
||||
}),
|
||||
sslConfig: {
|
||||
host: "",
|
||||
port: 443,
|
||||
degradedRemainingHours: 1,
|
||||
downRemainingHours: 0
|
||||
},
|
||||
sqlConfig: {
|
||||
connectionString: "",
|
||||
query: "SELECT 1",
|
||||
timeout: 5000,
|
||||
dbType: "pg" //mysql2
|
||||
},
|
||||
heartbeatConfig: {
|
||||
degradedRemainingMinutes: 1,
|
||||
downRemainingMinutes: 2,
|
||||
secretString: RandomString(32)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,6 +151,12 @@
|
||||
newMonitor.tcpConfig = JSON.parse(newMonitor.type_data);
|
||||
} else if (newMonitor.monitor_type == "GROUP") {
|
||||
newMonitor.groupConfig = createGroupConfig(JSON.parse(newMonitor.type_data));
|
||||
} else if (newMonitor.monitor_type == "SSL") {
|
||||
newMonitor.sslConfig = JSON.parse(newMonitor.type_data);
|
||||
} else if (newMonitor.monitor_type == "SQL") {
|
||||
newMonitor.sqlConfig = JSON.parse(newMonitor.type_data);
|
||||
} else if (newMonitor.monitor_type == "HEARTBEAT") {
|
||||
newMonitor.heartbeatConfig = JSON.parse(newMonitor.type_data);
|
||||
}
|
||||
showAddMonitor = true;
|
||||
}
|
||||
@@ -196,30 +229,37 @@
|
||||
});
|
||||
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"
|
||||
}
|
||||
};
|
||||
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 monitorTriggers;
|
||||
initMonitorTriggers();
|
||||
|
||||
let saveTriggerError = "";
|
||||
async function saveTriggers() {
|
||||
saveTriggerError = "";
|
||||
let data = {
|
||||
id: currentAlertMonitor.id,
|
||||
down_trigger: JSON.stringify(monitorTriggers.down_trigger),
|
||||
@@ -235,16 +275,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;
|
||||
@@ -255,15 +302,27 @@
|
||||
shareMenusToggle = true;
|
||||
}
|
||||
const flipDurationMs = 200;
|
||||
|
||||
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;
|
||||
@@ -337,6 +396,9 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !!orderErrorMessage}
|
||||
<p class="py-2 text-sm font-medium text-destructive">{orderErrorMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,17 +462,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">
|
||||
@@ -429,7 +493,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>
|
||||
@@ -445,7 +509,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
|
||||
@@ -459,22 +523,25 @@
|
||||
</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>
|
||||
{/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>
|
||||
@@ -548,7 +615,7 @@
|
||||
}}
|
||||
/>
|
||||
<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"
|
||||
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>
|
||||
@@ -654,9 +721,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,340 @@
|
||||
<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: ""
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
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,11 +3,13 @@
|
||||
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";
|
||||
@@ -15,6 +17,7 @@
|
||||
export let data;
|
||||
|
||||
let formState = "idle";
|
||||
let themeError = "";
|
||||
|
||||
let themeData = {
|
||||
pattern: "none",
|
||||
@@ -49,6 +52,7 @@
|
||||
|
||||
async function formSubmit() {
|
||||
formState = "loading";
|
||||
themeError = "";
|
||||
let themeDataAPI = { ...themeData };
|
||||
themeDataAPI.colors = JSON.stringify(themeDataAPI.colorsJ);
|
||||
themeDataAPI.font = JSON.stringify(themeDataAPI.fontJ);
|
||||
@@ -59,7 +63,7 @@
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
themeError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -345,7 +349,10 @@
|
||||
</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
|
||||
{#if formState === "loading"}
|
||||
|
||||
@@ -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 = "";
|
||||
@@ -149,6 +191,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newTrigger.trigger_meta.has_webhook_body) {
|
||||
if (newTrigger.trigger_meta.webhook_body == "") {
|
||||
invalidFormMessage = "Webhook Body is required";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
//newTrigger.name present not empty
|
||||
if (newTrigger.name == "") {
|
||||
@@ -338,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">
|
||||
@@ -374,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>
|
||||
@@ -423,7 +476,7 @@
|
||||
}}
|
||||
/>
|
||||
<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"
|
||||
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>
|
||||
@@ -543,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;
|
||||
@@ -559,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}>
|
||||
@@ -667,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}
|
||||
|
||||
@@ -3,12 +3,19 @@
|
||||
import { onMount } from "svelte";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { base } from "$app/paths";
|
||||
import { sub, startOfDay, getUnixTime } from "date-fns";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
import { 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";
|
||||
@@ -38,7 +45,7 @@
|
||||
.post(`${base}/api/today/incidents`, {
|
||||
tag: monitor.tag,
|
||||
startTs: monitor.pageData.midnight90DaysAgo,
|
||||
endTs: monitor.pageData.midnightTomorrow,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp,
|
||||
localTz: localTz
|
||||
})
|
||||
.then((response) => {
|
||||
@@ -52,10 +59,13 @@
|
||||
}
|
||||
|
||||
function getToday(startTs, incidentIDs) {
|
||||
let endTs = Math.min(startTs + 86400, monitor.pageData.maxDateTodayTimestamp);
|
||||
|
||||
axios
|
||||
.post(`${base}/api/today`, {
|
||||
monitor: monitor,
|
||||
localTz: localTz,
|
||||
endTs: endTs,
|
||||
startTs: startTs,
|
||||
incidentIDs: incidentIDs
|
||||
})
|
||||
@@ -85,27 +95,33 @@
|
||||
{
|
||||
text: `${l(lang, "90 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 90 }))),
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp,
|
||||
value: uptime90Day
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "60 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 59 })))
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 59 }))),
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "30 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 29 })))
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 29 }))),
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "14 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 13 })))
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 13 }))),
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "7 Days")}`,
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 })))
|
||||
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 }))),
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: l(lang, "Today"),
|
||||
startTs: getUnixTime(startOfDay(new Date()))
|
||||
startTs: getUnixTime(startOfDay(new Date())),
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
}
|
||||
];
|
||||
|
||||
@@ -113,7 +129,11 @@
|
||||
let rolledAt = 0;
|
||||
let rollerLoading = false;
|
||||
async function rollSummary(r) {
|
||||
let newRolledAt = (rolledAt + r) % uptimesRollers.length;
|
||||
let newRolledAt = r;
|
||||
analyticsEvent("monitor_interval_switch", {
|
||||
tag: monitor.tag,
|
||||
interval: uptimesRollers[newRolledAt].text
|
||||
});
|
||||
|
||||
if (uptimesRollers[newRolledAt].value === undefined) {
|
||||
rollerLoading = true;
|
||||
@@ -121,7 +141,8 @@
|
||||
|
||||
let resp = await axios.post(`${base}/api/today/aggregated`, {
|
||||
monitor: monitor,
|
||||
startTs: uptimesRollers[newRolledAt].startTs
|
||||
startTs: uptimesRollers[newRolledAt].startTs,
|
||||
endTs: uptimesRollers[newRolledAt].endTs
|
||||
});
|
||||
uptimesRollers[newRolledAt].value = resp.data.uptime;
|
||||
rollerLoading = false;
|
||||
@@ -157,19 +178,26 @@
|
||||
let dateFetchedFor = "";
|
||||
let dayUptime = "NA";
|
||||
let loadingDayData = false;
|
||||
|
||||
function dailyDataGetter(e, bar, incidentObj) {
|
||||
if (embed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let incidentIDs = incidentObj?.ids || [];
|
||||
dayUptime = "NA";
|
||||
dateFetchedFor = f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang);
|
||||
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);
|
||||
}, 750);
|
||||
}, 50);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -179,7 +207,12 @@
|
||||
<div class="pt-0">
|
||||
<div class="scroll-m-20 pr-5 text-xl font-medium tracking-tight">
|
||||
{#if monitor.image}
|
||||
<GMI src={monitor.image} classList="absolute left-6 top-6 inline h-5 w-5" alt={monitor.name} srcset="" />
|
||||
<GMI
|
||||
src={monitor.image}
|
||||
classList="absolute left-6 top-6 inline h-5 w-5 hidden md:block"
|
||||
alt={monitor.name}
|
||||
srcset=""
|
||||
/>
|
||||
{/if}
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{monitor.name}
|
||||
@@ -212,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>
|
||||
@@ -229,55 +252,51 @@
|
||||
{/if}
|
||||
<div class="col-span-12 min-h-[94px] pt-2 md:w-[546px]">
|
||||
<div class="col-span-12">
|
||||
<div class="flex justify-between">
|
||||
<div class=" ">
|
||||
<div class="flex gap-x-1">
|
||||
{#if rolledAt > 0}
|
||||
<Button variant="ghost" class="h-5 w-5 p-0" on:click={() => rollSummary(-1)}>
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex text-xs font-semibold">
|
||||
<span>
|
||||
<div class="flex flex-wrap justify-between gap-x-1">
|
||||
<div class="">
|
||||
<DropdownMenu.Root class="">
|
||||
<DropdownMenu.Trigger class="mr-2 flex ">
|
||||
<Button variant="secondary" class="h-6 px-2 py-2 text-xs">
|
||||
{uptimesRollers[rolledAt].text}
|
||||
</span>
|
||||
|
||||
<span class="">
|
||||
{#if rollerLoading}
|
||||
<Loader class="mx-1 -mt-0.5 inline h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<TrendingUp class="mx-1 -mt-0.5 inline h-3 w-3" />
|
||||
{/if}
|
||||
{#if isNaN(uptimesRollers[rolledAt].value)}
|
||||
<span class="text-muted-foreground">-</span>
|
||||
{:else}
|
||||
<NumberFlow
|
||||
value={uptimesRollers[rolledAt].value}
|
||||
format={{
|
||||
notation: "standard",
|
||||
minimumFractionDigits: 4,
|
||||
maximumFractionDigits: 4
|
||||
}}
|
||||
suffix="%"
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{#if rolledAt < uptimesRollers.length - 1}
|
||||
<Button variant="ghost" class="h-5 w-5 p-0" on:click={() => rollSummary(1)}>
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class={!!embed ? "h-[60px] overflow-y-auto" : ""}>
|
||||
{#each uptimesRollers as roller, i}
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item
|
||||
class="text-xs {rolledAt == i ? 'bg-secondary' : ''} font-semibold"
|
||||
on:click={() => rollSummary(i)}
|
||||
>
|
||||
{roller.text}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
<div class="pt-0.5 text-right">
|
||||
<div class="text-api-up truncate text-xs font-semibold text-{monitor.pageData.summaryColorClass}">
|
||||
<div class="flex gap-x-2 pt-2 text-right">
|
||||
{#if rollerLoading}
|
||||
<Loader class=" mt-0.5 inline h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
{/if}
|
||||
{#if !isNaN(uptimesRollers[rolledAt].value)}
|
||||
<NumberFlow
|
||||
class="border-r pr-2 text-xs font-semibold"
|
||||
value={uptimesRollers[rolledAt].value}
|
||||
format={{
|
||||
notation: "standard",
|
||||
minimumFractionDigits: 4,
|
||||
maximumFractionDigits: 4
|
||||
}}
|
||||
suffix="%"
|
||||
/>
|
||||
{/if}
|
||||
<div class="truncate text-xs font-semibold text-{monitor.pageData.summaryColorClass}">
|
||||
{monitor.pageData.summaryStatus}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative col-span-12 mt-1"
|
||||
class="relative col-span-12 mt-0.5"
|
||||
use:clickOutsideAction
|
||||
on:clickoutside={(e) => {
|
||||
showDailyDataModal = false;
|
||||
@@ -306,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"
|
||||
@@ -316,7 +334,7 @@
|
||||
{#if bar.showDetails}
|
||||
<div class="show-hover absolute text-sm">
|
||||
<div class="text-{bar.textClass} text-xs font-semibold">
|
||||
{f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang)}
|
||||
{f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang, $page.data.localTz)}
|
||||
-
|
||||
{bar.summaryStatus}
|
||||
</div>
|
||||
@@ -324,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" }}
|
||||
@@ -366,7 +399,7 @@
|
||||
<p>
|
||||
<span class="text-{bar.cssClass}"> ● </span>
|
||||
|
||||
{f(new Date(bar.timestamp * 1000), "hh:mm a", selectedLang)}
|
||||
{f(new Date(bar.timestamp * 1000), "hh:mm a", selectedLang, $page.data.localTz)}
|
||||
</p>
|
||||
{#if bar.status != "NO_DATA"}
|
||||
<p class="pl-2">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<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";
|
||||
@@ -31,7 +32,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} />
|
||||
@@ -40,22 +44,24 @@
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
<DropdownMenu.Root class="">
|
||||
<DropdownMenu.Trigger class="mr-2 flex md:hidden">
|
||||
<Button variant="outline" size="sm">
|
||||
<Menu size={14} />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{#each data.site.nav as navItem}
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<a rel="external" href={navItem.url}> {navItem.name} </a>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<div class="flex md:hidden">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="outline" size="sm">
|
||||
<Menu size={14} />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{#each data.site.nav as navItem}
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<a rel="external" href={navItem.url}> {navItem.name} </a>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
+47
-46
@@ -2,70 +2,71 @@
|
||||
|
||||
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 { formatInTimeZone } from "date-fns-tz";
|
||||
|
||||
const locales = {
|
||||
ru,
|
||||
en: enUS,
|
||||
hi,
|
||||
de,
|
||||
"zh-CN": zhCN,
|
||||
vi,
|
||||
ja,
|
||||
nl,
|
||||
dk: da,
|
||||
fr,
|
||||
ko,
|
||||
"pt-BR": ptBR,
|
||||
tr,
|
||||
"nb-NO": nb
|
||||
ru,
|
||||
en: enUS,
|
||||
hi,
|
||||
de,
|
||||
"zh-CN": zhCN,
|
||||
vi,
|
||||
ja,
|
||||
nl,
|
||||
dk: da,
|
||||
fr,
|
||||
ko,
|
||||
"pt-BR": ptBR,
|
||||
tr,
|
||||
"nb-NO": nb,
|
||||
};
|
||||
|
||||
const f = function (date, formatStr, locale) {
|
||||
return format(date, formatStr, {
|
||||
locale: locales[locale] || enUS
|
||||
});
|
||||
const f = function (date, formatStr, locale, tz) {
|
||||
return formatInTimeZone(date, tz, formatStr, {
|
||||
locale: locales[locale] || enUS,
|
||||
});
|
||||
};
|
||||
|
||||
const fd = function (start, end, locale) {
|
||||
return formatDistance(start, end, { addSuffix: false, locale: locales[locale] });
|
||||
return formatDistance(start, end, { addSuffix: false, locale: locales[locale] });
|
||||
};
|
||||
const fdn = function (start, locale) {
|
||||
return formatDistanceToNow(start, { addSuffix: true, locale: locales[locale] });
|
||||
return formatDistanceToNow(start, { addSuffix: true, locale: locales[locale] });
|
||||
};
|
||||
const fdm = function (duration, locale) {
|
||||
return formatDuration(duration, {
|
||||
format: ["days", "hours", "minutes"],
|
||||
zero: false,
|
||||
delimiter: " ",
|
||||
locale: locales[locale]
|
||||
});
|
||||
return formatDuration(duration, {
|
||||
format: ["days", "hours", "minutes"],
|
||||
zero: false,
|
||||
delimiter: " ",
|
||||
locale: locales[locale],
|
||||
});
|
||||
};
|
||||
|
||||
const l = function (sessionLangMap, key, args = {}) {
|
||||
try {
|
||||
let obj = sessionLangMap[key];
|
||||
try {
|
||||
let obj = sessionLangMap[key];
|
||||
|
||||
// Replace placeholders in the string using the args object
|
||||
if (obj && typeof obj === "string") {
|
||||
obj = obj.replace(/%\w+/g, (placeholder) => {
|
||||
const argKey = placeholder.slice(1); // Remove the `%` to get the key
|
||||
return args[argKey] !== undefined ? args[argKey] : placeholder;
|
||||
});
|
||||
}
|
||||
return obj || key;
|
||||
} catch (e) {
|
||||
return key;
|
||||
}
|
||||
// Replace placeholders in the string using the args object
|
||||
if (obj && typeof obj === "string") {
|
||||
obj = obj.replace(/%\w+/g, (placeholder) => {
|
||||
const argKey = placeholder.slice(1); // Remove the `%` to get the key
|
||||
return args[argKey] !== undefined ? args[argKey] : placeholder;
|
||||
});
|
||||
}
|
||||
return obj || key;
|
||||
} catch (e) {
|
||||
return key;
|
||||
}
|
||||
};
|
||||
const summaryTime = function (summaryStatus) {
|
||||
if (summaryStatus == "No Data") {
|
||||
return "No Data";
|
||||
}
|
||||
if (summaryStatus == "UP") {
|
||||
return "Status OK";
|
||||
}
|
||||
if (summaryStatus == "No Data") {
|
||||
return "No Data";
|
||||
}
|
||||
if (summaryStatus == "UP") {
|
||||
return "Status OK";
|
||||
}
|
||||
|
||||
return "%status for %duration";
|
||||
return "%status for %duration";
|
||||
};
|
||||
|
||||
export { l, summaryTime, f, formatDistance, fd, fdn, fdm };
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%status für %duration",
|
||||
"14 Days": "14 Tage",
|
||||
"30 Days": "30 Tage",
|
||||
"60 Days": "60 Tage",
|
||||
"7 Days": "7 Tage",
|
||||
"90 Days": "90 Tage",
|
||||
"Availability per Component": "Verfügbarkeit pro Komponente",
|
||||
"Back": "Zurück",
|
||||
"Badge Copied": "Abzeichen kopiert",
|
||||
"Badge": "Abzeichen",
|
||||
"Browse Events": "Ereignisse durchsuchen",
|
||||
"Code Copied": "Code kopiert",
|
||||
"Copy Code": "Code kopieren",
|
||||
"Copy Link": "Link kopieren",
|
||||
"DEGRADED": "EINGESCHRÄNKT",
|
||||
"DOWN": "AUSGEFALLEN",
|
||||
"Dark": "Dunkel",
|
||||
"Days": "Tage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
|
||||
"Embed": "Einbetten",
|
||||
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
|
||||
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
|
||||
"IDENTIFIED": "IDENTIFIZIERT",
|
||||
"INVESTIGATING": "UNTERSUCHUNG",
|
||||
"Incident Updates": "Vorfall-Updates",
|
||||
"LIVE Status": "LIVE-Status",
|
||||
"Lasted for about %lastedFor": "Dauerte etwa %lastedFor",
|
||||
"Light": "Hell",
|
||||
"Link Copied": "Link kopiert",
|
||||
"MAINTENANCE": "WARTUNG",
|
||||
"MONITORING": "ÜBERWACHUNG",
|
||||
"Maintenance Completed": "Wartung abgeschlossen",
|
||||
"Maintenance in Progress": "Wartung läuft",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Keine Daten",
|
||||
"No Incident in %date": "Kein Vorfall am %date",
|
||||
"No Incidents": "Keine Vorfälle",
|
||||
"No Monitor Found": "Kein Monitor gefunden",
|
||||
"No Updates Yet": "Noch keine Updates",
|
||||
"Ongoing Incidents": "Laufende Vorfälle",
|
||||
"Pinging": "Pingen",
|
||||
"RESOLVED": "GELÖST",
|
||||
"Recent Incidents": "Kürzliche Vorfälle",
|
||||
"Recent Maintenances": "Kürzliche Wartungen",
|
||||
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
|
||||
"Share": "Teilen",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
|
||||
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
|
||||
"Starts %startedAt": "Beginnt in %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Thema",
|
||||
"Today": "Heute",
|
||||
"UP": "VERFÜGBAR",
|
||||
"Upcoming Maintenance": "Bevorstehende Wartung",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Verfügbarkeit"
|
||||
"%status for %duration": "%status für %duration",
|
||||
"14 Days": "14 Tage",
|
||||
"30 Days": "30 Tage",
|
||||
"60 Days": "60 Tage",
|
||||
"7 Days": "7 Tage",
|
||||
"90 Days": "90 Tage",
|
||||
"Availability per Component": "Verfügbarkeit pro Komponente",
|
||||
"Back": "Zurück",
|
||||
"Badge Copied": "Abzeichen kopiert",
|
||||
"Badge": "Abzeichen",
|
||||
"Browse Events": "Ereignisse durchsuchen",
|
||||
"Code Copied": "Code kopiert",
|
||||
"Copy Code": "Code kopieren",
|
||||
"Copy Link": "Link kopieren",
|
||||
"DEGRADED": "EINGESCHRÄNKT",
|
||||
"DOWN": "AUSGEFALLEN",
|
||||
"Dark": "Dunkel",
|
||||
"Days": "Tage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
|
||||
"Embed": "Einbetten",
|
||||
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
|
||||
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
|
||||
"IDENTIFIED": "IDENTIFIZIERT",
|
||||
"INVESTIGATING": "UNTERSUCHUNG",
|
||||
"Incident Updates": "Vorfall-Updates",
|
||||
"LIVE Status": "LIVE-Status",
|
||||
"Lasted for about %lastedFor": "Dauerte etwa %lastedFor",
|
||||
"Light": "Hell",
|
||||
"Link Copied": "Link kopiert",
|
||||
"MAINTENANCE": "WARTUNG",
|
||||
"MONITORING": "ÜBERWACHUNG",
|
||||
"Maintenance Completed": "Wartung abgeschlossen",
|
||||
"Maintenance in Progress": "Wartung läuft",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Keine Daten",
|
||||
"No Incident in %date": "Kein Vorfall am %date",
|
||||
"No Incidents": "Keine Vorfälle",
|
||||
"No Monitor Found": "Kein Monitor gefunden",
|
||||
"No Updates Yet": "Noch keine Updates",
|
||||
"Ongoing Incidents": "Laufende Vorfälle",
|
||||
"Pinging": "Pingen",
|
||||
"RESOLVED": "GELÖST",
|
||||
"Recent Incidents": "Kürzliche Vorfälle",
|
||||
"Recent Maintenances": "Kürzliche Wartungen",
|
||||
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
|
||||
"Share": "Teilen",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
|
||||
"Started %startedAt, will last for %lastedFor more": "Begann vor etwa %startedAt und wird noch etwa %lastedFor dauern",
|
||||
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
|
||||
"Starts %startedAt": "Beginnt in %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Switch to your timezone": "Wechseln Sie zu Ihrer Zeitzone",
|
||||
"Theme": "Thema",
|
||||
"Timezone set to %tz": "Zeitzone auf %tz gesetzt",
|
||||
"Today": "Heute",
|
||||
"UP": "VERFÜGBAR",
|
||||
"Upcoming Maintenance": "Bevorstehende Wartung",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Verfügbarkeit",
|
||||
"View in detail": "Details anzeigen"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%status i %duration",
|
||||
"14 Days": "14 Dage",
|
||||
"30 Days": "30 Dage",
|
||||
"60 Days": "60 Dage",
|
||||
"7 Days": "7 Dage",
|
||||
"90 Days": "90 Dage",
|
||||
"Availability per Component": "Tilgængelighed pr. Komponent",
|
||||
"Back": "Tilbage",
|
||||
"Badge Copied": "Mærke Kopieret",
|
||||
"Badge": "Mærke",
|
||||
"Browse Events": "Gennemse begivenheder",
|
||||
"Code Copied": "Kode Kopieret",
|
||||
"Copy Code": "Kopier Kode",
|
||||
"Copy Link": "Kopier Link",
|
||||
"DEGRADED": "FORRINGET",
|
||||
"DOWN": "NEDE",
|
||||
"Dark": "Mørk",
|
||||
"Days": "Dage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
|
||||
"Embed": "Integrer",
|
||||
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
|
||||
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
|
||||
"IDENTIFIED": "IDENTIFICERET",
|
||||
"INVESTIGATING": "UNDERSØGER",
|
||||
"Incident Updates": "Hændelsesopdateringer",
|
||||
"LIVE Status": "LIVE-status",
|
||||
"Lasted for about %lastedFor": "Varede i cirka %lastedFor",
|
||||
"Light": "Lys",
|
||||
"Link Copied": "Link Kopieret",
|
||||
"MAINTENANCE": "VEDLIGEHOLDELSE",
|
||||
"MONITORING": "OVERVÅGNING",
|
||||
"Maintenance Completed": "Vedligeholdelse afsluttet",
|
||||
"Maintenance in Progress": "Vedligeholdelse i gang",
|
||||
"Mode": "Tilstand",
|
||||
"No Data": "Ingen Data",
|
||||
"No Incident in %date": "Ingen Hændelse Den %date",
|
||||
"No Incidents": "Ingen Hændelser",
|
||||
"No Monitor Found": "Ingen Monitor Fundet",
|
||||
"No Updates Yet": "Ingen Opdateringer Endnu",
|
||||
"Ongoing Incidents": "Igangværende Hændelser",
|
||||
"Pinging": "Pinger",
|
||||
"RESOLVED": "LØST",
|
||||
"Recent Incidents": "Seneste Hændelser",
|
||||
"Recent Maintenances": "Seneste Vedligeholdelser",
|
||||
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
|
||||
"Share": "Del",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
|
||||
"Starts %startedAt": "Starter om %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Tema",
|
||||
"Today": "I dag",
|
||||
"UP": "OPPE",
|
||||
"Upcoming Maintenance": "Kommende vedligeholdelse",
|
||||
"Updates": "Opdateringer",
|
||||
"Uptime": "Oppetid"
|
||||
"%status for %duration": "%status i %duration",
|
||||
"14 Days": "14 Dage",
|
||||
"30 Days": "30 Dage",
|
||||
"60 Days": "60 Dage",
|
||||
"7 Days": "7 Dage",
|
||||
"90 Days": "90 Dage",
|
||||
"Availability per Component": "Tilgængelighed pr. Komponent",
|
||||
"Back": "Tilbage",
|
||||
"Badge Copied": "Mærke Kopieret",
|
||||
"Badge": "Mærke",
|
||||
"Browse Events": "Gennemse begivenheder",
|
||||
"Code Copied": "Kode Kopieret",
|
||||
"Copy Code": "Kopier Kode",
|
||||
"Copy Link": "Kopier Link",
|
||||
"DEGRADED": "FORRINGET",
|
||||
"DOWN": "NEDE",
|
||||
"Dark": "Mørk",
|
||||
"Days": "Dage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
|
||||
"Embed": "Integrer",
|
||||
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
|
||||
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
|
||||
"IDENTIFIED": "IDENTIFICERET",
|
||||
"INVESTIGATING": "UNDERSØGER",
|
||||
"Incident Updates": "Hændelsesopdateringer",
|
||||
"LIVE Status": "LIVE-status",
|
||||
"Lasted for about %lastedFor": "Varede i cirka %lastedFor",
|
||||
"Light": "Lys",
|
||||
"Link Copied": "Link Kopieret",
|
||||
"MAINTENANCE": "VEDLIGEHOLDELSE",
|
||||
"MONITORING": "OVERVÅGNING",
|
||||
"Maintenance Completed": "Vedligeholdelse afsluttet",
|
||||
"Maintenance in Progress": "Vedligeholdelse i gang",
|
||||
"Mode": "Tilstand",
|
||||
"No Data": "Ingen Data",
|
||||
"No Incident in %date": "Ingen Hændelse Den %date",
|
||||
"No Incidents": "Ingen Hændelser",
|
||||
"No Monitor Found": "Ingen Monitor Fundet",
|
||||
"No Updates Yet": "Ingen Opdateringer Endnu",
|
||||
"Ongoing Incidents": "Igangværende Hændelser",
|
||||
"Pinging": "Pinger",
|
||||
"RESOLVED": "LØST",
|
||||
"Recent Incidents": "Seneste Hændelser",
|
||||
"Recent Maintenances": "Seneste Vedligeholdelser",
|
||||
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
|
||||
"Share": "Del",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
|
||||
"Started %startedAt, will last for %lastedFor more": "Startede for cirka %startedAt siden, vil vare i cirka %lastedFor mere",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
|
||||
"Starts %startedAt": "Starter om %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Switch to your timezone": "Skift til din tidszone",
|
||||
"Theme": "Tema",
|
||||
"Timezone set to %tz": "Tidszone sat til %tz",
|
||||
"Today": "I dag",
|
||||
"UP": "OPPE",
|
||||
"Upcoming Maintenance": "Kommende vedligeholdelse",
|
||||
"Updates": "Opdateringer",
|
||||
"Uptime": "Oppetid",
|
||||
"View in detail": "Vis i detaljer"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%status for %duration",
|
||||
"14 Days": "14 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"7 Days": "7 Days",
|
||||
"90 Days": "90 Days",
|
||||
"Availability per Component": "Availability per Component",
|
||||
"Back": "Back",
|
||||
"Badge Copied": "Badge Copied",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Browse Events",
|
||||
"Code Copied": "Code Copied",
|
||||
"Copy Code": "Copy Code",
|
||||
"Copy Link": "Copy Link",
|
||||
"DEGRADED": "DEGRADED",
|
||||
"DOWN": "DOWN",
|
||||
"Dark": "Dark",
|
||||
"Days": "Days",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"Embed": "Embed",
|
||||
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
|
||||
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Incident Updates": "Incident Updates",
|
||||
"LIVE Status": "LIVE Status",
|
||||
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
|
||||
"Light": "Light",
|
||||
"Link Copied": "Link Copied",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"MONITORING": "MONITORING",
|
||||
"Maintenance Completed": "Maintenance Completed",
|
||||
"Maintenance in Progress": "Maintenance in Progress",
|
||||
"Mode": "Mode",
|
||||
"No Data": "No Data",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
"Pinging": "Pinging",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"Recent Incidents": "Recent Incidents",
|
||||
"Recent Maintenances": "Recent Maintenances",
|
||||
"Share this monitor using a link with others": "Share this monitor using a link with others",
|
||||
"Share": "Share",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Theme",
|
||||
"Today": "Today",
|
||||
"UP": "UP",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"%status for %duration": "%status for %duration",
|
||||
"14 Days": "14 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"7 Days": "7 Days",
|
||||
"90 Days": "90 Days",
|
||||
"Availability per Component": "Availability per Component",
|
||||
"Back": "Back",
|
||||
"Badge Copied": "Badge Copied",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Browse Events",
|
||||
"Code Copied": "Code Copied",
|
||||
"Copy Code": "Copy Code",
|
||||
"Copy Link": "Copy Link",
|
||||
"DEGRADED": "DEGRADED",
|
||||
"DOWN": "DOWN",
|
||||
"Dark": "Dark",
|
||||
"Days": "Days",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"Embed": "Embed",
|
||||
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
|
||||
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Incident Updates": "Incident Updates",
|
||||
"LIVE Status": "LIVE Status",
|
||||
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
|
||||
"Light": "Light",
|
||||
"Link Copied": "Link Copied",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"MONITORING": "MONITORING",
|
||||
"Maintenance Completed": "Maintenance Completed",
|
||||
"Maintenance in Progress": "Maintenance in Progress",
|
||||
"Mode": "Mode",
|
||||
"No Data": "No Data",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
"Pinging": "Pinging",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"Recent Incidents": "Recent Incidents",
|
||||
"Recent Maintenances": "Recent Maintenances",
|
||||
"Share this monitor using a link with others": "Share this monitor using a link with others",
|
||||
"Share": "Share",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
"Started %startedAt, will last for %lastedFor more": "Started %startedAt, will last for %lastedFor more",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Switch to your timezone": "Switch to your timezone",
|
||||
"Theme": "Theme",
|
||||
"Timezone set to %tz": "Timezone set to %tz",
|
||||
"Today": "Today",
|
||||
"UP": "UP",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime",
|
||||
"View in detail": "View in detail"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%status pendant %duration",
|
||||
"14 Days": "14 jours",
|
||||
"30 Days": "30 jours",
|
||||
"60 Days": "60 jours",
|
||||
"7 Days": "7 jours",
|
||||
"90 Days": "90 jours",
|
||||
"Availability per Component": "Disponibilité par composant",
|
||||
"Back": "Retour",
|
||||
"Badge Copied": "Badge copié",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Parcourir les événements",
|
||||
"Code Copied": "Code copié",
|
||||
"Copy Code": "Copier le code",
|
||||
"Copy Link": "Copier le lien",
|
||||
"DEGRADED": "Dégradé",
|
||||
"DOWN": "Hors ligne",
|
||||
"Dark": "Sombre",
|
||||
"Days": "Jours",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
|
||||
"Embed": "Intégrer",
|
||||
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
|
||||
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
|
||||
"IDENTIFIED": "Identifié",
|
||||
"INVESTIGATING": "En cours d'investigation",
|
||||
"Incident Updates": "Mises à jour des incidents",
|
||||
"LIVE Status": "Statut LIVE",
|
||||
"Lasted for about %lastedFor": "A duré environ %lastedFor",
|
||||
"Light": "Clair",
|
||||
"Link Copied": "Lien copié",
|
||||
"MAINTENANCE": "Maintenance",
|
||||
"MONITORING": "En surveillance",
|
||||
"Maintenance Completed": "Maintenance terminée",
|
||||
"Maintenance in Progress": "Maintenance en cours",
|
||||
"Mode": "Mode",
|
||||
"No Data": "Pas de données",
|
||||
"No Incident in %date": "Pas d'incident le %date",
|
||||
"No Incidents": "Pas d'incidents",
|
||||
"No Monitor Found": "Aucun moniteur trouvé",
|
||||
"No Updates Yet": "Pas encore de mises à jour",
|
||||
"Ongoing Incidents": "Incidents en cours",
|
||||
"Pinging": "Ping en cours",
|
||||
"RESOLVED": "Résolu",
|
||||
"Recent Incidents": "Incidents récents",
|
||||
"Recent Maintenances": "Maintenances récentes",
|
||||
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
|
||||
"Share": "Partager",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
|
||||
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
|
||||
"Starts %startedAt": "Commence dans %startedAt",
|
||||
"Status OK": "Statut OK",
|
||||
"Status": "Statut",
|
||||
"Theme": "Thème",
|
||||
"Today": "Aujourd'hui",
|
||||
"UP": "En ligne",
|
||||
"Upcoming Maintenance": "Maintenance à venir",
|
||||
"Updates": "Mises à jour",
|
||||
"Uptime": "Temps de fonctionnement"
|
||||
"%status for %duration": "%status pendant %duration",
|
||||
"14 Days": "14 jours",
|
||||
"30 Days": "30 jours",
|
||||
"60 Days": "60 jours",
|
||||
"7 Days": "7 jours",
|
||||
"90 Days": "90 jours",
|
||||
"Availability per Component": "Disponibilité par composant",
|
||||
"Back": "Retour",
|
||||
"Badge Copied": "Badge copié",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Parcourir les événements",
|
||||
"Code Copied": "Code copié",
|
||||
"Copy Code": "Copier le code",
|
||||
"Copy Link": "Copier le lien",
|
||||
"DEGRADED": "Dégradé",
|
||||
"DOWN": "Hors ligne",
|
||||
"Dark": "Sombre",
|
||||
"Days": "Jours",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
|
||||
"Embed": "Intégrer",
|
||||
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
|
||||
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
|
||||
"IDENTIFIED": "Identifié",
|
||||
"INVESTIGATING": "En cours d'investigation",
|
||||
"Incident Updates": "Mises à jour des incidents",
|
||||
"LIVE Status": "Statut LIVE",
|
||||
"Lasted for about %lastedFor": "A duré environ %lastedFor",
|
||||
"Light": "Clair",
|
||||
"Link Copied": "Lien copié",
|
||||
"MAINTENANCE": "Maintenance",
|
||||
"MONITORING": "En surveillance",
|
||||
"Maintenance Completed": "Maintenance terminée",
|
||||
"Maintenance in Progress": "Maintenance en cours",
|
||||
"Mode": "Mode",
|
||||
"No Data": "Pas de données",
|
||||
"No Incident in %date": "Pas d'incident le %date",
|
||||
"No Incidents": "Pas d'incidents",
|
||||
"No Monitor Found": "Aucun moniteur trouvé",
|
||||
"No Updates Yet": "Pas encore de mises à jour",
|
||||
"Ongoing Incidents": "Incidents en cours",
|
||||
"Pinging": "Ping en cours",
|
||||
"RESOLVED": "Résolu",
|
||||
"Recent Incidents": "Incidents récents",
|
||||
"Recent Maintenances": "Maintenances récentes",
|
||||
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
|
||||
"Share": "Partager",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
|
||||
"Started %startedAt, will last for %lastedFor more": "Commencé il y a environ %startedAt, durera environ %lastedFor de plus",
|
||||
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
|
||||
"Starts %startedAt": "Commence dans %startedAt",
|
||||
"Status OK": "Statut OK",
|
||||
"Status": "Statut",
|
||||
"Switch to your timezone": "Passer à votre fuseau horaire",
|
||||
"Theme": "Thème",
|
||||
"Timezone set to %tz": "Fuseau horaire défini sur %tz",
|
||||
"Today": "Aujourd'hui",
|
||||
"UP": "En ligne",
|
||||
"Upcoming Maintenance": "Maintenance à venir",
|
||||
"Updates": "Mises à jour",
|
||||
"Uptime": "Temps de fonctionnement",
|
||||
"View in detail": "Voir en détail"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%duration के लिए %status",
|
||||
"14 Days": "14 दिन",
|
||||
"30 Days": "30 दिन",
|
||||
"60 Days": "60 दिन",
|
||||
"7 Days": "7 दिन",
|
||||
"90 Days": "90 दिन",
|
||||
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
|
||||
"Back": "वापस",
|
||||
"Badge Copied": "बैज कॉपी किया गया",
|
||||
"Badge": "बैज",
|
||||
"Browse Events": "ईवेंट ब्राउज़ करें",
|
||||
"Code Copied": "कोड कॉपी किया गया",
|
||||
"Copy Code": "कोड कॉपी करें",
|
||||
"Copy Link": "लिंक कॉपी करें",
|
||||
"DEGRADED": "क्षीण",
|
||||
"DOWN": "डाउन",
|
||||
"Dark": "डार्क",
|
||||
"Days": "दिन",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
|
||||
"Embed": "एम्बेड",
|
||||
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
|
||||
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
|
||||
"IDENTIFIED": "पहचाना गया",
|
||||
"INVESTIGATING": "जांच चल रही है",
|
||||
"Incident Updates": "घटना अपडेट",
|
||||
"LIVE Status": "लाइव स्टेटस",
|
||||
"Lasted for about %lastedFor": "%lastedFor तक चला",
|
||||
"Light": "लाइट",
|
||||
"Link Copied": "लिंक कॉपी किया गया",
|
||||
"MAINTENANCE": "मेंटेनेंस",
|
||||
"MONITORING": "निगरानी",
|
||||
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
|
||||
"Maintenance in Progress": "मेंटेनेंस जारी है",
|
||||
"Mode": "मोड",
|
||||
"No Data": "कोई डेटा नहीं",
|
||||
"No Incident in %date": "%date को कोई घटना नहीं",
|
||||
"No Incidents": "कोई घटना नहीं",
|
||||
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
|
||||
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
|
||||
"Ongoing Incidents": "चल रही घटनाएँ",
|
||||
"Pinging": "पिंगिंग",
|
||||
"RESOLVED": "सुलझाया गया",
|
||||
"Recent Incidents": "हाल की घटनाएँ",
|
||||
"Recent Maintenances": "हाल की मेंटेनेंस",
|
||||
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
|
||||
"Share": "साझा करें",
|
||||
"Standard": "मानक",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
|
||||
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
|
||||
"Starts %startedAt": "%startedAt में शुरू होगा",
|
||||
"Status OK": "स्थिति ठीक है",
|
||||
"Status": "स्थिति",
|
||||
"Theme": "थीम",
|
||||
"Today": "आज",
|
||||
"UP": "अप",
|
||||
"Upcoming Maintenance": "आगामी मेंटेनेंस",
|
||||
"Updates": "अपडेट्स",
|
||||
"Uptime": "अपटाइम"
|
||||
"%status for %duration": "%duration के लिए %status",
|
||||
"14 Days": "14 दिन",
|
||||
"30 Days": "30 दिन",
|
||||
"60 Days": "60 दिन",
|
||||
"7 Days": "7 दिन",
|
||||
"90 Days": "90 दिन",
|
||||
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
|
||||
"Back": "वापस",
|
||||
"Badge Copied": "बैज कॉपी किया गया",
|
||||
"Badge": "बैज",
|
||||
"Browse Events": "ईवेंट ब्राउज़ करें",
|
||||
"Code Copied": "कोड कॉपी किया गया",
|
||||
"Copy Code": "कोड कॉपी करें",
|
||||
"Copy Link": "लिंक कॉपी करें",
|
||||
"DEGRADED": "क्षीण",
|
||||
"DOWN": "डाउन",
|
||||
"Dark": "डार्क",
|
||||
"Days": "दिन",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
|
||||
"Embed": "एम्बेड",
|
||||
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
|
||||
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
|
||||
"IDENTIFIED": "पहचाना गया",
|
||||
"INVESTIGATING": "जांच चल रही है",
|
||||
"Incident Updates": "घटना अपडेट",
|
||||
"LIVE Status": "लाइव स्टेटस",
|
||||
"Lasted for about %lastedFor": "%lastedFor तक चला",
|
||||
"Light": "लाइट",
|
||||
"Link Copied": "लिंक कॉपी किया गया",
|
||||
"MAINTENANCE": "मेंटेनेंस",
|
||||
"MONITORING": "निगरानी",
|
||||
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
|
||||
"Maintenance in Progress": "मेंटेनेंस जारी है",
|
||||
"Mode": "मोड",
|
||||
"No Data": "कोई डेटा नहीं",
|
||||
"No Incident in %date": "%date को कोई घटना नहीं",
|
||||
"No Incidents": "कोई घटना नहीं",
|
||||
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
|
||||
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
|
||||
"Ongoing Incidents": "चल रही घटनाएँ",
|
||||
"Pinging": "पिंगिंग",
|
||||
"RESOLVED": "सुलझाया गया",
|
||||
"Recent Incidents": "हाल की घटनाएँ",
|
||||
"Recent Maintenances": "हाल की मेंटेनेंस",
|
||||
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
|
||||
"Share": "साझा करें",
|
||||
"Standard": "मानक",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
|
||||
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
|
||||
"Started %startedAt, will last for %lastedFor more": "%startedAt शुरू होगा, %lastedFor और चलेगा",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
|
||||
"Starts %startedAt": "%startedAt में शुरू होगा",
|
||||
"Status OK": "स्थिति ठीक है",
|
||||
"Status": "स्थिति",
|
||||
"Switch to your timezone": "अपने समय क्षेत्र में स्विच करें",
|
||||
"Theme": "थीम",
|
||||
"Timezone set to %tz": "समय क्षेत्र सेट किया गया %tz",
|
||||
"Today": "आज",
|
||||
"UP": "अप",
|
||||
"Upcoming Maintenance": "आगामी मेंटेनेंस",
|
||||
"Updates": "अपडेट्स",
|
||||
"Uptime": "अपटाइम",
|
||||
"View in detail": "विस्तार में देखें"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%duration の間 %status",
|
||||
"14 Days": "14日間",
|
||||
"30 Days": "30日間",
|
||||
"60 Days": "60日間",
|
||||
"7 Days": "7日間",
|
||||
"90 Days": "90日間",
|
||||
"Availability per Component": "コンポーネントごとの可用性",
|
||||
"Back": "戻る",
|
||||
"Badge Copied": "バッジがコピーされました",
|
||||
"Badge": "バッジ",
|
||||
"Browse Events": "イベントを閲覧",
|
||||
"Code Copied": "コードがコピーされました",
|
||||
"Copy Code": "コードをコピー",
|
||||
"Copy Link": "リンクをコピー",
|
||||
"DEGRADED": "劣化",
|
||||
"DOWN": "ダウン",
|
||||
"Dark": "ダーク",
|
||||
"Days": "日間",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "このモニターをアプリに <script> または <iframe> を使用して埋め込む。",
|
||||
"Embed": "埋め込み",
|
||||
"Get SVG badge for this monitor": "このモニターのSVGバッジを取得",
|
||||
"Get a LIVE Status for this monitor": "このモニターのライブステータスを取得",
|
||||
"IDENTIFIED": "識別済み",
|
||||
"INVESTIGATING": "調査中",
|
||||
"Incident Updates": "インシデント更新",
|
||||
"LIVE Status": "ライブステータス",
|
||||
"Lasted for about %lastedFor": "約 %lastedFor 続きました",
|
||||
"Light": "ライト",
|
||||
"Link Copied": "リンクがコピーされました",
|
||||
"MAINTENANCE": "メンテナンス",
|
||||
"MONITORING": "モニタリング中",
|
||||
"Maintenance Completed": "メンテナンス完了",
|
||||
"Maintenance in Progress": "メンテナンス中",
|
||||
"Mode": "モード",
|
||||
"No Data": "データなし",
|
||||
"No Incident in %date": "%date にインシデントなし",
|
||||
"No Incidents": "インシデントなし",
|
||||
"No Monitor Found": "モニターが見つかりません",
|
||||
"No Updates Yet": "まだ更新はありません",
|
||||
"Ongoing Incidents": "進行中のインシデント",
|
||||
"Pinging": "ピング中",
|
||||
"RESOLVED": "解決済み",
|
||||
"Recent Incidents": "最近のインシデント",
|
||||
"Recent Maintenances": "最近のメンテナンス",
|
||||
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
|
||||
"Share": "共有",
|
||||
"Standard": "標準",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
|
||||
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
|
||||
"Starts %startedAt": "%startedAt 後に開始",
|
||||
"Status OK": "ステータス正常",
|
||||
"Status": "ステータス",
|
||||
"Theme": "テーマ",
|
||||
"Today": "今日",
|
||||
"UP": "稼働中",
|
||||
"Upcoming Maintenance": "今後のメンテナンス",
|
||||
"Updates": "更新",
|
||||
"Uptime": "稼働時間"
|
||||
"%status for %duration": "%duration の間 %status",
|
||||
"14 Days": "14日間",
|
||||
"30 Days": "30日間",
|
||||
"60 Days": "60日間",
|
||||
"7 Days": "7日間",
|
||||
"90 Days": "90日間",
|
||||
"Availability per Component": "コンポーネントごとの可用性",
|
||||
"Back": "戻る",
|
||||
"Badge Copied": "バッジがコピーされました",
|
||||
"Badge": "バッジ",
|
||||
"Browse Events": "イベントを閲覧",
|
||||
"Code Copied": "コードがコピーされました",
|
||||
"Copy Code": "コードをコピー",
|
||||
"Copy Link": "リンクをコピー",
|
||||
"DEGRADED": "劣化",
|
||||
"DOWN": "ダウン",
|
||||
"Dark": "ダーク",
|
||||
"Days": "日間",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "このモニターをアプリに <script> または <iframe> を使用して埋め込む。",
|
||||
"Embed": "埋め込み",
|
||||
"Get SVG badge for this monitor": "このモニターのSVGバッジを取得",
|
||||
"Get a LIVE Status for this monitor": "このモニターのライブステータスを取得",
|
||||
"IDENTIFIED": "識別済み",
|
||||
"INVESTIGATING": "調査中",
|
||||
"Incident Updates": "インシデント更新",
|
||||
"LIVE Status": "ライブステータス",
|
||||
"Lasted for about %lastedFor": "約 %lastedFor 続きました",
|
||||
"Light": "ライト",
|
||||
"Link Copied": "リンクがコピーされました",
|
||||
"MAINTENANCE": "メンテナンス",
|
||||
"MONITORING": "モニタリング中",
|
||||
"Maintenance Completed": "メンテナンス完了",
|
||||
"Maintenance in Progress": "メンテナンス中",
|
||||
"Mode": "モード",
|
||||
"No Data": "データなし",
|
||||
"No Incident in %date": "%date にインシデントなし",
|
||||
"No Incidents": "インシデントなし",
|
||||
"No Monitor Found": "モニターが見つかりません",
|
||||
"No Updates Yet": "まだ更新はありません",
|
||||
"Ongoing Incidents": "進行中のインシデント",
|
||||
"Pinging": "ピング中",
|
||||
"RESOLVED": "解決済み",
|
||||
"Recent Incidents": "最近のインシデント",
|
||||
"Recent Maintenances": "最近のメンテナンス",
|
||||
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
|
||||
"Share": "共有",
|
||||
"Standard": "標準",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
|
||||
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
|
||||
"Started %startedAt, will last for %lastedFor more": "%startedAt 前に開始し、さらに %lastedFor 続きます",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
|
||||
"Starts %startedAt": "%startedAt 後に開始",
|
||||
"Status OK": "ステータス正常",
|
||||
"Status": "ステータス",
|
||||
"Switch to your timezone": "あなたのタイムゾーンに切り替え",
|
||||
"Theme": "テーマ",
|
||||
"Timezone set to %tz": "タイムゾーンを %tz に設定",
|
||||
"Today": "今日",
|
||||
"UP": "稼働中",
|
||||
"Upcoming Maintenance": "今後のメンテナンス",
|
||||
"Updates": "更新",
|
||||
"Uptime": "稼働時間",
|
||||
"View in detail": "詳細を表示"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%duration 동안 %status 상태였음",
|
||||
"14 Days": "14 일",
|
||||
"30 Days": "30 ",
|
||||
"60 Days": "60 일",
|
||||
"7 Days": "7 일",
|
||||
"90 Days": "90 일",
|
||||
"Availability per Component": "항목당 가용성",
|
||||
"Back": "뒤로",
|
||||
"Badge Copied": "배지 복사됨",
|
||||
"Badge": "배지",
|
||||
"Browse Events": "이벤트 찾아보기",
|
||||
"Code Copied": "소스코드 복사됨",
|
||||
"Copy Code": "소스코드 복사",
|
||||
"Copy Link": "링크 복사",
|
||||
"DEGRADED": "성능 저하됨",
|
||||
"DOWN": "중단",
|
||||
"Dark": "다크",
|
||||
"Days": "일",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "<script> 나 <iframe> 태그를 사용하여 내 앱에 이 모니터링을 임베딩 해보세요.",
|
||||
"Embed": "임베드",
|
||||
"Get SVG badge for this monitor": "이 모니터링 항목에 대한 SVG 배지를 구해보세요.",
|
||||
"Get a LIVE Status for this monitor": "이 모니터링 항목에 대한 실시간 상태를 구해보세요.",
|
||||
"IDENTIFIED": "확인됨",
|
||||
"INVESTIGATING": "조사 중",
|
||||
"Incident Updates": "사건 보고서",
|
||||
"LIVE Status": "실시간 상태",
|
||||
"Lasted for about %lastedFor": "%lastedFor 동안 지속",
|
||||
"Light": "라이트",
|
||||
"Link Copied": "링크 복사됨",
|
||||
"MAINTENANCE": "유지보수",
|
||||
"MONITORING": "감시 중",
|
||||
"Maintenance Completed": "유지보수 완료",
|
||||
"Maintenance in Progress": "유지보수 진행 중",
|
||||
"Mode": "모드",
|
||||
"No Data": "데이터 없음",
|
||||
"No Incident in %date": "%date 에는 아무 사건도 없었음",
|
||||
"No Incidents": "사건 없음",
|
||||
"No Monitor Found": "모니터링 항목을 찾을 수 없음",
|
||||
"No Updates Yet": "아직 아무 업데이트도 없음",
|
||||
"Ongoing Incidents": "진행중 사건",
|
||||
"Pinging": "핑 요청",
|
||||
"RESOLVED": "해결됨",
|
||||
"Recent Incidents": "최근 사건",
|
||||
"Recent Maintenances": "최근 유지보수",
|
||||
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
|
||||
"Share": "공유",
|
||||
"Standard": "기본",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
|
||||
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
|
||||
"Starts %startedAt": "%startedAt 에 시작되었습니다",
|
||||
"Status OK": "양호",
|
||||
"Status": "상태",
|
||||
"Theme": "테마",
|
||||
"Today": "오늘",
|
||||
"UP": "작동 중",
|
||||
"Upcoming Maintenance": "예정된 유지보수",
|
||||
"Updates": "업데이트",
|
||||
"Uptime": "업타임"
|
||||
"%status for %duration": "%duration 동안 %status 상태였음",
|
||||
"14 Days": "14 일",
|
||||
"30 Days": "30 ",
|
||||
"60 Days": "60 일",
|
||||
"7 Days": "7 일",
|
||||
"90 Days": "90 일",
|
||||
"Availability per Component": "항목당 가용성",
|
||||
"Back": "뒤로",
|
||||
"Badge Copied": "배지 복사됨",
|
||||
"Badge": "배지",
|
||||
"Browse Events": "이벤트 찾아보기",
|
||||
"Code Copied": "소스코드 복사됨",
|
||||
"Copy Code": "소스코드 복사",
|
||||
"Copy Link": "링크 복사",
|
||||
"DEGRADED": "성능 저하됨",
|
||||
"DOWN": "중단",
|
||||
"Dark": "다크",
|
||||
"Days": "일",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "<script> 나 <iframe> 태그를 사용하여 내 앱에 이 모니터링을 임베딩 해보세요.",
|
||||
"Embed": "임베드",
|
||||
"Get SVG badge for this monitor": "이 모니터링 항목에 대한 SVG 배지를 구해보세요.",
|
||||
"Get a LIVE Status for this monitor": "이 모니터링 항목에 대한 실시간 상태를 구해보세요.",
|
||||
"IDENTIFIED": "확인됨",
|
||||
"INVESTIGATING": "조사 중",
|
||||
"Incident Updates": "사건 보고서",
|
||||
"LIVE Status": "실시간 상태",
|
||||
"Lasted for about %lastedFor": "%lastedFor 동안 지속",
|
||||
"Light": "라이트",
|
||||
"Link Copied": "링크 복사됨",
|
||||
"MAINTENANCE": "유지보수",
|
||||
"MONITORING": "감시 중",
|
||||
"Maintenance Completed": "유지보수 완료",
|
||||
"Maintenance in Progress": "유지보수 진행 중",
|
||||
"Mode": "모드",
|
||||
"No Data": "데이터 없음",
|
||||
"No Incident in %date": "%date 에는 아무 사건도 없었음",
|
||||
"No Incidents": "사건 없음",
|
||||
"No Monitor Found": "모니터링 항목을 찾을 수 없음",
|
||||
"No Updates Yet": "아직 아무 업데이트도 없음",
|
||||
"Ongoing Incidents": "진행중 사건",
|
||||
"Pinging": "핑 요청",
|
||||
"RESOLVED": "해결됨",
|
||||
"Recent Incidents": "최근 사건",
|
||||
"Recent Maintenances": "최근 유지보수",
|
||||
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
|
||||
"Share": "공유",
|
||||
"Standard": "기본",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
|
||||
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
|
||||
"Started %startedAt, will last for %lastedFor more": "%startedAt 전에 시작되었으며, %lastedFor 더 지속될 예정입니다",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
|
||||
"Starts %startedAt": "%startedAt 에 시작되었습니다",
|
||||
"Status OK": "양호",
|
||||
"Status": "상태",
|
||||
"Switch to your timezone": "귀하의 시간대로 변경",
|
||||
"Theme": "테마",
|
||||
"Timezone set to %tz": "시간대 설정 %tz",
|
||||
"Today": "오늘",
|
||||
"UP": "작동 중",
|
||||
"Upcoming Maintenance": "예정된 유지보수",
|
||||
"Updates": "업데이트",
|
||||
"Uptime": "업타임",
|
||||
"View in detail": "자세히 보기"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%status i %duration",
|
||||
"14 Days": "14 dager",
|
||||
"30 Days": "30 dager",
|
||||
"60 Days": "60 dager",
|
||||
"7 Days": "7 dager",
|
||||
"90 Days": "90 dager",
|
||||
"Availability per Component": "Tilgjengelighet per komponent",
|
||||
"Back": "Tilbake",
|
||||
"Badge Copied": "Merke kopiert",
|
||||
"Badge": "Merke",
|
||||
"Browse Events": "Bla gjennom hendelser",
|
||||
"Code Copied": "Kode kopiert",
|
||||
"Copy Code": "Kopier kode",
|
||||
"Copy Link": "Kopier lenke",
|
||||
"DEGRADED": "NEDSATT",
|
||||
"DOWN": "NEDE",
|
||||
"Dark": "Mørk",
|
||||
"Days": "Dager",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Bygg inn denne monitoren ved å bruke <script> eller <iframe> i appen din.",
|
||||
"Embed": "Bygg inn",
|
||||
"Get SVG badge for this monitor": "Hent SVG-merke for denne monitoren",
|
||||
"Get a LIVE Status for this monitor": "Få en LIVE-status for denne monitoren",
|
||||
"IDENTIFIED": "IDENTIFISERT",
|
||||
"INVESTIGATING": "UNDERSØKER",
|
||||
"Incident Updates": "Hendelsesoppdateringer",
|
||||
"LIVE Status": "LIVE-status",
|
||||
"Lasted for about %lastedFor": "Varte i omtrent %lastedFor",
|
||||
"Light": "Lys",
|
||||
"Link Copied": "Lenke kopiert",
|
||||
"MAINTENANCE": "VEDLIKEHOLD",
|
||||
"MONITORING": "OVERVÅKER",
|
||||
"Maintenance Completed": "Vedlikehold fullført",
|
||||
"Maintenance in Progress": "Vedlikehold pågår",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Ingen data",
|
||||
"No Incident in %date": "Ingen hendelser på %date",
|
||||
"No Incidents": "Ingen hendelser",
|
||||
"No Monitor Found": "Ingen monitor funnet",
|
||||
"No Updates Yet": "Ingen oppdateringer ennå",
|
||||
"Ongoing Incidents": "Pågående hendelser",
|
||||
"Pinging": "Pinger",
|
||||
"RESOLVED": "LØST",
|
||||
"Recent Incidents": "Nylige hendelser",
|
||||
"Recent Maintenances": "Nylige vedlikehold",
|
||||
"Share this monitor using a link with others": "Del denne monitoren med andre via lenke",
|
||||
"Share": "Del",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starter %startedAt, vil vare i %lastedFor",
|
||||
"Starts %startedAt": "Starter %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Tema",
|
||||
"Today": "I dag",
|
||||
"UP": "OPPE",
|
||||
"Upcoming Maintenance": "Kommende vedlikehold",
|
||||
"Updates": "Oppdateringer",
|
||||
"Uptime": "Oppetid"
|
||||
"%status for %duration": "%status i %duration",
|
||||
"14 Days": "14 dager",
|
||||
"30 Days": "30 dager",
|
||||
"60 Days": "60 dager",
|
||||
"7 Days": "7 dager",
|
||||
"90 Days": "90 dager",
|
||||
"Availability per Component": "Tilgjengelighet per komponent",
|
||||
"Back": "Tilbake",
|
||||
"Badge Copied": "Merke kopiert",
|
||||
"Badge": "Merke",
|
||||
"Browse Events": "Bla gjennom hendelser",
|
||||
"Code Copied": "Kode kopiert",
|
||||
"Copy Code": "Kopier kode",
|
||||
"Copy Link": "Kopier lenke",
|
||||
"DEGRADED": "NEDSATT",
|
||||
"DOWN": "NEDE",
|
||||
"Dark": "Mørk",
|
||||
"Days": "Dager",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Bygg inn denne monitoren ved å bruke <script> eller <iframe> i appen din.",
|
||||
"Embed": "Bygg inn",
|
||||
"Get SVG badge for this monitor": "Hent SVG-merke for denne monitoren",
|
||||
"Get a LIVE Status for this monitor": "Få en LIVE-status for denne monitoren",
|
||||
"IDENTIFIED": "IDENTIFISERT",
|
||||
"INVESTIGATING": "UNDERSØKER",
|
||||
"Incident Updates": "Hendelsesoppdateringer",
|
||||
"LIVE Status": "LIVE-status",
|
||||
"Lasted for about %lastedFor": "Varte i omtrent %lastedFor",
|
||||
"Light": "Lys",
|
||||
"Link Copied": "Lenke kopiert",
|
||||
"MAINTENANCE": "VEDLIKEHOLD",
|
||||
"MONITORING": "OVERVÅKER",
|
||||
"Maintenance Completed": "Vedlikehold fullført",
|
||||
"Maintenance in Progress": "Vedlikehold pågår",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Ingen data",
|
||||
"No Incident in %date": "Ingen hendelser på %date",
|
||||
"No Incidents": "Ingen hendelser",
|
||||
"No Monitor Found": "Ingen monitor funnet",
|
||||
"No Updates Yet": "Ingen oppdateringer ennå",
|
||||
"Ongoing Incidents": "Pågående hendelser",
|
||||
"Pinging": "Pinger",
|
||||
"RESOLVED": "LØST",
|
||||
"Recent Incidents": "Nylige hendelser",
|
||||
"Recent Maintenances": "Nylige vedlikehold",
|
||||
"Share this monitor using a link with others": "Del denne monitoren med andre via lenke",
|
||||
"Share": "Del",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
|
||||
"Started %startedAt, will last for %lastedFor more": "Startet %startedAt, vil vare i %lastedFor til",
|
||||
"Starts %startedAt, will last for %lastedFor": "Starter %startedAt, vil vare i %lastedFor",
|
||||
"Starts %startedAt": "Starter %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Switch to your timezone": "Bytt til din tidssone",
|
||||
"Theme": "Tema",
|
||||
"Timezone set to %tz": "Tidssone satt til %tz",
|
||||
"Today": "I dag",
|
||||
"UP": "OPPE",
|
||||
"Upcoming Maintenance": "Kommende vedlikehold",
|
||||
"Updates": "Oppdateringer",
|
||||
"Uptime": "Oppetid",
|
||||
"View in detail": "Vis detaljer"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%status voor %duration",
|
||||
"14 Days": "14 dagen",
|
||||
"30 Days": "30 dagen",
|
||||
"60 Days": "60 dagen",
|
||||
"7 Days": "7 dagen",
|
||||
"90 Days": "90 dagen",
|
||||
"Availability per Component": "Beschikbaarheid per component",
|
||||
"Back": "Terug",
|
||||
"Badge Copied": "Badge gekopieerd",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Evenementen bekijken",
|
||||
"Code Copied": "Code gekopieerd",
|
||||
"Copy Code": "Kopieer code",
|
||||
"Copy Link": "Kopieer link",
|
||||
"DEGRADED": "BEPERKT",
|
||||
"DOWN": "NIET BEREIKBAAR",
|
||||
"Dark": "Donker",
|
||||
"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",
|
||||
"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",
|
||||
"INVESTIGATING": "ONDERZOEK BEZIG",
|
||||
"Incident Updates": "Incident updates",
|
||||
"LIVE Status": "LIVE status",
|
||||
"Lasted for about %lastedFor": "Duurde ongeveer %lastedFor",
|
||||
"Light": "Licht",
|
||||
"Link Copied": "Link gekopieerd",
|
||||
"MAINTENANCE": "ONDERHOUD",
|
||||
"MONITORING": "MONITOREN",
|
||||
"Maintenance Completed": "Onderhoud voltooid",
|
||||
"Maintenance in Progress": "Onderhoud bezig",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Geen gegevens",
|
||||
"No Incident in %date": "Geen incident op %date",
|
||||
"No Incidents": "Geen incidenten",
|
||||
"No Monitor Found": "Geen monitor gevonden",
|
||||
"No Updates Yet": "Nog geen updates",
|
||||
"Ongoing Incidents": "Lopende incidenten",
|
||||
"Pinging": "Pingen",
|
||||
"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",
|
||||
"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",
|
||||
"Starts %startedAt, will last for %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
|
||||
"Starts %startedAt": "Begint over %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Thema",
|
||||
"Today": "Vandaag",
|
||||
"UP": "BEREIKBAAR",
|
||||
"Upcoming Maintenance": "Aankomend onderhoud",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"%status for %duration": "%status voor %duration",
|
||||
"14 Days": "14 dagen",
|
||||
"30 Days": "30 dagen",
|
||||
"60 Days": "60 dagen",
|
||||
"7 Days": "7 dagen",
|
||||
"90 Days": "90 dagen",
|
||||
"Availability per Component": "Beschikbaarheid per component",
|
||||
"Back": "Terug",
|
||||
"Badge Copied": "Badge gekopieerd",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Evenementen bekijken",
|
||||
"Code Copied": "Code gekopieerd",
|
||||
"Copy Code": "Kopieer code",
|
||||
"Copy Link": "Kopieer link",
|
||||
"DEGRADED": "BEPERKT",
|
||||
"DOWN": "NIET BEREIKBAAR",
|
||||
"Dark": "Donker",
|
||||
"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",
|
||||
"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",
|
||||
"INVESTIGATING": "ONDERZOEK BEZIG",
|
||||
"Incident Updates": "Incident updates",
|
||||
"LIVE Status": "LIVE status",
|
||||
"Lasted for about %lastedFor": "Duurde ongeveer %lastedFor",
|
||||
"Light": "Licht",
|
||||
"Link Copied": "Link gekopieerd",
|
||||
"MAINTENANCE": "ONDERHOUD",
|
||||
"MONITORING": "MONITOREN",
|
||||
"Maintenance Completed": "Onderhoud voltooid",
|
||||
"Maintenance in Progress": "Onderhoud bezig",
|
||||
"Mode": "Modus",
|
||||
"No Data": "Geen gegevens",
|
||||
"No Incident in %date": "Geen incident op %date",
|
||||
"No Incidents": "Geen incidenten",
|
||||
"No Monitor Found": "Geen monitor gevonden",
|
||||
"No Updates Yet": "Nog geen updates",
|
||||
"Ongoing Incidents": "Lopende incidenten",
|
||||
"Pinging": "Pingen",
|
||||
"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",
|
||||
"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",
|
||||
"Started %startedAt, will last for %lastedFor more": "Begint %startedAt, zal nog ongeveer %lastedFor duren",
|
||||
"Starts %startedAt, will last for %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
|
||||
"Starts %startedAt": "Begint over %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Switch to your timezone": "Schakel over naar je tijdzone",
|
||||
"Theme": "Thema",
|
||||
"Timezone set to %tz": "Tijdzone ingesteld op %tz",
|
||||
"Today": "Vandaag",
|
||||
"UP": "BEREIKBAAR",
|
||||
"Upcoming Maintenance": "Aankomend onderhoud",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime",
|
||||
"View in detail": "Bekijk in detail"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%status por %duration",
|
||||
"14 Days": "14 Dias",
|
||||
"30 Days": "30 Dias",
|
||||
"60 Days": "60 Dias",
|
||||
"7 Days": "7 Dias",
|
||||
"90 Days": "90 Dias",
|
||||
"Availability per Component": "Disponibilidade por Componente",
|
||||
"Back": "Voltar",
|
||||
"Badge Copied": "Emblema Copiado",
|
||||
"Badge": "Emblema",
|
||||
"Browse Events": "Navegar Eventos",
|
||||
"Code Copied": "Código Copiado",
|
||||
"Copy Code": "Copiar Código",
|
||||
"Copy Link": "Copiar Link",
|
||||
"DEGRADED": "FALHA",
|
||||
"DOWN": "INDISPONÍVEL",
|
||||
"Dark": "Escuro",
|
||||
"Days": "Dias",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
|
||||
"Embed": "Incorporar",
|
||||
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
|
||||
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
|
||||
"IDENTIFIED": "IDENTIFICADO",
|
||||
"INVESTIGATING": "INVESTIGANDO",
|
||||
"Incident Updates": "Atualizações do Incidente",
|
||||
"LIVE Status": "Status AO VIVO",
|
||||
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
|
||||
"Light": "Claro",
|
||||
"Link Copied": "Link Copiado",
|
||||
"MAINTENANCE": "MANUTENÇÃO",
|
||||
"MONITORING": "MONITORANDO",
|
||||
"Maintenance Completed": "Manutenção Concluída",
|
||||
"Maintenance in Progress": "Manutenção em Progresso",
|
||||
"Mode": "Modo",
|
||||
"No Data": "Sem dados",
|
||||
"No Incident in %date": "Nenhum incidente em %date",
|
||||
"No Incidents": "Sem Incidentes",
|
||||
"No Monitor Found": "Nenhum monitor encontrado",
|
||||
"No Updates Yet": "Nenhuma atualização ainda",
|
||||
"Ongoing Incidents": "Incidentes em Andamento",
|
||||
"Pinging": "Animado",
|
||||
"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",
|
||||
"Standard": "Padrão",
|
||||
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
|
||||
"Starts %startedAt, will last for %lastedFor": "Terá início em %startedAt, com duração de %lastedFor",
|
||||
"Starts %startedAt": "Inicia em %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Theme": "Tema",
|
||||
"Today": "Hoje",
|
||||
"UP": "OPERACIONAL",
|
||||
"Upcoming Maintenance": "Manutenção Agendada",
|
||||
"Updates": "Atualizações",
|
||||
"Uptime": "Tempo de Atividade"
|
||||
"%status for %duration": "%status por %duration",
|
||||
"14 Days": "14 Dias",
|
||||
"30 Days": "30 Dias",
|
||||
"60 Days": "60 Dias",
|
||||
"7 Days": "7 Dias",
|
||||
"90 Days": "90 Dias",
|
||||
"Availability per Component": "Disponibilidade por Componente",
|
||||
"Back": "Voltar",
|
||||
"Badge Copied": "Emblema Copiado",
|
||||
"Badge": "Emblema",
|
||||
"Browse Events": "Navegar Eventos",
|
||||
"Code Copied": "Código Copiado",
|
||||
"Copy Code": "Copiar Código",
|
||||
"Copy Link": "Copiar Link",
|
||||
"DEGRADED": "FALHA",
|
||||
"DOWN": "INDISPONÍVEL",
|
||||
"Dark": "Escuro",
|
||||
"Days": "Dias",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
|
||||
"Embed": "Incorporar",
|
||||
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
|
||||
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
|
||||
"IDENTIFIED": "IDENTIFICADO",
|
||||
"INVESTIGATING": "INVESTIGANDO",
|
||||
"Incident Updates": "Atualizações do Incidente",
|
||||
"LIVE Status": "Status AO VIVO",
|
||||
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
|
||||
"Light": "Claro",
|
||||
"Link Copied": "Link Copiado",
|
||||
"MAINTENANCE": "MANUTENÇÃO",
|
||||
"MONITORING": "MONITORANDO",
|
||||
"Maintenance Completed": "Manutenção Concluída",
|
||||
"Maintenance in Progress": "Manutenção em Progresso",
|
||||
"Mode": "Modo",
|
||||
"No Data": "Sem dados",
|
||||
"No Incident in %date": "Nenhum incidente em %date",
|
||||
"No Incidents": "Sem Incidentes",
|
||||
"No Monitor Found": "Nenhum monitor encontrado",
|
||||
"No Updates Yet": "Nenhuma atualização ainda",
|
||||
"Ongoing Incidents": "Incidentes em Andamento",
|
||||
"Pinging": "Animado",
|
||||
"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",
|
||||
"Standard": "Padrão",
|
||||
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
|
||||
"Started %startedAt, will last for %lastedFor more": "Iniciará em %startedAt, durará mais %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor": "Terá início em %startedAt, com duração de %lastedFor",
|
||||
"Starts %startedAt": "Inicia em %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Switch to your timezone": "Mudar para o seu fuso horário",
|
||||
"Theme": "Tema",
|
||||
"Timezone set to %tz": "Fuso horário definido para %tz",
|
||||
"Today": "Hoje",
|
||||
"UP": "OPERACIONAL",
|
||||
"Upcoming Maintenance": "Manutenção Agendada",
|
||||
"Updates": "Atualizações",
|
||||
"Uptime": "Tempo de Atividade",
|
||||
"View in detail": "Ver em detalhes"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%status в течение %duration",
|
||||
"14 Days": "14 Дней",
|
||||
"30 Days": "30 Дней",
|
||||
"60 Days": "60 Дней",
|
||||
"7 Days": "7 Дней",
|
||||
"90 Days": "90 Дней",
|
||||
"Availability per Component": "Доступность по компонентам",
|
||||
"Back": "Назад",
|
||||
"Badge Copied": "Значок скопирован",
|
||||
"Badge": "Значок",
|
||||
"Browse Events": "Просмотр событий",
|
||||
"Code Copied": "Код скопирован",
|
||||
"Copy Code": "Копировать код",
|
||||
"Copy Link": "Копировать ссылку",
|
||||
"DEGRADED": "УХУДШЕНО",
|
||||
"DOWN": "НЕДОСТУПНО",
|
||||
"Dark": "Темная",
|
||||
"Days": "Дней",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Встройте этот монитор используя <script> или <iframe> в ваше приложение.",
|
||||
"Embed": "Встроить",
|
||||
"Get SVG badge for this monitor": "Получить SVG значок для этого монитора",
|
||||
"Get a LIVE Status for this monitor": "Получить ЖИВОЙ статус для этого монитора",
|
||||
"IDENTIFIED": "ВЫЯВЛЕНО",
|
||||
"INVESTIGATING": "ИССЛЕДУЕТСЯ",
|
||||
"Incident Updates": "Обновления инцидента",
|
||||
"LIVE Status": "ЖИВОЙ статус",
|
||||
"Lasted for about %lastedFor": "Длилось около %lastedFor",
|
||||
"Light": "Светлая",
|
||||
"Link Copied": "Ссылка скопирована",
|
||||
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
|
||||
"MONITORING": "МОНИТОРИНГ",
|
||||
"Maintenance Completed": "Техническое обслуживание завершено",
|
||||
"Maintenance in Progress": "Техническое обслуживание",
|
||||
"Mode": "Режим",
|
||||
"No Data": "Нет данных",
|
||||
"No Incident in %date": "Нет инцидентов на %date",
|
||||
"No Incidents": "Нет инцидентов",
|
||||
"No Monitor Found": "Монитор не найден",
|
||||
"No Updates Yet": "Пока нет обновлений",
|
||||
"Ongoing Incidents": "Текущие инциденты",
|
||||
"Pinging": "Пингуется",
|
||||
"RESOLVED": "РЕШЕНО",
|
||||
"Recent Incidents": "Недавние инциденты",
|
||||
"Recent Maintenances": "Последние обслуживания",
|
||||
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
|
||||
"Share": "Поделиться",
|
||||
"Standard": "Стандартная",
|
||||
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
|
||||
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
|
||||
"Starts %startedAt": "Начнется через %startedAt",
|
||||
"Status OK": "Статус ОК",
|
||||
"Status": "Статус",
|
||||
"Theme": "Тема",
|
||||
"Today": "Сегодня",
|
||||
"UP": "РАБОТАЕТ",
|
||||
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
|
||||
"Updates": "Обновления",
|
||||
"Uptime": "Время работы"
|
||||
"%status for %duration": "%status в течение %duration",
|
||||
"14 Days": "14 Дней",
|
||||
"30 Days": "30 Дней",
|
||||
"60 Days": "60 Дней",
|
||||
"7 Days": "7 Дней",
|
||||
"90 Days": "90 Дней",
|
||||
"Availability per Component": "Доступность по компонентам",
|
||||
"Back": "Назад",
|
||||
"Badge Copied": "Значок скопирован",
|
||||
"Badge": "Значок",
|
||||
"Browse Events": "Просмотр событий",
|
||||
"Code Copied": "Код скопирован",
|
||||
"Copy Code": "Копировать код",
|
||||
"Copy Link": "Копировать ссылку",
|
||||
"DEGRADED": "УХУДШЕНО",
|
||||
"DOWN": "НЕДОСТУПНО",
|
||||
"Dark": "Темная",
|
||||
"Days": "Дней",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Встройте этот монитор используя <script> или <iframe> в ваше приложение.",
|
||||
"Embed": "Встроить",
|
||||
"Get SVG badge for this monitor": "Получить SVG значок для этого монитора",
|
||||
"Get a LIVE Status for this monitor": "Получить ЖИВОЙ статус для этого монитора",
|
||||
"IDENTIFIED": "ВЫЯВЛЕНО",
|
||||
"INVESTIGATING": "ИССЛЕДУЕТСЯ",
|
||||
"Incident Updates": "Обновления инцидента",
|
||||
"LIVE Status": "ЖИВОЙ статус",
|
||||
"Lasted for about %lastedFor": "Длилось около %lastedFor",
|
||||
"Light": "Светлая",
|
||||
"Link Copied": "Ссылка скопирована",
|
||||
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
|
||||
"MONITORING": "МОНИТОРИНГ",
|
||||
"Maintenance Completed": "Техническое обслуживание завершено",
|
||||
"Maintenance in Progress": "Техническое обслуживание",
|
||||
"Mode": "Режим",
|
||||
"No Data": "Нет данных",
|
||||
"No Incident in %date": "Нет инцидентов на %date",
|
||||
"No Incidents": "Нет инцидентов",
|
||||
"No Monitor Found": "Монитор не найден",
|
||||
"No Updates Yet": "Пока нет обновлений",
|
||||
"Ongoing Incidents": "Текущие инциденты",
|
||||
"Pinging": "Пингуется",
|
||||
"RESOLVED": "РЕШЕНО",
|
||||
"Recent Incidents": "Недавние инциденты",
|
||||
"Recent Maintenances": "Последние обслуживания",
|
||||
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
|
||||
"Share": "Поделиться",
|
||||
"Standard": "Стандартная",
|
||||
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
|
||||
"Started %startedAt, will last for %lastedFor more": "Начнется около %startedAt, продлится еще около %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
|
||||
"Starts %startedAt": "Начнется через %startedAt",
|
||||
"Status OK": "Статус ОК",
|
||||
"Status": "Статус",
|
||||
"Switch to your timezone": "Переключиться на ваш часовой пояс",
|
||||
"Theme": "Тема",
|
||||
"Timezone set to %tz": "Часовой пояс установлен на %tz",
|
||||
"Today": "Сегодня",
|
||||
"UP": "РАБОТАЕТ",
|
||||
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
|
||||
"Updates": "Обновления",
|
||||
"Uptime": "Время работы",
|
||||
"View in detail": "Подробный просмотр"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%duration boyunca %status",
|
||||
"14 Days": "14 Gün",
|
||||
"30 Days": "30 Gün",
|
||||
"60 Days": "60 Gün",
|
||||
"7 Days": "7 Gün",
|
||||
"90 Days": "90 Gün",
|
||||
"Availability per Component": "Bileşen Kullanılabilirliği",
|
||||
"Back": "Geri",
|
||||
"Badge Copied": "Rozet Kopyalandı",
|
||||
"Badge": "Rozet",
|
||||
"Browse Events": "Etkinliklere Göz At",
|
||||
"Code Copied": "Kod Kopyalandı",
|
||||
"Copy Code": "Kodu Kopyala",
|
||||
"Copy Link": "Bağlantıyı Kopyala",
|
||||
"DEGRADED": "BOZUK",
|
||||
"DOWN": "ÇALIŞMIYOR",
|
||||
"Dark": "Koyu",
|
||||
"Days": "Günler",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
|
||||
"Embed": "Yerleştir",
|
||||
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
|
||||
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
|
||||
"IDENTIFIED": "TANIMLANDI",
|
||||
"INVESTIGATING": "İNCELENİYOR",
|
||||
"Incident Updates": "Olay Güncellemeleri",
|
||||
"LIVE Status": "CANLI Durum",
|
||||
"Lasted for about %lastedFor": "%lastedFor sürdü",
|
||||
"Light": "Açık",
|
||||
"Link Copied": "Bağlantı Kopyalandı",
|
||||
"MAINTENANCE": "BAKIM",
|
||||
"MONITORING": "İZLEME",
|
||||
"Maintenance Completed": "Bakım Tamamlandı",
|
||||
"Maintenance in Progress": "Bakım Devam Ediyor",
|
||||
"Mode": "Mod",
|
||||
"No Data": "Veri Yok",
|
||||
"No Incident in %date": "%date Tarihinde Olay Yok",
|
||||
"No Incidents": "Olay Yok",
|
||||
"No Monitor Found": "Servis Bulunamadı",
|
||||
"No Updates Yet": "Henüz Güncelleme Yok",
|
||||
"Ongoing Incidents": "Devam Eden Olaylar",
|
||||
"Pinging": "Pinging",
|
||||
"RESOLVED": "ÇÖZÜLDÜ",
|
||||
"Recent Incidents": "Son Olaylar",
|
||||
"Recent Maintenances": "Son Bakımlar",
|
||||
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
|
||||
"Share": "Paylaş",
|
||||
"Standard": "Standart",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
|
||||
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt başlıyor, %lastedFor sürecek",
|
||||
"Starts %startedAt": "%startedAt başlıyor",
|
||||
"Status OK": "Durum UYGUN",
|
||||
"Status": "Durum",
|
||||
"Theme": "Tema",
|
||||
"Today": "Bugün",
|
||||
"UP": "ÇALIŞIYOR",
|
||||
"Upcoming Maintenance": "Yaklaşan Bakım",
|
||||
"Updates": "Güncellemeler",
|
||||
"Uptime": "Çalışma Süresi"
|
||||
"%status for %duration": "%duration boyunca %status",
|
||||
"14 Days": "14 Gün",
|
||||
"30 Days": "30 Gün",
|
||||
"60 Days": "60 Gün",
|
||||
"7 Days": "7 Gün",
|
||||
"90 Days": "90 Gün",
|
||||
"Availability per Component": "Bileşen Kullanılabilirliği",
|
||||
"Back": "Geri",
|
||||
"Badge Copied": "Rozet Kopyalandı",
|
||||
"Badge": "Rozet",
|
||||
"Browse Events": "Etkinliklere Göz At",
|
||||
"Code Copied": "Kod Kopyalandı",
|
||||
"Copy Code": "Kodu Kopyala",
|
||||
"Copy Link": "Bağlantıyı Kopyala",
|
||||
"DEGRADED": "BOZUK",
|
||||
"DOWN": "ÇALIŞMIYOR",
|
||||
"Dark": "Koyu",
|
||||
"Days": "Günler",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
|
||||
"Embed": "Yerleştir",
|
||||
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
|
||||
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
|
||||
"IDENTIFIED": "TANIMLANDI",
|
||||
"INVESTIGATING": "İNCELENİYOR",
|
||||
"Incident Updates": "Olay Güncellemeleri",
|
||||
"LIVE Status": "CANLI Durum",
|
||||
"Lasted for about %lastedFor": "%lastedFor sürdü",
|
||||
"Light": "Açık",
|
||||
"Link Copied": "Bağlantı Kopyalandı",
|
||||
"MAINTENANCE": "BAKIM",
|
||||
"MONITORING": "İZLEME",
|
||||
"Maintenance Completed": "Bakım Tamamlandı",
|
||||
"Maintenance in Progress": "Bakım Devam Ediyor",
|
||||
"Mode": "Mod",
|
||||
"No Data": "Veri Yok",
|
||||
"No Incident in %date": "%date Tarihinde Olay Yok",
|
||||
"No Incidents": "Olay Yok",
|
||||
"No Monitor Found": "Servis Bulunamadı",
|
||||
"No Updates Yet": "Henüz Güncelleme Yok",
|
||||
"Ongoing Incidents": "Devam Eden Olaylar",
|
||||
"Pinging": "Pinging",
|
||||
"RESOLVED": "ÇÖZÜLDÜ",
|
||||
"Recent Incidents": "Son Olaylar",
|
||||
"Recent Maintenances": "Son Bakımlar",
|
||||
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
|
||||
"Share": "Paylaş",
|
||||
"Standard": "Standart",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
|
||||
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
|
||||
"Started %startedAt, will last for %lastedFor more": "%startedAt başladı, %lastedFor daha sürecek",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt başlıyor, %lastedFor sürecek",
|
||||
"Starts %startedAt": "%startedAt başlıyor",
|
||||
"Status OK": "Durum UYGUN",
|
||||
"Status": "Durum",
|
||||
"Switch to your timezone": "Saat diliminize geçin",
|
||||
"Theme": "Tema",
|
||||
"Timezone set to %tz": "Saat dilimi %tz olarak ayarlandı",
|
||||
"Today": "Bugün",
|
||||
"UP": "ÇALIŞIYOR",
|
||||
"Upcoming Maintenance": "Yaklaşan Bakım",
|
||||
"Updates": "Güncellemeler",
|
||||
"Uptime": "Çalışma Süresi",
|
||||
"View in detail": "Detaylı Görüntüle"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%status trong %duration",
|
||||
"14 Days": "14 ngày",
|
||||
"30 Days": "30 ngày",
|
||||
"60 Days": "60 ngày",
|
||||
"7 Days": "7 ngày",
|
||||
"90 Days": "90 ngày",
|
||||
"Availability per Component": "Tính khả dụng theo thành phần",
|
||||
"Back": "Quay lại",
|
||||
"Badge Copied": "Huy hiệu đã được sao chép",
|
||||
"Badge": "Huy hiệu",
|
||||
"Browse Events": "Duyệt sự kiện",
|
||||
"Code Copied": "Mã đã được sao chép",
|
||||
"Copy Code": "Sao chép mã",
|
||||
"Copy Link": "Sao chép liên kết",
|
||||
"DEGRADED": "Suy giảm",
|
||||
"DOWN": "Ngừng hoạt động",
|
||||
"Dark": "Tối",
|
||||
"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",
|
||||
"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",
|
||||
"INVESTIGATING": "Đang điều tra",
|
||||
"Incident Updates": "Cập nhật sự cố",
|
||||
"LIVE Status": "Trạng thái TRỰC TIẾP",
|
||||
"Lasted for about %lastedFor": "Kéo dài khoảng %lastedFor",
|
||||
"Light": "Sáng",
|
||||
"Link Copied": "Liên kết đã được sao chép",
|
||||
"MAINTENANCE": "Bảo trì",
|
||||
"MONITORING": "Đang giám sát",
|
||||
"Maintenance Completed": "Bảo trì đã hoàn thành",
|
||||
"Maintenance in Progress": "Bảo trì đang diễn ra",
|
||||
"Mode": "Chế độ",
|
||||
"No Data": "Không có dữ liệu",
|
||||
"No Incident in %date": "Không có sự cố vào %date",
|
||||
"No Incidents": "Không có sự cố",
|
||||
"No Monitor Found": "Không tìm thấy trình theo dõi",
|
||||
"No Updates Yet": "Chưa có cập nhật",
|
||||
"Ongoing Incidents": "Sự cố đang diễn ra",
|
||||
"Pinging": "Đang kiểm tra",
|
||||
"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ẻ",
|
||||
"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",
|
||||
"Starts %startedAt, will last for %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
|
||||
"Starts %startedAt": "Bắt đầu trong %startedAt",
|
||||
"Status OK": "Trạng thái OK",
|
||||
"Status": "Trạng thái",
|
||||
"Theme": "Giao diện",
|
||||
"Today": "Hôm nay",
|
||||
"UP": "Hoạt động",
|
||||
"Upcoming Maintenance": "Bảo trì sắp tới",
|
||||
"Updates": "Cập nhật",
|
||||
"Uptime": "Thời gian hoạt động"
|
||||
"%status for %duration": "%status trong %duration",
|
||||
"14 Days": "14 ngày",
|
||||
"30 Days": "30 ngày",
|
||||
"60 Days": "60 ngày",
|
||||
"7 Days": "7 ngày",
|
||||
"90 Days": "90 ngày",
|
||||
"Availability per Component": "Tính khả dụng theo thành phần",
|
||||
"Back": "Quay lại",
|
||||
"Badge Copied": "Huy hiệu đã được sao chép",
|
||||
"Badge": "Huy hiệu",
|
||||
"Browse Events": "Duyệt sự kiện",
|
||||
"Code Copied": "Mã đã được sao chép",
|
||||
"Copy Code": "Sao chép mã",
|
||||
"Copy Link": "Sao chép liên kết",
|
||||
"DEGRADED": "Suy giảm",
|
||||
"DOWN": "Ngừng hoạt động",
|
||||
"Dark": "Tối",
|
||||
"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",
|
||||
"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",
|
||||
"INVESTIGATING": "Đang điều tra",
|
||||
"Incident Updates": "Cập nhật sự cố",
|
||||
"LIVE Status": "Trạng thái TRỰC TIẾP",
|
||||
"Lasted for about %lastedFor": "Kéo dài khoảng %lastedFor",
|
||||
"Light": "Sáng",
|
||||
"Link Copied": "Liên kết đã được sao chép",
|
||||
"MAINTENANCE": "Bảo trì",
|
||||
"MONITORING": "Đang giám sát",
|
||||
"Maintenance Completed": "Bảo trì đã hoàn thành",
|
||||
"Maintenance in Progress": "Bảo trì đang diễn ra",
|
||||
"Mode": "Chế độ",
|
||||
"No Data": "Không có dữ liệu",
|
||||
"No Incident in %date": "Không có sự cố vào %date",
|
||||
"No Incidents": "Không có sự cố",
|
||||
"No Monitor Found": "Không tìm thấy trình theo dõi",
|
||||
"No Updates Yet": "Chưa có cập nhật",
|
||||
"Ongoing Incidents": "Sự cố đang diễn ra",
|
||||
"Pinging": "Đang kiểm tra",
|
||||
"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ẻ",
|
||||
"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",
|
||||
"Started %startedAt, will last for %lastedFor more": "Bắt đầu khoảng %startedAt trước, sẽ kéo dài thêm khoảng %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
|
||||
"Starts %startedAt": "Bắt đầu trong %startedAt",
|
||||
"Status OK": "Trạng thái OK",
|
||||
"Status": "Trạng thái",
|
||||
"Switch to your timezone": "Chuyển sang múi giờ của bạn",
|
||||
"Theme": "Giao diện",
|
||||
"Timezone set to %tz": "Đã đặt múi giờ %tz",
|
||||
"Today": "Hôm nay",
|
||||
"UP": "Hoạt động",
|
||||
"Upcoming Maintenance": "Bảo trì sắp tới",
|
||||
"Updates": "Cập nhật",
|
||||
"Uptime": "Thời gian hoạt động",
|
||||
"View in detail": "Xem chi tiết"
|
||||
}
|
||||
|
||||
+63
-59
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"%status for %duration": "%duration 内的 %status",
|
||||
"14 Days": "14天",
|
||||
"30 Days": "30天",
|
||||
"60 Days": "60天",
|
||||
"7 Days": "7天",
|
||||
"90 Days": "90天",
|
||||
"Availability per Component": "每个组件的可用性",
|
||||
"Back": "返回",
|
||||
"Badge Copied": "徽章已复制",
|
||||
"Badge": "徽章",
|
||||
"Browse Events": "浏览事件",
|
||||
"Code Copied": "代码已复制",
|
||||
"Copy Code": "复制代码",
|
||||
"Copy Link": "复制链接",
|
||||
"DEGRADED": "性能下降",
|
||||
"DOWN": "不可用",
|
||||
"Dark": "深色",
|
||||
"Days": "天",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "使用 <script> 或 <iframe> 将此监控嵌入您的应用。",
|
||||
"Embed": "嵌入",
|
||||
"Get SVG badge for this monitor": "获取此监控的SVG徽章",
|
||||
"Get a LIVE Status for this monitor": "获取此监控的实时状态",
|
||||
"IDENTIFIED": "已识别",
|
||||
"INVESTIGATING": "正在调查",
|
||||
"Incident Updates": "事件更新",
|
||||
"LIVE Status": "实时状态",
|
||||
"Lasted for about %lastedFor": "持续了约 %lastedFor",
|
||||
"Light": "浅色",
|
||||
"Link Copied": "链接已复制",
|
||||
"MAINTENANCE": "维护",
|
||||
"MONITORING": "正在监控",
|
||||
"Maintenance Completed": "维护已完成",
|
||||
"Maintenance in Progress": "维护进行中",
|
||||
"Mode": "模式",
|
||||
"No Data": "无数据",
|
||||
"No Incident in %date": "%date 无事件",
|
||||
"No Incidents": "无事件",
|
||||
"No Monitor Found": "未找到监控",
|
||||
"No Updates Yet": "尚无更新",
|
||||
"Ongoing Incidents": "正在处理的事件",
|
||||
"Pinging": "正在测试",
|
||||
"RESOLVED": "已解决",
|
||||
"Recent Incidents": "最近的事件",
|
||||
"Recent Maintenances": "最近的维护",
|
||||
"Share this monitor using a link with others": "通过链接与他人分享此监控",
|
||||
"Share": "分享",
|
||||
"Standard": "标准",
|
||||
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
|
||||
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
|
||||
"Starts %startedAt": "%startedAt 后开始",
|
||||
"Status OK": "状态正常",
|
||||
"Status": "状态",
|
||||
"Theme": "主题",
|
||||
"Today": "今天",
|
||||
"UP": "可用",
|
||||
"Upcoming Maintenance": "即将进行的维护",
|
||||
"Updates": "更新",
|
||||
"Uptime": "正常运行时间"
|
||||
"%status for %duration": "%duration 内的 %status",
|
||||
"14 Days": "14天",
|
||||
"30 Days": "30天",
|
||||
"60 Days": "60天",
|
||||
"7 Days": "7天",
|
||||
"90 Days": "90天",
|
||||
"Availability per Component": "每个组件的可用性",
|
||||
"Back": "返回",
|
||||
"Badge Copied": "徽章已复制",
|
||||
"Badge": "徽章",
|
||||
"Browse Events": "浏览事件",
|
||||
"Code Copied": "代码已复制",
|
||||
"Copy Code": "复制代码",
|
||||
"Copy Link": "复制链接",
|
||||
"DEGRADED": "性能下降",
|
||||
"DOWN": "不可用",
|
||||
"Dark": "深色",
|
||||
"Days": "天",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "使用 <script> 或 <iframe> 将此监控嵌入您的应用。",
|
||||
"Embed": "嵌入",
|
||||
"Get SVG badge for this monitor": "获取此监控的SVG徽章",
|
||||
"Get a LIVE Status for this monitor": "获取此监控的实时状态",
|
||||
"IDENTIFIED": "已识别",
|
||||
"INVESTIGATING": "正在调查",
|
||||
"Incident Updates": "事件更新",
|
||||
"LIVE Status": "实时状态",
|
||||
"Lasted for about %lastedFor": "持续了约 %lastedFor",
|
||||
"Light": "浅色",
|
||||
"Link Copied": "链接已复制",
|
||||
"MAINTENANCE": "维护",
|
||||
"MONITORING": "正在监控",
|
||||
"Maintenance Completed": "维护已完成",
|
||||
"Maintenance in Progress": "维护进行中",
|
||||
"Mode": "模式",
|
||||
"No Data": "无数据",
|
||||
"No Incident in %date": "%date 无事件",
|
||||
"No Incidents": "无事件",
|
||||
"No Monitor Found": "未找到监控",
|
||||
"No Updates Yet": "尚无更新",
|
||||
"Ongoing Incidents": "正在处理的事件",
|
||||
"Pinging": "正在测试",
|
||||
"RESOLVED": "已解决",
|
||||
"Recent Incidents": "最近的事件",
|
||||
"Recent Maintenances": "最近的维护",
|
||||
"Share this monitor using a link with others": "通过链接与他人分享此监控",
|
||||
"Share": "分享",
|
||||
"Standard": "标准",
|
||||
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
|
||||
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
|
||||
"Started %startedAt, will last for %lastedFor more": "大约 %startedAt 前开始,将持续约 %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
|
||||
"Starts %startedAt": "%startedAt 后开始",
|
||||
"Status OK": "状态正常",
|
||||
"Status": "状态",
|
||||
"Switch to your timezone": "切换到您的时区",
|
||||
"Theme": "主题",
|
||||
"Timezone set to %tz": "时区设置为 %tz",
|
||||
"Today": "今天",
|
||||
"UP": "可用",
|
||||
"Upcoming Maintenance": "即将进行的维护",
|
||||
"Updates": "更新",
|
||||
"Uptime": "正常运行时间",
|
||||
"View in detail": "详细查看"
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ async function createNewIncident(monitor, alert, commonData) {
|
||||
let payload = {
|
||||
start_date_time: startDateTime,
|
||||
title: commonData.alert_name,
|
||||
incident_source: "ALERT",
|
||||
};
|
||||
|
||||
let update = commonData.description;
|
||||
|
||||
@@ -102,6 +102,9 @@ const TIMEOUT = "timeout";
|
||||
const ERROR = "error";
|
||||
const MANUAL = "manual";
|
||||
const WEBHOOK = "webhook";
|
||||
const DEFAULT_STATUS = "default_status";
|
||||
const SIGNAL = "signal";
|
||||
const INVITE_VERIFY_EMAIL = "invite_verify_email";
|
||||
|
||||
export {
|
||||
MONITOR,
|
||||
@@ -119,4 +122,7 @@ export {
|
||||
MANUAL,
|
||||
NO_DATA,
|
||||
WEBHOOK,
|
||||
DEFAULT_STATUS,
|
||||
SIGNAL,
|
||||
INVITE_VERIFY_EMAIL,
|
||||
};
|
||||
|
||||
@@ -15,9 +15,13 @@ import {
|
||||
import db from "../db/db.js";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Resend } from "resend";
|
||||
|
||||
import crypto from "crypto";
|
||||
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
|
||||
import { UP, DOWN, DEGRADED, NO_DATA } from "../constants.js";
|
||||
import { UP, DOWN, DEGRADED, NO_DATA, REALTIME, SIGNAL } from "../constants.js";
|
||||
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC, ReplaceAllOccurrences, ValidateEmail } from "../tool.js";
|
||||
import getSMTPTransport from "../notification/smtps.js";
|
||||
|
||||
const saltRounds = 10;
|
||||
const DUMMY_SECRET = "DUMMY_SECRET";
|
||||
@@ -106,6 +110,12 @@ const siteDataKeys = [
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "tzToggle",
|
||||
//boolean
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "barStyle",
|
||||
//PARTIAL or FULL
|
||||
@@ -159,6 +169,31 @@ const siteDataKeys = [
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
export function InsertKeyValue(key, value) {
|
||||
@@ -193,6 +228,20 @@ export async function GetAllSiteData() {
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
//get all analytics data as json
|
||||
export async function GetAllAnalyticsData() {
|
||||
let data = await db.getAllSiteDataAnalytics();
|
||||
//return all data as key value pairs, transform using data_type
|
||||
let transformedData = [];
|
||||
for (const d of data) {
|
||||
transformedData.push({
|
||||
key: d.key,
|
||||
value: JSON.parse(d.value),
|
||||
});
|
||||
}
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
export const CreateUpdateMonitor = async (monitor) => {
|
||||
let monitorData = { ...monitor };
|
||||
if (monitorData.id) {
|
||||
@@ -232,6 +281,16 @@ export const GetMonitorsParsed = async (query) => {
|
||||
|
||||
export const CreateUpdateTrigger = async (alert) => {
|
||||
let alertData = { ...alert };
|
||||
let alertMetaJSON = JSON.parse(alertData.trigger_meta);
|
||||
if (alertData.trigger_type === "email") {
|
||||
let emailsArray = alertMetaJSON.to.split(",").map((email) => email.trim());
|
||||
for (let i = 0; i < emailsArray.length; i++) {
|
||||
if (!ValidateEmail(emailsArray[i])) {
|
||||
throw new Error(`Invalid email: ${emailsArray[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (alertData.id) {
|
||||
return await db.updateTrigger(alertData);
|
||||
} else {
|
||||
@@ -292,6 +351,57 @@ export const VerifyPassword = async (plainTextPassword, hashedPassword) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const GetLatestMonitoringData = async (monitor_tag) => {
|
||||
return await db.getLatestMonitoringData(monitor_tag);
|
||||
};
|
||||
export const GetLatestStatusActiveAll = async (monitor_tags) => {
|
||||
let latestData = await db.getLatestMonitoringDataAllActive(monitor_tags);
|
||||
let status = "NO_DATA";
|
||||
for (let i = 0; i < latestData.length; i++) {
|
||||
//if any status is down then status = down, if any is degraded then status = degraded, down > degraded > up
|
||||
if (latestData[i].status === "DOWN") {
|
||||
status = "DOWN";
|
||||
} else if (latestData[i].status === "DEGRADED" && status !== "DOWN") {
|
||||
status = "DEGRADED";
|
||||
} else if (latestData[i].status === "UP" && status !== "DOWN" && status !== "DEGRADED") {
|
||||
status = "UP";
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: status,
|
||||
};
|
||||
};
|
||||
export const GetLastHeartbeat = async (monitor_tag) => {
|
||||
return await db.getLastHeartbeat(monitor_tag);
|
||||
};
|
||||
|
||||
export const RegisterHeartbeat = async (tag, secret) => {
|
||||
let monitor = await db.getMonitorByTag(tag);
|
||||
if (!monitor) {
|
||||
return null;
|
||||
}
|
||||
let typeData = monitor.type_data;
|
||||
if (!typeData) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
let heartbeatConfig = JSON.parse(typeData);
|
||||
let heartbeatSecret = heartbeatConfig.secretString;
|
||||
if (heartbeatSecret === secret) {
|
||||
return await db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: GetMinuteStartNowTimestampUTC(GetNowTimestampUTC()),
|
||||
status: UP,
|
||||
latency: 0,
|
||||
type: SIGNAL,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error registering heartbeat:", e);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const GenerateToken = async (data) => {
|
||||
try {
|
||||
const token = jwt.sign(data, process.env.KENER_SECRET_KEY || DUMMY_SECRET, {
|
||||
@@ -321,7 +431,6 @@ export const VerifyToken = async (token) => {
|
||||
const decoded = jwt.verify(token, process.env.KENER_SECRET_KEY || DUMMY_SECRET);
|
||||
return decoded; // Returns the decoded payload if the token is valid
|
||||
} catch (err) {
|
||||
console.error("Error verifying token:", err);
|
||||
return undefined; // Returns null if the token is invalid
|
||||
}
|
||||
};
|
||||
@@ -356,6 +465,12 @@ export const CreateNewAPIKey = async (data) => {
|
||||
const apiKey = generateApiKey();
|
||||
const hashed_key = await createHash(apiKey);
|
||||
//insert into db
|
||||
|
||||
//data.name cant be empty
|
||||
if (!data.name) {
|
||||
throw new Error("Name is required");
|
||||
}
|
||||
|
||||
await db.createNewApiKey({
|
||||
name: data.name,
|
||||
hashed_key: hashed_key,
|
||||
@@ -397,12 +512,6 @@ export const IsSetupComplete = async () => {
|
||||
return data.length > 0;
|
||||
};
|
||||
|
||||
export const HashString = (str) => {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(str);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
export const InterpolateData = (rawData, startTimestamp, initialStatus, overrideEndTimestamp) => {
|
||||
const interpolatedData = [];
|
||||
let currentStatus = initialStatus || "UP";
|
||||
@@ -438,6 +547,31 @@ export const GetLastStatusBefore = async (monitor_tag, timestamp) => {
|
||||
}
|
||||
return NO_DATA;
|
||||
};
|
||||
export const GetMonitoringData = async (tag, since, now) => {
|
||||
return await db.getMonitoringData(tag, since, now);
|
||||
};
|
||||
export const GetMonitoringDataAll = async (tags, since, now) => {
|
||||
return await db.getMonitoringDataAll(tags, since, now);
|
||||
};
|
||||
export const GetLastStatusBeforeAll = async (monitor_tags, timestamp) => {
|
||||
let data = await db.getLastStatusBeforeAll(monitor_tags, timestamp);
|
||||
if (data) {
|
||||
return data.status;
|
||||
}
|
||||
return NO_DATA;
|
||||
};
|
||||
|
||||
export const AggregateData = (rawData) => {
|
||||
//data like [{ timestamp: 1732435920, status: 'NO_DATA' }]
|
||||
let rawDataWithStatus = rawData.filter((data) => data.status !== NO_DATA);
|
||||
const total = rawDataWithStatus.length;
|
||||
const UPs = rawDataWithStatus.filter((data) => data.status === UP).length;
|
||||
const DOWNs = rawDataWithStatus.filter((data) => data.status === DOWN).length;
|
||||
const DEGRADEDs = rawDataWithStatus.filter((data) => data.status === DEGRADED).length;
|
||||
const NO_DATAs = total - (UPs + DOWNs + DEGRADEDs);
|
||||
|
||||
return { total, UPs, DOWNs, DEGRADEDs, NO_DATAs };
|
||||
};
|
||||
|
||||
export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, timezoneOffsetMinutes = 0) => {
|
||||
const offsetMinutes = Number(timezoneOffsetMinutes);
|
||||
@@ -449,7 +583,7 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
|
||||
|
||||
let rawData = await db.getDataGroupByDayAlternative(monitor_tag, start, end);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor_tag, start);
|
||||
rawData = InterpolateData(rawData, start, anchorStatus, rawData.length == 0 ? end - 60 : null);
|
||||
rawData = InterpolateData(rawData, start, anchorStatus, end);
|
||||
|
||||
const groupedData = rawData.reduce((acc, row) => {
|
||||
// Calculate day group considering timezone offset
|
||||
@@ -496,6 +630,7 @@ export const CreateIncident = async (data) => {
|
||||
end_date_time: !!data.end_date_time ? data.end_date_time : null,
|
||||
state: !!data.state ? data.state : "INVESTIGATING",
|
||||
incident_type: !!data.incident_type ? data.incident_type : "INCIDENT",
|
||||
incident_source: !!data.incident_source ? data.incident_source : "DASHBOARD",
|
||||
};
|
||||
|
||||
//incident_type == INCIDENT delete endDateTime
|
||||
@@ -845,6 +980,257 @@ export const GetSMTPFromENV = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const IsResendSetup = () => {
|
||||
return !!process.env.RESEND_API_KEY && !!process.env.RESEND_SENDER_EMAIL;
|
||||
};
|
||||
|
||||
export const IsEmailSetup = () => {
|
||||
return !!GetSMTPFromENV || IsResendSetup();
|
||||
};
|
||||
|
||||
export const UpdateUserData = async (data) => {
|
||||
let userID = data.userID;
|
||||
let updateKey = data.updateKey;
|
||||
let updateValue = data.updateValue;
|
||||
|
||||
//if updateKey is password, throw error
|
||||
if (updateKey === "password") {
|
||||
throw new Error("Password cannot be updated using this method");
|
||||
}
|
||||
//if updateValue is empty, throw error
|
||||
if (!!!updateValue) {
|
||||
throw new Error("Update value cannot be empty");
|
||||
}
|
||||
|
||||
switch (updateKey) {
|
||||
case "name":
|
||||
return await db.updateUserName(userID, updateValue);
|
||||
case "role":
|
||||
return await db.updateUserRole(userID, updateValue);
|
||||
case "is_verified":
|
||||
return await db.updateIsVerified(userID, updateValue);
|
||||
default:
|
||||
throw new Error("Invalid update key");
|
||||
}
|
||||
};
|
||||
|
||||
export const CreateNewInvitation = async (data) => {
|
||||
let invite = {};
|
||||
|
||||
//create a token
|
||||
let token = crypto.randomBytes(32).toString("hex");
|
||||
let hashedToken = createHash(token);
|
||||
let invitation_token = data.invitation_type.toLowerCase() + "_" + hashedToken;
|
||||
|
||||
invite.invitation_token = invitation_token;
|
||||
invite.invitation_type = data.invitation_type;
|
||||
invite.invited_user_id = data.invited_user_id;
|
||||
invite.invited_by_user_id = data.invited_by_user_id;
|
||||
invite.invitation_meta = data.invitation_meta;
|
||||
invite.invitation_expiry = data.invitation_expiry;
|
||||
invite.invitation_status = "PENDING";
|
||||
|
||||
//update old invitations to VOID
|
||||
await db.updateInvitationStatusToVoid(invite.invited_user_id, invite.invitation_type);
|
||||
|
||||
let res = await db.insertInvitation(invite);
|
||||
return {
|
||||
invitation_token,
|
||||
};
|
||||
};
|
||||
|
||||
//check if there is a row for given invited_user_id,invitation_type and invitation_status = PENDING
|
||||
export const CheckInvitationExists = async (invited_user_id, invitation_type) => {
|
||||
let invitation = await db.invitationExists(invited_user_id, invitation_type);
|
||||
return !!invitation;
|
||||
};
|
||||
|
||||
//getInvitationByToken
|
||||
export const GetActiveInvitationByToken = async (invitation_token) => {
|
||||
let invitation = await db.getActiveInvitationByToken(invitation_token);
|
||||
return invitation;
|
||||
};
|
||||
|
||||
//updateInvitationStatusToAccepted
|
||||
export const UpdateInvitationStatusToAccepted = async (invitation_token) => {
|
||||
return await db.updateInvitationStatusToAccepted(invitation_token);
|
||||
};
|
||||
|
||||
//getUserById
|
||||
export const GetUserByID = async (userID) => {
|
||||
return await db.getUserById(userID);
|
||||
};
|
||||
|
||||
//getUserByEmail
|
||||
export const GetUserByEmail = async (email) => {
|
||||
return await db.getUserByEmail(email);
|
||||
};
|
||||
|
||||
export const SendEmailWithTemplate = async (template, data, email, subject, emailText) => {
|
||||
//for each key in data, replace the key in template with value
|
||||
for (const key in data) {
|
||||
if (Object.hasOwnProperty.call(data, key)) {
|
||||
const value = data[key];
|
||||
template = ReplaceAllOccurrences(template, `{{${key}}}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
const senderEmail = process.env.RESEND_SENDER_EMAIL;
|
||||
const resendKey = process.env.RESEND_API_KEY;
|
||||
|
||||
let mail = {
|
||||
from: senderEmail,
|
||||
to: [email],
|
||||
subject: subject,
|
||||
text: emailText,
|
||||
html: template,
|
||||
};
|
||||
|
||||
let smtpData = GetSMTPFromENV();
|
||||
|
||||
try {
|
||||
if (!!smtpData) {
|
||||
const transporter = getSMTPTransport(smtpData);
|
||||
const mailOptions = {
|
||||
from: smtpData.smtp_from_email,
|
||||
to: email,
|
||||
subject: mail.subject,
|
||||
html: mail.html,
|
||||
text: mail.text,
|
||||
};
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} else {
|
||||
const resend = new Resend(resendKey);
|
||||
return await resend.emails.send(mail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
throw new Error("Error sending email v");
|
||||
}
|
||||
};
|
||||
|
||||
export const GetSiteLogoURL = async (siteURL, logo, base) => {
|
||||
if (logo.startsWith("http")) {
|
||||
return logo;
|
||||
}
|
||||
return siteURL + base + logo;
|
||||
};
|
||||
export const GetAllUsers = async () => {
|
||||
return await db.getAllUsers();
|
||||
};
|
||||
|
||||
//get all users paginated
|
||||
export const GetAllUsersPaginated = async (data) => {
|
||||
return await db.getUsersPaginated(data.page, data.limit);
|
||||
};
|
||||
|
||||
//given a limit return total pages
|
||||
export const GetTotalUserPages = async (limit) => {
|
||||
let totalUsers = await db.getTotalUsers();
|
||||
let totalPages = Math.ceil(totalUsers.count / limit);
|
||||
return totalPages;
|
||||
};
|
||||
|
||||
export const ValidatePassword = (password) => {
|
||||
return /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/.test(password);
|
||||
};
|
||||
|
||||
export const UpdatePassword = async (data) => {
|
||||
let { userID, newPassword, newPlainPassword } = data;
|
||||
if (!ValidatePassword(newPassword)) {
|
||||
throw new Error(
|
||||
"Password must contain at least 8 characters, one uppercase letter, one lowercase letter and one number",
|
||||
);
|
||||
}
|
||||
// newPassword should match newPlainPassword
|
||||
if (newPassword !== newPlainPassword) {
|
||||
throw new Error("Passwords do not match");
|
||||
}
|
||||
|
||||
//hash the password
|
||||
let hashedPassword = await HashPassword(newPassword);
|
||||
|
||||
return await db.updateUserPassword({
|
||||
id: userID,
|
||||
password_hash: hashedPassword,
|
||||
});
|
||||
};
|
||||
|
||||
export const ManualUpdateUserData = async (byUser, forUserId, data) => {
|
||||
let forUser = await db.getUserById(forUserId);
|
||||
//only admins can update
|
||||
if (byUser.role !== "admin") {
|
||||
throw new Error("You do not have permission to update user");
|
||||
}
|
||||
if (data.updateType == "role") {
|
||||
return await db.updateUserRole(forUser.id, data.role);
|
||||
} else if (data.updateType == "is_active") {
|
||||
return await db.updateUserIsActive(forUser.id, data.is_active);
|
||||
} else if (data.updateType == "password") {
|
||||
return await UpdatePassword({
|
||||
userID: forUser.id,
|
||||
newPassword: data.password,
|
||||
newPlainPassword: data.passwordPlain,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const CreateNewUser = async (currentUser, data) => {
|
||||
let acceptedRoles = ["member", "editor"];
|
||||
if (!acceptedRoles.includes(data.role)) {
|
||||
throw new Error("Invalid role");
|
||||
}
|
||||
|
||||
if (currentUser.role === "member") {
|
||||
throw new Error("Only admins and editors can create new users");
|
||||
}
|
||||
|
||||
//if data.email empty, throw error
|
||||
if (!!!data.email) {
|
||||
throw new Error("Email cannot be empty");
|
||||
}
|
||||
|
||||
//if data.name empty, throw error
|
||||
if (!!!data.name) {
|
||||
throw new Error("Name cannot be empty");
|
||||
}
|
||||
|
||||
//if data.password empty, throw error
|
||||
if (!!!data.password) {
|
||||
throw new Error("Password cannot be empty");
|
||||
}
|
||||
|
||||
//if data.role empty, throw error
|
||||
if (!!!data.role) {
|
||||
throw new Error("Role cannot be empty");
|
||||
}
|
||||
|
||||
//if data.password not equal to data.plainPassword, throw error
|
||||
if (data.password !== data.plainPassword) {
|
||||
throw new Error("Passwords do not match");
|
||||
}
|
||||
|
||||
if (!ValidatePassword(data.password)) {
|
||||
throw new Error(
|
||||
"Password must contain at least 8 characters, one uppercase letter, one lowercase letter and one number",
|
||||
);
|
||||
}
|
||||
let user = {
|
||||
email: data.email,
|
||||
password_hash: await HashPassword(data.password),
|
||||
name: data.name,
|
||||
role: data.role,
|
||||
};
|
||||
return await db.insertUser(user);
|
||||
};
|
||||
|
||||
export const DeleteMonitorCompletelyUsingTag = async (tag) => {
|
||||
await db.deleteMonitorDataByTag(tag);
|
||||
await db.deleteIncidentMonitorsByTag(tag);
|
||||
await db.deleteMonitorAlertsByTag(tag);
|
||||
return await db.deleteMonitorsByTag(tag);
|
||||
};
|
||||
|
||||
export const GetSiteMap = async (cookies) => {
|
||||
let siteMapData = [];
|
||||
let siteURLData = await GetSiteDataByKey("siteURL");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import axios from "axios";
|
||||
import { Ping, ExtractIPv6HostAndPort, TCP } from "./ping.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "./constants.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL, DEFAULT_STATUS } from "./constants.js";
|
||||
import Service from "./services/service.js";
|
||||
import { GetMinuteStartNowTimestampUTC, ReplaceAllOccurrences, GetRequiredSecrets, Wait } from "./tool.js";
|
||||
|
||||
@@ -41,12 +41,6 @@ async function manualIncident(monitor) {
|
||||
for (let i = 0; i < impactArr.length; i++) {
|
||||
const element = impactArr[i];
|
||||
|
||||
let autoIncidents = await db.getActiveAlertIncident(monitor.tag, element.monitor_impact, element.id);
|
||||
|
||||
if (!!autoIncidents) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element.monitor_impact === "DOWN") {
|
||||
impact = "DOWN";
|
||||
break;
|
||||
@@ -67,83 +61,105 @@ async function manualIncident(monitor) {
|
||||
type: MANUAL,
|
||||
},
|
||||
};
|
||||
|
||||
return manualData;
|
||||
}
|
||||
|
||||
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 db.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);
|
||||
} 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];
|
||||
db.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);
|
||||
} 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) {
|
||||
mergedData[timestamp] = realTimeData[timestamp];
|
||||
}
|
||||
|
||||
for (const timestamp in manualData) {
|
||||
mergedData[timestamp] = manualData[timestamp];
|
||||
}
|
||||
|
||||
for (const timestamp in mergedData) {
|
||||
const element = mergedData[timestamp];
|
||||
db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: parseInt(timestamp),
|
||||
status: element.status,
|
||||
latency: element.latency,
|
||||
type: element.type,
|
||||
});
|
||||
}
|
||||
alertingQueue.push(async (cb) => {
|
||||
setTimeout(async () => {
|
||||
await alerting(monitor);
|
||||
cb();
|
||||
try {
|
||||
await alerting(monitor);
|
||||
cb();
|
||||
} catch (e) {
|
||||
console.log(`[Error] in Running Alerting name: ${monitor.name}, tag: ${monitor.tag} error:`, e);
|
||||
cb();
|
||||
}
|
||||
}, 1042);
|
||||
});
|
||||
};
|
||||
|
||||
+261
-21
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
|
||||
import { MANUAL } from "../constants.js";
|
||||
import { GetMinuteStartNowTimestampUTC, GetDbType } from "../tool.js";
|
||||
import { MANUAL, SIGNAL } from "../constants.js";
|
||||
import Knex from "knex";
|
||||
|
||||
class DbImpl {
|
||||
@@ -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,47 @@ class DbImpl {
|
||||
.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)
|
||||
.where("type", SIGNAL)
|
||||
.orderBy("timestamp", "desc")
|
||||
.limit(1)
|
||||
.first();
|
||||
}
|
||||
|
||||
//given monitor_tag, start and end timestamp in utc seconds return total degraded, up, down, avg(latency), max(latency), min(latency)
|
||||
async getAggregatedMonitoringData(monitor_tag, start, end) {
|
||||
return await this.knex("monitoring_data")
|
||||
@@ -66,6 +115,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
|
||||
@@ -80,9 +138,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"),
|
||||
@@ -95,8 +153,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")
|
||||
@@ -178,6 +243,17 @@ class DbImpl {
|
||||
.first();
|
||||
}
|
||||
|
||||
//get active alert given incident id, monitor tag, monitor status
|
||||
async getAllActiveAlertIncidents(monitor_tag) {
|
||||
return await this.knex("monitor_alerts")
|
||||
.where({
|
||||
monitor_tag,
|
||||
alert_status: "TRIGGERED",
|
||||
})
|
||||
.andWhere("incident_number", ">", 0)
|
||||
.orderBy("id", "desc");
|
||||
}
|
||||
|
||||
//return active alert for a monitor_tag, monitor_status, trigger_status = ACTIVE
|
||||
async getActiveAlert(monitor_tag, monitor_status, alert_status) {
|
||||
return await this.knex("monitor_alerts")
|
||||
@@ -248,6 +324,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({
|
||||
@@ -383,6 +464,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({
|
||||
@@ -403,6 +492,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({
|
||||
@@ -438,19 +548,39 @@ class DbImpl {
|
||||
}
|
||||
|
||||
async createIncident(data) {
|
||||
const [incident] = await this.knex("incidents")
|
||||
.insert({
|
||||
title: data.title,
|
||||
start_date_time: data.start_date_time,
|
||||
end_date_time: data.end_date_time,
|
||||
status: data.status,
|
||||
state: data.state,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
incident_type: data.incident_type,
|
||||
})
|
||||
.returning("*");
|
||||
return incident;
|
||||
const dbType = GetDbType(); // sqlite, postgresql, mysql
|
||||
|
||||
// Common insert data
|
||||
const insertData = {
|
||||
title: data.title,
|
||||
start_date_time: data.start_date_time,
|
||||
end_date_time: data.end_date_time,
|
||||
status: data.status,
|
||||
state: data.state,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
incident_type: data.incident_type,
|
||||
incident_source: data.incident_source,
|
||||
};
|
||||
|
||||
// PostgreSQL supports returning clause
|
||||
if (dbType === "postgresql") {
|
||||
const [incident] = await this.knex("incidents").insert(insertData).returning("*");
|
||||
return incident;
|
||||
}
|
||||
// MySQL and SQLite need different approaches
|
||||
else {
|
||||
// Insert and get the ID
|
||||
const result = await this.knex("incidents").insert(insertData);
|
||||
|
||||
// Different handling for MySQL vs SQLite
|
||||
let id = result[0];
|
||||
|
||||
// Fetch the newly created record
|
||||
const incident = await this.knex("incidents").where("id", id).first();
|
||||
|
||||
return incident;
|
||||
}
|
||||
}
|
||||
|
||||
//get incidents paginated
|
||||
@@ -609,7 +739,8 @@ class DbImpl {
|
||||
.andWhere("i.start_date_time", "<=", timestamp)
|
||||
.andWhere("i.status", "OPEN")
|
||||
.andWhere("i.incident_type", "INCIDENT")
|
||||
.andWhere("i.state", "!=", "RESOLVED");
|
||||
.andWhere("i.state", "!=", "RESOLVED")
|
||||
.andWhere("i.incident_source", "!=", "ALERT");
|
||||
}
|
||||
|
||||
//get maintenance incidents by monitor tag
|
||||
@@ -627,7 +758,8 @@ class DbImpl {
|
||||
.andWhere("i.end_date_time", ">=", timestamp)
|
||||
.andWhere("i.status", "OPEN")
|
||||
.andWhere("i.incident_type", "MAINTENANCE")
|
||||
.andWhere("i.state", "=", "RESOLVED");
|
||||
.andWhere("i.state", "=", "RESOLVED")
|
||||
.andWhere("i.incident_source", "!=", "ALERT");
|
||||
}
|
||||
|
||||
//given array of ids get incidents
|
||||
@@ -700,6 +832,114 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
export default DbImpl;
|
||||
|
||||
@@ -72,6 +72,7 @@ const seedSiteData = {
|
||||
],
|
||||
theme: "none",
|
||||
themeToggle: "YES",
|
||||
tzToggle: "YES",
|
||||
barStyle: "PARTIAL",
|
||||
barRoundness: "SHARP",
|
||||
summaryStyle: "CURRENT",
|
||||
|
||||
@@ -1,93 +1,72 @@
|
||||
// @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;
|
||||
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(data) {
|
||||
let siteURL = this.siteData.siteURL;
|
||||
let logo =
|
||||
this.siteData.siteURL +
|
||||
(!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "") +
|
||||
this.siteData.logo;
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
transformData(data) {
|
||||
let view = variables(this.siteData, data);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
let discordTemplate = DiscordJSONTemplate;
|
||||
if (!!this.trigger_meta.has_discord_body && !!this.trigger_meta.discord_body) {
|
||||
discordTemplate = this.trigger_meta.discord_body;
|
||||
}
|
||||
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: toSend,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from discord`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Discord;
|
||||
|
||||
@@ -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,94 +1,106 @@
|
||||
// @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;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
trigger_meta;
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
trigger_meta;
|
||||
|
||||
constructor(trigger_meta, method, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener/3.0.0"
|
||||
};
|
||||
let headers = trigger_meta.headers;
|
||||
this.trigger_meta = trigger_meta;
|
||||
this.url = trigger_meta.url;
|
||||
this.headers = kenerHeader;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const header = headers[i];
|
||||
this.headers[header.key] = header.value;
|
||||
}
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
constructor(trigger_meta, method, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
let headers = trigger_meta.headers;
|
||||
this.trigger_meta = trigger_meta;
|
||||
this.url = trigger_meta.url;
|
||||
this.headers = kenerHeader;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const header = headers[i];
|
||||
this.headers[header.key] = header.value;
|
||||
}
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
|
||||
let envSecrets = GetRequiredSecrets(
|
||||
`${this.url} ${JSON.stringify(this.headers)} ${JSON.stringify(this.trigger_meta.webhook_body)}`
|
||||
);
|
||||
//replace secrets in url and headers
|
||||
let envSecrets = GetRequiredSecrets(
|
||||
`${this.url} ${JSON.stringify(this.headers)} ${JSON.stringify(this.trigger_meta.webhook_body)}`,
|
||||
);
|
||||
//replace secrets in url and headers
|
||||
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.url = ReplaceAllOccurrences(this.url, secret.find, secret.replace);
|
||||
this.headers = JSON.parse(
|
||||
ReplaceAllOccurrences(JSON.stringify(this.headers), secret.find, secret.replace)
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.url = ReplaceAllOccurrences(this.url, secret.find, secret.replace);
|
||||
this.headers = JSON.parse(ReplaceAllOccurrences(JSON.stringify(this.headers), secret.find, secret.replace));
|
||||
}
|
||||
|
||||
if (!!this.trigger_meta.has_webhook_body && !!this.trigger_meta.webhook_body) {
|
||||
envSecrets = GetRequiredSecrets(this.trigger_meta.webhook_body);
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.trigger_meta.webhook_body = ReplaceAllOccurrences(
|
||||
this.trigger_meta.webhook_body,
|
||||
secret.find,
|
||||
secret.replace
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!!this.trigger_meta.has_webhook_body && !!this.trigger_meta.webhook_body) {
|
||||
envSecrets = GetRequiredSecrets(this.trigger_meta.webhook_body);
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.trigger_meta.webhook_body = ReplaceAllOccurrences(
|
||||
this.trigger_meta.webhook_body,
|
||||
secret.find,
|
||||
secret.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
|
||||
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
|
||||
let body = this.trigger_meta.webhook_body;
|
||||
body = ReplaceAllOccurrences(body, "${id}", data.id);
|
||||
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
|
||||
body = ReplaceAllOccurrences(body, "${severity}", data.severity);
|
||||
body = ReplaceAllOccurrences(body, "${status}", data.status);
|
||||
body = ReplaceAllOccurrences(body, "${source}", data.source);
|
||||
body = ReplaceAllOccurrences(body, "${timestamp}", data.timestamp);
|
||||
body = ReplaceAllOccurrences(body, "${description}", data.description);
|
||||
body = ReplaceAllOccurrences(body, "${metric}", data.details.metric);
|
||||
body = ReplaceAllOccurrences(body, "${current_value}", data.details.current_value);
|
||||
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
|
||||
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
|
||||
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
|
||||
return body;
|
||||
}
|
||||
transformData(data) {
|
||||
let view = variables(this.siteData, data);
|
||||
|
||||
type() {
|
||||
return "webhook";
|
||||
}
|
||||
if (!!!this.trigger_meta.has_webhook_body || !!!this.trigger_meta.webhook_body) {
|
||||
return Mustache.render(WebhookJSONTemplate, view);
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: this.transformData(data)
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
//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);
|
||||
body = ReplaceAllOccurrences(body, "${severity}", data.severity);
|
||||
body = ReplaceAllOccurrences(body, "${status}", data.status);
|
||||
body = ReplaceAllOccurrences(body, "${source}", data.source);
|
||||
body = ReplaceAllOccurrences(body, "${timestamp}", data.timestamp);
|
||||
body = ReplaceAllOccurrences(body, "${description}", data.description);
|
||||
body = ReplaceAllOccurrences(body, "${metric}", data.details.metric);
|
||||
body = ReplaceAllOccurrences(body, "${current_value}", data.details.current_value);
|
||||
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
|
||||
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
|
||||
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
|
||||
//have to keep this for backward compatibility
|
||||
|
||||
return Mustache.render(body, view);
|
||||
}
|
||||
|
||||
type() {
|
||||
return "webhook";
|
||||
}
|
||||
|
||||
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 webhook`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Webhook;
|
||||
|
||||
+22
-17
@@ -7,6 +7,7 @@ import {
|
||||
StatusObj,
|
||||
ParseUptime,
|
||||
GetDayStartTimestampUTC,
|
||||
BeginningOfMinute,
|
||||
} from "$lib/server/tool.js";
|
||||
import { formatDuration, intervalToDuration } from "date-fns";
|
||||
import { fdm, l, summaryTime } from "$lib/i18n/client";
|
||||
@@ -80,16 +81,22 @@ function getCountOfSimilarStatuesEnd(arr, statusType) {
|
||||
const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
const secondsInDay = 24 * 60 * 60;
|
||||
//get offset from utc in minutes
|
||||
const now = GetMinuteStartNowTimestampUTC() + 60;
|
||||
const midnight = BeginningOfDay({ timeZone: localTz });
|
||||
const midnight90DaysAgo = midnight - 90 * 24 * 60 * 60;
|
||||
const nowUTC = GetMinuteStartNowTimestampUTC();
|
||||
const midnightUTC = GetDayStartTimestampUTC(nowUTC);
|
||||
const midnightTz = BeginningOfDay({ timeZone: localTz });
|
||||
const midnight90DaysAgoTz = midnightTz - 90 * 24 * 60 * 60;
|
||||
const NO_DATA = "No Data";
|
||||
const midnightTomorrow = midnight + secondsInDay;
|
||||
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(now) - midnight) / 60);
|
||||
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(nowUTC) - midnightTz) / 60);
|
||||
const maxDateTodayTimestampTz = BeginningOfMinute({ timeZone: localTz });
|
||||
const _90Day = {};
|
||||
let latestTimestamp = 0;
|
||||
|
||||
let dbData = await GetDataGroupByDayAlternative(monitor.tag, midnight90DaysAgo, midnightTomorrow, offsetInMinutes);
|
||||
let dbData = await GetDataGroupByDayAlternative(
|
||||
monitor.tag,
|
||||
midnight90DaysAgoTz,
|
||||
maxDateTodayTimestampTz,
|
||||
offsetInMinutes,
|
||||
);
|
||||
let totalDegradedCount = 0;
|
||||
let totalDownCount = 0;
|
||||
let totalUpCount = 0;
|
||||
@@ -104,7 +111,6 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
totalDegradedCount += dayData.DEGRADED;
|
||||
totalDownCount += dayData.DOWN;
|
||||
totalUpCount += dayData.UP;
|
||||
|
||||
if (dayData.DEGRADED >= monitor.day_degraded_minimum_count) {
|
||||
cssClass = returnStatusClass(dayData.DEGRADED, dayData.total, StatusObj.DEGRADED, site.barStyle);
|
||||
summaryDuration = getSummaryDuration(dayData.DEGRADED, selectedLang);
|
||||
@@ -150,14 +156,9 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
|
||||
let summaryColorClass = "api-nodata";
|
||||
|
||||
let todayDataDb = await db.getMonitoringData(monitor.tag, midnight, midnight + secondsInDay);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, midnight);
|
||||
todayDataDb = InterpolateData(
|
||||
todayDataDb,
|
||||
midnight,
|
||||
anchorStatus,
|
||||
todayDataDb.length == 0 ? midnight + secondsInDay : null,
|
||||
);
|
||||
let todayDataDb = await db.getMonitoringData(monitor.tag, midnightTz, maxDateTodayTimestampTz);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, midnightTz);
|
||||
todayDataDb = InterpolateData(todayDataDb, midnightTz, anchorStatus, maxDateTodayTimestampTz);
|
||||
|
||||
if (site.summaryStyle === "CURRENT") {
|
||||
summaryColorClass = "api-up";
|
||||
@@ -174,6 +175,10 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
summaryStatus = "DOWN";
|
||||
summaryColorClass = "api-down";
|
||||
}
|
||||
if (lastRow.status === "NO_DATA") {
|
||||
summaryStatus = NO_DATA;
|
||||
summaryColorClass = "api-nodata";
|
||||
}
|
||||
summaryStatus = l(lang, summaryTime(summaryStatus), {
|
||||
status: l(lang, summaryStatus),
|
||||
duration: summaryDuration,
|
||||
@@ -190,8 +195,8 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
summaryStatus: summaryStatus,
|
||||
summaryColorClass: summaryColorClass,
|
||||
barRoundness: site.barRoundness,
|
||||
midnight90DaysAgo: midnight90DaysAgo,
|
||||
midnightTomorrow: midnightTomorrow,
|
||||
midnight90DaysAgo: midnight90DaysAgoTz,
|
||||
maxDateTodayTimestamp: maxDateTodayTimestampTz,
|
||||
};
|
||||
};
|
||||
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 };
|
||||
|
||||
@@ -2,20 +2,10 @@
|
||||
import axios from "axios";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
|
||||
const defaultEval = `(async function (statusCode, responseTime, responseData) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
return {
|
||||
status: 'UP',
|
||||
latency: responseTime,
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})`;
|
||||
import * as cheerio from "cheerio";
|
||||
import { DefaultAPIEval } from "../../anywhere.js";
|
||||
import version from "../../version.js";
|
||||
import https from "https";
|
||||
|
||||
class ApiCall {
|
||||
monitor;
|
||||
@@ -30,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;
|
||||
@@ -43,9 +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.monitorEval ? this.monitor.type_data.monitorEval : 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];
|
||||
@@ -80,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;
|
||||
}
|
||||
@@ -113,12 +108,18 @@ class ApiCall {
|
||||
}
|
||||
}
|
||||
|
||||
resp = Buffer.from(resp).toString("base64");
|
||||
|
||||
let evalResp = undefined;
|
||||
let modules = { cheerio };
|
||||
|
||||
try {
|
||||
evalResp = await eval(monitorEval + `(${statusCode}, ${latency}, "${resp}")`);
|
||||
const evalFunction = new Function(
|
||||
"statusCode",
|
||||
"responseTime",
|
||||
"responseRaw",
|
||||
"modules",
|
||||
`return (${monitorEval})(statusCode, responseTime, responseRaw, modules);`,
|
||||
);
|
||||
evalResp = await evalFunction(statusCode, latency, resp, modules);
|
||||
} catch (error) {
|
||||
console.log(`Error in monitorEval for ${tag}`, error.message);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
// @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) {
|
||||
data = await db.getLastStatusBeforeCombined(arr, ts, ts - 86400);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
@@ -29,6 +34,9 @@ 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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
|
||||
import { GetLastHeartbeat } from "../controllers/controller.js";
|
||||
|
||||
class HeartbeatCall {
|
||||
monitor;
|
||||
|
||||
constructor(monitor) {
|
||||
this.monitor = monitor;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
let nowMinute = GetMinuteStartNowTimestampUTC();
|
||||
let latestData = await GetLastHeartbeat(this.monitor.tag);
|
||||
if (!latestData) {
|
||||
return {};
|
||||
}
|
||||
let latency = nowMinute - latestData.timestamp;
|
||||
let downRemainingMinutes = Number(this.monitor.type_data.downRemainingMinutes);
|
||||
if (latency > downRemainingMinutes * 60) {
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
}
|
||||
let degradedRemainingMinutes = Number(this.monitor.type_data.degradedRemainingMinutes);
|
||||
if (latency > degradedRemainingMinutes * 60) {
|
||||
return {
|
||||
status: DEGRADED,
|
||||
latency: latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: UP,
|
||||
latency: latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default HeartbeatCall;
|
||||
@@ -1,31 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import axios from "axios";
|
||||
import { Ping } from "../ping.js";
|
||||
import {
|
||||
UP,
|
||||
DOWN,
|
||||
DEGRADED,
|
||||
REALTIME,
|
||||
TIMEOUT,
|
||||
ERROR,
|
||||
MANUAL,
|
||||
} from "../constants.js";
|
||||
|
||||
const defaultPingEval = `(async function (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64));
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive;
|
||||
}, true);
|
||||
|
||||
return {
|
||||
status: alive ? 'UP' : 'DOWN',
|
||||
latency: latencyTotal / arrayOfPings.length,
|
||||
}
|
||||
})`;
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import { DefaultPingEval } from "../../anywhere.js";
|
||||
|
||||
class PingCall {
|
||||
monitor;
|
||||
@@ -36,9 +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(
|
||||
@@ -54,20 +29,21 @@ class PingCall {
|
||||
let arrayOfPings = [];
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
const host = hosts[i];
|
||||
arrayOfPings.push(
|
||||
await Ping(host.type, host.host, host.timeout, host.count),
|
||||
);
|
||||
arrayOfPings.push(await Ping(host.type, host.host, host.timeout, host.count));
|
||||
}
|
||||
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString(
|
||||
"base64",
|
||||
);
|
||||
|
||||
let evalResp = undefined;
|
||||
|
||||
try {
|
||||
evalResp = await eval(pingEval + `("${respBase64}")`);
|
||||
const evalFunction = new Function("arrayOfPings", `return (${pingEval})(arrayOfPings);`);
|
||||
evalResp = await evalFunction(arrayOfPings);
|
||||
} catch (error) {
|
||||
console.log(`Error in pingEval for ${tag}`, error.message);
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
type: ERROR,
|
||||
};
|
||||
}
|
||||
//reduce to get the status
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,9 @@ import PingCall from "./pingCall.js";
|
||||
import TcpCall from "./tcpCall.js";
|
||||
import DnsCall from "./dnsCall.js";
|
||||
import GroupCall from "./groupCall.js";
|
||||
import SSLCall from "./sslCall.js";
|
||||
import SqlCall from "./sqlCall.js";
|
||||
import HeartbeatCall from "./heartbeatCall.js";
|
||||
|
||||
class Service {
|
||||
service;
|
||||
@@ -21,6 +24,12 @@ class Service {
|
||||
this.service = null;
|
||||
} else if (monitor.monitor_type === "GROUP") {
|
||||
this.service = new GroupCall(monitor);
|
||||
} else if (monitor.monitor_type === "SSL") {
|
||||
this.service = new SSLCall(monitor);
|
||||
} else if (monitor.monitor_type === "SQL") {
|
||||
this.service = new SqlCall(monitor);
|
||||
} else if (monitor.monitor_type === "HEARTBEAT") {
|
||||
this.service = new HeartbeatCall(monitor);
|
||||
} else {
|
||||
console.log("Invalid monitor.monitor_type ", monitor.monitor_type);
|
||||
process.exit(1);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// @ts-nocheck
|
||||
import Knex from "knex";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
|
||||
class SqlCall {
|
||||
monitor;
|
||||
envSecrets;
|
||||
constructor(monitor) {
|
||||
this.monitor = monitor;
|
||||
this.envSecrets = GetRequiredSecrets(`${monitor.type_data.connectionString}`);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
let client = this.monitor.type_data.dbType;
|
||||
let connection = this.monitor.type_data.connectionString;
|
||||
for (let i = 0; i < this.envSecrets.length; i++) {
|
||||
const secret = this.envSecrets[i];
|
||||
connection = ReplaceAllOccurrences(connection, secret.find, secret.replace);
|
||||
}
|
||||
let query = this.monitor.type_data.query;
|
||||
let timeout = this.monitor.type_data.timeout || 5000;
|
||||
|
||||
const startTime = Date.now();
|
||||
let knexInstance = null;
|
||||
|
||||
try {
|
||||
// Create database configuration
|
||||
const config = {
|
||||
client: client,
|
||||
connection: connection,
|
||||
pool: { min: 0, max: 1 },
|
||||
acquireConnectionTimeout: timeout,
|
||||
};
|
||||
|
||||
// Initialize knex
|
||||
knexInstance = Knex(config);
|
||||
|
||||
// Set up a timeout for the query execution
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Query timeout")), timeout);
|
||||
});
|
||||
|
||||
// Execute query
|
||||
const queryPromise = knexInstance.raw(query);
|
||||
|
||||
// Race the promises to handle timeouts
|
||||
await Promise.race([queryPromise, timeoutPromise]);
|
||||
|
||||
// Calculate latency
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
status: UP,
|
||||
latency: latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
} catch (error) {
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
// Handle timeout specifically
|
||||
if (error.message === "Query timeout") {
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: TIMEOUT,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle other connection or query errors
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: ERROR,
|
||||
};
|
||||
} finally {
|
||||
// Destroy knex instance to clean up connections
|
||||
if (knexInstance) {
|
||||
try {
|
||||
await knexInstance.destroy();
|
||||
} catch (destroyError) {
|
||||
console.error("Error closing database connection:", destroyError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SqlCall;
|
||||
@@ -0,0 +1,74 @@
|
||||
// @ts-nocheck
|
||||
import tls from "tls";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
|
||||
const getSSLExpiry = (domain, port = 443) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
const socket = tls.connect(port, domain, { servername: domain }, () => {
|
||||
const { valid_to } = socket.getPeerCertificate();
|
||||
if (!valid_to) {
|
||||
reject(new Error("No certificate found."));
|
||||
return;
|
||||
}
|
||||
const latency = Date.now() - startTime;
|
||||
const timeTillExpiryMs = new Date(valid_to) - new Date();
|
||||
resolve({
|
||||
domain,
|
||||
expiryDate: new Date(valid_to),
|
||||
timeTillExpiryMs,
|
||||
latency,
|
||||
});
|
||||
socket.end();
|
||||
});
|
||||
|
||||
socket.on("error", (err) => reject(err));
|
||||
});
|
||||
};
|
||||
|
||||
class SSLCall {
|
||||
monitor;
|
||||
|
||||
constructor(monitor) {
|
||||
this.monitor = monitor;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
let domain = this.monitor.type_data.host;
|
||||
let port = parseInt(this.monitor.type_data.port || "443");
|
||||
let degradedRemainingHours = Number(this.monitor.type_data.degradedRemainingHours);
|
||||
let downRemainingHours = Number(this.monitor.type_data.downRemainingHours);
|
||||
try {
|
||||
const { timeTillExpiryMs, latency } = await getSSLExpiry(domain, port);
|
||||
const timeTillExpiryHours = timeTillExpiryMs / 1000 / 60 / 60;
|
||||
if (timeTillExpiryHours > degradedRemainingHours) {
|
||||
return {
|
||||
status: UP,
|
||||
latency: latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
} else if (timeTillExpiryHours > downRemainingHours) {
|
||||
return {
|
||||
status: DEGRADED,
|
||||
latency: latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: REALTIME,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error checking SSL for ${domain}:`, error.message);
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
type: ERROR,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SSLCall;
|
||||
@@ -2,26 +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 (responseDataBase64) {
|
||||
let arrayOfPings = JSON.parse(atob(responseDataBase64));
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
if (ping.status === "open") {
|
||||
return acc && true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
return {
|
||||
status: alive ? 'UP' : 'DOWN',
|
||||
latency: latencyTotal / arrayOfPings.length,
|
||||
}
|
||||
})`;
|
||||
import { DefaultTCPEval } from "../../anywhere.js";
|
||||
|
||||
class TcpCall {
|
||||
monitor;
|
||||
@@ -32,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) {
|
||||
@@ -51,14 +32,19 @@ class TcpCall {
|
||||
const host = hosts[i];
|
||||
arrayOfPings.push(await TCP(host.type, host.host, host.port, host.timeout));
|
||||
}
|
||||
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString("base64");
|
||||
|
||||
let evalResp = undefined;
|
||||
|
||||
try {
|
||||
evalResp = await eval(tcpEval + `("${respBase64}")`);
|
||||
const evalFunction = new Function("arrayOfPings", `return (${tcpEval})(arrayOfPings);`);
|
||||
evalResp = await evalFunction(arrayOfPings);
|
||||
} catch (error) {
|
||||
console.log(`Error in tcpEval for ${tag}`, error.message);
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
type: ERROR,
|
||||
};
|
||||
}
|
||||
//reduce to get the status
|
||||
return {
|
||||
|
||||
+34
-29
@@ -1,46 +1,46 @@
|
||||
// @ts-nocheck
|
||||
|
||||
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 } from "path";
|
||||
import { dirname, resolve } from "path";
|
||||
import fs from "fs";
|
||||
import version from "../version.js";
|
||||
|
||||
const jobs = [];
|
||||
process.env.TZ = "UTC";
|
||||
let isStartUP = true;
|
||||
|
||||
const jobSchedule = async () => {
|
||||
const activeMonitors = (await GetMonitorsParsed({ status: "ACTIVE" })).map((m) => {
|
||||
return {
|
||||
...m,
|
||||
hash: "Tag: " + m.tag + " Hash:" + HashString(JSON.stringify(m)),
|
||||
};
|
||||
});
|
||||
const scheduleCronJobs = async () => {
|
||||
// Fetch and map all active monitors, creating a unique hash for each
|
||||
const activeMonitors = (await GetMonitorsParsed({ status: "ACTIVE" })).map((monitor) => ({
|
||||
...monitor,
|
||||
hash: "Tag: " + monitor.tag + " Hash:" + HashString(JSON.stringify(monitor)),
|
||||
}));
|
||||
|
||||
//stop and remove jobs not present in activeMonitors
|
||||
// Remove any cron jobs that are no longer in activeMonitors
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
const job = jobs[i];
|
||||
const monitor = activeMonitors.find((m) => m.hash === job.name);
|
||||
if (!monitor) {
|
||||
job.stop();
|
||||
const existingJob = jobs[i];
|
||||
const matchingMonitor = activeMonitors.find((m) => m.hash === existingJob.name);
|
||||
if (!matchingMonitor) {
|
||||
existingJob.stop();
|
||||
jobs.splice(i, 1);
|
||||
console.log("REMOVING JOB WITH NAME " + existingJob.name);
|
||||
i--;
|
||||
console.log("REMOVING JOB WITH NAME " + job.name);
|
||||
}
|
||||
}
|
||||
|
||||
//add new jobs from activeMonitors if not present already in jobs
|
||||
for (let i = 0; i < activeMonitors.length; i++) {
|
||||
const monitor = activeMonitors[i];
|
||||
const job = jobs.find((j) => j.name === monitor.hash);
|
||||
if (!job) {
|
||||
const j = Cron(
|
||||
// Create new cron jobs for monitors that don't already have one
|
||||
for (const monitor of activeMonitors) {
|
||||
const existingJob = jobs.find((j) => j.name === monitor.hash);
|
||||
if (!existingJob) {
|
||||
const newJob = Cron(
|
||||
monitor.cron,
|
||||
async () => {
|
||||
async (job) => {
|
||||
await Minuter(monitor);
|
||||
},
|
||||
{
|
||||
@@ -48,18 +48,22 @@ const jobSchedule = async () => {
|
||||
name: monitor.hash,
|
||||
},
|
||||
);
|
||||
console.log("ADDING NEW JOB WITH NAME " + j.name);
|
||||
j.trigger();
|
||||
jobs.push(j);
|
||||
console.log("ADDING NEW JOB WITH NAME " + newJob.name);
|
||||
if (isStartUP) {
|
||||
newJob.trigger();
|
||||
}
|
||||
|
||||
jobs.push(newJob);
|
||||
}
|
||||
}
|
||||
isStartUP = false;
|
||||
};
|
||||
|
||||
async function Startup() {
|
||||
const mainJob = Cron(
|
||||
"* * * * *",
|
||||
async () => {
|
||||
await jobSchedule();
|
||||
await scheduleCronJobs();
|
||||
},
|
||||
{
|
||||
protect: true,
|
||||
@@ -69,12 +73,13 @@ async function Startup() {
|
||||
|
||||
mainJob.trigger();
|
||||
|
||||
figlet("Kener is UP!", 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!`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
+251
-243
@@ -1,325 +1,333 @@
|
||||
// @ts-nocheck
|
||||
import { AllRecordTypes } from "./constants.js";
|
||||
import knexOb from "../../../knexfile.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
const IsValidURL = function (url) {
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
};
|
||||
const IsStringURLSafe = function (str) {
|
||||
const regex = /^[A-Za-z0-9\-_.~]+$/;
|
||||
return regex.test(str);
|
||||
const regex = /^[A-Za-z0-9\-_.~]+$/;
|
||||
return regex.test(str);
|
||||
};
|
||||
const IsValidHTTPMethod = function (method) {
|
||||
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
|
||||
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
|
||||
};
|
||||
|
||||
//return given timestamp in UTC
|
||||
const GetNowTimestampUTC = function () {
|
||||
//use js date instead of moment
|
||||
const now = new Date();
|
||||
const timestamp = now.getTime();
|
||||
return Math.floor(timestamp / 1000);
|
||||
//use js date instead of moment
|
||||
const now = new Date();
|
||||
const timestamp = now.getTime();
|
||||
return Math.floor(timestamp / 1000);
|
||||
};
|
||||
//return given timestamp minute start timestamp in UTC
|
||||
const GetMinuteStartTimestampUTC = function (timestamp) {
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const minuteStart = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
now.getHours(),
|
||||
now.getMinutes(),
|
||||
0,
|
||||
0
|
||||
);
|
||||
const minuteStartTimestamp = minuteStart.getTime();
|
||||
return Math.floor(minuteStartTimestamp / 1000);
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const minuteStart = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
now.getHours(),
|
||||
now.getMinutes(),
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const minuteStartTimestamp = minuteStart.getTime();
|
||||
return Math.floor(minuteStartTimestamp / 1000);
|
||||
};
|
||||
//return current timestamp minute start timestamp in UTC
|
||||
const GetMinuteStartNowTimestampUTC = function () {
|
||||
//use js date instead of moment
|
||||
const now = new Date();
|
||||
const minuteStart = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
now.getHours(),
|
||||
now.getMinutes(),
|
||||
0,
|
||||
0
|
||||
);
|
||||
const minuteStartTimestamp = minuteStart.getTime();
|
||||
return Math.floor(minuteStartTimestamp / 1000);
|
||||
//use js date instead of moment
|
||||
const now = new Date();
|
||||
const minuteStart = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
now.getHours(),
|
||||
now.getMinutes(),
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const minuteStartTimestamp = minuteStart.getTime();
|
||||
return Math.floor(minuteStartTimestamp / 1000);
|
||||
};
|
||||
//return given timestamp day start timestamp in UTC
|
||||
const GetDayStartTimestampUTC = function (timestamp) {
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const dayStart = new Date(
|
||||
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0)
|
||||
);
|
||||
const dayStartTimestamp = dayStart.getTime();
|
||||
return Math.floor(dayStartTimestamp / 1000);
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const dayStart = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0));
|
||||
const dayStartTimestamp = dayStart.getTime();
|
||||
return Math.floor(dayStartTimestamp / 1000);
|
||||
};
|
||||
const GetDayEndTimestampUTC = function (timestamp) {
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const dayEnd = new Date(
|
||||
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999)
|
||||
);
|
||||
const dayEndTimestamp = dayEnd.getTime();
|
||||
return Math.floor(dayEndTimestamp / 1000) + 60;
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const dayEnd = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999));
|
||||
const dayEndTimestamp = dayEnd.getTime();
|
||||
return Math.floor(dayEndTimestamp / 1000) + 60;
|
||||
};
|
||||
const DurationInMinutes = function (start, end) {
|
||||
return Math.floor((end - start) / 60);
|
||||
return Math.floor((end - start) / 60);
|
||||
};
|
||||
const GetDayStartWithOffset = function (timeStampInSeconds, offsetInMinutes) {
|
||||
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
|
||||
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
|
||||
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
|
||||
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
|
||||
//have to figure out when to add a day
|
||||
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
|
||||
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
|
||||
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
|
||||
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
|
||||
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
|
||||
//have to figure out when to add a day
|
||||
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
|
||||
|
||||
//if xtm - 330 > 1 day , add a day to xtm - 330
|
||||
//if xtm - 330 > 1 day , add a day to xtm - 330
|
||||
|
||||
if (offsetInMinutes < 0) {
|
||||
//add one day to dayStartThen
|
||||
dayStartThen = dayStartThen + 24 * 60 * 60;
|
||||
}
|
||||
return dayStartThen + offsetInMinutes * 60;
|
||||
if (offsetInMinutes < 0) {
|
||||
//add one day to dayStartThen
|
||||
dayStartThen = dayStartThen + 24 * 60 * 60;
|
||||
}
|
||||
return dayStartThen + offsetInMinutes * 60;
|
||||
};
|
||||
const BeginningOfDay = (options = {}) => {
|
||||
const { date = new Date(), timeZone } = options;
|
||||
const parts = Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
hourCycle: "h23",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric"
|
||||
}).formatToParts(date);
|
||||
const hour = parseInt(parts.find((i) => i.type === "hour").value);
|
||||
const minute = parseInt(parts.find((i) => i.type === "minute").value);
|
||||
const second = parseInt(parts.find((i) => i.type === "second").value);
|
||||
const dt = new Date(
|
||||
1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000)
|
||||
);
|
||||
return dt.getTime() / 1000;
|
||||
const { date = new Date(), timeZone } = options;
|
||||
const parts = Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
hourCycle: "h23",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
}).formatToParts(date);
|
||||
const hour = parseInt(parts.find((i) => i.type === "hour").value);
|
||||
const minute = parseInt(parts.find((i) => i.type === "minute").value);
|
||||
const second = parseInt(parts.find((i) => i.type === "second").value);
|
||||
const dt = new Date(1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000));
|
||||
return dt.getTime() / 1000;
|
||||
};
|
||||
const BeginningOfMinute = (options = {}) => {
|
||||
const { date = new Date(), timeZone } = options;
|
||||
const parts = Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
hourCycle: "h23",
|
||||
second: "numeric"
|
||||
}).formatToParts(date);
|
||||
const second = parseInt(parts.find((i) => i.type === "second").value);
|
||||
const dt = new Date(1000 * Math.floor((date - second * 1000) / 1000));
|
||||
return dt.getTime() / 1000;
|
||||
const { date = new Date(), timeZone } = options;
|
||||
const parts = Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
hourCycle: "h23",
|
||||
second: "numeric",
|
||||
}).formatToParts(date);
|
||||
const second = parseInt(parts.find((i) => i.type === "second").value);
|
||||
const dt = new Date(1000 * Math.floor((date - second * 1000) / 1000));
|
||||
return dt.getTime() / 1000;
|
||||
};
|
||||
const ValidateIpAddress = function (input) {
|
||||
// Check if input is a valid IPv4 address
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(input)) {
|
||||
return "IPv4";
|
||||
}
|
||||
// Check if input is a valid IPv4 address
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (ipv4Regex.test(input)) {
|
||||
return "IPv4";
|
||||
}
|
||||
|
||||
// Check if input is a valid IPv6 address
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||||
if (ipv6Regex.test(input)) {
|
||||
return "IPv6";
|
||||
}
|
||||
// Check if input is a valid IPv6 address
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||||
if (ipv6Regex.test(input)) {
|
||||
return "IPv6";
|
||||
}
|
||||
|
||||
// Check if input is a valid domain name
|
||||
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
if (domainRegex.test(input)) {
|
||||
return "Domain Name";
|
||||
}
|
||||
// Check if input is a valid domain name
|
||||
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
if (domainRegex.test(input)) {
|
||||
return "Domain Name";
|
||||
}
|
||||
|
||||
// If none of the above conditions match, the input is invalid
|
||||
return "Invalid";
|
||||
// If none of the above conditions match, the input is invalid
|
||||
return "Invalid";
|
||||
};
|
||||
function checkIfDuplicateExists(arr) {
|
||||
return new Set(arr).size !== arr.length;
|
||||
return new Set(arr).size !== arr.length;
|
||||
}
|
||||
function GetWordsStartingWithDollar(text) {
|
||||
const regex = /\$\w+/g;
|
||||
const wordsArray = text.match(regex);
|
||||
return wordsArray || [];
|
||||
const regex = /\$\w+/g;
|
||||
const wordsArray = text.match(regex);
|
||||
return wordsArray || [];
|
||||
}
|
||||
const StatusObj = {
|
||||
UP: "api-up",
|
||||
DEGRADED: "api-degraded",
|
||||
DOWN: "api-down",
|
||||
NO_DATA: "api-nodata"
|
||||
UP: "api-up",
|
||||
DEGRADED: "api-degraded",
|
||||
DOWN: "api-down",
|
||||
NO_DATA: "api-nodata",
|
||||
};
|
||||
// @ts-ignore
|
||||
const ParseUptime = function (up, all) {
|
||||
if (all === 0) return String("-");
|
||||
if (up == 0) return String("0");
|
||||
if (up == all) {
|
||||
return String(((up / all) * parseFloat(100)).toFixed(0));
|
||||
}
|
||||
//return 50% as 50% and not 50.0000%
|
||||
if (((up / all) * 100) % 10 == 0) {
|
||||
return String(((up / all) * parseFloat(100)).toFixed(0));
|
||||
}
|
||||
return String(((up / all) * parseFloat(100)).toFixed(4));
|
||||
if (all === 0) return String("-");
|
||||
if (up == 0) return String("0");
|
||||
if (up == all) {
|
||||
return String(((up / all) * parseFloat(100)).toFixed(0));
|
||||
}
|
||||
//return 50% as 50% and not 50.0000%
|
||||
if (((up / all) * 100) % 10 == 0) {
|
||||
return String(((up / all) * parseFloat(100)).toFixed(0));
|
||||
}
|
||||
return String(((up / all) * parseFloat(100)).toFixed(4));
|
||||
};
|
||||
const ParsePercentage = function (n) {
|
||||
if (isNaN(n)) return "-";
|
||||
if (n == 0) {
|
||||
return "0";
|
||||
}
|
||||
if (n == 100) {
|
||||
return "100";
|
||||
}
|
||||
return n.toFixed(4);
|
||||
if (isNaN(n)) return "-";
|
||||
if (n == 0) {
|
||||
return "0";
|
||||
}
|
||||
if (n == 100) {
|
||||
return "100";
|
||||
}
|
||||
return n.toFixed(4);
|
||||
};
|
||||
|
||||
//valid domain name
|
||||
function IsValidHost(domain) {
|
||||
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
return regex.test(domain);
|
||||
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
|
||||
return regex.test(domain);
|
||||
}
|
||||
|
||||
//valid nameserver
|
||||
function IsValidNameServer(nameServer) {
|
||||
//8.8.8.8 example
|
||||
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
return regex.test(nameServer);
|
||||
//8.8.8.8 example
|
||||
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
return regex.test(nameServer);
|
||||
}
|
||||
|
||||
//valid dns record type
|
||||
function IsValidRecordType(recordType) {
|
||||
return AllRecordTypes.hasOwnProperty(recordType);
|
||||
return AllRecordTypes.hasOwnProperty(recordType);
|
||||
}
|
||||
function ReplaceAllOccurrences(originalString, searchString, replacement) {
|
||||
const regex = new RegExp(`\\${searchString}`, "g");
|
||||
const replacedString = originalString.replace(regex, replacement);
|
||||
return replacedString;
|
||||
const regex = new RegExp(`\\${searchString}`, "g");
|
||||
const replacedString = originalString.replace(regex, replacement);
|
||||
return replacedString;
|
||||
}
|
||||
|
||||
function GetRequiredSecrets(str) {
|
||||
let envSecrets = [];
|
||||
const requiredSecrets = GetWordsStartingWithDollar(str).map((x) => x.substr(1));
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (requiredSecrets.indexOf(key) !== -1) {
|
||||
envSecrets.push({
|
||||
find: `$${key}`,
|
||||
replace: value
|
||||
});
|
||||
}
|
||||
}
|
||||
return envSecrets;
|
||||
let envSecrets = [];
|
||||
const requiredSecrets = GetWordsStartingWithDollar(str).map((x) => x.substr(1));
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (requiredSecrets.indexOf(key) !== -1) {
|
||||
envSecrets.push({
|
||||
find: `$${key}`,
|
||||
replace: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
return envSecrets;
|
||||
}
|
||||
|
||||
function ValidateMonitorAlerts(alerts) {
|
||||
if (!alerts) {
|
||||
console.log("Alerts object is not provided.");
|
||||
return false;
|
||||
}
|
||||
// if down degraded not present return false
|
||||
if (!alerts.hasOwnProperty("DOWN") && !alerts.hasOwnProperty("DEGRADED")) {
|
||||
console.log("Alerts object does not have DOWN or DEGRADED properties.");
|
||||
return false;
|
||||
}
|
||||
let statues = Object.keys(alerts);
|
||||
//can be either DOWN or DEGRADED
|
||||
let validKeys = ["DOWN", "DEGRADED"];
|
||||
for (let i = 0; i < statues.length; i++) {
|
||||
const keyName = statues[i];
|
||||
if (!validKeys.includes(keyName)) {
|
||||
console.log(`Invalid key found in alerts: ${keyName}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const key in alerts) {
|
||||
if (Object.prototype.hasOwnProperty.call(alerts, key)) {
|
||||
const element = alerts[key];
|
||||
const triggers = element.triggers;
|
||||
//if triggers not present return false
|
||||
if (!element.hasOwnProperty("triggers")) {
|
||||
console.log(`Triggers not present for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
//if length of triggers is 0 return false
|
||||
if (triggers.length === 0) {
|
||||
console.log(`Triggers length is 0 for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!element.hasOwnProperty("failureThreshold") ||
|
||||
!element.hasOwnProperty("successThreshold")
|
||||
) {
|
||||
console.log(`Thresholds not present for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
element.hasOwnProperty("failureThreshold") &&
|
||||
!Number.isInteger(element.failureThreshold)
|
||||
) {
|
||||
console.log(`Failure threshold is not an integer for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
element.hasOwnProperty("successThreshold") &&
|
||||
!Number.isInteger(element.successThreshold)
|
||||
) {
|
||||
console.log(`Success threshold is not an integer for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (element.hasOwnProperty("failureThreshold") && element.failureThreshold <= 0) {
|
||||
console.log(`Failure threshold is less than or equal to 0 for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (element.hasOwnProperty("successThreshold") && element.successThreshold <= 0) {
|
||||
console.log(`Success threshold is less than or equal to 0 for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
if (!alerts) {
|
||||
console.log("Alerts object is not provided.");
|
||||
return false;
|
||||
}
|
||||
// if down degraded not present return false
|
||||
if (!alerts.hasOwnProperty("DOWN") && !alerts.hasOwnProperty("DEGRADED")) {
|
||||
console.log("Alerts object does not have DOWN or DEGRADED properties.");
|
||||
return false;
|
||||
}
|
||||
let statues = Object.keys(alerts);
|
||||
//can be either DOWN or DEGRADED
|
||||
let validKeys = ["DOWN", "DEGRADED"];
|
||||
for (let i = 0; i < statues.length; i++) {
|
||||
const keyName = statues[i];
|
||||
if (!validKeys.includes(keyName)) {
|
||||
console.log(`Invalid key found in alerts: ${keyName}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const key in alerts) {
|
||||
if (Object.prototype.hasOwnProperty.call(alerts, key)) {
|
||||
const element = alerts[key];
|
||||
const triggers = element.triggers;
|
||||
//if triggers not present return false
|
||||
if (!element.hasOwnProperty("triggers")) {
|
||||
console.log(`Triggers not present for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
//if length of triggers is 0 return false
|
||||
if (triggers.length === 0) {
|
||||
console.log(`Triggers length is 0 for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (!element.hasOwnProperty("failureThreshold") || !element.hasOwnProperty("successThreshold")) {
|
||||
console.log(`Thresholds not present for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (element.hasOwnProperty("failureThreshold") && !Number.isInteger(element.failureThreshold)) {
|
||||
console.log(`Failure threshold is not an integer for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (element.hasOwnProperty("successThreshold") && !Number.isInteger(element.successThreshold)) {
|
||||
console.log(`Success threshold is not an integer for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (element.hasOwnProperty("failureThreshold") && element.failureThreshold <= 0) {
|
||||
console.log(`Failure threshold is less than or equal to 0 for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
if (element.hasOwnProperty("successThreshold") && element.successThreshold <= 0) {
|
||||
console.log(`Success threshold is less than or equal to 0 for key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function GenerateRandomColor() {
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
return randomColor;
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
return randomColor;
|
||||
}
|
||||
|
||||
//wait for x ms promise
|
||||
function Wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
function MaskString(str) {
|
||||
return "*".repeat(str.length - 4) + str.slice(-4);
|
||||
return "*".repeat(str.length - 4) + str.slice(-4);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export {
|
||||
IsValidURL,
|
||||
IsValidHTTPMethod,
|
||||
GetMinuteStartTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
GetDayStartTimestampUTC,
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
DurationInMinutes,
|
||||
GetDayStartWithOffset,
|
||||
BeginningOfDay,
|
||||
IsStringURLSafe,
|
||||
ValidateIpAddress,
|
||||
checkIfDuplicateExists,
|
||||
GetWordsStartingWithDollar,
|
||||
StatusObj,
|
||||
ParseUptime,
|
||||
ParsePercentage,
|
||||
IsValidHost,
|
||||
IsValidRecordType,
|
||||
IsValidNameServer,
|
||||
ReplaceAllOccurrences,
|
||||
GetRequiredSecrets,
|
||||
ValidateMonitorAlerts,
|
||||
GenerateRandomColor,
|
||||
BeginningOfMinute,
|
||||
Wait,
|
||||
MaskString
|
||||
IsValidURL,
|
||||
IsValidHTTPMethod,
|
||||
GetMinuteStartTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
GetDayStartTimestampUTC,
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
DurationInMinutes,
|
||||
GetDayStartWithOffset,
|
||||
BeginningOfDay,
|
||||
IsStringURLSafe,
|
||||
ValidateIpAddress,
|
||||
checkIfDuplicateExists,
|
||||
GetWordsStartingWithDollar,
|
||||
StatusObj,
|
||||
ParseUptime,
|
||||
ParsePercentage,
|
||||
IsValidHost,
|
||||
IsValidRecordType,
|
||||
IsValidNameServer,
|
||||
ReplaceAllOccurrences,
|
||||
GetRequiredSecrets,
|
||||
ValidateMonitorAlerts,
|
||||
GenerateRandomColor,
|
||||
BeginningOfMinute,
|
||||
Wait,
|
||||
MaskString,
|
||||
GetDbType,
|
||||
HashString,
|
||||
ValidateEmail,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* ***************************************************
|
||||
* * *
|
||||
* * Amplitude Analytics Capture Snippet *
|
||||
* * *
|
||||
* ***************************************************
|
||||
**/
|
||||
// @ts-nocheck
|
||||
|
||||
(function () {
|
||||
let loadJS = function (url, implementationCode, location) {
|
||||
//url is URL of external file, implementationCode is the code
|
||||
//to be called from the file, location is the location to
|
||||
//insert the <script> element
|
||||
|
||||
let scriptTag = document.createElement("script");
|
||||
scriptTag.src = url;
|
||||
|
||||
scriptTag.onload = implementationCode;
|
||||
scriptTag.onreadystatechange = implementationCode;
|
||||
|
||||
location.appendChild(scriptTag);
|
||||
};
|
||||
let initJS = function () {
|
||||
window.amplitude.add(window.sessionReplay.plugin({ sampleRate: 1 }));
|
||||
window.amplitude.init("{{api_key}}", { fetchRemoteConfig: true, autocapture: true });
|
||||
|
||||
window.addEventListener("analyticsEvent", function (e) {
|
||||
// Extract event name and data from the custom event
|
||||
const eventName = e.detail.event;
|
||||
const eventData = e.detail.data || {};
|
||||
if (eventName) {
|
||||
window.amplitude.track(eventName, eventData);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//on dom ready
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
loadJS("https://cdn.amplitude.com/script/{{api_key}}.js", initJS, document.getElementsByTagName("head")[0]);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* ***************************************************
|
||||
* * *
|
||||
* * Clarity Analytics Capture Snippet *
|
||||
* * *
|
||||
* ***************************************************
|
||||
**/
|
||||
// @ts-nocheck
|
||||
(function (c, l, a, r, i, t, y) {
|
||||
c[a] =
|
||||
c[a] ||
|
||||
function () {
|
||||
(c[a].q = c[a].q || []).push(arguments);
|
||||
};
|
||||
t = l.createElement(r);
|
||||
t.async = 1;
|
||||
t.src = "https://www.clarity.ms/tag/" + i;
|
||||
y = l.getElementsByTagName(r)[0];
|
||||
y.parentNode.insertBefore(t, y);
|
||||
})(window, document, "clarity", "script", "{{project_id}}");
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user