Compare commits

..

47 Commits

Author SHA1 Message Date
Raj Nandan Sharma a1064771f8 fix: increment package version from 3.2.7 to 3.2.8 in package.json 2025-03-24 20:34:24 +05:30
Raj Nandan Sharma 463ee286bf fix: increment package version from 3.2.6 to 3.2.7 in package.json 2025-03-24 20:30:05 +05:30
Raj Nandan Sharma a7ae6e72ef fix: downgrade package version from 3.2.8 to 3.2.6 in package.json 2025-03-24 20:28:01 +05:30
github-actions 36c67f60dd Auto-generate README.md with release versions 2025-03-24 14:56:20 +00:00
Raj Nandan Sharma bb718b04a6 fix: update Dockerfile to use ARG variables for Node.js version tags 2025-03-24 20:10:13 +05:30
Raj Nandan Sharma b6ea060054 fix: update publish-images workflow and increment package version to 3.2.8 2025-03-24 20:01:53 +05:30
Raj Nandan Sharma 5dba54c048 fix: update publish-images workflow to set GH_TOKEN environment variable correctly 2025-03-24 19:53:29 +05:30
Raj Nandan Sharma 415276163f Merge pull request #356 from rajnandan1/release/3.2.8-x
fix: update Node.js version to 23 and increment package version to 3.2.7
2025-03-24 19:45:44 +05:30
Raj Nandan Sharma 017b9e2e84 fix: update Node.js version to 23 and increment package version to 3.2.7 2025-03-24 19:45:15 +05:30
Raj Nandan Sharma 5740991ee1 fix: update GitHub Actions workflow to use GH_PAT instead of GITHUB_TOKEN for authentication 2025-03-24 15:12:40 +05:30
Raj Nandan Sharma 06008bd719 fix: streamline Dockerfile by removing unnecessary paths and using variable references for base images 2025-03-24 12:11:03 +05:30
Raj Nandan Sharma b53cd485e5 Merge pull request #263 from kaffolder7/feature/dependabot-version-updates
feat: automate dependency updates 🤖
2025-03-24 11:59:41 +05:30
Raj Nandan Sharma 7c1fd39158 Merge branch 'main' into feature/dependabot-version-updates 2025-03-24 11:58:58 +05:30
Raj Nandan Sharma f7888f1ba4 fix: update package versions in Dockerfile for consistency and stability 2025-03-24 11:32:05 +05:30
Raj Nandan Sharma ece5ac37ad feat: update embed monitor URLs to use siteURL and base variables for improved flexibility 2025-03-24 11:19:51 +05:30
Raj Nandan Sharma a51183fb23 Merge pull request #348 from rajnandan1/rbac
Features role-based access control and user management
2025-03-24 10:45:39 +05:30
Raj Nandan Sharma 7875192fb0 feat: update package dependencies and improve invitation expiry date formatting 2025-03-24 10:17:21 +05:30
Raj Nandan Sharma eb128ad431 feat: add "View in detail" button for group monitors and update localization files 2025-03-23 22:44:11 +05:30
Raj Nandan Sharma ba61ed87ab feat: enhance incident month handling with improved date calculations and formatting, fixing #349 2025-03-23 21:51:51 +05:30
Raj Nandan Sharma 8b5348081d docs: update changelogs to include improvements in build time and component migration 2025-03-23 14:29:15 +05:30
Raj Nandan Sharma bb7e69ace5 fix: update icon imports to use specific paths from lucide-svelte 2025-03-23 14:28:04 +05:30
Raj Nandan Sharma 3b8bb60423 fix: update print width in configuration files and improve documentation links 2025-03-23 13:36:22 +05:30
Raj Nandan Sharma 93339ce9df feat: add support for self-signed certificates and update changelog, fixes #351 2025-03-23 12:32:36 +05:30
Raj Nandan Sharma d30d99d8c2 docs: update changelogs to include resolved GitHub issues and improve formatting 2025-03-22 22:57:26 +05:30
Raj Nandan Sharma 3468df16e1 fix: improve error handling and code structure in Minuter function #345 2025-03-22 22:50:17 +05:30
Raj Nandan Sharma ccdeff98dc feat: implement dynamic versioning in headers and documentation as reported in #346 2025-03-22 21:54:24 +05:30
Raj Nandan Sharma 7e182b1df2 fix: replace showModal with closeModal for better modal handling #350 2025-03-22 21:06:37 +05:30
Raj Nandan Sharma ff652ad435 fix: #238 #210 #309 #334 2025-03-22 16:42:21 +05:30
Raj Nandan Sharma 1fdafa7966 Adds badges and embed features
Implements badges for monitors, including status, uptime, and liveness, along with a dedicated management page.

Adds embed options for various platforms with customizable styles.
2025-03-22 16:36:19 +05:30
Raj Nandan Sharma 1f5e683af5 feat: integrate CodeMirror editor for enhanced JavaScript and JSON editing experience 2025-03-20 23:23:54 +05:30
Raj Nandan Sharma db8c881571 refactor: remove performance logging and clean up code in various files 2025-03-19 19:45:12 +05:30
Raj Nandan Sharma 14f49c5b3b Features role-based access control and user management
Implements role-based access control with admin, editor, and member roles.

Introduces a user management system with profiles, activation/deactivation, and password reset.

Adds an email authentication system with verification and password reset via email.

Includes an invitation system with token-based invitations and admin controls.

Improves performance, security, UI, and developer experience.

Updates package version to 3.2.5 and adds vite-plugin-package-version.

Removes libcap related code from Dockerfile.
2025-03-18 23:08:51 +05:30
Raj Nandan Sharma 52286f26a5 fix: update version to 3.2.5 and reflect changes in User-Agent and documentation 2025-03-13 09:55:04 +05:30
github-actions 45e7567cb6 Auto-generate README.md with release versions 2025-03-12 17:48:41 +00:00
Raj Nandan Sharma c0edb59884 chore: update version to 3.2.4 and reflect changes in User-Agent and documentation 2025-03-12 23:16:48 +05:30
Raj Nandan Sharma 1e86c429ca fix: install libcap tools for setting capabilities in Dockerfile 2025-03-12 23:00:18 +05:30
Kyle Affolder 8c95c94472 update: combine to avoid multiple processes
Combined to reduce spawning multiple `rm` processes.
2025-02-17 13:29:34 -05:00
Kyle Affolder 7dafb2eddc add: GHA job to confirm if Dependabot PRs exist
Adds job to check if any Dependabot PRs are open and if so, fail the Docker build (since we need to ensure OS packages exist and are in their correct versions when using pinned versions for security purposes).
2025-02-17 13:24:22 -05:00
Kyle Affolder 7c9f3eb87f add: add back pinned tzdata version
Necessary for Dependabot to track
2025-02-17 13:22:15 -05:00
Kyle ab34dd81f8 Merge branch 'rajnandan1:main' into feature/dependabot-version-updates 2025-02-17 13:04:23 -05:00
Kyle Affolder a8be878a87 update: commented out node.js deps. monitoring
Not sure if we are wanting Dependabot to track Node.js packages, so for the time being, commenting this block out, but leaving for now w/ “TODO” to come back to at a later point.
2025-02-11 17:54:37 -05:00
Kyle Affolder 2b1849f1a3 update: grammar updates 2025-02-11 17:47:45 -05:00
Kyle Affolder 25a6590324 Merge branch 'feature/dependabot-version-updates' of https://github.com/kaffolder7/kener into feature/dependabot-version-updates 2025-02-11 17:31:48 -05:00
Kyle Affolder fa251dc07f update: README generation workflow 2025-02-11 17:31:33 -05:00
Kyle 4de93e7e95 Merge branch 'main' into feature/dependabot-version-updates 2025-02-11 17:25:28 -05:00
Kyle Affolder a5e5f33dc8 Merge branch 'main' into feature/dependabot-version-updates
# Conflicts:
#	.github/workflows/publish-images.yml
#	Dockerfile
2025-02-11 17:17:45 -05:00
Kyle Affolder 4060094404 add(dependabot): to automate dependency updates
Integrating Dependabot into the workflow ensures automatic dependency updates, improving security, reducing technical debt, and keeping packages up to date with minimal manual effort. This helps prevent vulnerabilities and maintain code stability over time.

