mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b56d20ac35 | |||
| 9ee7a7b861 | |||
| f73ffce3f0 | |||
| 0997967787 | |||
| 3476ec2b29 | |||
| e0cf568aa9 | |||
| c12d7f8345 | |||
| 9ba115d5ab | |||
| 3756bbfd06 | |||
| 9096bcc641 | |||
| c4d04854e5 | |||
| d95fc0005b | |||
| 2b9093184a | |||
| 831376054b | |||
| 885ee0b6d6 | |||
| 511d0f26dd | |||
| 5263698625 | |||
| 7859f9564e | |||
| 149456682d | |||
| 8c040997da | |||
| 868fed0176 | |||
| c7f44a48da | |||
| 6300bff4e7 | |||
| 3e6b1ffdbb | |||
| 408fd5318b | |||
| cfe419f6df | |||
| 5403726099 | |||
| 057c14909f | |||
| 37b18c06bb | |||
| 3ad1eb9a03 | |||
| a1064771f8 | |||
| 463ee286bf | |||
| a7ae6e72ef | |||
| 36c67f60dd | |||
| bb718b04a6 | |||
| b6ea060054 | |||
| 5dba54c048 | |||
| 415276163f | |||
| 017b9e2e84 | |||
| 5740991ee1 | |||
| 06008bd719 | |||
| b53cd485e5 | |||
| 7c1fd39158 | |||
| f7888f1ba4 | |||
| ece5ac37ad | |||
| a51183fb23 | |||
| 7875192fb0 | |||
| eb128ad431 | |||
| ba61ed87ab | |||
| 8b5348081d | |||
| bb7e69ace5 | |||
| 3b8bb60423 | |||
| 93339ce9df | |||
| d30d99d8c2 | |||
| 3468df16e1 | |||
| ccdeff98dc | |||
| 7e182b1df2 | |||
| ff652ad435 | |||
| 1fdafa7966 | |||
| 1f5e683af5 | |||
| db8c881571 | |||
| 14f49c5b3b | |||
| 52286f26a5 | |||
| 45e7567cb6 | |||
| c0edb59884 | |||
| 1e86c429ca | |||
| fbaebdcbcf | |||
| 3f9716b0a4 | |||
| 57d32197cf | |||
| 9e6a3f26c2 | |||
| 23314beb15 | |||
| 64b94da585 | |||
| 8c95c94472 | |||
| 7dafb2eddc | |||
| 7c9f3eb87f | |||
| ab34dd81f8 | |||
| a8be878a87 | |||
| 2b1849f1a3 | |||
| 25a6590324 | |||
| fa251dc07f | |||
| 4de93e7e95 | |||
| a5e5f33dc8 | |||
| 4060094404 |
@@ -43,6 +43,7 @@ indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
print_width = 180
|
||||
|
||||
# Dockerfile
|
||||
[Dockerfile*]
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
name: Create Incident Template
|
||||
about: Create Incident Template
|
||||
title: Title of Incident
|
||||
labels: incident
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Your Incident Description goes here. Markdown Supported
|
||||
|
||||
[start_datetime:utcSeconds]
|
||||
[end_datetime:utcSeconds]
|
||||
@@ -0,0 +1,47 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Track base image versions via .env.build
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
file-patterns:
|
||||
- ".env.build"
|
||||
- "node:*" # Ensures Node.js images are correctly detected
|
||||
|
||||
# Monitor OS package versions in Dockerfile (Debian/Alpine)
|
||||
- package-ecosystem: "gitsubmodule" # Alternative method to track OS packages in Dockerfile
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "os-packages"
|
||||
commit-message:
|
||||
prefix: "os"
|
||||
include: "scope"
|
||||
|
||||
# Monitor Node.js dependencies from package.json
|
||||
# TODO: Uncomment below if we want to begin letting Dependabot monitor & open PRs for Node.js project dependencies
|
||||
# - package-ecosystem: "npm"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# labels:
|
||||
# - "dependencies"
|
||||
# - "npm"
|
||||
# commit-message:
|
||||
# prefix: "npm"
|
||||
# include: "scope"
|
||||
|
||||
# Monitor GitHub Actions dependencies
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
commit-message:
|
||||
prefix: "actions"
|
||||
include: "scope"
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20' # Adjust as needed
|
||||
node-version: '23' # Adjust as needed
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -153,4 +153,4 @@ jobs:
|
||||
echo "Setting BUILD_VERSION to $VERSION"
|
||||
gh variable set BUILD_VERSION --body "$VERSION"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
|
||||
@@ -9,6 +9,7 @@ config/server.yaml
|
||||
/src/lib/.kener
|
||||
/package
|
||||
.env
|
||||
.vscode
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@
|
||||
"semi": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
"printWidth": 180
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+22
-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.2.0"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.0-blue" alt="Kener latest stable version: 3.2.0" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.11"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.11-blue" alt="Kener latest stable version: 3.2.11" /></a>
|
||||
|
||||
#### Available Tags
|
||||
|
||||
@@ -93,38 +93,38 @@ Official Docker images for **Kener** are available on [Docker Hub](https://hub.d
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
|
||||
<td>Latest stable release (aka 3.2.0)</td>
|
||||
<td>Latest stable release (aka 3.2.11)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.0" target="_blank"><code>3.2.0</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.11" target="_blank"><code>3.2.11</code></a></td>
|
||||
<td>Specific release version</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2" target="_blank"><code>3.2</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.0) release within that minor version (3.2.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.11) release within that minor version (3.2.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.0) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.11) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 <strong><em>(smallest image size)</em></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
|
||||
<td>Latest stable release (aka 3.2.0)</td>
|
||||
<td>Latest stable release (aka 3.2.11)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.0-alpine" target="_blank"><code>3.2.0-alpine</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.11-alpine" target="_blank"><code>3.2.11-alpine</code></a></td>
|
||||
<td>Specific release version</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2-alpine" target="_blank"><code>3.2-alpine</code></a></td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.0) release within that minor version (3.2.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.11) release within that minor version (3.2.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.0) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.11) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -7,6 +7,46 @@ 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>
|
||||
|
||||
@@ -33,10 +33,53 @@ To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS the
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
<div class="note danger">
|
||||
Do not include <style> tags.
|
||||
</div>
|
||||
|
||||
## Grid Layout
|
||||
|
||||
To change from a column layout to a grid layout in your Kener instance, you can use the following CSS:
|
||||
|
||||
```css
|
||||
@media (min-width: 1330px) {
|
||||
.section-monitors,
|
||||
.section-categories,
|
||||
.section-hero,
|
||||
.section-back,
|
||||
.section-events,
|
||||
.section-legend,
|
||||
.section-categories,
|
||||
.section-browser-events {
|
||||
width: 1330px !important;
|
||||
max-width: 1330px !important;
|
||||
}
|
||||
|
||||
.section-monitors .monitor-root {
|
||||
background-color: transparent;
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.section-monitors .monitor-root .monitors-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.section-monitors .monitor-root .monitors-card .monitor {
|
||||
grid-column: span 1;
|
||||
border-radius: 0.375rem;
|
||||
background-color: hsl(var(--card) / var(--tw-bg-opacity, 1));
|
||||
border-width: 1px;
|
||||
}
|
||||
.section-monitors .monitor-root .monitors-card .monitor:last-child {
|
||||
border-bottom: inherit;
|
||||
border-bottom-width: 1px !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -25,6 +25,13 @@ Set this to the origin of your website(protocl + hostname + port if there). This
|
||||
export ORIGIN=http://localhost:3000
|
||||
```
|
||||
|
||||
If your host is at let us say `https://example.com`, then set the ORIGIN to `https://example.com`. This is required for CORS.
|
||||
If you are using a reverse proxy like nginx, make sure to set the `ORIGIN` to the domain name of your website.
|
||||
|
||||
```bash
|
||||
export ORIGIN=https://example.com
|
||||
```
|
||||
|
||||
## PORT
|
||||
|
||||
Defaults to 3000 if not specified
|
||||
@@ -37,8 +44,8 @@ export PORT=4242
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path.
|
||||
|
||||
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
|
||||
- Important: This env variable should be present during both build and run time
|
||||
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
|
||||
- Important: This env variable should be present during both build and run time
|
||||
|
||||
```bash
|
||||
export KENER_BASE_PATH=/status
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Kener Role Based Access Control (RBAC)
|
||||
description: Role-Based Access Control (RBAC) in Kener
|
||||
---
|
||||
|
||||
# Role-Based Access Control (RBAC) in Kener
|
||||
|
||||
Kener includes a comprehensive role-based access control system that allows you to manage user permissions and access to various features. This document explains the available roles, their permissions, and how to manage users effectively.
|
||||
|
||||
## Available Roles
|
||||
|
||||
Kener offers three different roles with varying levels of permissions:
|
||||
|
||||
| Role | Description |
|
||||
| ------ | ------------------------------------------------------------------------------ |
|
||||
| Admin | Full access to all features including user management and system configuration |
|
||||
| Editor | Can create and edit monitors, triggers, incidents, and other operational data |
|
||||
| Member | Read-only access with limited interaction capabilities |
|
||||
|
||||
## Role Permissions
|
||||
|
||||
### Admin
|
||||
|
||||
Admins have unrestricted access to the entire system:
|
||||
|
||||
- **User Management**:
|
||||
|
||||
- Create, update, and deactivate users
|
||||
- Change user roles
|
||||
- Reset passwords for other users
|
||||
- Send verification emails
|
||||
|
||||
- **System Configuration**:
|
||||
|
||||
- Configure all site settings
|
||||
- Manage API keys
|
||||
- Set up triggers and integrations
|
||||
|
||||
- **Operational Access**:
|
||||
- Full access to create and manage monitors
|
||||
- Create and update incidents
|
||||
- Run tests on monitors and triggers
|
||||
- Configure all notification channels
|
||||
|
||||
### Editor
|
||||
|
||||
Editors can manage most operational aspects but cannot administer users:
|
||||
|
||||
- **Content Management**:
|
||||
|
||||
- Create and edit monitors
|
||||
- Configure monitor settings
|
||||
- Create and manage triggers
|
||||
- Create and update incidents
|
||||
- Add incident updates and comments
|
||||
|
||||
- **Limited Access**:
|
||||
- Cannot manage users
|
||||
- Cannot change role assignments
|
||||
- Cannot access certain system-level configurations
|
||||
|
||||
### Member
|
||||
|
||||
Members have read-only access with minimal interaction capabilities:
|
||||
|
||||
- **View Access**:
|
||||
|
||||
- View all monitors and their status
|
||||
- View incidents and their history
|
||||
- See system configuration (but cannot modify)
|
||||
|
||||
- **Limited Interactions**:
|
||||
- Can test existing triggers but cannot create or edit them
|
||||
- Cannot create or update incidents
|
||||
- Cannot modify any system configuration
|
||||
|
||||
## Managing Users
|
||||
|
||||
### Adding New Users
|
||||
|
||||
Only Admins and Editors can add new users to the system:
|
||||
|
||||
1. Navigate to the **Users** page in the management dashboard
|
||||
2. Click the **Add New User** button
|
||||
3. Fill in the required information:
|
||||
- Name
|
||||
- Email
|
||||
- Password
|
||||
- Role (Member or Editor)
|
||||
4. Click **Add User** to create the account
|
||||
|
||||
When a new user is added, a verification email can be sent to confirm their email address if email sending is configured.
|
||||
|
||||
### User Settings
|
||||
|
||||
Admins can manage user accounts through the user settings page:
|
||||
|
||||
- **Email Verification**: Send verification emails to users
|
||||
- **Password Reset**: Reset a user's password
|
||||
- **Role Management**: Change a user's role between Member and Editor
|
||||
- **Account Status**: Activate or deactivate user accounts
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Follow the Principle of Least Privilege**:
|
||||
|
||||
- Assign the minimum necessary permissions for users to perform their job
|
||||
- Start users with the Member role and elevate as needed
|
||||
|
||||
2. **Regular Access Reviews**:
|
||||
|
||||
- Periodically review user access and roles
|
||||
- Remove access for users who no longer need it
|
||||
|
||||
3. **Admin Accounts**:
|
||||
- Limit the number of Admin accounts
|
||||
- Use strong passwords for Admin accounts
|
||||
- Consider using email verification for all users
|
||||
|
||||
## Role Limitations
|
||||
|
||||
- The Admin role can only be assigned during initial setup
|
||||
- Members cannot create or modify content
|
||||
- Users cannot modify their own role (only an Admin can change roles)
|
||||
|
||||
## Email Verification
|
||||
|
||||
When email sending is configured, users can verify their email addresses:
|
||||
|
||||
1. Admins can send verification emails from the user management interface
|
||||
2. Users receive an email with a verification link
|
||||
3. After clicking the link, the user's email is marked as verified
|
||||
|
||||
Email verification improves security and ensures that users have provided valid email addresses for notifications.
|
||||
+11
-1
@@ -21,7 +21,17 @@ Add API key of your Amplitude account
|
||||
|
||||
### Mixpanel
|
||||
|
||||
Add token of your Mixpanel account.
|
||||
Add project token of your Mixpanel account.
|
||||
|
||||
### Plausible
|
||||
|
||||
- Add domain of your Plausible account. Example: `example.com`
|
||||
- Add API key of your Plausible account. Default is `https://plausible.io/api/event`
|
||||
- Add Script Source of your Plausible account. Default is `https://plausible.io/js/script.pageview-props.tagged-events.js`
|
||||
|
||||
### Microsoft Clarity
|
||||
|
||||
Add Project ID of your Microsoft Clarity account.
|
||||
|
||||
## Search Engine Optimization
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"title": "Databases",
|
||||
"link": "/docs/database",
|
||||
"file": "/database.md"
|
||||
},
|
||||
{
|
||||
"title": "Access Control",
|
||||
"link": "/docs/rbac",
|
||||
"file": "/rbac.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+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>
|
||||
|
||||
@@ -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
+347
-46
@@ -1,17 +1,24 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.1.8",
|
||||
"version": "3.2.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kener",
|
||||
"version": "3.1.8",
|
||||
"version": "3.2.11",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@number-flow/svelte": "^0.2.1",
|
||||
"@scalar/express-api-reference": "^0.4.167",
|
||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||
"analytics": "^0.8.14",
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
@@ -31,11 +38,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",
|
||||
@@ -46,9 +54,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",
|
||||
@@ -60,7 +70,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",
|
||||
@@ -176,10 +185,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"
|
||||
@@ -188,6 +196,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",
|
||||
@@ -195,7 +355,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -212,7 +371,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -229,7 +387,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -246,7 +403,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -263,7 +419,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -280,7 +435,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -297,7 +451,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -314,7 +467,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -331,7 +483,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -348,7 +499,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -365,7 +515,6 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -382,7 +531,6 @@
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -399,7 +547,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -416,7 +563,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -433,7 +579,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -450,7 +595,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -467,7 +611,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -484,7 +627,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -501,7 +643,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -518,7 +659,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -535,7 +675,6 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -552,7 +691,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -756,6 +894,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",
|
||||
@@ -776,6 +992,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",
|
||||
@@ -1204,6 +1426,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",
|
||||
@@ -1525,9 +1778,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",
|
||||
@@ -2095,6 +2348,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",
|
||||
@@ -2267,6 +2536,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",
|
||||
@@ -2994,7 +3269,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": {
|
||||
@@ -4779,12 +5053,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": {
|
||||
@@ -5106,7 +5380,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"
|
||||
@@ -6337,9 +6610,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"
|
||||
@@ -6624,7 +6897,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": {
|
||||
@@ -6771,7 +7043,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"
|
||||
@@ -7584,6 +7855,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",
|
||||
@@ -7794,6 +8071,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",
|
||||
@@ -8419,7 +8706,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",
|
||||
@@ -8471,6 +8757,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",
|
||||
@@ -8486,6 +8781,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
||||
+13
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.2.2",
|
||||
"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,9 +67,16 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@number-flow/svelte": "^0.2.1",
|
||||
"@scalar/express-api-reference": "^0.4.167",
|
||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||
"analytics": "^0.8.14",
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
@@ -90,11 +96,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",
|
||||
@@ -105,9 +112,11 @@
|
||||
"queue": "^7.0.0",
|
||||
"randomstring": "^1.3.0",
|
||||
"resend": "^4.0.1",
|
||||
"svelte-codemirror-editor": "^1.4.1",
|
||||
"svelte-legos": "^0.2.5",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-variants": "^0.1.18"
|
||||
"tailwind-variants": "^0.1.18",
|
||||
"vite-plugin-package-version": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
code:not([class^="language-"]) {
|
||||
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.sidebar-item.active,
|
||||
.sidebar-item:hover {
|
||||
color: #ed702d;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ event, resolve }) {
|
||||
const response = await resolve(event);
|
||||
response.headers.delete("Link");
|
||||
return response;
|
||||
}
|
||||
+69
-98
@@ -1,108 +1,10 @@
|
||||
/*one is the class for dotted background*/
|
||||
|
||||
.dots-pattern {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
height: 100svh;
|
||||
background: linear-gradient(177deg, rgba(255, 137, 131, 0.5) 0%, rgba(35, 136, 224, 0.05) 60%);
|
||||
clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%);
|
||||
}
|
||||
|
||||
.dots-pattern::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-image: radial-gradient(rgba(0, 0, 0, 0) 1.5px, var(--background-kener) 1px);
|
||||
background-size: 14px 14px;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
transform: blur(3px);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.squares-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
height: 100svh;
|
||||
|
||||
/* clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%); */
|
||||
}
|
||||
|
||||
.squares-pattern::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background-image:
|
||||
linear-gradient(#444cf7 1px, transparent 1px),
|
||||
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0.8) 20%,
|
||||
rgba(0, 0, 0, 0.6) 40%,
|
||||
rgba(0, 0, 0, 0.4) 60%,
|
||||
rgba(0, 0, 0, 0.2) 80%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0.2) 0%,
|
||||
rgba(0, 0, 0, 0.1) 20%,
|
||||
rgba(0, 0, 0, 0.05) 40%,
|
||||
rgba(0, 0, 0, 0.025) 60%,
|
||||
rgba(0, 0, 0, 0.0125) 80%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
background-size: 32px 32px;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
|
||||
left: 0;
|
||||
}
|
||||
.dark .squares-pattern::after {
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0.5) 0%,
|
||||
rgba(0, 0, 0, 0.25) 20%,
|
||||
rgba(0, 0, 0, 0.125) 40%,
|
||||
rgba(0, 0, 0, 0.0625) 60%,
|
||||
rgba(0, 0, 0, 0.03125) 80%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0.5) 0%,
|
||||
rgba(0, 0, 0, 0.25) 20%,
|
||||
rgba(0, 0, 0, 0.125) 40%,
|
||||
rgba(0, 0, 0, 0.0625) 60%,
|
||||
rgba(0, 0, 0, 0.03125) 80%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
background-image:
|
||||
linear-gradient(#616fbf 1px, transparent 1px),
|
||||
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
|
||||
}
|
||||
|
||||
/*Needed to overlay content on top of dotted bg*/
|
||||
section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
/*Needed overlay content on top of dotted bg*/
|
||||
.blurry-bg {
|
||||
/* background-color: var(--background-kener-rgba);
|
||||
box-shadow: 0 0 64px 64px var(--background-kener-rgba); */
|
||||
}
|
||||
|
||||
:root {
|
||||
--up-color: #4ead94;
|
||||
@@ -905,3 +807,72 @@ textarea::placeholder {
|
||||
content: ".";
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
--cp-bg-color: hsl(var(--background) / var(--tw-bg-opacity, 1));
|
||||
--cp-border-color: hsl(var(--border) / var(--tw-border-opacity));
|
||||
--cp-text-color: hsl(var(--card-foreground) / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.color-picker .container {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.color-picker .color {
|
||||
border: 1px solid hsl(var(--border) / var(--tw-border-opacity));
|
||||
}
|
||||
.color-picker button {
|
||||
display: none;
|
||||
}
|
||||
.color-picker [aria-label="color picker"] input {
|
||||
color: hsl(var(--foreground) / var(--tw-border-opacity)) !important;
|
||||
background-color: hsl(var(--input) / var(--tw-text-opacity, 1)) !important;
|
||||
}
|
||||
|
||||
.dark .color-picker {
|
||||
--cp-bg-color: hsl(var(--background) / var(--tw-bg-opacity, 1));
|
||||
--cp-border-color: hsl(var(--border) / var(--tw-border-opacity));
|
||||
--cp-text-color: hsl(var(--card-foreground) / var(--tw-text-opacity, 1));
|
||||
--cp-input-color: hsl(var(--input) / var(--tw-text-opacity, 1));
|
||||
--cp-button-hover-color: hsl(var(--input) / var(--tw-text-opacity, 1));
|
||||
}
|
||||
.like-input {
|
||||
background-color: rgba(0, 0, 0, 0.02) !important;
|
||||
}
|
||||
.dark .like-input {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
.ͼo .cm-gutters {
|
||||
border-top-left-radius: var(--radius);
|
||||
border-bottom-left-radius: var(--radius);
|
||||
}
|
||||
.no-gutter .ͼo .cm-gutters {
|
||||
display: none;
|
||||
}
|
||||
.kener-md {
|
||||
@apply text-sm;
|
||||
}
|
||||
p code:not([class^="language-"]) {
|
||||
@apply rounded-sm bg-yellow-100 px-1 py-0.5 font-mono text-xs dark:bg-neutral-700 dark:text-neutral-100;
|
||||
}
|
||||
|
||||
.kener-theme-mono .section-hero .h1,
|
||||
.kener-theme-mono-hero-h1 {
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
.kener-theme-default .section-hero .h1,
|
||||
.kener-theme-default-hero-h1 {
|
||||
@apply bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-transparent;
|
||||
}
|
||||
.kener-theme-sunset .section-hero .h1,
|
||||
.kener-theme-sunset-hero-h1 {
|
||||
@apply bg-gradient-to-r from-yellow-500 via-red-500 to-pink-500 bg-clip-text text-transparent;
|
||||
}
|
||||
.kener-theme-forest .section-hero .h1,
|
||||
.kener-theme-forest-hero-h1 {
|
||||
@apply bg-gradient-to-l from-blue-500 via-teal-500 to-green-500 bg-clip-text text-transparent;
|
||||
}
|
||||
.kener-theme-ocean .section-hero .h1,
|
||||
.kener-theme-ocean-hero-h1 {
|
||||
@apply bg-gradient-to-l from-cyan-500 via-blue-500 to-indigo-500 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
+14
-10
@@ -1,13 +1,17 @@
|
||||
// @ts-ignore
|
||||
export const analyticsEvent = (event, data) => {
|
||||
// Do something with the event and data
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("analyticsEvent", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
event,
|
||||
data
|
||||
}
|
||||
})
|
||||
);
|
||||
// Do something with the event and data
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
event = "kener_" + event;
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("analyticsEvent", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
event,
|
||||
data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import { formatDistanceToNow, formatDistance } from "date-fns";
|
||||
import { Settings, ArrowRight } from "lucide-svelte";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import * as Accordion from "$lib/components/ui/accordion";
|
||||
import { l, f, fd, fdn } from "$lib/i18n/client";
|
||||
import { base } from "$app/paths";
|
||||
@@ -8,6 +10,7 @@
|
||||
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;
|
||||
@@ -78,7 +81,10 @@
|
||||
<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"}
|
||||
@@ -184,7 +190,11 @@
|
||||
</time>
|
||||
|
||||
<div class="mb-4 text-sm font-normal">
|
||||
{@html comment.comment}
|
||||
<div
|
||||
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
|
||||
>
|
||||
{@html marked.parse(comment.comment)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -202,9 +212,13 @@
|
||||
)}
|
||||
</time>
|
||||
|
||||
<p class="mb-2 text-sm font-normal">
|
||||
{comment.comment}
|
||||
</p>
|
||||
<div class="mb-2 text-sm font-normal">
|
||||
<div
|
||||
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
|
||||
>
|
||||
{@html marked.parse(comment.comment)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
export let data;
|
||||
let pageNo = 1;
|
||||
let limit = 20;
|
||||
|
||||
@@ -1,271 +1,251 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Settings, Bell, Loader, Copy, Check } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import Bell from "lucide-svelte/icons/bell";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Copy from "lucide-svelte/icons/copy";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
let apiKeys = [];
|
||||
let loaderLoadingAll = false;
|
||||
let loaderCreateNew = false;
|
||||
let newAPIKeyName = "";
|
||||
let newKeyResp = {};
|
||||
let showCreateModal = false;
|
||||
let apiKeys = [];
|
||||
let loaderLoadingAll = false;
|
||||
let loaderCreateNew = false;
|
||||
let newAPIKeyName = "";
|
||||
let newKeyResp = {};
|
||||
let showCreateModal = false;
|
||||
|
||||
async function loadAPIKeys() {
|
||||
loaderLoadingAll = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "getAPIKeys",
|
||||
data: {}
|
||||
})
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
apiKeys = resp;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderLoadingAll = false;
|
||||
}
|
||||
}
|
||||
async function loadAPIKeys() {
|
||||
loaderLoadingAll = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "getAPIKeys",
|
||||
data: {}
|
||||
})
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
apiKeys = resp;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderLoadingAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createNew() {
|
||||
newKeyResp = {};
|
||||
loaderCreateNew = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "createNewApiKey",
|
||||
data: {
|
||||
name: newAPIKeyName
|
||||
}
|
||||
})
|
||||
});
|
||||
newKeyResp = await apiResp.json();
|
||||
loadAPIKeys();
|
||||
showCreateModal = false;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderCreateNew = false;
|
||||
}
|
||||
}
|
||||
async function createNew() {
|
||||
newKeyResp = {};
|
||||
loaderCreateNew = true;
|
||||
try {
|
||||
let apiResp = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "createNewApiKey",
|
||||
data: {
|
||||
name: newAPIKeyName
|
||||
}
|
||||
})
|
||||
});
|
||||
newKeyResp = await apiResp.json();
|
||||
loadAPIKeys();
|
||||
showCreateModal = false;
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
loaderCreateNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadAPIKeys();
|
||||
});
|
||||
onMount(() => {
|
||||
loadAPIKeys();
|
||||
});
|
||||
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(newKeyResp.apiKey);
|
||||
}
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(newKeyResp.apiKey);
|
||||
}
|
||||
|
||||
function updateStatus(apiKey) {
|
||||
apiKey.status = apiKey.status == "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "updateApiKeyStatus",
|
||||
data: {
|
||||
id: apiKey.id,
|
||||
status: apiKey.status
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
function updateStatus(apiKey) {
|
||||
apiKey.status = apiKey.status == "ACTIVE" ? "INACTIVE" : "ACTIVE";
|
||||
fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "updateApiKeyStatus",
|
||||
data: {
|
||||
id: apiKey.id,
|
||||
status: apiKey.status
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<div>
|
||||
{#if loaderLoadingAll}
|
||||
<Loader class="mt-6 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
on:click={(e) => {
|
||||
showCreateModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" /> Create New API Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 flex justify-between">
|
||||
<div>
|
||||
{#if loaderLoadingAll}
|
||||
<Loader class="mt-6 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button
|
||||
on:click={(e) => {
|
||||
showCreateModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" /> Create New API Key
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if !!newKeyResp && !!newKeyResp.apiKey}
|
||||
<div class="my-4 rounded-lg border border-green-700 bg-green-800 bg-opacity-20 p-4">
|
||||
<p class="font-medium">
|
||||
<picture class="mr-1 inline-block">
|
||||
<source
|
||||
srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.webp"
|
||||
type="image/webp"
|
||||
/>
|
||||
<img
|
||||
src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif"
|
||||
alt="🎉"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</picture>
|
||||
API Key Created
|
||||
</p>
|
||||
<p
|
||||
class="relative my-2 rounded-sm border bg-card px-4 py-2 pr-8 font-mono text-sm font-medium"
|
||||
>
|
||||
{newKeyResp.apiKey}
|
||||
<div class="my-4 rounded-lg border border-green-700 bg-green-800 bg-opacity-20 p-4">
|
||||
<p class="font-medium">
|
||||
<picture class="mr-1 inline-block">
|
||||
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.webp" type="image/webp" />
|
||||
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif" alt="🎉" width="24" height="24" />
|
||||
</picture>
|
||||
API Key Created
|
||||
</p>
|
||||
<p class="relative my-2 rounded-sm border bg-card px-4 py-2 pr-8 font-mono text-sm font-medium">
|
||||
{newKeyResp.apiKey}
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="copybtn absolute right-2 top-2 h-5 w-5 p-1"
|
||||
on:click={copyKey}
|
||||
>
|
||||
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
|
||||
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
|
||||
</Button>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Your new API key has been created. It will be <b class="uppercase underline"
|
||||
>not be shown again</b
|
||||
>, so make sure to save it.
|
||||
</p>
|
||||
</div>
|
||||
<Button size="icon" variant="ghost" class="copybtn absolute right-2 top-2 h-5 w-5 p-1" on:click={copyKey}>
|
||||
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
|
||||
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
|
||||
</Button>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Your new API key has been created. It will be <b class="uppercase underline">not be shown again</b>, so make sure
|
||||
to save it.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<div class="-m-1.5 overflow-x-auto">
|
||||
<div class="inline-block min-w-full p-1.5 align-middle">
|
||||
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Name</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Key</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Created At</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{#each apiKeys as apiKey}
|
||||
<tr>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{apiKey.name}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{apiKey.masked_key.slice(-32)}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
{moment
|
||||
.utc(apiKey.created_at, "YYYY-MM-DD HH:mm:ss")
|
||||
.local()
|
||||
.format("YYYY-MM-DD HH:mm:ss")}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
|
||||
>
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
value=""
|
||||
class="peer sr-only"
|
||||
checked={apiKey.status == "ACTIVE"}
|
||||
on:change={() => {
|
||||
updateStatus(apiKey);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
|
||||
></div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="-m-1.5 overflow-x-auto">
|
||||
<div class="inline-block min-w-full p-1.5 align-middle">
|
||||
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500">Name</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500">Key</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
>Created At</th
|
||||
>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
|
||||
></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
{#each apiKeys as apiKey}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200">
|
||||
{apiKey.name}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200">
|
||||
{apiKey.masked_key.slice(-32)}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200">
|
||||
{moment.utc(apiKey.created_at, "YYYY-MM-DD HH:mm:ss").local().format("YYYY-MM-DD HH:mm:ss")}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200">
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={$page.data.user.role == "member"}
|
||||
value=""
|
||||
class="peer sr-only"
|
||||
checked={apiKey.status == "ACTIVE"}
|
||||
on:change={() => {
|
||||
updateStatus(apiKey);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 peer-disabled:opacity-55 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
|
||||
></div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if showCreateModal}
|
||||
<div
|
||||
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
showCreateModal = false;
|
||||
}}
|
||||
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
|
||||
>
|
||||
<X class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Create a new API Key</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
API keys are used to authenticate your requests to the API. They are unique to
|
||||
your account and should be kept secret.
|
||||
</p>
|
||||
<hr class="my-4" />
|
||||
<form on:submit|preventDefault={createNew}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label for="newAPIKeyName">Name</Label>
|
||||
<Input
|
||||
bind:value={newAPIKeyName}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="newAPIKeyName"
|
||||
placeholder="eg. My API Key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Button variant="secondary" type="submit" disabled={loaderCreateNew}>
|
||||
Create
|
||||
{#if loaderCreateNew}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm">
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
showCreateModal = false;
|
||||
}}
|
||||
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
|
||||
>
|
||||
<X class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Create a new API Key</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
API keys are used to authenticate your requests to the API. They are unique to your account and should be kept
|
||||
secret.
|
||||
</p>
|
||||
<hr class="my-4" />
|
||||
<form on:submit|preventDefault={createNew}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label for="newAPIKeyName">Name</Label>
|
||||
<Input
|
||||
bind:value={newAPIKeyName}
|
||||
class="mt-2"
|
||||
type="text"
|
||||
id="newAPIKeyName"
|
||||
required
|
||||
placeholder="eg. My API Key"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Button variant="secondary" type="submit" disabled={loaderCreateNew}>
|
||||
Create
|
||||
{#if loaderCreateNew}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -8,10 +8,20 @@
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import locales from "$lib/locales/locales.json?raw";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
|
||||
import { Loader, X, Plus, Info, Play } from "lucide-svelte";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import Play from "lucide-svelte/icons/play";
|
||||
import { Tooltip } from "bits-ui";
|
||||
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
export let data;
|
||||
|
||||
let formStateHero = "idle";
|
||||
@@ -30,6 +40,15 @@
|
||||
let homeIncidentCount = 10;
|
||||
let homeIncidentStartTimeWithin = 30;
|
||||
let incidentGroupView = "EXPAND_FIRST";
|
||||
let kenerTheme = "default";
|
||||
|
||||
let availableThemes = [
|
||||
{ name: "Default", value: "default" },
|
||||
{ name: "Mono", value: "mono" },
|
||||
{ name: "Sunset", value: "sunset" },
|
||||
{ name: "Forest", value: "forest" },
|
||||
{ name: "Ocean", value: "ocean" }
|
||||
];
|
||||
|
||||
let nav = [];
|
||||
let categories = [];
|
||||
@@ -70,6 +89,9 @@
|
||||
if (data.siteData.footerHTML) {
|
||||
footerHTML = data.siteData.footerHTML;
|
||||
}
|
||||
if (data.siteData.kenerTheme) {
|
||||
kenerTheme = data.siteData.kenerTheme;
|
||||
}
|
||||
if (data.siteData.tzToggle) {
|
||||
tzToggle = data.siteData.tzToggle;
|
||||
}
|
||||
@@ -87,8 +109,9 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let heroError = "";
|
||||
async function formSubmitHero() {
|
||||
heroError = "";
|
||||
formStateHero = "loading";
|
||||
let resp = await storeSiteData({
|
||||
hero: JSON.stringify(hero)
|
||||
@@ -96,6 +119,19 @@
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formStateHero = "idle";
|
||||
if (data.error) {
|
||||
heroError = data.error;
|
||||
return;
|
||||
}
|
||||
saveTheme();
|
||||
}
|
||||
|
||||
async function saveTheme() {
|
||||
let resp = await storeSiteData({
|
||||
kenerTheme: kenerTheme
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
@@ -117,7 +153,10 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let categoriesErrorMessage = "";
|
||||
async function formSubmitCategories() {
|
||||
categoriesErrorMessage = "";
|
||||
formStateCategories = "loading";
|
||||
let resp = await storeSiteData({
|
||||
categories: JSON.stringify(categories)
|
||||
@@ -126,11 +165,14 @@
|
||||
let data = await resp.json();
|
||||
formStateCategories = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
categoriesErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let footerErrorMessage = "";
|
||||
async function formSubmitFooter() {
|
||||
footerErrorMessage = "";
|
||||
formStateFooter = "loading";
|
||||
let resp = await storeSiteData({
|
||||
footerHTML: footerHTML
|
||||
@@ -139,11 +181,14 @@
|
||||
let data = await resp.json();
|
||||
formStateFooter = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
footerErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let incidentErrorMessage = "";
|
||||
async function formSubmitIncident() {
|
||||
incidentErrorMessage = "";
|
||||
formStateIncident = "loading";
|
||||
let resp = await storeSiteData({
|
||||
homeIncidentCount: homeIncidentCount,
|
||||
@@ -154,7 +199,7 @@
|
||||
let data = await resp.json();
|
||||
formStateIncident = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
incidentErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -220,7 +265,9 @@
|
||||
i18n.defaultLocale = e.value;
|
||||
}
|
||||
|
||||
let i18nErrorMessage = "";
|
||||
async function formSubmiti18n() {
|
||||
i18nErrorMessage = "";
|
||||
formStatei18n = "loading";
|
||||
let resp = await storeSiteData({
|
||||
i18n: JSON.stringify(i18n),
|
||||
@@ -230,7 +277,7 @@
|
||||
let data = await resp.json();
|
||||
formStatei18n = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
i18nErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -242,44 +289,16 @@
|
||||
<Card.Description>Configure the hero section of your site.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitHero}>
|
||||
<form class="mx-auto mt-4 flex flex-col gap-y-4" on:submit|preventDefault={formSubmitHero}>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="hero_title">
|
||||
Title
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
You can set the title of the hero section here. This will be displayed on the top of the page.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Label for="hero_title">Title</Label>
|
||||
<Input bind:value={hero.title} class="mt-2" type="text" id="hero_title" placeholder="Status Page for Kener" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="hero_subtitle">
|
||||
Subtitle
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
You can set the subtitle of the hero section here. This will be displayed below the title.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Label>
|
||||
<Label for="hero_subtitle">Subtitle</Label>
|
||||
<Input
|
||||
bind:value={hero.subtitle}
|
||||
class="mt-2"
|
||||
@@ -289,9 +308,49 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div>
|
||||
<Label>Color</Label>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-2 rounded-md border p-2">
|
||||
<div class="w-3/4 pt-2">
|
||||
<span class="kener-theme-{kenerTheme}-hero-h1 font-medium">{hero.title}</span>
|
||||
</div>
|
||||
<div class="flex w-1/4 justify-end">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="outline">
|
||||
{availableThemes.find((el) => el.value === kenerTheme)?.name || "Mono"}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{#each availableThemes as theme}
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="h-6 w-full justify-start text-xs"
|
||||
on:click={() => {
|
||||
kenerTheme = theme.value;
|
||||
}}
|
||||
>
|
||||
{theme.name}
|
||||
</Button>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!heroError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{heroError}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStateHero === "loading"}>
|
||||
Save
|
||||
Save Hero Section
|
||||
{#if formStateHero === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -351,7 +410,7 @@
|
||||
</div>
|
||||
<div class="pt-8">
|
||||
<Button type="submit" disabled={formStateIncident === "loading"}>
|
||||
Save
|
||||
Save Incidents Settings
|
||||
{#if formStateIncident === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -362,6 +421,9 @@
|
||||
Translates to "Show {homeIncidentCount} incidents that have started in the last {homeIncidentStartTimeWithin}
|
||||
days in the past or going to start in {homeIncidentStartTimeWithin} days in the future"
|
||||
</p>
|
||||
{#if !!incidentErrorMessage}
|
||||
<p class="text-sm font-medium text-destructive">{incidentErrorMessage}</p>
|
||||
{/if}
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -470,10 +532,10 @@
|
||||
</div>
|
||||
<div class="flex w-full justify-end" style="margin-top:32px">
|
||||
<p class="px-4 pt-1">
|
||||
<span class="text-xs text-red-500"> {navErrorMessage} </span>
|
||||
<span class="text-sm font-medium text-destructive"> {navErrorMessage} </span>
|
||||
</p>
|
||||
<Button type="submit" disabled={formStateHero === "loading"}>
|
||||
Save
|
||||
Save Navigation
|
||||
{#if formStateHero === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -547,9 +609,12 @@
|
||||
Allow users to switch timezones
|
||||
</label>
|
||||
</p>
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!i18nErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{i18nErrorMessage}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStatei18n === "loading"}>
|
||||
Save
|
||||
Save Languages
|
||||
{#if formStatei18n === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -607,9 +672,12 @@
|
||||
<Plus class="h-4 w-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!categoriesErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{categoriesErrorMessage}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formStateCategories === "loading"}>
|
||||
Save
|
||||
Save Categories
|
||||
{#if formStateCategories === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
@@ -628,21 +696,42 @@
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitFooter}>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="w-full">
|
||||
<textarea
|
||||
bind:value={footerHTML}
|
||||
class="mt-2 h-48 w-full rounded-sm border p-2"
|
||||
placeholder="eg. <p>Powered by Kener</p>"
|
||||
></textarea>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={footerHTML}
|
||||
lang={html()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "320px",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-end">
|
||||
<Button type="submit" disabled={formStateFooter === "loading"}>
|
||||
Save
|
||||
{#if formStateFooter === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
<div class="flex w-full justify-between gap-x-2">
|
||||
<div>
|
||||
<Button variant="outline" class="mt-2" on:click={() => (footerHTML = data.defaultFooterHTML)}>
|
||||
<span class="ml-2">Reset to default</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end gap-x-2">
|
||||
{#if !!footerErrorMessage}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
|
||||
{/if}
|
||||
</Button>
|
||||
<Button type="submit" disabled={formStateFooter === "loading"}>
|
||||
Save Custom Footer
|
||||
{#if formStateFooter === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Loader, Clipboard, Check } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { clickOutsideAction, slide } from "svelte-legos";
|
||||
import { ChevronRight, Trash } from "lucide-svelte";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Clipboard from "lucide-svelte/icons/clipboard";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import Trash from "lucide-svelte/icons/trash";
|
||||
import { base } from "$app/paths";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
import {
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
@@ -100,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.";
|
||||
@@ -179,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";
|
||||
@@ -213,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";
|
||||
@@ -410,6 +431,35 @@
|
||||
pg: "PostgreSQL",
|
||||
mysql2: "MySQL"
|
||||
};
|
||||
let deleteMonitorConfirmText = "";
|
||||
let deletingMonitor = false;
|
||||
|
||||
async function deleteMonitor() {
|
||||
if (deleteMonitorConfirmText != `delete ${newMonitor.tag}`) {
|
||||
invalidFormMessage = "Please type the correct text to delete the monitor";
|
||||
return;
|
||||
}
|
||||
deletingMonitor = true;
|
||||
try {
|
||||
let data = await fetch(base + "/manage/app/api/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ action: "deleteMonitor", data: { tag: newMonitor.tag } })
|
||||
});
|
||||
let resp = await data.json();
|
||||
if (resp.error) {
|
||||
invalidFormMessage = resp.error;
|
||||
} else {
|
||||
dispatch("closeModal", {});
|
||||
}
|
||||
} catch (error) {
|
||||
invalidFormMessage = "Error while deleting monitor";
|
||||
} finally {
|
||||
deletingMonitor = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
|
||||
@@ -718,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">
|
||||
@@ -728,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"}
|
||||
@@ -823,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>
|
||||
@@ -900,7 +984,7 @@
|
||||
];
|
||||
}}
|
||||
>
|
||||
<Plus class="h-5 w-5" /> Add New
|
||||
<Plus class="mr-1 h-5 w-5" /> Add New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -914,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>
|
||||
@@ -1211,6 +1305,37 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !!newMonitor.id}
|
||||
<div class="mt-4 flex">
|
||||
<div class="flex w-full flex-col rounded-md border border-destructive p-2">
|
||||
<div class="w-full">
|
||||
<h2 class="flex justify-between text-lg">
|
||||
<span class="text-destructive">Danger Zone</span>
|
||||
</h2>
|
||||
<p class="text-sm">Deleting a monitor is irreversible. Please be sure before deleting.</p>
|
||||
</div>
|
||||
<form class="flex gap-x-2" on:submit|preventDefault={deleteMonitor}>
|
||||
<div class="mt-1 flex flex-col gap-y-2">
|
||||
<Label for="deleteMonitor">
|
||||
Type <i class="text-destructive">delete {newMonitor.tag}</i> to confirm
|
||||
</Label>
|
||||
<Input bind:value={deleteMonitorConfirmText} id="deleteMonitor" required />
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="submit"
|
||||
class="mt-7"
|
||||
disabled={deleteMonitorConfirmText != `delete ${newMonitor.tag}`}
|
||||
>
|
||||
Delete
|
||||
{#if deletingMonitor}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
|
||||
<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";
|
||||
@@ -13,6 +20,8 @@
|
||||
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: "",
|
||||
@@ -219,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),
|
||||
@@ -258,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;
|
||||
@@ -278,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;
|
||||
@@ -360,6 +396,9 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !!orderErrorMessage}
|
||||
<p class="py-2 text-sm font-medium text-destructive">{orderErrorMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,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">
|
||||
@@ -452,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>
|
||||
@@ -468,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
|
||||
@@ -482,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>
|
||||
@@ -677,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,18 +3,26 @@
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Plus, X, Info } from "lucide-svelte";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
|
||||
import { base } from "$app/paths";
|
||||
import { Loader } from "lucide-svelte";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import ColorPicker from "svelte-awesome-color-picker";
|
||||
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { css } from "@codemirror/lang-css";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
export let data;
|
||||
|
||||
let formState = "idle";
|
||||
let themeError = "";
|
||||
|
||||
let themeData = {
|
||||
pattern: "none",
|
||||
@@ -23,6 +31,7 @@
|
||||
barStyle: "PARTIAL",
|
||||
barRoundness: "ROUNDED",
|
||||
summaryStyle: "CURRENT",
|
||||
kenerTheme: "default",
|
||||
colorsJ: {
|
||||
UP: "#4ead94",
|
||||
DOWN: "#ca3038",
|
||||
@@ -49,6 +58,7 @@
|
||||
|
||||
async function formSubmit() {
|
||||
formState = "loading";
|
||||
themeError = "";
|
||||
let themeDataAPI = { ...themeData };
|
||||
themeDataAPI.colors = JSON.stringify(themeDataAPI.colorsJ);
|
||||
themeDataAPI.font = JSON.stringify(themeDataAPI.fontJ);
|
||||
@@ -59,7 +69,7 @@
|
||||
let data = await resp.json();
|
||||
formState = "idle";
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
themeError = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -91,16 +101,64 @@
|
||||
</p>
|
||||
|
||||
<div class="flex">
|
||||
<RadioGroup.Root bind:value={themeData.pattern} onValueChange={(e) => (themeData.pattern = e)}>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Root
|
||||
class="flex flex-1 flex-wrap justify-start gap-4"
|
||||
bind:value={themeData.pattern}
|
||||
onValueChange={(e) => (themeData.pattern = e)}
|
||||
>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="squares" id="rsq" />
|
||||
<Label for="rsq">Squares</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="dots" id="rdot" />
|
||||
<Label for="rdot">Dots</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="tiles" id="tiles" />
|
||||
<Label for="tiles">Tiles</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="radial-blue" id="radial-blue" />
|
||||
<Label for="radial-blue">Radial Blue</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="radial-mono" id="radial-mono" />
|
||||
<Label for="radial-mono">Radial Mono</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="radial-midnight" id="radial-midnight" />
|
||||
<Label for="radial-midnight">Radial Midnight</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="circle-mono" id="circle-mono" />
|
||||
<Label for="circle-mono">Circle Mono</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="carbon-fibre" id="carbon-fibre" />
|
||||
<Label for="carbon-fibre">Carbon Fibre</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="texture-sky" id="texture-sky" />
|
||||
<Label for="texture-sky">Texture Sky</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="angular-mono" id="angular-mono" />
|
||||
<Label for="angular-mono">Angular Mono</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="angular-spring" id="angular-spring" />
|
||||
<Label for="angular-spring">Angular Spring</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="angular-bloom" id="angular-bloom" />
|
||||
<Label for="angular-bloom">Angular Bloom</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="pets" id="pets" />
|
||||
<Label for="pets">Pets</Label>
|
||||
</div>
|
||||
<div class="flex w-1/6 items-center space-x-2">
|
||||
<RadioGroup.Item value="none" id="rnone" />
|
||||
<Label for="rnone">None</Label>
|
||||
</div>
|
||||
@@ -235,53 +293,51 @@
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<hr />
|
||||
<p class="font-medium">
|
||||
Colors
|
||||
<Tooltip.Root openDelay={100}>
|
||||
<Tooltip.Trigger class="">
|
||||
<Info class="inline-block h-4 w-4 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="max-w-md">
|
||||
<div
|
||||
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
|
||||
>
|
||||
Choose the colors for the monitor. You can choose between UP, DEGRADED, and DOWN.
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</p>
|
||||
<div class="flex w-full flex-row justify-evenly gap-2">
|
||||
<div class="relative w-full">
|
||||
<div>
|
||||
<p class="font-medium">Status Colors</p>
|
||||
<p class=" text-xs font-semibold text-muted-foreground">
|
||||
Choose the colors for the monitor. You can choose between UP, DEGRADED, and DOWN.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-row justify-start gap-x-8">
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.UP}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
label="UP"
|
||||
--cp-input-color="#777"
|
||||
--cp-button-hover-color="#767676"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.UP = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full">
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.DEGRADED}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
label="DEGRADED"
|
||||
--cp-input-color="#777"
|
||||
--cp-button-hover-color="#767676"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.DEGRADED = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full">
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.DOWN}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
label="DOWN"
|
||||
--cp-input-color="#777"
|
||||
--cp-button-hover-color="#767676"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.DOWN = event.detail.hex;
|
||||
}}
|
||||
@@ -334,20 +390,37 @@
|
||||
<div class="flex w-full flex-row justify-start gap-2">
|
||||
<div class="w-full">
|
||||
<Label for="customCSS" class="text-sm text-muted-foreground">
|
||||
You can add custom CSS to style your site. Do not include style tags.
|
||||
You can add custom CSS to style your site. Do not include style tags. Go to <a
|
||||
href="https://kener.ing/docs/custom-js-css-guide#grid-layout"
|
||||
class="text-card-foreground"
|
||||
target="_blank">this link</a
|
||||
> to learn more about it.
|
||||
</Label>
|
||||
<textarea
|
||||
bind:value={themeData.customCSS}
|
||||
id="customCSS"
|
||||
class="mt-1 h-48 w-full rounded-sm border p-2"
|
||||
placeholder=".className{color: red;}"
|
||||
></textarea>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={themeData.customCSS}
|
||||
lang={css()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "320px",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex w-full justify-end">
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
{#if !!themeError}
|
||||
<div class="py-2 text-sm font-medium text-destructive">{themeError}</div>
|
||||
{/if}
|
||||
<Button type="submit" disabled={formState === "loading"}>
|
||||
Save
|
||||
Save Theme Settings
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Loader, Settings, Check, ChevronRight } from "lucide-svelte";
|
||||
import { page } from "$app/stores";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { base } from "$app/paths";
|
||||
@@ -9,8 +15,14 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { IsValidURL } from "$lib/clientTools.js";
|
||||
import { DiscordJSONTemplate, WebhookJSONTemplate, SlackJSONTemplate } from "$lib/anywhere.js";
|
||||
import { clickOutsideAction, slide } from "svelte-legos";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import CodeMirror from "svelte-codemirror-editor";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let status = "ACTIVE";
|
||||
let formState = "idle";
|
||||
let showAddTrigger = false;
|
||||
@@ -29,8 +41,12 @@
|
||||
headers: [],
|
||||
to: "",
|
||||
from: data.fromEmail,
|
||||
webhook_body: "",
|
||||
webhook_body: WebhookJSONTemplate,
|
||||
has_webhook_body: false,
|
||||
has_discord_body: false,
|
||||
discord_body: DiscordJSONTemplate,
|
||||
has_slack_body: false,
|
||||
slack_body: SlackJSONTemplate,
|
||||
email_type: data.preferredModeEmail,
|
||||
smtp_host: data.smtp?.smtp_host ? data.smtp.smtp_host : "",
|
||||
smtp_port: data.smtp?.smtp_port ? data.smtp.smtp_port : "",
|
||||
@@ -52,8 +68,12 @@
|
||||
headers: [],
|
||||
to: "",
|
||||
from: data.fromEmail,
|
||||
webhook_body: "",
|
||||
webhook_body: WebhookJSONTemplate,
|
||||
has_webhook_body: false,
|
||||
has_discord_body: false,
|
||||
discord_body: DiscordJSONTemplate,
|
||||
has_slack_body: false,
|
||||
slack_body: SlackJSONTemplate,
|
||||
email_type: data.preferredModeEmail,
|
||||
smtp_host: data.smtp?.smtp_host ? data.smtp.smtp_host : "",
|
||||
smtp_port: data.smtp?.smtp_port ? data.smtp.smtp_port : "",
|
||||
@@ -91,13 +111,35 @@
|
||||
|
||||
async function addNewTrigger() {
|
||||
invalidFormMessage = "";
|
||||
formState = "loading";
|
||||
// formState = "loading";
|
||||
//newTrigger.trigger_type present not empty
|
||||
if (newTrigger.trigger_type == "") {
|
||||
invalidFormMessage = "Trigger Type is required";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
if (newTrigger.trigger_type == "discord") {
|
||||
if (newTrigger.trigger_meta.has_discord_body) {
|
||||
try {
|
||||
JSON.parse(newTrigger.trigger_meta.discord_body);
|
||||
} catch (e) {
|
||||
invalidFormMessage = "Invalid JSON in Discord Body";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newTrigger.trigger_type == "slack") {
|
||||
if (newTrigger.trigger_meta.has_slack_body) {
|
||||
try {
|
||||
JSON.parse(newTrigger.trigger_meta.slack_body);
|
||||
} catch (e) {
|
||||
invalidFormMessage = "Invalid JSON in Slack Body";
|
||||
formState = "idle";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newTrigger.trigger_type == "email") {
|
||||
newTrigger.trigger_meta.url = "";
|
||||
@@ -346,10 +388,12 @@
|
||||
<Loader class="ml-2 mt-2 inline h-6 w-6 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<Button on:click={addNewTriggerFn}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
New Trigger
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button on:click={addNewTriggerFn}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
New Trigger
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -382,12 +426,13 @@
|
||||
{#if trigger.testLoaders == "success"}
|
||||
<Check class="mr-1 inline h-3 w-3 text-green-500 " />
|
||||
{/if}
|
||||
<span class="h-4 text-xs font-medium"> TEST </span>
|
||||
</Button>
|
||||
|
||||
<Button variant="secondary" class=" h-8 w-8 p-2" href={"#" + trigger.id}>
|
||||
<Settings class="inline h-4 w-4" />
|
||||
<span class="h-4 text-xs font-medium"> Test Trigger </span>
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
<Button variant="secondary" class=" h-8 w-8 p-2" href={"#" + trigger.id}>
|
||||
<Settings class="inline h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
@@ -551,7 +596,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<label class="text-sm font-medium">
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_webhook_body = e.target.checked;
|
||||
@@ -567,21 +612,116 @@
|
||||
set. The default is JSON. There are <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#webhook-body">variables</a
|
||||
href="https://kener.ing/docs/triggers#triggers-trigger-variables">mustache variables</a
|
||||
>
|
||||
that you can use for the webhook body. To use a variable wrap it like
|
||||
<code>${variable}</code>
|
||||
<code>{{variable}}</code>. There are some examples
|
||||
<a target="_blank" class="text-blue-500" href="https://kener.ing/docs/triggers#webhook-examples"
|
||||
>here</a
|
||||
>
|
||||
</p>
|
||||
<div class="w-full">
|
||||
<textarea
|
||||
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newTrigger.trigger_meta.webhook_body}
|
||||
class="mt-2 h-[500px] w-full rounded-sm border p-2"
|
||||
placeholder={placeholderWebhookBody}
|
||||
></textarea>
|
||||
lang={json()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "320px",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "slack"}
|
||||
<div class="mt-4 flex w-full flex-col gap-y-2">
|
||||
<div>
|
||||
<label class="text-sm font-medium">
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_slack_body = e.target.checked;
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={newTrigger.trigger_meta.has_slack_body}
|
||||
/>
|
||||
Use Custom Slack Payload
|
||||
</label>
|
||||
</div>
|
||||
{#if !!newTrigger.trigger_meta.has_slack_body}
|
||||
<p class="my-2 text-xs text-muted-foreground">
|
||||
You can use a custom slack body. You can use mustache variable to generate a body. See the <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#triggers-trigger-variables">variables</a
|
||||
>
|
||||
that you can use for the slack body.
|
||||
</p>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newTrigger.trigger_meta.slack_body}
|
||||
lang={json()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "320px",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "discord"}
|
||||
<div class="mt-4 flex w-full flex-col gap-y-2">
|
||||
<div>
|
||||
<label class="text-sm font-medium">
|
||||
<input
|
||||
on:change={(e) => {
|
||||
newTrigger.trigger_meta.has_discord_body = e.target.checked;
|
||||
}}
|
||||
type="checkbox"
|
||||
checked={newTrigger.trigger_meta.has_discord_body}
|
||||
/>
|
||||
Use Custom Discord Payload
|
||||
</label>
|
||||
</div>
|
||||
{#if !!newTrigger.trigger_meta.has_discord_body}
|
||||
<p class="my-2 text-xs text-muted-foreground">
|
||||
You can use a custom discord body. You can use mustache variable to generate a body. See the <a
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
href="https://kener.ing/docs/triggers#triggers-trigger-variables">variables</a
|
||||
>
|
||||
that you can use for the discord body.
|
||||
</p>
|
||||
<div class="overflow-hidden rounded-md">
|
||||
<CodeMirror
|
||||
bind:value={newTrigger.trigger_meta.discord_body}
|
||||
lang={json()}
|
||||
theme={$mode == "dark" ? githubDark : githubLight}
|
||||
styles={{
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "320px",
|
||||
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
|
||||
borderRadius: "0.375rem"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if newTrigger.trigger_type == "email"}
|
||||
<div class="mt-4 w-full">
|
||||
<RadioGroup.Root class="my-4 flex" bind:value={newTrigger.trigger_meta.email_type}>
|
||||
@@ -675,11 +815,11 @@
|
||||
</div>
|
||||
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
|
||||
<div class="col-span-5 py-2.5">
|
||||
<p class="text-right text-xs font-medium text-red-500">{invalidFormMessage}</p>
|
||||
<p class="text-right text-sm font-medium text-destructive">{invalidFormMessage}</p>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Button class="col-span-1 w-full" disabled={formState === "loading"} on:click={addNewTrigger}>
|
||||
Save
|
||||
Save Trigger
|
||||
{#if formState === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
import { sub, startOfDay, getUnixTime } from "date-fns";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
import { Share2, ArrowRight, Settings, TrendingUp, Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import Share2 from "lucide-svelte/icons/share-2";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
import Settings from "lucide-svelte/icons/settings";
|
||||
import TrendingUp from "lucide-svelte/icons/trending-up";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { afterUpdate } from "svelte";
|
||||
@@ -124,6 +130,10 @@
|
||||
let rollerLoading = false;
|
||||
async function rollSummary(r) {
|
||||
let newRolledAt = r;
|
||||
analyticsEvent("monitor_interval_switch", {
|
||||
tag: monitor.tag,
|
||||
interval: uptimesRollers[newRolledAt].text
|
||||
});
|
||||
|
||||
if (uptimesRollers[newRolledAt].value === undefined) {
|
||||
rollerLoading = true;
|
||||
@@ -173,12 +183,18 @@
|
||||
if (embed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let incidentIDs = incidentObj?.ids || [];
|
||||
dayUptime = "NA";
|
||||
dateFetchedFor = f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang, $page.data.localTz);
|
||||
showDailyDataModal = true;
|
||||
|
||||
loadingDayData = true;
|
||||
dayIncidentsFull = [];
|
||||
analyticsEvent("monitor_day_data", {
|
||||
tag: monitor.tag,
|
||||
data_date: dateFetchedFor
|
||||
});
|
||||
setTimeout(() => {
|
||||
getToday(bar.timestamp, incidentIDs);
|
||||
}, 50);
|
||||
@@ -207,7 +223,7 @@
|
||||
{@html monitor.description}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="absolute right-14 top-5 flex gap-x-2">
|
||||
<div class="absolute right-4 top-5 flex gap-x-2 md:right-14">
|
||||
{#if $page.data.isLoggedIn}
|
||||
<Button
|
||||
href="{base}/manage/app/monitors#{monitor.tag}"
|
||||
@@ -229,16 +245,6 @@
|
||||
>
|
||||
<Share2 class="h-4 w-4 " />
|
||||
</Button>
|
||||
{#if monitor.monitor_type === "GROUP"}
|
||||
<Button
|
||||
class="bounce-right h-5 p-0 text-muted-foreground hover:text-primary"
|
||||
variant="link"
|
||||
href="{base}?group={monitor.tag}"
|
||||
rel="external"
|
||||
>
|
||||
<ArrowRight class="arrow h-4 w-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,7 +325,6 @@
|
||||
></div>
|
||||
{#if !!incidents[ts]}
|
||||
<div
|
||||
style="transition-delay: {Math.floor(Math.random() * (1500 - 500 + 1)) + 500}ms;"
|
||||
class="bg-api-{incidents[
|
||||
ts
|
||||
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] left-[1px] h-[4px] w-[4px] rounded-full"
|
||||
@@ -337,6 +342,21 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if monitor.monitor_type === "GROUP" && !!!embed}
|
||||
<div class="-mt-4 flex justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
href="{base}?group={monitor.tag}"
|
||||
rel="external"
|
||||
class="bounce-right h-8 text-xs"
|
||||
on:click={scrollToRight}
|
||||
>
|
||||
{l(lang, "View in detail")}
|
||||
<ArrowRight class="arrow ml-1.5 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showDailyDataModal}
|
||||
<div
|
||||
transition:slide={{ direction: "bottom" }}
|
||||
|
||||
@@ -1,17 +1,69 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { Languages, Menu } from "lucide-svelte";
|
||||
import Languages from "lucide-svelte/icons/languages";
|
||||
import Menu from "lucide-svelte/icons/menu";
|
||||
import { base } from "$app/paths";
|
||||
import { analyticsEvent } from "$lib/boringOne";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
export let data;
|
||||
let defaultPattern = data.site?.pattern || "squares";
|
||||
let allPets = [
|
||||
{
|
||||
url: base + "/chicken.gif",
|
||||
bottom: "-5"
|
||||
},
|
||||
{
|
||||
url: base + "/dog.gif",
|
||||
bottom: "-17"
|
||||
},
|
||||
{
|
||||
url: base + "/cockatiel.gif",
|
||||
bottom: "-10"
|
||||
},
|
||||
{
|
||||
url: base + "/crab.gif",
|
||||
bottom: "-20"
|
||||
},
|
||||
{
|
||||
url: base + "/fox.gif",
|
||||
bottom: "-9"
|
||||
},
|
||||
{
|
||||
url: base + "/horse.gif",
|
||||
bottom: "-11"
|
||||
},
|
||||
{
|
||||
url: base + "/panda.gif",
|
||||
bottom: "0"
|
||||
},
|
||||
{
|
||||
url: base + "/totoro.gif",
|
||||
bottom: "-27"
|
||||
},
|
||||
{
|
||||
url: base + "/rabbit.gif",
|
||||
bottom: "0"
|
||||
},
|
||||
{
|
||||
url: base + "/duck.gif",
|
||||
bottom: "-5"
|
||||
},
|
||||
{
|
||||
url: base + "/snake.gif",
|
||||
bottom: "0"
|
||||
}
|
||||
];
|
||||
let randomPet = allPets[Math.floor(Math.random() * allPets.length)];
|
||||
</script>
|
||||
|
||||
<div class="{defaultPattern}-pattern"></div>
|
||||
{#if defaultPattern == "pets" && !!randomPet}
|
||||
<div class="pets-pattern" style="background-image: url({randomPet.url});bottom: {randomPet.bottom}px"></div>
|
||||
{:else}
|
||||
<div class="{defaultPattern}-pattern"></div>
|
||||
{/if}
|
||||
|
||||
<header class="blurry-bg sticky top-0 z-50 mx-auto md:mt-2">
|
||||
<header class="sticky top-0 z-50 mx-auto md:mt-2">
|
||||
<div class="container flex h-14 max-w-[820px] items-center border bg-card px-3 md:rounded-md">
|
||||
<a rel="external" href={data.site.home ? data.site.home : base} class="mr-6 flex items-center space-x-2">
|
||||
{#if data.site.logo}
|
||||
@@ -31,7 +83,10 @@
|
||||
rel="external"
|
||||
href={navItem.url}
|
||||
class="flex rounded-md px-3 py-2 text-card-foreground transition-all ease-linear hover:bg-background"
|
||||
on:click={() => analyticsEvent("nav", navItem.name)}
|
||||
on:click={() =>
|
||||
analyticsEvent("navigation", {
|
||||
name: navItem.name
|
||||
})}
|
||||
>
|
||||
{#if navItem.iconURL}
|
||||
<GMI src={navItem.iconURL} classList="mr-1.5 mt-0.5 inline h-4" alt={navItem.name} />
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script>
|
||||
import { Share2, Link, CopyCheck, Code, TrendingUp, Percent, Loader, ExternalLink } from "lucide-svelte";
|
||||
import Share2 from "lucide-svelte/icons/share-2";
|
||||
import Link from "lucide-svelte/icons/link";
|
||||
import CopyCheck from "lucide-svelte/icons/copy-check";
|
||||
import Code from "lucide-svelte/icons/code";
|
||||
import TrendingUp from "lucide-svelte/icons/trending-up";
|
||||
import Percent from "lucide-svelte/icons/percent";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import ExternalLink from "lucide-svelte/icons/external-link";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { base } from "$app/paths";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "VERFÜGBAR",
|
||||
"Upcoming Maintenance": "Bevorstehende Wartung",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Verfügbarkeit"
|
||||
"Uptime": "Verfügbarkeit",
|
||||
"View in detail": "Details anzeigen"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "OPPE",
|
||||
"Upcoming Maintenance": "Kommende vedligeholdelse",
|
||||
"Updates": "Opdateringer",
|
||||
"Uptime": "Oppetid"
|
||||
"Uptime": "Oppetid",
|
||||
"View in detail": "Vis i detaljer"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "UP",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"Uptime": "Uptime",
|
||||
"View in detail": "View in detail"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "En ligne",
|
||||
"Upcoming Maintenance": "Maintenance à venir",
|
||||
"Updates": "Mises à jour",
|
||||
"Uptime": "Temps de fonctionnement"
|
||||
"Uptime": "Temps de fonctionnement",
|
||||
"View in detail": "Voir en détail"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "अप",
|
||||
"Upcoming Maintenance": "आगामी मेंटेनेंस",
|
||||
"Updates": "अपडेट्स",
|
||||
"Uptime": "अपटाइम"
|
||||
"Uptime": "अपटाइम",
|
||||
"View in detail": "विस्तार में देखें"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "稼働中",
|
||||
"Upcoming Maintenance": "今後のメンテナンス",
|
||||
"Updates": "更新",
|
||||
"Uptime": "稼働時間"
|
||||
"Uptime": "稼働時間",
|
||||
"View in detail": "詳細を表示"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "작동 중",
|
||||
"Upcoming Maintenance": "예정된 유지보수",
|
||||
"Updates": "업데이트",
|
||||
"Uptime": "업타임"
|
||||
"Uptime": "업타임",
|
||||
"View in detail": "자세히 보기"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "OPPE",
|
||||
"Upcoming Maintenance": "Kommende vedlikehold",
|
||||
"Updates": "Oppdateringer",
|
||||
"Uptime": "Oppetid"
|
||||
"Uptime": "Oppetid",
|
||||
"View in detail": "Vis detaljer"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "BEREIKBAAR",
|
||||
"Upcoming Maintenance": "Aankomend onderhoud",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"Uptime": "Uptime",
|
||||
"View in detail": "Bekijk in detail"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "OPERACIONAL",
|
||||
"Upcoming Maintenance": "Manutenção Agendada",
|
||||
"Updates": "Atualizações",
|
||||
"Uptime": "Tempo de Atividade"
|
||||
"Uptime": "Tempo de Atividade",
|
||||
"View in detail": "Ver em detalhes"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "РАБОТАЕТ",
|
||||
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
|
||||
"Updates": "Обновления",
|
||||
"Uptime": "Время работы"
|
||||
"Uptime": "Время работы",
|
||||
"View in detail": "Подробный просмотр"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "ÇALIŞIYOR",
|
||||
"Upcoming Maintenance": "Yaklaşan Bakım",
|
||||
"Updates": "Güncellemeler",
|
||||
"Uptime": "Çalışma Süresi"
|
||||
"Uptime": "Çalışma Süresi",
|
||||
"View in detail": "Detaylı Görüntüle"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"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"
|
||||
"Uptime": "Thời gian hoạt động",
|
||||
"View in detail": "Xem chi tiết"
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@
|
||||
"UP": "可用",
|
||||
"Upcoming Maintenance": "即将进行的维护",
|
||||
"Updates": "更新",
|
||||
"Uptime": "正常运行时间"
|
||||
"Uptime": "正常运行时间",
|
||||
"View in detail": "详细查看"
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ const MANUAL = "manual";
|
||||
const WEBHOOK = "webhook";
|
||||
const DEFAULT_STATUS = "default_status";
|
||||
const SIGNAL = "signal";
|
||||
const INVITE_VERIFY_EMAIL = "invite_verify_email";
|
||||
|
||||
export {
|
||||
MONITOR,
|
||||
@@ -123,4 +124,5 @@ export {
|
||||
WEBHOOK,
|
||||
DEFAULT_STATUS,
|
||||
SIGNAL,
|
||||
INVITE_VERIFY_EMAIL,
|
||||
};
|
||||
|
||||
@@ -15,10 +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, REALTIME, SIGNAL } from "../constants.js";
|
||||
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC } from "../tool.js";
|
||||
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC, ReplaceAllOccurrences, ValidateEmail } from "../tool.js";
|
||||
import getSMTPTransport from "../notification/smtps.js";
|
||||
|
||||
const saltRounds = 10;
|
||||
const DUMMY_SECRET = "DUMMY_SECRET";
|
||||
@@ -74,6 +77,11 @@ const siteDataKeys = [
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "kenerTheme",
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "customCSS",
|
||||
isValid: (value) => typeof value === "string",
|
||||
@@ -86,8 +94,25 @@ const siteDataKeys = [
|
||||
},
|
||||
{
|
||||
key: "pattern",
|
||||
//string dots or sqaures or circ
|
||||
isValid: (value) => typeof value === "string" && ["dots", "squares", "none"].includes(value),
|
||||
//string dots or squares or circle
|
||||
isValid: (value) =>
|
||||
typeof value === "string" &&
|
||||
[
|
||||
"dots",
|
||||
"squares",
|
||||
"tiles",
|
||||
"none",
|
||||
"radial-blue",
|
||||
"radial-mono",
|
||||
"radial-midnight",
|
||||
"circle-mono",
|
||||
"carbon-fibre",
|
||||
"texture-sky",
|
||||
"angular-mono",
|
||||
"angular-spring",
|
||||
"angular-bloom",
|
||||
"pets",
|
||||
].includes(value),
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
@@ -166,6 +191,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) {
|
||||
@@ -200,6 +250,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) {
|
||||
@@ -239,6 +303,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 {
|
||||
@@ -302,6 +376,23 @@ 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);
|
||||
};
|
||||
@@ -362,7 +453,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
|
||||
}
|
||||
};
|
||||
@@ -397,6 +487,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,
|
||||
@@ -438,12 +534,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";
|
||||
@@ -479,6 +569,19 @@ 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' }]
|
||||
@@ -899,6 +1002,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");
|
||||
|
||||
@@ -66,90 +66,100 @@ async function manualIncident(monitor) {
|
||||
}
|
||||
|
||||
const Minuter = async (monitor) => {
|
||||
let realTimeData = {};
|
||||
let manualData = {};
|
||||
try {
|
||||
let realTimeData = {};
|
||||
let manualData = {};
|
||||
|
||||
const startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
const serviceClient = new Service(monitor);
|
||||
if (monitor.monitor_type === "API") {
|
||||
let apiResponse = await serviceClient.execute();
|
||||
const startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
const serviceClient = new Service(monitor);
|
||||
if (monitor.monitor_type === "API") {
|
||||
let apiResponse = await serviceClient.execute();
|
||||
|
||||
realTimeData[startOfMinute] = apiResponse;
|
||||
realTimeData[startOfMinute] = apiResponse;
|
||||
|
||||
//if timeout, retry after 500ms
|
||||
if (apiResponse.type === TIMEOUT) {
|
||||
apiQueue.push(async (cb) => {
|
||||
await Wait(500); //wait for 500ms
|
||||
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
|
||||
serviceClient.execute().then(async (data) => {
|
||||
await db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: startOfMinute,
|
||||
status: data.status,
|
||||
latency: data.latency,
|
||||
type: data.type,
|
||||
//if timeout, retry after 500ms
|
||||
if (apiResponse.type === TIMEOUT) {
|
||||
apiQueue.push(async (cb) => {
|
||||
await Wait(500); //wait for 500ms
|
||||
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
|
||||
serviceClient.execute().then(async (data) => {
|
||||
await 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);
|
||||
} else if (monitor.monitor_type === "SSL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "SQL") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "HEARTBEAT") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} catch (e) {
|
||||
console.log(`[Error] in Running Monitor name: ${monitor.name}, tag: ${monitor.tag} error:`, e);
|
||||
}
|
||||
|
||||
manualData = await manualIncident(monitor);
|
||||
|
||||
//merge noData, apiData, webhookData, dayData
|
||||
let mergedData = {};
|
||||
|
||||
if (monitor.default_status !== undefined && monitor.default_status !== null) {
|
||||
if ([UP, DOWN, DEGRADED].indexOf(monitor.default_status) !== -1) {
|
||||
mergedData[startOfMinute] = {
|
||||
status: monitor.default_status,
|
||||
latency: 0,
|
||||
type: DEFAULT_STATUS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in realTimeData) {
|
||||
if (!!realTimeData[timestamp] && !!realTimeData[timestamp].status) {
|
||||
mergedData[timestamp] = realTimeData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in manualData) {
|
||||
if (!!manualData[timestamp] && !!manualData[timestamp].status) {
|
||||
mergedData[timestamp] = manualData[timestamp];
|
||||
}
|
||||
}
|
||||
|
||||
for (const timestamp in mergedData) {
|
||||
const element = mergedData[timestamp];
|
||||
db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: parseInt(timestamp),
|
||||
status: element.status,
|
||||
latency: element.latency,
|
||||
type: element.type,
|
||||
});
|
||||
}
|
||||
alertingQueue.push(async (cb) => {
|
||||
setTimeout(async () => {
|
||||
await alerting(monitor);
|
||||
cb();
|
||||
try {
|
||||
await alerting(monitor);
|
||||
cb();
|
||||
} catch (e) {
|
||||
console.log(`[Error] in Running Alerting name: ${monitor.name}, tag: ${monitor.tag} error:`, e);
|
||||
cb();
|
||||
}
|
||||
}, 1042);
|
||||
});
|
||||
};
|
||||
|
||||
+202
-4
@@ -30,6 +30,14 @@ class DbImpl {
|
||||
.where("timestamp", "<=", end)
|
||||
.orderBy("timestamp", "asc");
|
||||
}
|
||||
//given monitor_tags array of string, start and end timestamp in utc seconds return data
|
||||
async getMonitoringDataAll(monitor_tags, start, end) {
|
||||
return await this.knex("monitoring_data")
|
||||
.whereIn("monitor_tag", monitor_tags)
|
||||
.where("timestamp", ">=", start)
|
||||
.where("timestamp", "<=", end)
|
||||
.orderBy("timestamp", "asc");
|
||||
}
|
||||
|
||||
//get latest data for a monitor_tag
|
||||
async getLatestMonitoringData(monitor_tag) {
|
||||
@@ -40,6 +48,38 @@ 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)
|
||||
@@ -75,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
|
||||
@@ -89,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"),
|
||||
@@ -104,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")
|
||||
@@ -268,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({
|
||||
@@ -403,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({
|
||||
@@ -423,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({
|
||||
@@ -742,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;
|
||||
|
||||
@@ -43,8 +43,20 @@ const seedSiteData = {
|
||||
title: "Build stunning status pages in minutes",
|
||||
subtitle: "Let your users know what's going on.",
|
||||
},
|
||||
footerHTML:
|
||||
'<p class="text-center">\nMade using \n<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">\n Kener\n</a>\nan open source status page system built with Svelte and TailwindCSS.<br/>\nCreated with ❤️ by <a href="https://rajnandan.com" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.\n</p>\n',
|
||||
footerHTML: `<div
|
||||
class="container relative mt-4 max-w-[655px]"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
|
||||
<p class="text-center text-sm leading-loose text-muted-foreground ">
|
||||
Made using
|
||||
<a href="https://github.com/rajnandan1/kener" target="_blank" class="font-medium underline underline-offset-4">
|
||||
Kener
|
||||
</a>
|
||||
an open source status page system built with Svelte and TailwindCSS.<br/>
|
||||
Created with ❤️ by <a href="https://rajnandan.com" target="_blank" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>`,
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: [{ code: "en", name: "English", selected: true, disabled: false }],
|
||||
@@ -88,6 +100,7 @@ const seedSiteData = {
|
||||
categories: [{ name: "Home", description: "Monitors for Home Page" }],
|
||||
homeIncidentCount: 5,
|
||||
homeIncidentStartTimeWithin: 30,
|
||||
kenerTheme: "default",
|
||||
};
|
||||
|
||||
export default seedSiteData;
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import Mustache from "mustache";
|
||||
import { DiscordJSONTemplate } from "../../anywhere.js";
|
||||
import variables from "./variables.js";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Discord {
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
trigger_meta;
|
||||
|
||||
constructor(url, siteData, monitorData) {
|
||||
constructor(url, siteData, monitorData, trigger_meta) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
|
||||
this.url = url;
|
||||
@@ -17,77 +24,43 @@ class Discord {
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
this.trigger_meta = trigger_meta;
|
||||
|
||||
if (!!this.trigger_meta.has_discord_body && !!this.trigger_meta.discord_body) {
|
||||
let envSecrets = GetRequiredSecrets(this.trigger_meta.discord_body);
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.trigger_meta.discord_body = ReplaceAllOccurrences(
|
||||
this.trigger_meta.discord_body,
|
||||
secret.find,
|
||||
secret.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
let siteURL = this.siteData.siteURL;
|
||||
let view = variables(this.siteData, data);
|
||||
|
||||
//remove / if there at the end of the url
|
||||
if (siteURL.endsWith("/")) {
|
||||
siteURL = siteURL.slice(0, -1);
|
||||
let discordTemplate = DiscordJSONTemplate;
|
||||
if (!!this.trigger_meta.has_discord_body && !!this.trigger_meta.discord_body) {
|
||||
discordTemplate = this.trigger_meta.discord_body;
|
||||
}
|
||||
|
||||
if (!this.siteData.logo.startsWith("http"))
|
||||
this.siteData.logo = `${siteURL}${!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : ""}${this.siteData.logo}`;
|
||||
|
||||
let logo = this.siteData.logo;
|
||||
|
||||
let color = 13250616; //down;
|
||||
if (data.severity === "warning") {
|
||||
color = 15125089;
|
||||
}
|
||||
if (data.status === "RESOLVED") {
|
||||
color = 5156244;
|
||||
}
|
||||
return {
|
||||
username: this.siteData.siteName,
|
||||
avatar_url: logo,
|
||||
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
|
||||
embeds: [
|
||||
{
|
||||
title: data.alert_name,
|
||||
description: data.description,
|
||||
url: data.actions[0].url,
|
||||
color: color,
|
||||
fields: [
|
||||
{
|
||||
name: "Monitor",
|
||||
value: data.details.metric,
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Alert ID",
|
||||
value: data.id,
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "Current Value",
|
||||
value: data.details.current_value,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Threshold",
|
||||
value: data.details.threshold,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
text: "Kener",
|
||||
icon_url: logo,
|
||||
},
|
||||
timestamp: data.timestamp,
|
||||
},
|
||||
],
|
||||
};
|
||||
return Mustache.render(discordTemplate, view);
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const toSend = this.transformData(data);
|
||||
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(this.transformData(data)),
|
||||
body: toSend,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from discord`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
|
||||
@@ -3,56 +3,57 @@ import { Resend } from "resend";
|
||||
import nodemailer from "nodemailer";
|
||||
import getSMTPTransport from "./smtps.js";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Email {
|
||||
to;
|
||||
from;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
meta;
|
||||
to;
|
||||
from;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
meta;
|
||||
|
||||
constructor(meta, siteData, monitorData) {
|
||||
this.to = meta.to;
|
||||
this.from = meta.from;
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
constructor(meta, siteData, monitorData) {
|
||||
this.to = meta.to;
|
||||
this.from = meta.from;
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
|
||||
let metaString = JSON.stringify(meta);
|
||||
let metaString = JSON.stringify(meta);
|
||||
|
||||
let envSecrets = GetRequiredSecrets(`${JSON.stringify(meta)}`);
|
||||
let envSecrets = GetRequiredSecrets(`${JSON.stringify(meta)}`);
|
||||
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
metaString = ReplaceAllOccurrences(metaString, secret.find, secret.replace);
|
||||
}
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
metaString = ReplaceAllOccurrences(metaString, secret.find, secret.replace);
|
||||
}
|
||||
|
||||
this.meta = JSON.parse(metaString);
|
||||
}
|
||||
this.meta = JSON.parse(metaString);
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
let ctaLink = data.actions[0].url;
|
||||
let ctaText = data.actions[0].text;
|
||||
let emailApiRequest = {
|
||||
from: this.from,
|
||||
to: this.to.split(",").map((email) => email.trim()),
|
||||
subject: `[${data.status}] ${data.alert_name} at ${data.timestamp}`,
|
||||
text: `Alert ${data.alert_name} has been ${data.status} at ${data.timestamp}. Click here to view the alert: ${ctaLink}`,
|
||||
html: ""
|
||||
};
|
||||
transformData(data) {
|
||||
let ctaLink = data.actions[0].url;
|
||||
let ctaText = data.actions[0].text;
|
||||
let emailApiRequest = {
|
||||
from: this.from,
|
||||
to: this.to.split(",").map((email) => email.trim()),
|
||||
subject: `[${data.status}] ${data.alert_name} at ${data.timestamp}`,
|
||||
text: `Alert ${data.alert_name} has been ${data.status} at ${data.timestamp}. Click here to view the alert: ${ctaLink}`,
|
||||
html: "",
|
||||
};
|
||||
|
||||
let bgColor = "#f4f4f4";
|
||||
if (data.severity == "critical") {
|
||||
bgColor = this.siteData.colors.DOWN;
|
||||
} else if (data.severity == "warning") {
|
||||
bgColor = this.siteData.colors.DEGRADED;
|
||||
}
|
||||
let bgColor = "#f4f4f4";
|
||||
if (data.severity == "critical") {
|
||||
bgColor = this.siteData.colors.DOWN;
|
||||
} else if (data.severity == "warning") {
|
||||
bgColor = this.siteData.colors.DEGRADED;
|
||||
}
|
||||
|
||||
if (data.status === "RESOLVED") {
|
||||
bgColor = this.siteData.colors.UP;
|
||||
}
|
||||
if (data.status === "RESOLVED") {
|
||||
bgColor = this.siteData.colors.UP;
|
||||
}
|
||||
|
||||
let html = `
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -177,53 +178,53 @@ class Email {
|
||||
</table>
|
||||
|
||||
<div class="footer">
|
||||
This is an automated alert notification from ${this.siteData.siteName} monitoring system.
|
||||
This is an automated alert notification from ${this.siteData.siteName} monitoring system. It is being powered by <a href='https://kener.ing'>Kener</a> v${version()}.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
emailApiRequest.html = html;
|
||||
emailApiRequest.html = html;
|
||||
|
||||
return emailApiRequest;
|
||||
}
|
||||
return emailApiRequest;
|
||||
}
|
||||
|
||||
type() {
|
||||
return "email";
|
||||
}
|
||||
type() {
|
||||
return "email";
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
let emailBody = this.transformData(data); // object containing email data (to, subject, text, html, etc)
|
||||
if (!this.meta.email_type || this.meta.email_type === "resend") {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
try {
|
||||
return await resend.emails.send(emailBody);
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
if (this.meta.email_type === "smtp") {
|
||||
// Configure the SMTP transporter using environment variables
|
||||
const transporter = getSMTPTransport(this.meta);
|
||||
async send(data) {
|
||||
let emailBody = this.transformData(data); // object containing email data (to, subject, text, html, etc)
|
||||
if (!this.meta.email_type || this.meta.email_type === "resend") {
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
try {
|
||||
return await resend.emails.send(emailBody);
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
if (this.meta.email_type === "smtp") {
|
||||
// Configure the SMTP transporter using environment variables
|
||||
const transporter = getSMTPTransport(this.meta);
|
||||
|
||||
const mailOptions = {
|
||||
from: emailBody.from, // sender address
|
||||
to: Array.isArray(emailBody.to) ? emailBody.to.join(",") : emailBody.to, // recipient address(es)
|
||||
subject: emailBody.subject, // email subject
|
||||
text: emailBody.text, // plain text body
|
||||
html: emailBody.html // HTML body (if any)
|
||||
};
|
||||
const mailOptions = {
|
||||
from: emailBody.from, // sender address
|
||||
to: Array.isArray(emailBody.to) ? emailBody.to.join(",") : emailBody.to, // recipient address(es)
|
||||
subject: emailBody.subject, // email subject
|
||||
text: emailBody.text, // plain text body
|
||||
html: emailBody.html, // HTML body (if any)
|
||||
};
|
||||
|
||||
try {
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Email;
|
||||
|
||||
@@ -5,26 +5,26 @@ import Slack from "./slack.js";
|
||||
import Email from "./email.js";
|
||||
|
||||
class Notification {
|
||||
client;
|
||||
client;
|
||||
|
||||
constructor(trigger, siteData, monitorData) {
|
||||
let trigger_meta = JSON.parse(trigger.trigger_meta);
|
||||
if (trigger.trigger_type === "webhook") {
|
||||
this.client = new Webhook(trigger_meta, "POST", siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "discord") {
|
||||
this.client = new Discord(trigger_meta.url, siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "slack") {
|
||||
this.client = new Slack(trigger_meta.url, siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "email") {
|
||||
this.client = new Email(trigger_meta, siteData, monitorData);
|
||||
} else {
|
||||
console.log("Invalid Notification");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
constructor(trigger, siteData, monitorData) {
|
||||
let trigger_meta = JSON.parse(trigger.trigger_meta);
|
||||
if (trigger.trigger_type === "webhook") {
|
||||
this.client = new Webhook(trigger_meta, "POST", siteData, monitorData);
|
||||
} else if (trigger.trigger_type === "discord") {
|
||||
this.client = new Discord(trigger_meta.url, siteData, monitorData, trigger_meta);
|
||||
} else if (trigger.trigger_type === "slack") {
|
||||
this.client = new Slack(trigger_meta.url, siteData, monitorData, trigger_meta);
|
||||
} else if (trigger.trigger_type === "email") {
|
||||
this.client = new Email(trigger_meta, siteData, monitorData);
|
||||
} else {
|
||||
console.log("Invalid Notification");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
return await this.client.send(data);
|
||||
}
|
||||
async send(data) {
|
||||
return await this.client.send(data);
|
||||
}
|
||||
}
|
||||
export default Notification;
|
||||
|
||||
@@ -1,100 +1,66 @@
|
||||
// @ts-nocheck
|
||||
import variables from "./variables.js";
|
||||
import { SlackJSONTemplate } from "../../anywhere.js";
|
||||
import Mustache from "mustache";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Slack {
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
url;
|
||||
headers;
|
||||
method;
|
||||
siteData;
|
||||
monitorData;
|
||||
trigger_meta;
|
||||
|
||||
constructor(url, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener"
|
||||
};
|
||||
constructor(url, siteData, monitorData, trigger_meta) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
|
||||
this.url = url;
|
||||
this.headers = Object.assign(kenerHeader, {});
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
}
|
||||
this.url = url;
|
||||
this.headers = Object.assign(kenerHeader, {});
|
||||
this.method = "POST";
|
||||
this.siteData = siteData;
|
||||
this.monitorData = monitorData;
|
||||
this.trigger_meta = trigger_meta;
|
||||
|
||||
transformData(alert) {
|
||||
return {
|
||||
blocks: [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: alert.alert_name,
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: alert.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved",
|
||||
emoji: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `${alert.description}\n*Source:* ${alert.source}\n*Severity:* ${alert.severity}\n*Status:* ${alert.status}`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
fields: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Metric:*\n${alert.details.metric}`
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Current Value:*\n${alert.details.current_value}`
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Threshold:*\n${alert.details.threshold}`
|
||||
},
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `*Timestamp:*\n<!date^${Math.floor(new Date(alert.timestamp).getTime() / 1000)}^{date} at {time}|${alert.timestamp}>`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: alert.actions.map((action) => ({
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: action.text
|
||||
},
|
||||
url: action.url,
|
||||
style: "primary"
|
||||
}))
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
if (!!this.trigger_meta.has_slack_body && !!this.trigger_meta.slack_body) {
|
||||
let envSecrets = GetRequiredSecrets(this.trigger_meta.slack_body);
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
this.trigger_meta.slack_body = ReplaceAllOccurrences(this.trigger_meta.slack_body, secret.find, secret.replace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(this.transformData(data))
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
transformData(alert) {
|
||||
let view = variables(this.siteData, alert);
|
||||
|
||||
let slackTemplate = SlackJSONTemplate;
|
||||
if (!!this.trigger_meta.has_slack_body && !!this.trigger_meta.slack_body) {
|
||||
slackTemplate = this.trigger_meta.slack_body;
|
||||
}
|
||||
return Mustache.render(slackTemplate, view);
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
try {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
headers: this.headers,
|
||||
body: this.transformData(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from slack`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Slack;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import nodemailer from "nodemailer";
|
||||
import { HashString } from "../controllers/controller.js";
|
||||
import { HashString } from "../tool.js";
|
||||
const transports = {};
|
||||
export default function getSMTPTransport(meta) {
|
||||
//convert meta to string and generate has id
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// @ts-nocheck
|
||||
export default function variables(siteData, data) {
|
||||
let siteURL = siteData.siteURL;
|
||||
let siteName = siteData.siteName;
|
||||
if (siteURL.endsWith("/")) {
|
||||
siteURL = siteURL.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!siteData.logo.startsWith("http")) {
|
||||
siteData.logo = `${siteURL}${!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : ""}${siteData.logo}`;
|
||||
}
|
||||
let view = {
|
||||
site_name: siteName,
|
||||
logo_url: siteData.logo,
|
||||
site_url: siteURL,
|
||||
alert_name: data.alert_name,
|
||||
status: data.status,
|
||||
is_resolved: data.status === "RESOLVED",
|
||||
is_triggered: data.status === "TRIGGERED",
|
||||
description: data.description,
|
||||
action_text: data.actions[0].text,
|
||||
action_url: data.actions[0].url,
|
||||
metric: data.details.metric,
|
||||
severity: data.severity,
|
||||
id: data.id,
|
||||
current_value: data.details.current_value,
|
||||
threshold: data.details.threshold,
|
||||
source: data.source,
|
||||
timestamp: data.timestamp,
|
||||
};
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import Mustache from "mustache";
|
||||
import { WebhookJSONTemplate } from "../../anywhere.js";
|
||||
import variables from "./variables.js";
|
||||
import version from "../../version.js";
|
||||
|
||||
class Webhook {
|
||||
url;
|
||||
@@ -13,7 +17,7 @@ class Webhook {
|
||||
constructor(trigger_meta, method, siteData, monitorData) {
|
||||
const kenerHeader = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Kener/3.2.2",
|
||||
"User-Agent": `Kener/${version()}`,
|
||||
};
|
||||
let headers = trigger_meta.headers;
|
||||
this.trigger_meta = trigger_meta;
|
||||
@@ -52,8 +56,13 @@ class Webhook {
|
||||
}
|
||||
|
||||
transformData(data) {
|
||||
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
|
||||
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
|
||||
let view = variables(this.siteData, data);
|
||||
|
||||
if (!!!this.trigger_meta.has_webhook_body || !!!this.trigger_meta.webhook_body) {
|
||||
return Mustache.render(WebhookJSONTemplate, view);
|
||||
}
|
||||
|
||||
//have to keep this for backward compatibility
|
||||
let body = this.trigger_meta.webhook_body;
|
||||
body = ReplaceAllOccurrences(body, "${id}", data.id);
|
||||
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
|
||||
@@ -67,7 +76,9 @@ class Webhook {
|
||||
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
|
||||
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
|
||||
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
|
||||
return body;
|
||||
//have to keep this for backward compatibility
|
||||
|
||||
return Mustache.render(body, view);
|
||||
}
|
||||
|
||||
type() {
|
||||
@@ -81,6 +92,9 @@ class Webhook {
|
||||
headers: this.headers,
|
||||
body: this.transformData(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from webhook`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
|
||||
+64
-63
@@ -3,85 +3,86 @@ import net from "net"; // Use import instead of require
|
||||
import ping from "ping";
|
||||
|
||||
const TCP = function (type, host, port, timeout) {
|
||||
port = parseInt(port, 10);
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
const start = process.hrtime.bigint(); // High-precision timestamp
|
||||
let resolved = false;
|
||||
timeout = Number(timeout);
|
||||
port = parseInt(port, 10);
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
const start = process.hrtime.bigint(); // High-precision timestamp
|
||||
let resolved = false;
|
||||
|
||||
const onFinish = (status) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
const end = process.hrtime.bigint();
|
||||
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
|
||||
socket.destroy();
|
||||
resolve({ status, latency, host, port, type });
|
||||
}
|
||||
};
|
||||
const onFinish = (status) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
const end = process.hrtime.bigint();
|
||||
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
|
||||
socket.destroy();
|
||||
resolve({ status, latency, host, port, type });
|
||||
}
|
||||
};
|
||||
|
||||
socket.setTimeout(timeout);
|
||||
socket.once("connect", () => onFinish("open"));
|
||||
socket.once("timeout", () => onFinish("timeout"));
|
||||
socket.once("error", () => onFinish("error"));
|
||||
socket.setTimeout(timeout);
|
||||
socket.once("connect", () => onFinish("open"));
|
||||
socket.once("timeout", () => onFinish("timeout"));
|
||||
socket.once("error", () => onFinish("error"));
|
||||
|
||||
// Check if it's an IPv6 address (contains ':')
|
||||
const options = {
|
||||
host,
|
||||
port,
|
||||
family: type === "IP6" ? 6 : 4
|
||||
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
|
||||
socket.connect(options);
|
||||
});
|
||||
// Check if it's an IPv6 address (contains ':')
|
||||
const options = {
|
||||
host,
|
||||
port,
|
||||
family: type === "IP6" ? 6 : 4,
|
||||
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
|
||||
socket.connect(options);
|
||||
});
|
||||
};
|
||||
|
||||
const Ping = async function (type, host, timeout, count) {
|
||||
let output = {
|
||||
alive: false,
|
||||
min: null,
|
||||
max: null,
|
||||
avg: null,
|
||||
latencies: [],
|
||||
latency: null,
|
||||
host: host,
|
||||
type: type
|
||||
};
|
||||
let output = {
|
||||
alive: false,
|
||||
min: null,
|
||||
max: null,
|
||||
avg: null,
|
||||
latencies: [],
|
||||
latency: null,
|
||||
host: host,
|
||||
type: type,
|
||||
};
|
||||
|
||||
try {
|
||||
let res = await ping.promise.probe(host, {
|
||||
v6: type === "IP6",
|
||||
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
|
||||
min_reply: count
|
||||
});
|
||||
output.alive = res.alive;
|
||||
output.min = res.min;
|
||||
output.max = res.max;
|
||||
output.avg = res.avg;
|
||||
output.latencies = res.times;
|
||||
output.latency = res.time;
|
||||
} catch (error) {
|
||||
console.log(`Error in pingCall IP4 for ${host}`, error);
|
||||
}
|
||||
return output;
|
||||
try {
|
||||
let res = await ping.promise.probe(host, {
|
||||
v6: type === "IP6",
|
||||
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
|
||||
min_reply: count,
|
||||
});
|
||||
output.alive = res.alive;
|
||||
output.min = res.min;
|
||||
output.max = res.max;
|
||||
output.avg = res.avg;
|
||||
output.latencies = res.times;
|
||||
output.latency = res.time;
|
||||
} catch (error) {
|
||||
console.log(`Error in pingCall IP4 for ${host}`, error);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
*/
|
||||
function ExtractIPv6HostAndPort(input) {
|
||||
const parts = input.split(":"); // Split by colons
|
||||
const parts = input.split(":"); // Split by colons
|
||||
|
||||
// If there's a valid port at the end, extract it
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
|
||||
// If there's a valid port at the end, extract it
|
||||
const lastPart = parts[parts.length - 1];
|
||||
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
|
||||
|
||||
// Reconstruct the IPv6 address
|
||||
const host = parts.join(":");
|
||||
// Reconstruct the IPv6 address
|
||||
const host = parts.join(":");
|
||||
|
||||
// Ensure it's a valid IPv6 format
|
||||
if (host.includes(":")) {
|
||||
return { host, port }; // Port may be null if not present
|
||||
}
|
||||
return null; // Return null if the format is incorrect
|
||||
// Ensure it's a valid IPv6 format
|
||||
if (host.includes(":")) {
|
||||
return { host, port }; // Port may be null if not present
|
||||
}
|
||||
return null; // Return null if the format is incorrect
|
||||
}
|
||||
|
||||
export { TCP, ExtractIPv6HostAndPort, Ping };
|
||||
|
||||
@@ -3,20 +3,9 @@ import axios from "axios";
|
||||
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
const defaultEval = `(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
|
||||
return {
|
||||
status: 'UP',
|
||||
latency: responseTime,
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: 'DOWN',
|
||||
latency: responseTime,
|
||||
}
|
||||
})`;
|
||||
import { DefaultAPIEval } from "../../anywhere.js";
|
||||
import version from "../../version.js";
|
||||
import https from "https";
|
||||
|
||||
class ApiCall {
|
||||
monitor;
|
||||
@@ -31,7 +20,7 @@ class ApiCall {
|
||||
|
||||
async execute() {
|
||||
let axiosHeaders = {};
|
||||
axiosHeaders["User-Agent"] = "Kener/" + "3.1.0";
|
||||
axiosHeaders["User-Agent"] = `Kener/${version()}`;
|
||||
axiosHeaders["Accept"] = "*/*";
|
||||
|
||||
let body = this.monitor.type_data.body;
|
||||
@@ -44,10 +33,10 @@ class ApiCall {
|
||||
}
|
||||
|
||||
let method = this.monitor.type_data.method;
|
||||
let timeout = this.monitor.type_data.timeout || 5000;
|
||||
let timeout = this.monitor.type_data.timeout || 10000;
|
||||
let tag = this.monitor.tag;
|
||||
|
||||
let monitorEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : defaultEval;
|
||||
let monitorEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : DefaultAPIEval;
|
||||
|
||||
for (let i = 0; i < this.envSecrets.length; i++) {
|
||||
const secret = this.envSecrets[i];
|
||||
@@ -82,6 +71,10 @@ class ApiCall {
|
||||
transformResponse: (r) => r,
|
||||
};
|
||||
|
||||
if (!!this.monitor.type_data.allowSelfSignedCert) {
|
||||
options.httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||
}
|
||||
|
||||
if (!!body) {
|
||||
options.data = body;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2,21 +2,7 @@
|
||||
import axios from "axios";
|
||||
import { Ping } from "../ping.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
|
||||
const defaultPingEval = `(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc && ping.alive;
|
||||
}, true);
|
||||
|
||||
return {
|
||||
status: alive ? 'UP' : 'DOWN',
|
||||
latency: latencyTotal / arrayOfPings.length,
|
||||
}
|
||||
})`;
|
||||
import { DefaultPingEval } from "../../anywhere.js";
|
||||
|
||||
class PingCall {
|
||||
monitor;
|
||||
@@ -27,7 +13,7 @@ class PingCall {
|
||||
|
||||
async execute() {
|
||||
let hosts = this.monitor.type_data.hosts;
|
||||
let pingEval = !!this.monitor.type_data.pingEval ? this.monitor.type_data.pingEval : defaultPingEval;
|
||||
let pingEval = !!this.monitor.type_data.pingEval ? this.monitor.type_data.pingEval : DefaultPingEval;
|
||||
let tag = this.monitor.tag;
|
||||
if (hosts === undefined) {
|
||||
console.log(
|
||||
|
||||
@@ -2,25 +2,7 @@
|
||||
import axios from "axios";
|
||||
import { TCP } from "../ping.js";
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
|
||||
const defaultTcpEval = `(async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency;
|
||||
}, 0);
|
||||
|
||||
let alive = arrayOfPings.reduce((acc, ping) => {
|
||||
if (ping.status === "open") {
|
||||
return acc && true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
return {
|
||||
status: alive ? 'UP' : 'DOWN',
|
||||
latency: latencyTotal / arrayOfPings.length,
|
||||
}
|
||||
})`;
|
||||
import { DefaultTCPEval } from "../../anywhere.js";
|
||||
|
||||
class TcpCall {
|
||||
monitor;
|
||||
@@ -31,7 +13,7 @@ class TcpCall {
|
||||
|
||||
async execute() {
|
||||
let hosts = this.monitor.type_data.hosts;
|
||||
let tcpEval = !!this.monitor.type_data.tcpEval ? this.monitor.type_data.tcpEval : defaultTcpEval;
|
||||
let tcpEval = !!this.monitor.type_data.tcpEval ? this.monitor.type_data.tcpEval : DefaultTCPEval;
|
||||
let tag = this.monitor.tag;
|
||||
|
||||
if (hosts === undefined) {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// @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";
|
||||
@@ -72,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>
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { AllRecordTypes } from "./constants.js";
|
||||
import knexOb from "../../../knexfile.js";
|
||||
import crypto from "crypto";
|
||||
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
@@ -279,6 +280,9 @@ function Wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
function MaskString(str) {
|
||||
if (str.length <= 4) {
|
||||
return "*".repeat(str.length);
|
||||
}
|
||||
return "*".repeat(str.length - 4) + str.slice(-4);
|
||||
}
|
||||
|
||||
@@ -286,6 +290,18 @@ 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,
|
||||
@@ -315,4 +331,6 @@ export {
|
||||
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}}");
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* ***************************************************
|
||||
* * *
|
||||
* * Google 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.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
window.dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
|
||||
gtag("config", "{{id}}");
|
||||
|
||||
window.addEventListener("analyticsEvent", function (e) {
|
||||
// Extract event name and data from the custom event
|
||||
const eventName = e.detail.event;
|
||||
const eventData = e.detail.data || {};
|
||||
// Send the event to Google Tag Manager
|
||||
if (window.dataLayer && eventName) {
|
||||
gtag("event", eventName, eventData);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//on dom ready
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
loadJS("https://www.googletagmanager.com/gtag/js?id={{id}}", initJS, document.getElementsByTagName("head")[0]);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* ***************************************************
|
||||
* * *
|
||||
* * Mixpanel Analytics Capture Snippet *
|
||||
* * *
|
||||
* ***************************************************
|
||||
**/
|
||||
// @ts-nocheck
|
||||
|
||||
(function (f, b) {
|
||||
if (!b.__SV) {
|
||||
var e, g, i, h;
|
||||
window.mixpanel = b;
|
||||
b._i = [];
|
||||
b.init = function (e, f, c) {
|
||||
function g(a, d) {
|
||||
var b = d.split(".");
|
||||
2 == b.length && ((a = a[b[0]]), (d = b[1]));
|
||||
a[d] = function () {
|
||||
a.push([d].concat(Array.prototype.slice.call(arguments, 0)));
|
||||
};
|
||||
}
|
||||
var a = b;
|
||||
"undefined" !== typeof c ? (a = b[c] = []) : (c = "mixpanel");
|
||||
a.people = a.people || [];
|
||||
a.toString = function (a) {
|
||||
var d = "mixpanel";
|
||||
"mixpanel" !== c && (d += "." + c);
|
||||
a || (d += " (stub)");
|
||||
return d;
|
||||
};
|
||||
a.people.toString = function () {
|
||||
return a.toString(1) + ".people (stub)";
|
||||
};
|
||||
i =
|
||||
"disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(
|
||||
" ",
|
||||
);
|
||||
for (h = 0; h < i.length; h++) g(a, i[h]);
|
||||
var j = "set set_once union unset remove delete".split(" ");
|
||||
a.get_group = function () {
|
||||
function b(c) {
|
||||
d[c] = function () {
|
||||
call2_args = arguments;
|
||||
call2 = [c].concat(Array.prototype.slice.call(call2_args, 0));
|
||||
a.push([e, call2]);
|
||||
};
|
||||
}
|
||||
for (var d = {}, e = ["get_group"].concat(Array.prototype.slice.call(arguments, 0)), c = 0; c < j.length; c++)
|
||||
b(j[c]);
|
||||
return d;
|
||||
};
|
||||
b._i.push([e, f, c]);
|
||||
};
|
||||
b.__SV = 1.2;
|
||||
e = f.createElement("script");
|
||||
e.type = "text/javascript";
|
||||
e.async = !0;
|
||||
e.src =
|
||||
"undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL
|
||||
? MIXPANEL_CUSTOM_LIB_URL
|
||||
: "file:" === f.location.protocol && "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)
|
||||
? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"
|
||||
: "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";
|
||||
g = f.getElementsByTagName("script")[0];
|
||||
g.parentNode.insertBefore(e, g);
|
||||
}
|
||||
})(document, window.mixpanel || []);
|
||||
|
||||
window.mixpanel.init("{{YOUR_PROJECT_TOKEN}}", { track_pageview: true, autocapture: true });
|
||||
|
||||
window.addEventListener("analyticsEvent", function (e) {
|
||||
const eventName = e.detail.event;
|
||||
const eventData = e.detail.data || {};
|
||||
if (eventName) {
|
||||
window.mixpanel.track(eventName, eventData);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* ***************************************************
|
||||
* * *
|
||||
* * Plausible 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.defer = true;
|
||||
scriptTag.dataset.domain = "{{domain}}";
|
||||
scriptTag.dataset.api = "{{api}}";
|
||||
scriptTag.onload = implementationCode;
|
||||
scriptTag.onreadystatechange = implementationCode;
|
||||
|
||||
location.appendChild(scriptTag);
|
||||
};
|
||||
let initJS = function () {
|
||||
window.plausible =
|
||||
window.plausible ||
|
||||
function () {
|
||||
(window.plausible.q = window.plausible.q || []).push(arguments);
|
||||
};
|
||||
|
||||
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.plausible(eventName, { props: eventData });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//on dom ready
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
loadJS("{{script_src}}", initJS, document.getElementsByTagName("head")[0]);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Component, Input, ElementRef, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-embed-monitor',
|
||||
template: `<div [id]="containerId"></div>`
|
||||
})
|
||||
export class EmbedMonitorComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
@Input() monitor!: string;
|
||||
@Input() theme: string = "light";
|
||||
@Input() bgc: string = "transparent";
|
||||
@Input() locale: string = "en";
|
||||
|
||||
containerId = `embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
|
||||
private iframe: HTMLIFrameElement | null = null;
|
||||
private messageListener!: (event: MessageEvent) => void;
|
||||
|
||||
constructor(private el: ElementRef) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.createIframe();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.createIframe();
|
||||
}
|
||||
|
||||
createIframe() {
|
||||
const container = this.el.nativeElement.querySelector(`#${this.containerId}`);
|
||||
if (!this.monitor || !container) return;
|
||||
|
||||
// Remove previous iframe if it exists
|
||||
if (this.iframe) {
|
||||
this.iframe.remove();
|
||||
}
|
||||
|
||||
this.iframe = document.createElement("iframe");
|
||||
this.iframe.src = `${this.monitor}?theme=${this.theme}&bgc=${this.bgc}&locale=${this.locale}`;
|
||||
this.iframe.width = "0%";
|
||||
this.iframe.height = "0";
|
||||
this.iframe.frameBorder = "0";
|
||||
this.iframe.allowTransparency = true;
|
||||
this.iframe.sandbox =
|
||||
"allow-modals allow-forms allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation allow-downloads";
|
||||
this.iframe.allow = "midi; geolocation; microphone; camera; display-capture; encrypted-media;";
|
||||
|
||||
container.appendChild(this.iframe);
|
||||
|
||||
this.messageListener = (event: MessageEvent) => {
|
||||
if (event.data?.height) this.iframe!.height = event.data.height;
|
||||
if (event.data?.width) this.iframe!.width = event.data.width;
|
||||
};
|
||||
|
||||
window.addEventListener("message", this.messageListener);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
window.removeEventListener("message", this.messageListener);
|
||||
if (this.iframe) {
|
||||
this.iframe.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<app-embed-monitor monitor="{siteURL}{base}/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"></app-embed-monitor>
|
||||
@@ -0,0 +1,8 @@
|
||||
<iframe
|
||||
src="{siteURL}{base}/embed/monitor-{selectedMonitor.value}?bgc={embedBgc.substr(1)}&locale={embedLocale.value}&theme={embedTheme}"
|
||||
width="100%"
|
||||
height="200"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allowpaymentrequest
|
||||
frameborder="0">
|
||||
</iframe>
|
||||
@@ -0,0 +1 @@
|
||||
<script async src="{siteURL}{base}/embed/monitor-{selectedMonitor.value}/js?bgc={embedBgc.substr(1)}&locale={embedLocale.value}&theme={embedTheme}&monitor={siteURL}{base}/embed/monitor-{selectedMonitor.value}"></script>
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const EmbedMonitor = ({ monitor, theme = "light", bgc = "transparent", locale = "en" }) => {
|
||||
const containerRef = useRef(null);
|
||||
const iframeRef = useRef(null);
|
||||
const [uid] = useState(`embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`);
|
||||
|
||||
useEffect(() => {
|
||||
if (!monitor || !containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
|
||||
// Remove previous iframe if exists
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.remove();
|
||||
}
|
||||
|
||||
// Create a new iframe
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.src = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
|
||||
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);
|
||||
iframeRef.current = iframe;
|
||||
|
||||
const setHeight = (data) => {
|
||||
if (data.height !== undefined) {
|
||||
iframe.height = data.height;
|
||||
}
|
||||
};
|
||||
|
||||
const setWidth = (data) => {
|
||||
if (data.width !== undefined) {
|
||||
iframe.width = data.width;
|
||||
}
|
||||
};
|
||||
|
||||
const messageListener = (event) => {
|
||||
if (event.data && event.data.height !== undefined) {
|
||||
setHeight(event.data);
|
||||
}
|
||||
if (event.data && event.data.width !== undefined) {
|
||||
setWidth(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", messageListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", messageListener);
|
||||
if (iframe) {
|
||||
iframe.remove();
|
||||
}
|
||||
};
|
||||
}, [monitor, theme, bgc, locale]);
|
||||
|
||||
return <div id={uid} ref={containerRef}></div>;
|
||||
};
|
||||
|
||||
export default EmbedMonitor;
|
||||
@@ -0,0 +1,12 @@
|
||||
import EmbedMonitor from "./EmbedMonitor";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<h2>React Embed</h2>
|
||||
<EmbedMonitor monitor="{siteURL}{base}/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,91 @@
|
||||
<script>
|
||||
import { onMount, onDestroy, beforeUpdate } from "svelte";
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (iframe) {
|
||||
window.removeEventListener("message", listeners);
|
||||
iframe.remove();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Dynamic container with unique ID -->
|
||||
<div id={containerId}></div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import EmbedMonitor from "./EmbedMonitor.svelte";
|
||||
</script>
|
||||
|
||||
<EmbedMonitor monitor="{siteURL}{base}/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div :id="containerId" ref="containerRef"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, watchEffect, onMounted, onBeforeUnmount } from "vue";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
monitor: String,
|
||||
theme: { type: String, default: "light" },
|
||||
bgc: { type: String, default: "transparent" },
|
||||
locale: { type: String, default: "en" }
|
||||
},
|
||||
setup(props) {
|
||||
const containerRef = ref(null);
|
||||
const iframeRef = ref(null);
|
||||
const containerId = ref(`embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`);
|
||||
|
||||
let messageListener = null;
|
||||
|
||||
const createIframe = () => {
|
||||
if (!props.monitor || !containerRef.value) return;
|
||||
|
||||
// Remove existing iframe if any
|
||||
if (iframeRef.value) {
|
||||
iframeRef.value.remove();
|
||||
}
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.src = `${props.monitor}?theme=${props.theme}&bgc=${props.bgc}&locale=${props.locale}`;
|
||||
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;";
|
||||
|
||||
containerRef.value.appendChild(iframe);
|
||||
iframeRef.value = iframe;
|
||||
|
||||
messageListener = (event) => {
|
||||
if (event.data?.height) iframe.height = event.data.height;
|
||||
if (event.data?.width) iframe.width = event.data.width;
|
||||
};
|
||||
|
||||
window.addEventListener("message", messageListener);
|
||||
};
|
||||
|
||||
onMounted(createIframe);
|
||||
|
||||
watchEffect(() => {
|
||||
createIframe();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("message", messageListener);
|
||||
if (iframeRef.value) {
|
||||
iframeRef.value.remove();
|
||||
}
|
||||
});
|
||||
|
||||
return { containerRef, containerId };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Vue Embed</h2>
|
||||
<EmbedMonitor monitor="{siteURL}{base}/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmbedMonitor from "./EmbedMonitor.vue";
|
||||
|
||||
export default {
|
||||
components: { EmbedMonitor }
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import fs from "fs";
|
||||
|
||||
function getVersionUsingVite() {
|
||||
try {
|
||||
if (!!import.meta.env.PACKAGE_VERSION) {
|
||||
return import.meta.env.PACKAGE_VERSION;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function version() {
|
||||
let v = getVersionUsingVite();
|
||||
if (!!v) {
|
||||
return v;
|
||||
} else {
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const packagePath = resolve(__dirname, "../../package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
return packageJson.version;
|
||||
} catch (e) {
|
||||
return "0.0.0";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
}
|
||||
.copybtn .copy-btn {
|
||||
transform: scale(1);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.copybtn .check-btn {
|
||||
transform: scale(0);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.copybtn:focus .copy-btn {
|
||||
transform: scale(0);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { marked } from "marked";
|
||||
import version from "$lib/version.js";
|
||||
|
||||
function extractMetadataAndContent(fileContent) {
|
||||
// Regular expression to match metadata section
|
||||
@@ -15,9 +16,7 @@ function extractMetadataAndContent(fileContent) {
|
||||
|
||||
if (match) {
|
||||
// Extract metadata section and remaining content
|
||||
const metadataLines = match[1]
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "");
|
||||
const metadataLines = match[1].split("\n").filter((line) => line.trim() !== "");
|
||||
result.content = fileContent.slice(match[0].length).trim(); // Remaining content after metadata
|
||||
|
||||
metadataLines.forEach((line) => {
|
||||
@@ -40,10 +39,7 @@ export async function load({ params, route, url, cookies, request }) {
|
||||
docFilePath = docFolderPath + `/${docFile}.md`;
|
||||
}
|
||||
const fileContents = await fs.readFileSync(docFilePath, "utf-8");
|
||||
const siteStructure = await fs.readFileSync(
|
||||
docFolderPath + "/structure.json",
|
||||
"utf-8",
|
||||
);
|
||||
const siteStructure = await fs.readFileSync(docFolderPath + "/structure.json", "utf-8");
|
||||
const { metadata, content } = extractMetadataAndContent(fileContents);
|
||||
|
||||
const selectedDoc = docFilePath.replace(docFolderPath, "");
|
||||
@@ -54,5 +50,6 @@ export async function load({ params, route, url, cookies, request }) {
|
||||
title: metadata.title || "Kener Docs",
|
||||
description: metadata.description || "Kener Docs",
|
||||
siteStructure: siteStructureJSON,
|
||||
kenerVersion: version(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import Moon from "lucide-svelte/icons/moon";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
let defaultTheme = "light";
|
||||
export let data;
|
||||
let siteStructure = data.siteStructure;
|
||||
@@ -110,7 +111,9 @@
|
||||
<!-- Document Icon - Replace with your own logo -->
|
||||
<img src="https://kener.ing/logo.png" class="h-8 w-8" alt="" />
|
||||
<span class="text-xl font-medium">Kener Documentation</span>
|
||||
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.2.2 </span>
|
||||
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium">
|
||||
{$page.data.kenerVersion}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -202,7 +205,7 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<a
|
||||
href="https://github.com/sponsors/rajnandan1"
|
||||
href="https://buymeacoffee.com/rajnandan1"
|
||||
class="rounded-md bg-amber-500 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-amber-600"
|
||||
>
|
||||
Sponsor
|
||||
@@ -214,7 +217,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-svelte";
|
||||
import ArrowLeft from "lucide-svelte/icons/arrow-left";
|
||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
||||
|
||||
import { afterNavigate } from "$app/navigation";
|
||||
export let data;
|
||||
let subHeadings = [];
|
||||
@@ -65,21 +67,28 @@
|
||||
//function to find next and previous path
|
||||
function findNextAndPreviousPath() {
|
||||
let structure = data.siteStructure.sidebar;
|
||||
for (let i = 0; i < structure.length; i++) {
|
||||
let children = structure[i].children;
|
||||
|
||||
for (let j = 0; j < children.length; j++) {
|
||||
if (children[j].file === data.docFilePath) {
|
||||
if (j > 0) {
|
||||
previousPath = children[j - 1];
|
||||
}
|
||||
if (j < children.length - 1) {
|
||||
nextPath = children[j + 1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Flatten the structure - combine all children arrays into one
|
||||
const flattenedPages = [];
|
||||
|
||||
for (let i = 0; i < structure.length; i++) {
|
||||
const section = structure[i];
|
||||
if (section.children && Array.isArray(section.children)) {
|
||||
flattenedPages.push(...section.children);
|
||||
}
|
||||
}
|
||||
|
||||
// Find current page index in the flattened array
|
||||
const currentIndex = flattenedPages.findIndex((page) => page.file === data.docFilePath);
|
||||
|
||||
// Set previous and next paths if they exist
|
||||
if (currentIndex > 0) {
|
||||
previousPath = flattenedPages[currentIndex - 1];
|
||||
}
|
||||
|
||||
if (currentIndex < flattenedPages.length - 1) {
|
||||
nextPath = flattenedPages[currentIndex + 1];
|
||||
}
|
||||
}
|
||||
|
||||
afterNavigate(() => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user