Dependabot will automatically monitor the project’s dependencies and open pull requests (PRs) to update them when new versions are released. Here’s how it works:

1. Scans for Outdated Dependencies – It checks project dependency files (e.g., package.json, Dockerfile, .env.build, etc.) for outdated versions.
2. Fetches Latest Versions – When a newer version of a dependency is available, Dependabot retrieves it and updates the dependency files accordingly.
3. Opens a Pull Request – It then creates a PR with the updated dependency, detailing the changes and linking to release notes, changelogs, or security advisories.
4. Runs CI/CD Tests – If we end up setting up continuous integration (CI) tests, the PR will trigger the tests to check for breaking changes.
5. Security Updates – Dependabot also detects vulnerable dependencies and creates PRs to update them to a secure version.
6. Auto-Merging (Optional) – We might consider this at a later point, but if configured, it can automatically merge PRs when updates pass all tests and meet the requirements.
2025-02-09 22:08:06 -05:00
122 changed files with 6193 additions and 1882 deletions
+1
View File
@@ -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]
+47
View File
@@ -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"
+2 -2
View File
@@ -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
+1 -1
View File
@@ -54,7 +54,7 @@
"semi": false,
"tabWidth": 4,
"trailingComma": "none",
"printWidth": 100
"printWidth": 180
}
},
{
+18 -27
View File
@@ -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 \
@@ -31,7 +31,7 @@ RUN apk add --no-cache --update \
g++=14.2.0-r4 \
sqlite=3.48.0-r0 \
sqlite-dev=3.48.0-r0 \
tzdata \
tzdata=2025a-r0 \
iputils=20240905-r0
FROM builder-${VARIANT} AS builder
@@ -58,10 +58,9 @@ COPY . .
# TODO: Reevaluate permissions (possibly reduce?)...
# Remove docs directory and ensure required directories exist
RUN rm -rf src/routes/\(docs\) \
static/documentation \
static/fonts/lato/full && \
mkdir -p uploads database && \
# TODO: Consider changing below to `chmod -R u-rwX,g=rX,o= uploads database`
static/documentation \
static/fonts/lato/full && \
mkdir -p uploads database && \
chmod -R 750 uploads database
# Build the application and remove `devDependencies`
@@ -72,21 +71,21 @@ RUN npm run build && \
# STAGE 2: PRODUCTION/FINAL STAGE #
#==========================================================#
FROM node:${DEBIAN_VERSION} AS final-debian
FROM node:${DEBIAN_VERSION_TAG} AS final-debian
# TODO: Consider adding `--no-install-recommends`, but will need testing (may further help reduce final build size)
RUN apt-get update && apt-get install -y \
iputils-ping=3:20221126-1+deb12u1 \
sqlite3=3.40.1-2+deb12u1 \
tzdata=2024b-0+deb12u1 \
tzdata \
# TODO: Is it ok to change to `curl` here so that we don't have to maintain `wget` version mismatch between Debian architectures? (`curl` is only used for the container healthcheck and because there is an Alpine variant (best!) we probably don't care if the Debian image ends up building bigger due to `curl`.)
curl=7.88.1-10+deb12u8 && \
curl && \
rm -rf /var/lib/apt/lists/*
FROM node:${ALPINE_VERSION} AS final-alpine
FROM node:${ALPINE_VERSION_TAG} AS final-alpine
RUN apk add --no-cache --update \
iputils=20240905-r0 \
sqlite=3.48.0-r0 \
tzdata
iputils=20240905-r0 \
sqlite=3.48.0-r0 \
tzdata=2025a-r0
FROM final-${VARIANT} AS final
@@ -119,15 +118,7 @@ COPY --chown=node:node --from=builder /app/knexfile.js ./knexfile.js
COPY --chown=node:node --from=builder /app/main.js ./main.js
COPY --chown=node:node --from=builder /app/openapi.json ./openapi.json
COPY --chown=node:node --from=builder /app/openapi.yaml ./openapi.yaml
# Set capabilities for ping (before changing to non-root user)
RUN if [ -f "/etc/alpine-release" ]; then \
# Alpine path
setcap cap_net_raw+ep /bin/ping; \
else \
# Debian path
setcap cap_net_raw+ep /usr/bin/ping; \
fi
COPY --chown=node:node --from=builder /app/package.json ./package.json
# Ensure necessary directories are writable
VOLUME ["/uploads", "/database"]
+9 -9
View File
@@ -79,7 +79,7 @@ npm run dev
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.2-blue" alt="Kener latest stable version: 3.2.2" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.6"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.6-blue" alt="Kener latest stable version: 3.2.6" /></a>
#### Available Tags
@@ -93,38 +93,38 @@ Official Docker images for **Kener** are available on [Docker Hub](https://hub.d
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
<td>Latest stable release (aka 3.2.2)</td>
<td>Latest stable release (aka 3.2.6)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2" target="_blank"><code>3.2.2</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.6" target="_blank"><code>3.2.6</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2" target="_blank"><code>3.2</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.2) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.6) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.2) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.6) 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 &nbsp;<strong><em>(smallest image size)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
<td>Latest stable release (aka 3.2.2)</td>
<td>Latest stable release (aka 3.2.6)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2-alpine" target="_blank"><code>3.2.2-alpine</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.6-alpine" target="_blank"><code>3.2.6-alpine</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2-alpine" target="_blank"><code>3.2-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.2) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.6) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.2) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.6) release within that major version (3.x.x)</td>
</tr>
</table>
+40
View File
@@ -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>
+134
View File
@@ -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 or by existing Admins
- 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.
+5
View File
@@ -28,6 +28,11 @@
"title": "Databases",
"link": "/docs/database",
"file": "/database.md"
},
{
"title": "Access Control",
"link": "/docs/rbac",
"file": "/rbac.md"
}
]
},
+221 -45
View File
@@ -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
![Discord](/documentation/discord_resolved.png)
### 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
![Slack](/documentation/slack_resolved.png)
### 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
View File
@@ -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");
}
+267 -46
View File
@@ -1,17 +1,21 @@
{
"name": "kener",
"version": "3.1.8",
"version": "3.2.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kener",
"version": "3.1.8",
"version": "3.2.5",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@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 +35,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 +51,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 +67,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 +182,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 +193,113 @@
"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-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/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 +307,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -212,7 +323,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -229,7 +339,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -246,7 +355,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -263,7 +371,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -280,7 +387,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -297,7 +403,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -314,7 +419,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -331,7 +435,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -348,7 +451,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -365,7 +467,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -382,7 +483,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -399,7 +499,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -416,7 +515,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -433,7 +531,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -450,7 +547,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -467,7 +563,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -484,7 +579,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -501,7 +595,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -518,7 +611,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -535,7 +627,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -552,7 +643,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -756,6 +846,52 @@
"@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/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/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/@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 +912,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 +1346,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 +1698,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 +2268,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 +2456,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 +3189,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 +4973,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 +5300,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 +6530,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 +6817,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 +6963,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 +7775,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 +7991,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 +8626,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 +8677,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 +8701,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",
+10 -4
View File
@@ -1,6 +1,6 @@
{
"name": "kener",
"version": "3.2.3",
"version": "3.2.8",
"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,13 @@
},
"type": "module",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@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 +93,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 +109,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"
+34
View File
@@ -905,3 +905,37 @@ 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;
}
+100
View File
@@ -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>
+194
View File
@@ -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>`;
+3 -2
View File
@@ -1,6 +1,7 @@
<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 * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
@@ -78,7 +79,7 @@
<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">
<div class="w-full text-left hover:no-underline">
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
+3 -2
View File
@@ -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;
+233 -253
View File
@@ -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}
+43 -12
View File
@@ -9,7 +9,11 @@
import locales from "$lib/locales/locales.json?raw";
import GMI from "$lib/components/gmi.svelte";
import { Loader, X, Plus, Info, Play } from "lucide-svelte";
import Loader from "lucide-svelte/icons/loader";
import X from "lucide-svelte/icons/x";
import Plus from "lucide-svelte/icons/plus";
import Info from "lucide-svelte/icons/info";
import Play from "lucide-svelte/icons/play";
import { Tooltip } from "bits-ui";
export let data;
@@ -87,8 +91,9 @@
}
});
}
let heroError = "";
async function formSubmitHero() {
heroError = "";
formStateHero = "loading";
let resp = await storeSiteData({
hero: JSON.stringify(hero)
@@ -97,7 +102,7 @@
let data = await resp.json();
formStateHero = "idle";
if (data.error) {
alert(data.error);
heroError = data.error;
return;
}
}
@@ -117,7 +122,10 @@
return;
}
}
let categoriesErrorMessage = "";
async function formSubmitCategories() {
categoriesErrorMessage = "";
formStateCategories = "loading";
let resp = await storeSiteData({
categories: JSON.stringify(categories)
@@ -126,11 +134,14 @@
let data = await resp.json();
formStateCategories = "idle";
if (data.error) {
alert(data.error);
categoriesErrorMessage = data.error;
return;
}
}
let footerErrorMessage = "";
async function formSubmitFooter() {
footerErrorMessage = "";
formStateFooter = "loading";
let resp = await storeSiteData({
footerHTML: footerHTML
@@ -139,11 +150,14 @@
let data = await resp.json();
formStateFooter = "idle";
if (data.error) {
alert(data.error);
footerErrorMessage = data.error;
return;
}
}
let incidentErrorMessage = "";
async function formSubmitIncident() {
incidentErrorMessage = "";
formStateIncident = "loading";
let resp = await storeSiteData({
homeIncidentCount: homeIncidentCount,
@@ -154,7 +168,7 @@
let data = await resp.json();
formStateIncident = "idle";
if (data.error) {
alert(data.error);
incidentErrorMessage = data.error;
return;
}
}
@@ -220,7 +234,9 @@
i18n.defaultLocale = e.value;
}
let i18nErrorMessage = "";
async function formSubmiti18n() {
i18nErrorMessage = "";
formStatei18n = "loading";
let resp = await storeSiteData({
i18n: JSON.stringify(i18n),
@@ -230,7 +246,7 @@
let data = await resp.json();
formStatei18n = "idle";
if (data.error) {
alert(data.error);
i18nErrorMessage = data.error;
return;
}
}
@@ -289,7 +305,10 @@
/>
</div>
</div>
<div class="flex w-full justify-end">
<div class="flex w-full justify-end gap-x-2">
{#if !!heroError}
<div class="py-2 text-sm font-medium text-destructive">{heroError}</div>
{/if}
<Button type="submit" disabled={formStateHero === "loading"}>
Save
{#if formStateHero === "loading"}
@@ -362,6 +381,9 @@
Translates to "Show {homeIncidentCount} incidents that have started in the last {homeIncidentStartTimeWithin}
days in the past or going to start in {homeIncidentStartTimeWithin} days in the future"
</p>
{#if !!incidentErrorMessage}
<p class="text-sm font-medium text-destructive">{incidentErrorMessage}</p>
{/if}
</form>
</Card.Content>
</Card.Root>
@@ -470,7 +492,7 @@
</div>
<div class="flex w-full justify-end" style="margin-top:32px">
<p class="px-4 pt-1">
<span class="text-xs text-red-500"> {navErrorMessage} </span>
<span class="text-sm font-medium text-destructive"> {navErrorMessage} </span>
</p>
<Button type="submit" disabled={formStateHero === "loading"}>
Save
@@ -547,7 +569,10 @@
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
{#if formStatei18n === "loading"}
@@ -607,7 +632,10 @@
<Plus class="h-4 w-4 " />
</Button>
</div>
<div class="flex w-full justify-end">
<div class="flex w-full justify-end gap-x-2">
{#if !!categoriesErrorMessage}
<div class="py-2 text-sm font-medium text-destructive">{categoriesErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStateCategories === "loading"}>
Save
{#if formStateCategories === "loading"}
@@ -636,7 +664,10 @@
</div>
</div>
<div class="flex w-full justify-end">
<div class="flex w-full justify-end gap-x-2">
{#if !!footerErrorMessage}
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStateFooter === "loading"}>
Save
{#if formStateFooter === "loading"}
+74 -21
View File
@@ -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.";
@@ -718,6 +727,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 +749,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 +855,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>
@@ -914,12 +957,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>
+77 -36
View File
@@ -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: "",
@@ -242,7 +252,9 @@
}
};
let saveTriggerError = "";
async function saveTriggers() {
saveTriggerError = "";
let data = {
id: currentAlertMonitor.id,
down_trigger: JSON.stringify(monitorTriggers.down_trigger),
@@ -258,12 +270,18 @@
},
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;
@@ -278,15 +296,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 +390,9 @@
</div>
{/each}
</div>
{#if !!orderErrorMessage}
<p class="py-2 text-sm font-medium text-destructive">{orderErrorMessage}</p>
{/if}
</div>
</div>
</div>
@@ -423,17 +456,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 +487,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>
@@ -482,22 +517,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 +715,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}
+197 -205
View File
@@ -1,221 +1,213 @@
<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 { base } from "$app/paths";
import Plus from "lucide-svelte/icons/plus";
import X from "lucide-svelte/icons/x";
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: ""
}
];
}
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);
}
metaTags = siteDataExtractFromDb(data.siteData, {
metaTags: JSON.stringify(metaTags)
}).metaTags;
metaTags = siteDataExtractFromDb(data.siteData, {
metaTags: JSON.stringify(metaTags)
}).metaTags;
let analytics = siteDataExtractFromDb(data.siteData, {
analytics: []
}).analytics;
let analytics = siteDataExtractFromDb(data.siteData, {
analytics: []
}).analytics;
//merge analyticsSupported with analytics
analytics = analyticsSupported.map((supported) => {
let found = analytics.find((a) => a.type === supported.type);
if (found) {
return found;
}
return supported;
});
//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";
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 formState = "idle";
let formStateAnalytics = "idle";
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 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";
let resp = await storeSiteData({
analytics: JSON.stringify(analytics)
});
//print data
let data = await resp.json();
formStateAnalytics = "idle";
if (data.error) {
seoError = data.error;
return;
}
}
</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>
<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>
<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>
<div class="flex w-full justify-end gap-x-2">
{#if !!seoError}
<div class="py-2 text-sm font-medium text-destructive">{seoError}</div>
{/if}
<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>
</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"
>
&#x3C;meta name=&#x22;{metaTag.key}&#x22; content=&#x22;{metaTag.value}&#x22;&#x3E;
</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"
>
&#x3C;meta name=&#x22;{metaTag.key}&#x22; content=&#x22;{metaTag.value}&#x22;&#x3E;
</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
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
</Card.Root>
+6 -6
View File
@@ -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
+11 -4
View File
@@ -3,11 +3,13 @@
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Plus, X, Info } from "lucide-svelte";
import Plus from "lucide-svelte/icons/plus";
import X from "lucide-svelte/icons/x";
import Info from "lucide-svelte/icons/info";
import Loader from "lucide-svelte/icons/loader";
import autoAnimate from "@formkit/auto-animate";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import { base } from "$app/paths";
import { Loader } from "lucide-svelte";
import * as RadioGroup from "$lib/components/ui/radio-group";
import { Tooltip } from "bits-ui";
import ColorPicker from "svelte-awesome-color-picker";
@@ -15,6 +17,7 @@
export let data;
let formState = "idle";
let themeError = "";
let themeData = {
pattern: "none",
@@ -49,6 +52,7 @@
async function formSubmit() {
formState = "loading";
themeError = "";
let themeDataAPI = { ...themeData };
themeDataAPI.colors = JSON.stringify(themeDataAPI.colorsJ);
themeDataAPI.font = JSON.stringify(themeDataAPI.fontJ);
@@ -59,7 +63,7 @@
let data = await resp.json();
formState = "idle";
if (data.error) {
alert(data.error);
themeError = data.error;
return;
}
}
@@ -345,7 +349,10 @@
</div>
</div>
<hr />
<div class="flex w-full justify-end">
<div class="flex w-full justify-end gap-x-2">
{#if !!themeError}
<div class="py-2 text-sm font-medium text-destructive">{themeError}</div>
{/if}
<Button type="submit" disabled={formState === "loading"}>
Save
{#if formState === "loading"}
+163 -23
View File
@@ -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>$&#123;variable&#125;</code>
<code>&#123;&#123;variable&#125;&#125;</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}
+22 -12
View File
@@ -9,7 +9,13 @@
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { Share2, ArrowRight, Settings, TrendingUp, Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
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";
@@ -229,16 +235,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 +315,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 +332,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" }}
+2 -1
View File
@@ -1,7 +1,8 @@
<script>
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { Languages, Menu } from "lucide-svelte";
import Languages from "lucide-svelte/icons/languages";
import Menu from "lucide-svelte/icons/menu";
import { base } from "$app/paths";
import { analyticsEvent } from "$lib/boringOne";
import GMI from "$lib/components/gmi.svelte";
+8 -1
View File
@@ -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>
+30 -30
View File
@@ -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>
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "VERFÜGBAR",
"Upcoming Maintenance": "Bevorstehende Wartung",
"Updates": "Updates",
"Uptime": "Verfügbarkeit"
"Uptime": "Verfügbarkeit",
"View in detail": "Details anzeigen"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "OPPE",
"Upcoming Maintenance": "Kommende vedligeholdelse",
"Updates": "Opdateringer",
"Uptime": "Oppetid"
"Uptime": "Oppetid",
"View in detail": "Vis i detaljer"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "UP",
"Upcoming Maintenance": "Upcoming Maintenance",
"Updates": "Updates",
"Uptime": "Uptime"
"Uptime": "Uptime",
"View in detail": "View in detail"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "अप",
"Upcoming Maintenance": "आगामी मेंटेनेंस",
"Updates": "अपडेट्स",
"Uptime": "अपटाइम"
"Uptime": "अपटाइम",
"View in detail": "विस्तार में देखें"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "稼働中",
"Upcoming Maintenance": "今後のメンテナンス",
"Updates": "更新",
"Uptime": "稼働時間"
"Uptime": "稼働時間",
"View in detail": "詳細を表示"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "작동 중",
"Upcoming Maintenance": "예정된 유지보수",
"Updates": "업데이트",
"Uptime": "업타임"
"Uptime": "업타임",
"View in detail": "자세히 보기"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "OPPE",
"Upcoming Maintenance": "Kommende vedlikehold",
"Updates": "Oppdateringer",
"Uptime": "Oppetid"
"Uptime": "Oppetid",
"View in detail": "Vis detaljer"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "BEREIKBAAR",
"Upcoming Maintenance": "Aankomend onderhoud",
"Updates": "Updates",
"Uptime": "Uptime"
"Uptime": "Uptime",
"View in detail": "Bekijk in detail"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "РАБОТАЕТ",
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
"Updates": "Обновления",
"Uptime": "Время работы"
"Uptime": "Время работы",
"View in detail": "Подробный просмотр"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -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"
}
+2 -1
View File
@@ -60,5 +60,6 @@
"UP": "可用",
"Upcoming Maintenance": "即将进行的维护",
"Updates": "更新",
"Uptime": "正常运行时间"
"Uptime": "正常运行时间",
"View in detail": "详细查看"
}
+2
View File
@@ -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,
};
+284 -7
View File
@@ -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 } from "../tool.js";
import getSMTPTransport from "../notification/smtps.js";
const saltRounds = 10;
const DUMMY_SECRET = "DUMMY_SECRET";
@@ -302,6 +305,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);
};
@@ -397,6 +417,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 +464,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 +499,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 +932,250 @@ 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 GetSiteMap = async (cookies) => {
let siteMapData = [];
let siteURLData = await GetSiteDataByKey("siteURL");
+83 -73
View File
@@ -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);
});
};
+169
View File
@@ -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
@@ -403,6 +452,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 +480,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 +820,97 @@ 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();
}
}
export default DbImpl;
+33 -60
View File
@@ -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);
+76 -75
View File
@@ -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;
+19 -19
View File
@@ -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;
+57 -91
View File
@@ -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 -1
View File
@@ -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
+33
View File
@@ -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;
}
+18 -4
View File
@@ -1,6 +1,10 @@
// @ts-nocheck
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
import Mustache from "mustache";
import { WebhookJSONTemplate } from "../../anywhere.js";
import variables from "./variables.js";
import version from "../../version.js";
class Webhook {
url;
@@ -13,7 +17,7 @@ class Webhook {
constructor(trigger_meta, method, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener/3.2.3",
"User-Agent": `Kener/${version()}`,
};
let headers = trigger_meta.headers;
this.trigger_meta = trigger_meta;
@@ -52,8 +56,13 @@ class Webhook {
}
transformData(data) {
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
let view = variables(this.siteData, data);
if (!!!this.trigger_meta.has_webhook_body || !!!this.trigger_meta.webhook_body) {
return Mustache.render(WebhookJSONTemplate, view);
}
//have to keep this for backward compatibility
let body = this.trigger_meta.webhook_body;
body = ReplaceAllOccurrences(body, "${id}", data.id);
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
@@ -67,7 +76,9 @@ class Webhook {
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
return body;
//have to keep this for backward compatibility
return Mustache.render(body, view);
}
type() {
@@ -81,6 +92,9 @@ class Webhook {
headers: this.headers,
body: this.transformData(data),
});
if (!response.ok) {
throw new Error(`Error from webhook`);
}
return response;
} catch (error) {
console.error("Error sending webhook", error);
+10 -17
View File
@@ -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;
}
+4 -1
View File
@@ -1,6 +1,6 @@
// @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";
@@ -29,6 +29,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 -16
View File
@@ -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 -20
View File
@@ -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) {
+5 -19
View File
@@ -4,29 +4,17 @@ import figlet from "figlet";
import { Cron } from "croner";
import { Minuter } from "./cron-minute.js";
import db from "./db/db.js";
import { GetAllSiteData, GetMonitorsParsed, HashString } from "./controllers/controller.js";
import { GetAllSiteData, GetMonitorsParsed } from "./controllers/controller.js";
import { HashString } from "./tool.js";
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
import fs from "fs";
import version from "../version.js";
const jobs = [];
process.env.TZ = "UTC";
let isStartUP = true;
// Get the version from package.json
const getVersion = () => {
try {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packagePath = resolve(__dirname, "../../../package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
return packageJson.version;
} catch (error) {
console.error("Error reading version:", error);
return "unknown";
}
};
const scheduleCronJobs = async () => {
// Fetch and map all active monitors, creating a unique hash for each
const activeMonitors = (await GetMonitorsParsed({ status: "ACTIVE" })).map((monitor) => ({
@@ -85,15 +73,13 @@ async function Startup() {
mainJob.trigger();
const version = getVersion();
figlet("Kener v" + version, function (err, data) {
figlet("Kener v" + version(), function (err, data) {
if (err) {
console.log("Something went wrong...");
return;
}
console.log(data);
console.log(`Kener version ${version} is running!`);
console.log(`Kener version ${version()} is running!`);
});
}
+217
View File
@@ -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, &quot;Apple Color Emoji&quot;,
&quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;
"
>
<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&#x27;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
>&#8202;&#8202;&#8202;</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
>&#8202;&#8202;&#8202;&#8203;</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, &quot;Apple Color Emoji&quot;,
&quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;
"
>
<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
>&#8202;&#8202;&#8202;</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
>&#8202;&#8202;&#8202;&#8203;</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&#x27;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&#x27;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>
+7
View File
@@ -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();
@@ -286,6 +287,11 @@ function GetDbType() {
//sqlite, postgresql, mysql
return knexOb.databaseType;
}
function HashString(str) {
const hash = crypto.createHash("sha256");
hash.update(str);
return hash.digest("hex");
}
export {
IsValidURL,
@@ -315,4 +321,5 @@ export {
Wait,
MaskString,
GetDbType,
HashString,
};
+64
View File
@@ -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();
}
}
}
+1
View File
@@ -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>
+8
View File
@@ -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>
+1
View File
@@ -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>
+66
View File
@@ -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;
+12
View File
@@ -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;
+91
View File
@@ -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>
+5
View File
@@ -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}"/>
+67
View File
@@ -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>
+14
View File
@@ -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>
+31
View File
@@ -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";
}
}
}
+2
View File
@@ -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);
+4 -7
View File
@@ -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(),
};
}
+4 -1
View File
@@ -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.3 </span>
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium">
{$page.data.kenerVersion}
</span>
</a>
</div>
+22 -13
View File
@@ -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(() => {
+71 -73
View File
@@ -1,76 +1,74 @@
<script>
import "../../app.postcss";
import "../../kener.css";
import Nav from "$lib/components/nav.svelte";
import { onMount } from "svelte";
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import Sun from "lucide-svelte/icons/sun";
import Moon from "lucide-svelte/icons/moon";
import { Languages } from "lucide-svelte";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { setMode, mode, ModeWatcher } from "mode-watcher";
export let data;
import "../../app.postcss";
import "../../kener.css";
import Nav from "$lib/components/nav.svelte";
import { onMount } from "svelte";
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import Sun from "lucide-svelte/icons/sun";
import Moon from "lucide-svelte/icons/moon";
import Languages from "lucide-svelte/icons/languages";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { setMode, mode, ModeWatcher } from "mode-watcher";
export let data;
let defaultLocaleKey = data.selectedLang;
let defaultTheme = data.site.theme;
const allLocales = data.site.i18n?.locales.filter((locale) => locale.selected === true);
let defaultLocaleKey = data.selectedLang;
let defaultTheme = data.site.theme;
const allLocales = data.site.i18n?.locales.filter((locale) => locale.selected === true);
function toggleMode() {
if ($mode === "light") {
setMode("dark");
} else {
setMode("light");
}
}
let defaultLocaleValue;
if (!allLocales) {
defaultLocaleValue = "English";
} else {
defaultLocaleValue = allLocales.find((locale) => locale.code === defaultLocaleKey).name;
}
/**
* @param {string} locale
*/
function setLanguage(locale) {
document.cookie = `localLang=${locale};max-age=${60 * 60 * 24 * 365 * 30}`;
if (locale === defaultLocaleKey) return;
defaultLocaleValue = allLocales[locale];
function toggleMode() {
if ($mode === "light") {
setMode("dark");
} else {
setMode("light");
}
}
let defaultLocaleValue;
if (!allLocales) {
defaultLocaleValue = "English";
} else {
defaultLocaleValue = allLocales.find((locale) => locale.code === defaultLocaleKey).name;
}
/**
* @param {string} locale
*/
function setLanguage(locale) {
document.cookie = `localLang=${locale};max-age=${60 * 60 * 24 * 365 * 30}`;
if (locale === defaultLocaleKey) return;
defaultLocaleValue = allLocales[locale];
}
location.reload();
}
let Analytics;
onMount(async () => {
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (localTz != data.localTz) {
if (data.isBot === false) {
document.cookie = "localTz=" + localTz + ";max-age=" + 60 * 60 * 24 * 365 * 30;
location.reload();
}
}
if (defaultTheme != "none") {
setMode(defaultTheme);
}
//
});
let Analytics;
onMount(async () => {
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (localTz != data.localTz) {
if (data.isBot === false) {
document.cookie = "localTz=" + localTz + ";max-age=" + 60 * 60 * 24 * 365 * 30;
location.reload();
}
}
if (defaultTheme != "none") {
setMode(defaultTheme);
}
//
});
</script>
<svelte:head>
<title>{data.site.title}</title>
{#if data.site.favicon && data.site.favicon[0] == "/"}
<link rel="icon" id="kener-app-favicon" href="{base}{data.site.favicon}" />
{:else if data.site.favicon}
<link rel="icon" id="kener-app-favicon" href={data.site.favicon} />
{/if}
<link href={data.site.font.cssSrc} rel="stylesheet" />
{#each Object.entries(data.site.metaTags) as [key, value]}
<meta name={key} content={value} />
{/each}
<title>{data.site.title}</title>
{#if data.site.favicon && data.site.favicon[0] == "/"}
<link rel="icon" id="kener-app-favicon" href="{base}{data.site.favicon}" />
{:else if data.site.favicon}
<link rel="icon" id="kener-app-favicon" href={data.site.favicon} />
{/if}
<link href={data.site.font.cssSrc} rel="stylesheet" />
{#each Object.entries(data.site.metaTags) as [key, value]}
<meta name={key} content={value} />
{/each}
</svelte:head>
<ModeWatcher />
<main
style="
style="
--font-family: {data.site.font.family};
--bg-custom: {data.bgc};
--up-color: {data.site.colors.UP};
@@ -78,17 +76,17 @@
--degraded-color: {data.site.colors.DEGRADED}
"
>
<div class="">
<slot />
</div>
<div class="">
<slot />
</div>
</main>
<style>
/* Apply the global font family using the CSS variable */
* {
font-family: var(--font-family);
}
main {
background-color: var(--bg-custom);
}
/* Apply the global font family using the CSS variable */
* {
font-family: var(--font-family);
}
main {
background-color: var(--bg-custom);
}
</style>
+2 -1
View File
@@ -8,7 +8,8 @@
import { Button } from "$lib/components/ui/button";
import Sun from "lucide-svelte/icons/sun";
import Moon from "lucide-svelte/icons/moon";
import { Languages, Globe } from "lucide-svelte";
import Languages from "lucide-svelte/icons/languages";
import Globe from "lucide-svelte/icons/globe";
import * as Popover from "$lib/components/ui/popover";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { analyticsEvent } from "$lib/boringOne";
+9
View File
@@ -3,6 +3,7 @@ import { FetchData } from "$lib/server/page";
import { GetMonitors, GetIncidentsOpenHome } from "$lib/server/controllers/controller.js";
import { SortMonitor } from "$lib/clientTools.js";
import moment from "moment";
function removeTags(str) {
if (str === null || str === "") return false;
else str = str.toString();
@@ -29,7 +30,9 @@ async function returnTypeOfMonitorsPageMeta(url) {
}
if (!!query.get("group")) {
const groupStart = performance.now();
let g = await GetMonitors({ status: "ACTIVE", tag: query.get("group") });
if (g.length > 0) {
group = g[0];
let typeData = JSON.parse(g[0].type_data);
@@ -39,14 +42,20 @@ async function returnTypeOfMonitorsPageMeta(url) {
}
}
const monitorStart = performance.now();
let monitors = await GetMonitors(filter);
return { monitors, pageType, group };
}
export async function load({ parent, url }) {
const totalLoadStart = performance.now();
const query = url.searchParams;
const metaStart = performance.now();
let { monitors, pageType, group } = await returnTypeOfMonitorsPageMeta(url);
const processStart = performance.now();
let hiddenGroupedMonitorsTags = [];
for (let i = 0; i < monitors.length; i++) {
if (pageType === "home" && monitors[i].monitor_type === "GROUP") {
+3 -1
View File
@@ -6,7 +6,9 @@
import { Badge } from "$lib/components/ui/badge";
import { l } from "$lib/i18n/client";
import { base } from "$app/paths";
import { ArrowRight, ChevronLeft, X } from "lucide-svelte";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import X from "lucide-svelte/icons/x";
import { hotKeyAction, clickOutsideAction } from "svelte-legos";
import { onMount } from "svelte";
import ShareMenu from "$lib/components/shareMenu.svelte";
+24 -4
View File
@@ -1,14 +1,34 @@
// @ts-nocheck
import { GetMonitors } from "$lib/server/controllers/controller.js";
import { GetMonitors, GetLatestMonitoringData, GetLatestStatusActiveAll } from "$lib/server/controllers/controller.js";
import StatusColor from "$lib/color.js";
import { makeBadge } from "badge-maker";
import db from "$lib/server/db/db.js";
import { ErrorSvg } from "$lib/anywhere.js";
export async function GET({ params, setHeaders, url }) {
// @ts-ignore
let monitors = await GetMonitors({ status: "ACTIVE" });
const { tag } = monitors.find((monitor) => monitor.tag === params.tag);
const lastObj = await db.getLatestMonitoringData(tag);
let lastObj;
let activeTags = monitors.map((monitor) => monitor.tag);
if (params.tag == "_") {
lastObj = await GetLatestStatusActiveAll(activeTags);
} else {
if (monitors.length === 0) {
return new Response(ErrorSvg, {
headers: {
"Content-Type": "image/svg+xml",
},
});
}
let m = monitors.find((monitor) => monitor.tag === params.tag);
if (!m) {
return new Response(ErrorSvg, {
headers: {
"Content-Type": "image/svg+xml",
},
});
}
lastObj = await GetLatestMonitoringData(params.tag);
}
//read query params
const query = url.searchParams;
const animate = query.get("animate") || "";
@@ -1,37 +1,62 @@
// @ts-nocheck
import { GetMonitors } from "$lib/server/controllers/controller.js";
import {
GetMonitors,
GetLatestMonitoringData,
GetLatestStatusActiveAll,
GetSiteDataByKey,
} from "$lib/server/controllers/controller.js";
import StatusColor from "$lib/color.js";
import { makeBadge } from "badge-maker";
import db from "$lib/server/db/db.js";
import { ErrorSvg } from "$lib/anywhere.js";
export async function GET({ params, setHeaders, url }) {
// @ts-ignore
let monitors = await GetMonitors({ status: "ACTIVE" });
const { tag, name } = monitors.find((monitor) => monitor.tag === params.tag);
const lastObj = await db.getLatestMonitoringData(tag);
let lastObj;
let name;
let tag;
//read query params
let myColors = await StatusColor();
const query = url.searchParams;
const labelColor = query.get("labelColor") || "#333";
let label = query.get("label") || name;
const color = query.get("color") || myColors[lastObj.status].substr(1);
const style = query.get("style") || "flat";
const siteName = await GetSiteDataByKey("siteName");
let monitors = await GetMonitors({ status: "ACTIVE" });
let activeTags = monitors.map((monitor) => monitor.tag);
if (params.tag == "_") {
lastObj = await GetLatestStatusActiveAll(activeTags);
name = siteName;
tag = "";
} else {
if (monitors.length === 0) {
return new Response(ErrorSvg, {
headers: {
"Content-Type": "image/svg+xml",
},
});
}
let m = monitors.find((monitor) => monitor.tag === params.tag);
tag = m.tag;
name = m.name;
lastObj = await GetLatestMonitoringData(tag);
}
label = label.trim();
//read query params
let myColors = await StatusColor();
const query = url.searchParams;
const labelColor = query.get("labelColor") || "333";
let label = query.get("label") || name;
const color = query.get("color") || myColors[lastObj.status].substr(1);
const style = query.get("style") || "flat";
const format = {
label: label,
message: lastObj.status,
color: color,
labelColor: labelColor,
style: style
};
const svg = makeBadge(format);
label = label.trim();
return new Response(svg, {
headers: {
"Content-Type": "image/svg+xml"
}
});
const format = {
label: label,
message: lastObj.status,
color: color,
labelColor: labelColor,
style: style,
};
const svg = makeBadge(format);
return new Response(svg, {
headers: {
"Content-Type": "image/svg+xml",
},
});
}
@@ -2,18 +2,32 @@
import { ParseUptime, GetMinuteStartNowTimestampUTC } from "$lib/server/tool.js";
import { makeBadge } from "badge-maker";
import moment from "moment";
import db from "$lib/server/db/db.js";
import {
GetMonitors,
GetLastStatusBefore,
InterpolateData,
AggregateData,
GetMonitoringData,
GetMonitoringDataAll,
GetSiteDataByKey,
GetLastStatusBeforeAll,
} from "$lib/server/controllers/controller.js";
import { ErrorSvg } from "$lib/anywhere.js";
export async function GET({ params, url }) {
// @ts-ignore
let monitors = await GetMonitors({ status: "ACTIVE" });
const { name, tag, include_degraded_in_downtime } = monitors.find((monitor) => monitor.tag === params.tag);
if (monitors.length === 0) {
return new Response(ErrorSvg, {
headers: {
"Content-Type": "image/svg+xml",
},
});
}
let name, tag, include_degraded_in_downtime;
const siteName = await GetSiteDataByKey("siteName");
const query = url.searchParams;
let sinceLast = query.get("sinceLast");
if (sinceLast == undefined || isNaN(sinceLast) || sinceLast < 60) {
@@ -22,7 +36,7 @@ export async function GET({ params, url }) {
const rangeInSeconds = sinceLast;
const now = Math.floor(Date.now() / 1000);
const since = GetMinuteStartNowTimestampUTC() - rangeInSeconds;
let label = query.get("label") || name;
const hideDuration = query.get("hideDuration") === "true";
const duration = moment.duration(rangeInSeconds, "seconds");
@@ -35,10 +49,32 @@ export async function GET({ params, url }) {
formatted = `${duration.minutes()}m`;
}
let todayDataDb = await db.getMonitoringData(tag, since, now);
let anchorStatus = await GetLastStatusBefore(tag, since);
todayDataDb = InterpolateData(todayDataDb, since, anchorStatus, now);
let todayDataDb, anchorStatus;
if (params.tag == "_") {
name = siteName;
let activeTags = monitors.map((monitor) => monitor.tag);
//if anyone has include_degraded_in_downtime = "YES" then we will include degraded in downtime
let hasIncludeDowntime = monitors.find((monitor) => monitor.include_degraded_in_downtime === "YES");
include_degraded_in_downtime = hasIncludeDowntime ? "YES" : "NO";
todayDataDb = await GetMonitoringDataAll(activeTags, since, now);
anchorStatus = await GetLastStatusBeforeAll(activeTags, since);
} else {
const m = monitors.find((monitor) => monitor.tag === params.tag);
if (!!!m) {
return new Response(ErrorSvg, {
headers: {
"Content-Type": "image/svg+xml",
},
});
}
tag = m.tag;
name = m.name;
include_degraded_in_downtime = m.include_degraded_in_downtime;
todayDataDb = await GetMonitoringData(tag, since, now);
anchorStatus = await GetLastStatusBefore(tag, since);
}
todayDataDb = InterpolateData(todayDataDb, since, anchorStatus, now);
let calculatedData = AggregateData(todayDataDb);
let numerator = calculatedData.UPs + calculatedData.DEGRADEDs;
@@ -48,7 +84,7 @@ export async function GET({ params, url }) {
}
let uptime = ParseUptime(numerator, denominator) + "%";
let label = query.get("label") || name;
const labelColor = query.get("labelColor") || "#333";
const color = query.get("color") || "#0079FF";
const style = query.get("style") || "flat";
+1 -1
View File
@@ -5,5 +5,5 @@ import { base } from "$app/paths";
import { format } from "date-fns";
export async function load({ parent, url }) {
throw redirect(302, base + "/incidents/" + format(new Date(), "MMMM-yyyy"));
throw redirect(302, base + "/incidents/" + format(new Date(), "MMMM-yyyy"));
}
@@ -8,7 +8,7 @@ import {
} from "$lib/server/controllers/controller.js";
import { BeginningOfDay } from "$lib/server/tool.js";
import db from "$lib/server/db/db.js";
import { format, addMonths, parse, subMonths, getUnixTime, startOfMonth, endOfMonth } from "date-fns";
import { format, addDays, subDays, parse, getUnixTime, startOfMonth, endOfMonth } from "date-fns";
import { f } from "$lib/i18n/client";
export async function load({ parent, url, params, cookies }) {
@@ -16,17 +16,33 @@ export async function load({ parent, url, params, cookies }) {
const siteData = parentData.site;
let monitors = await GetMonitors({ status: "ACTIVE" });
const query = url.searchParams;
let todayStart = BeginningOfDay({ timeZone: parentData.localTz });
let tomorrowStart = todayStart + 86400;
let locale = GetLocaleFromCookie(parentData.site, cookies);
let month = params.month; //January-2021
if (!!!month) {
month = format(new Date(), "MMMM-yyyy");
}
let monthStart = startOfMonth(parse(month, "MMMM-yyyy", new Date()));
let monthEnd = endOfMonth(parse(month, "MMMM-yyyy", new Date()));
let nextMonth = addDays(monthEnd, 1);
let prevMonth = subDays(monthStart, 1);
let monthStartTs = getUnixTime(startOfMonth(parse(month, "MMMM-yyyy", new Date())));
// let xx = parse(month, "MMMM-yyyy", new Date());
let monthEndTs = getUnixTime(endOfMonth(parse(month, "MMMM-yyyy", new Date())));
let midnightMonthStartUTCTimestamp = getUnixTime(monthStart);
let midnightMonthEndUTCTimestamp = getUnixTime(monthEnd);
let midnightNextMonthUTCTimestamp = getUnixTime(nextMonth);
let midnightPrevMonthUTCTimestamp = getUnixTime(prevMonth);
let monthStartTs = BeginningOfDay({
date: new Date(midnightMonthStartUTCTimestamp * 1000),
timeZone: parentData.localTz,
});
let monthEndTs = BeginningOfDay({
date: new Date(midnightMonthEndUTCTimestamp * 1000),
timeZone: parentData.localTz,
});
let incidents = await GetIncidentsPage(monthStartTs, monthEndTs);
@@ -49,9 +65,12 @@ export async function load({ parent, url, params, cookies }) {
return {
incidents: incidents,
nextMonthName: f(addMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en", parentData.localTz),
prevMonthName: f(subMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en", parentData.localTz),
thisMonthName: f(parse(month, "MMMM-yyyy", new Date()), "MMMM-yyyy", "en", parentData.localTz),
nextMonthName: f(new Date(midnightNextMonthUTCTimestamp * 1000), "MMMM-yyyy", "en", parentData.localTz),
prevMonthName: f(new Date(midnightPrevMonthUTCTimestamp * 1000), "MMMM-yyyy", "en", parentData.localTz),
thisMonthName: f(monthEnd, "MMMM-yyyy", "en", parentData.localTz),
canonical: siteData.siteURL + "/incidents/" + month,
midnightNextMonthUTCTimestamp: midnightNextMonthUTCTimestamp,
midnightPrevMonthUTCTimestamp: midnightPrevMonthUTCTimestamp,
midnightMonthStartUTCTimestamp: midnightMonthStartUTCTimestamp + 86400,
};
}
@@ -2,7 +2,9 @@
import { Badge } from "$lib/components/ui/badge";
import Incident from "$lib/components/IncidentNew.svelte";
import { Button } from "$lib/components/ui/button";
import { ArrowRight, ArrowLeft, CalendarCheck2, ChevronLeft } from "lucide-svelte";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import ArrowLeft from "lucide-svelte/icons/arrow-left";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { base } from "$app/paths";
import { goto } from "$app/navigation";
import { l, f } from "$lib/i18n/client";
@@ -85,7 +87,7 @@
<div class="mesh mx-auto min-w-full max-w-[655px] rounded-md px-4 py-12 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center text-muted">
<h1 class=" text-5xl font-extrabold leading-tight">
{f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
{f(new Date(data.midnightMonthStartUTCTimestamp * 1000), "MMMM, yyyy", selectedLang, $page.data.localTz)}
</h1>
<p class="mx-auto mt-4 max-w-xl font-medium sm:text-xl">
{l(data.lang, "Incident Updates")}
@@ -141,7 +143,7 @@
}}
>
<ArrowLeft class="arrow mr-2 h-4 w-4" />
{f(parse(data.prevMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
{f(new Date(data.midnightPrevMonthUTCTimestamp * 1000), "MMMM, yyyy", selectedLang, $page.data.localTz)}
</Button>
<Button
variant="secondary"
@@ -150,7 +152,7 @@
window.location.href = `${base}/incidents/${data.nextMonthName}`;
}}
>
{f(parse(data.nextMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
{f(new Date(data.midnightNextMonthUTCTimestamp * 1000), "MMMM, yyyy", selectedLang, $page.data.localTz)}
<ArrowRight class="arrow ml-2 h-4 w-4" />
</Button>
</div>
@@ -1,22 +1,19 @@
<script>
import "../../../../app.postcss";
import "../../../../kener.css";
import "../../../../manage.css";
import { base } from "$app/paths";
import { setMode, mode, ModeWatcher } from "mode-watcher";
setMode("dark");
import "../../../../app.postcss";
import "../../../../kener.css";
import "../../../../manage.css";
import { base } from "$app/paths";
import { setMode, mode, ModeWatcher } from "mode-watcher";
setMode("dark");
</script>
<ModeWatcher />
<svelte:head>
<meta name="description" content="Set up Kener" />
<meta name="robots" content="noindex" />
<link rel="icon" href="{base}/logo96.png" />
<meta name="description" content="Set up Kener" />
<meta name="robots" content="noindex" />
<link rel="icon" href="{base}/logo96.png" />
</svelte:head>
<main class="setup">
<!-- ========== HEADER ========== -->
<!-- ========== END HEADER ========== -->
<slot></slot>
<slot></slot>
</main>
@@ -1,117 +1,118 @@
<script>
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Alert from "$lib/components/ui/alert";
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Alert from "$lib/components/ui/alert";
let form = {
email: ""
};
export let data;
let isResendSet = data.isResendSet && data.isSecretSet;
if (!isResendSet && !data.isSMTPSet) {
data.view = "error";
data.error = `<p>Environment variables are not set. Read the documentation to set them. https://kener.ing/docs/environment-vars</p>
let form = {
email: ""
};
export let data;
let isResendSet = data.isResendSet && data.isSecretSet;
if (!isResendSet && !data.isSMTPSet) {
data.view = "error";
data.error = `<p>Environment variables are not set. Read the documentation to set them. https://kener.ing/docs/environment-vars</p>
<br/>
<p>Either Set RESEND_API_KEY, RESEND_SENDER_EMAIL <br>or<br> Set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM_EMAIL</p>
<br/>
<p>If both are set then SMTP will take priority</p>`;
}
}
if (data.isSiteURLSet === false) {
data.view = "error";
data.error =
"Site URL is not set. Read the documentation to set it. https://kener.ing/docs/site";
}
if (data.isSiteURLSet === false) {
data.view = "error";
data.error = "Site URL is not set. Read the documentation to set it. https://kener.ing/docs/site";
}
</script>
<svelte:head>
<title>Forgot Password Kener</title>
<title>Forgot Password Kener</title>
</svelte:head>
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="{base}/logo.png" alt="Your Company" />
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight">Reset Password</h2>
<p class="mt-4 text-center">Follow instructions to reset your password</p>
</div>
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="{base}/logo.png" alt="Your Company" />
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight">Reset Password</h2>
<p class="mt-4 text-center">Follow instructions to reset your password</p>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
{#if data.view == "error"}
<Alert.Root variant="destructive" class="my-4">
<Alert.Title>Error</Alert.Title>
<Alert.Description>{@html data.error}</Alert.Description>
</Alert.Root>
{/if}
{#if data.view == "forgot"}
<form class="space-y-6" action="{base}/manage/forgot/submit" method="POST">
<div>
<label for="email" class="block text-sm/6 font-medium">Email address</label>
<div class="mt-2">
<Input
bind:value={form.email}
type="email"
name="email"
id="form_email"
autocomplete="email"
placeholder="user@example.com"
required
/>
</div>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
{#if data.view == "error"}
<Alert.Root variant="destructive" class="my-4">
<Alert.Title>Error</Alert.Title>
<Alert.Description>{@html data.error}</Alert.Description>
</Alert.Root>
{/if}
{#if data.view == "forgot"}
<form class="space-y-6" action="{base}/manage/forgot/submit" method="POST">
<div>
<label for="email" class="block text-sm/6 font-medium">Email address</label>
<div class="mt-2">
<Input
bind:value={form.email}
type="email"
name="email"
id="form_email"
autocomplete="email"
placeholder="user@example.com"
required
/>
</div>
</div>
<div>
<Button type="submit" class="w-full">Reset Password</Button>
</div>
</form>
{/if}
{#if data.view == "sent"}
<Alert.Root variant="success" class="my-4">
<Alert.Title>Success</Alert.Title>
<Alert.Description
>An email has been sent to {data.email} with instructions on how to reset your password.</Alert.Description
>
</Alert.Root>
{/if}
<div>
<Button type="submit" class="w-full">Reset Password</Button>
</div>
</form>
{/if}
{#if data.view == "sent"}
<Alert.Root variant="success" class="my-4">
<Alert.Title>Success</Alert.Title>
<Alert.Description class="flex flex-col gap-y-2">
<p>An email has been sent to {data.email} with instructions on how to reset your password.</p>
<p>
If you don't see the email, check other places it might be, like your junk, spam, social, or other folders.
</p>
<p>
Once you have reset your password, you can <a
class="font-semibold text-blue-500"
href="{base}/manage/signin">login</a
>
</p>
</Alert.Description>
</Alert.Root>
{/if}
{#if data.view == "token"}
<form class="space-y-6" action="{base}/manage/forgot/reset" method="POST">
<div>
<label for="password" class="block text-sm/6 font-medium">New password</label>
<div class="mt-2">
<Input
type="password"
name="password"
id="form_password"
autocomplete="new-password"
placeholder="********"
required
/>
</div>
<Input
type="hidden"
name="token"
id="form_password"
bind:value={data.token}
required
/>
</div>
{#if data.view == "token"}
<form class="space-y-6" action="{base}/manage/forgot/reset" method="POST">
<div>
<label for="password" class="block text-sm/6 font-medium">New password</label>
<div class="mt-2">
<Input
type="password"
name="password"
id="form_password"
autocomplete="new-password"
placeholder="********"
required
/>
</div>
<Input type="hidden" name="token" id="form_password" bind:value={data.token} required />
</div>
<div>
<Button type="submit" class="w-full">Confirm Password</Button>
</div>
</form>
{/if}
{#if data.view == "success"}
<Alert.Root variant="success" class="my-4">
<Alert.Title>Success</Alert.Title>
<Alert.Description
>Your password has been reset successfully. <a
class="font-semibold text-blue-500"
href="{base}/manage/signin">Login</a
> with you new password</Alert.Description
>
</Alert.Root>
{/if}
</div>
<div>
<Button type="submit" class="w-full">Confirm Password</Button>
</div>
</form>
{/if}
{#if data.view == "success"}
<Alert.Root variant="success" class="my-4">
<Alert.Title>Success</Alert.Title>
<Alert.Description
>Your password has been reset successfully. <a class="font-semibold text-blue-500" href="{base}/manage/signin"
>Login</a
> with you new password</Alert.Description
>
</Alert.Root>
{/if}
</div>
</div>
@@ -2,88 +2,56 @@
import { json, redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import db from "$lib/server/db/db.js";
import { Resend } from "resend";
import getSMTPTransport from "$lib/server/notification/smtps.js";
import { SendEmailWithTemplate, GetSiteLogoURL } from "$lib/server/controllers/controller.js";
import forgotPasswordTemplate from "$lib/server/templates/forgot_pass.html?raw";
import {
HashPassword,
GenerateSalt,
GenerateToken,
VerifyToken,
GetSMTPFromENV
HashPassword,
GenerateSalt,
GenerateToken,
VerifyToken,
GetSMTPFromENV,
} from "$lib/server/controllers/controller.js";
export async function POST({ request, cookies }) {
//read form post data email and password
const formdata = await request.formData();
const email = formdata.get("email");
const senderEmail = process.env.RESEND_SENDER_EMAIL;
const resendKey = process.env.RESEND_API_KEY;
const siteURL = await db.getSiteData("siteURL");
//read form post data email and password
const formdata = await request.formData();
const email = formdata.get("email");
const senderEmail = process.env.RESEND_SENDER_EMAIL;
const resendKey = process.env.RESEND_API_KEY;
const siteURL = await db.getSiteData("siteURL");
const siteName = await db.getSiteData("siteName");
const logo = await db.getSiteData("logo");
//check if any entry in user table is already there
let userDB = await db.getUserByEmail(email);
if (!!!userDB) {
let errorMessage = "User does not exist";
throw redirect(302, base + "/manage/forgot?view=sent&email=" + email);
}
//check if any entry in user table is already there
let userDB = await db.getUserByEmail(email);
if (!!!userDB) {
let errorMessage = "User does not exist";
throw redirect(302, base + "/manage/forgot?view=sent&email=" + email);
}
//generate token
let token = await GenerateToken({
email: email
});
//generate token
let token = await GenerateToken({
email: email,
});
//send email with link to reset password
let link = siteURL.value + base + "/manage/forgot?view=token&token=" + token;
let subject = "[Reset Password] Kener Reset password request";
let message = `
<!DOCTYPE html>
<html>
<head>
<title>Reset Password</title>
</head>
<body>
<p>Click on the link below to reset your password:</p>
<p><a href="${link}">Reset Password</a></p>
<p>You can also copy paste this link ${link}</p>
<p></p>This link will expire in 1 hour.</p>
<p>If you did not request a password reset, please ignore this email.</p>
</body>
</html>
`;
let emailText = `
//send email with link to reset password
let link = siteURL.value + base + "/manage/forgot?view=token&token=" + token;
let subject = "[Reset Password] Kener Reset password request";
let emailText = `
Click on the link below to reset your password:
${link}
This link will expire in 1 hour.
If you did not request a password reset, please ignore this email.
`;
let mail = {
from: senderEmail,
to: [email],
subject: subject,
text: emailText,
html: message
};
let smtpData = GetSMTPFromENV();
if (!!smtpData) {
const transporter = getSMTPTransport(smtpData);
const mailOptions = {
from: smtpData.smtp_from_email,
to: email,
subject: mail.subject,
html: mail.html,
text: mail.text
};
try {
await transporter.sendMail(mailOptions);
} catch (error) {
console.error("Error sending email via SMTP", error);
return;
}
} else {
const resend = new Resend(resendKey);
await resend.emails.send(mail);
}
let emailData = {
logo_url: await GetSiteLogoURL(siteURL.value, logo.value, base),
link: link,
brand_name: siteName.value,
};
throw redirect(302, base + "/manage/forgot?view=sent&email=" + email);
await SendEmailWithTemplate(forgotPasswordTemplate, emailData, email, subject, emailText);
throw redirect(302, base + "/manage/forgot?view=sent&email=" + email);
}
@@ -0,0 +1,40 @@
// @ts-nocheck
import {
GetActiveInvitationByToken,
UpdateUserData,
UpdateInvitationStatusToAccepted,
} from "$lib/server/controllers/controller.js";
import { INVITE_VERIFY_EMAIL } from "$lib/server/constants.js";
export async function load({ parent, url }) {
const query = url.searchParams;
const invitation_token = query.get("token");
let invite = await GetActiveInvitationByToken(invitation_token);
let header = "Error";
let message = "Invalid Invitation Token. The invitation token is invalid or expired.";
let error = true;
if (!!invite) {
if (invite.invitation_type === INVITE_VERIFY_EMAIL) {
let updateData = {
userID: invite.invited_user_id,
updateKey: "is_verified",
updateValue: 1,
};
try {
let updateResp = await UpdateUserData(updateData);
let updateInvitationResp = await UpdateInvitationStatusToAccepted(invitation_token);
error = false;
header = "Email Verified Successfully";
message = "Thanks for verifying your email";
} catch (e) {
console.error(e);
}
}
}
return {
error: error,
message: message,
header: header,
};
}

Some files were not shown because too many files have changed in this diff Show More