Compare commits

..

79 Commits

Author SHA1 Message Date
Raj Nandan Sharma 78d8f311c0 Merge pull request #400 from ToxykAuBleu/fix/398-docker-not-starting 2025-04-30 17:50:14 +05:30
ToxykAuBleu c52ba95fd0 Removing bad import of "@sveltejs/kit" in gamedigCall.js. 2025-04-30 11:37:43 +00:00
Raj Nandan Sharma aad7b5c6ea chore: update Dockerfile to use latest package versions for alpine build 2025-04-30 07:37:47 +05:30
Raj Nandan Sharma abec24d65b Merge pull request #397 from ToxykAuBleu/fix/396-gamedig-install
Updating Gamedig to 5.3.0 + fix build app.
2025-04-30 07:32:50 +05:30
Raj Nandan Sharma e0c3d1864c chore: remove specific version for iputils in Dockerfile 2025-04-30 07:14:54 +05:30
ToxykAuBleu bd25c6fe05 Updating Gamedig to 5.3.0.
Moving Gamedig to dependencies (not devDependencies).
2025-04-29 20:34:13 +00:00
Raj Nandan Sharma 4ec5e14431 chore: update changelog for version 3.2.14 with new features and bug fixes 2025-04-29 22:58:09 +05:30
Raj Nandan Sharma c9b8c5bec3 chore: update changelogs for version 3.2.14 with new features and bug fixes 2025-04-29 22:50:28 +05:30
Raj Nandan Sharma 223883adf7 Merge pull request #385 from rajnandan1/event-subscription
Event subscription
2025-04-29 22:21:22 +05:30
Raj Nandan Sharma 83c2d7ff79 fix: improve subscription UX and email code handling
Enhances subscription menu by showing a default icon when no image is available.
Replaces hardcoded verification code in email template with a variable.
Prevents background scrolling when subscription menu is open.
Removes redundant new subscriber response in API.
Removes unused documentation button from subscriptions page.
Adjusts checkbox styling for better UI consistency.
2025-04-29 22:07:39 +05:30
Raj Nandan Sharma cf33f5a2f6 refactor: improve downtime duration calculation in closure comment #387 2025-04-29 21:20:04 +05:30
Raj Nandan Sharma 92b4596924 refactor: centralize monitoring data insertion logic
Introduces a unified function for monitoring data insertion,
adds support for group monitor status updates, and ensures
consistency by routing all monitoring data writes through the
new logic. Removes duplicated validation and streamlines
group monitor handling for improved reliability.

Relates to #123
2025-04-29 20:45:14 +05:30
Raj Nandan Sharma ce9b748570 style: adjust height of checkbox and refine status color banner with hover effects 2025-04-28 22:57:51 +05:30
Raj Nandan Sharma 87a9069ab4 feat: add configurable site status banner with i18n
Introduces an option to enable a site status banner summarizing the operational state of monitored systems. Calculates and displays aggregated status with a progress bar. Localizes banner messages in all supported languages.

Helps users quickly assess overall system health from the homepage.
2025-04-28 22:18:15 +05:30
Raj Nandan Sharma 3e6a76c8c3 Refactor monitoring data update logic to include type parameter and optimize database insertion with batching 2025-04-28 10:48:33 +05:30
Raj Nandan Sharma 3509955374 Implement date input for monitoring data modification and enhance validation logic 2025-04-28 10:08:58 +05:30
Raj Nandan Sharma 225711f95d Merge branch 'main' into event-subscription 2025-04-27 16:10:35 +05:30
Raj Nandan Sharma 5436647cd3 Merge pull request #380 from mcorbin/feature/allow-modifying-monitors-data
Allow users to modify monitors data
2025-04-27 16:10:22 +05:30
Raj Nandan Sharma fbd9da3b33 Merge branch 'main' into feature/allow-modifying-monitors-data 2025-04-27 16:07:31 +05:30
Raj Nandan Sharma edb6705ac8 Merge branch 'main' into event-subscription 2025-04-27 15:18:47 +05:30
Raj Nandan Sharma 382a2a5bcc Merge pull request #384 from jensvandenreyt/feature/extend-monitor-api
feat: add CRUD api for monitors
2025-04-27 15:18:29 +05:30
Raj Nandan Sharma b5967607d2 Refactor game data handling and improve metadata for event pages
- Updated monitorSheet.svelte to use raw JSON data for AllGamesList, replacing the previous import method.
- Refactored game retrieval logic in monitorSheet.svelte to utilize GetGameFromId function with the new AllGamesList structure.
- Modified monitorsAdd.svelte to parse AllGamesList from raw JSON, ensuring consistent data usage across components.
- Enhanced event pages (+page.svelte) with appropriate meta tags for better SEO, including titles and descriptions.
- Added a new all-games-list.json file containing comprehensive game data for improved functionality and maintainability.
- Updated subscriptions page to clarify SMTP setup instructions for users.
2025-04-27 15:05:35 +05:30
Raj Nandan Sharma 03bbb6c3b6 Merge branch 'main' into event-subscription 2025-04-27 12:57:17 +05:30
Raj Nandan Sharma 6054be0ff1 feat: enhance subscription table with index for optimized queries 2025-04-27 12:56:31 +05:30
Raj Nandan Sharma ff7355bcc2 Merge pull request #389 from ToxykAuBleu/feat/gamedig
Adding Gamedig monitor functionality and related documentation
2025-04-27 12:55:52 +05:30
Raj Nandan Sharma 23a9124e03 feat(i18n): localizes subscription and maintenance UI
Integrates translation function into subscription and maintenance components,
localizing all user-facing strings. Expands translation resources for multiple languages
to cover newly localized phrases, improving accessibility and user experience for non-English users.
2025-04-27 12:12:10 +05:30
Raj Nandan Sharma ac41807cdd Merge pull request #392 from myned/fix-smtp-secure
Fix SMTP_SECURE environment variable always being truthy when set
2025-04-27 11:08:36 +05:30
Raj Nandan Sharma d95bb13e6e refactor: unify incident URL paths and add incident pagination
Unifies URLs for incident events by switching from singular to plural path segments and removes obsolete routes to streamline navigation. Introduces paginated incident fetching with filtering and sorting to improve performance and scalability. Updates UI components to reflect new paths and behaviors.

Relates to improved incident management and navigation.
2025-04-27 10:13:00 +05:30
Raj Nandan Sharma b9d58ba3c1 remove debug console log from +page.svelte 2025-04-26 14:01:06 +05:30
Raj Nandan Sharma a648b29f31 feat: add incident link copy button and chevron hide option
Introduces a copy-to-clipboard button for incident links with animated feedback. Adds ability to hide accordion chevrons and refactors chevron and copy button styles. Updates incident URL generation and allows querying monitors by tag.

Relates to improved UX for incident management.
2025-04-26 14:00:23 +05:30
Myned e702feb04f fix: coerce SMTP_SECURE to Number before boolean 2025-04-26 00:18:51 -05:00
Raj Nandan Sharma ce83355e58 feat: add incident event email notifications via queue
Introduces a queue-based system to send email notifications for incident events, including creation, updates, monitor changes, and comments. Fetches eligible subscribers dynamically and uses configurable templates for email delivery. Also updates the UI to improve the Preview button styling for better user experience.
2025-04-25 20:38:59 +05:30
ToxykAuBleu 683beed3b7 Refactoring port validation into a function: IsValidPort(port). 2025-04-24 07:01:37 +00:00
ToxykAuBleu c9095036eb Fixing auto formatting done in #e63992a. 2025-04-24 06:36:36 +00:00
Jens Vandenreyt 29ec935d67 feat: update json OpenAPI spec
Also improve 400 and 404 error messages
2025-04-23 16:17:53 +02:00
ToxykAuBleu 339015a093 Adding documentation related to Gamedig monitor. 2025-04-23 11:49:48 +00:00
Jens Vandenreyt b8b5b4170f Merge branch 'rajnandan1:main' into feature/extend-monitor-api 2025-04-23 11:59:21 +02:00
ToxykAuBleu 8a104e4480 Adding timeout feature for Gamedig monitor. 2025-04-23 06:31:41 +00:00
Raj Nandan Sharma 89e1a5e88a Merge branch 'main' into event-subscription 2025-04-23 11:31:58 +05:30
Raj Nandan Sharma 8e5746d068 style: Update logout button design and improve subscription info display 2025-04-23 11:31:40 +05:30
Raj Nandan Sharma 1df7c6560a Merge pull request #386 from lolen/main
feat: Polish translation
2025-04-23 11:24:38 +05:30
Piotr Lasota 0ff4546379 feat: Polish translation 2025-04-22 22:56:13 +02:00
Raj Nandan Sharma ab036c968d Refactor subscription trigger configuration and update database schema for subscription management 2025-04-22 22:58:45 +05:30
Raj Nandan Sharma 625ece2e7b feat: Add token generation with expiry and enhance subscription management
- Implemented GenerateTokenWithExpiry function for JWT token generation with a specified expiry time.
- Added GetSubscriberByID function to retrieve subscriber details by ID.
- Updated CreateNewSubscription to accept subscriber ID directly.
- Introduced GetSubscribersPaginated for paginated retrieval of subscribers.
- Enhanced subscription trigger management with CreateSubscriptionTrigger and GetSubscriptionTriggerByEmail functions.
- Updated email_code.html to change expiration notice from 1 day to 5 minutes.
- Modified layout.server.js to include canSendEmail check for conditional rendering.
- Updated subscription page to display subscribers with pagination and subscription status management.
- Refactored subscription-related API endpoints for improved clarity and functionality.
- Added server-side logic for managing subscription triggers and email settings.
2025-04-22 21:30:17 +05:30
Jens Vandenreyt 41842cc1b9 feat: add CRUD api for monitors
Added API endpoints for search, get, create, update and delete of monitors
2025-04-22 16:07:54 +02:00
ToxykAuBleu e63992a7a5 Adding eval feature for Gamedig monitor.
Sorting by default games by name.
2025-04-22 11:54:07 +00:00
ToxykAuBleu 7ae5d8f1b1 Adding Gamedig monitor functionality. 2025-04-19 19:13:40 +00:00
Raj Nandan Sharma 0a34f2138a feat: Implement subscriptions page and update navigation 2025-04-19 13:56:31 +05:30
Raj Nandan Sharma 38b326fe72 feat: Implement subscription management features
- Added functionality to create, update, and delete subscribers and their subscriptions in the controller and database implementation.
- Introduced new API endpoints for subscribing and unsubscribing users, including email verification and management of subscription preferences.
- Created a new Svelte component for managing subscriptions, allowing users to subscribe to updates and manage their preferences.
- Added email template for sending verification codes to subscribers.
- Implemented utility functions for generating random numbers and validating email addresses.
2025-04-19 13:09:51 +05:30
Mathieu Corbin 8785c609a9 Allow users to modify monitors data
Sometimes, it's useful to modify data in the monitoring_data table, for example, in case of a false positive.

I'm adding a new button in the monitor list that opens a popup
allowing users to change the status of monitor data for a given time
range.
2025-04-17 15:13:44 +02:00
github-actions 1a67560137 Auto-generate README.md with release versions 2025-04-08 05:33:49 +00:00
Raj Nandan Sharma 6c7534b9b6 Merge pull request #374 from rajnandan1/release/3.2.13
Release/3.2.13
2025-04-08 10:46:07 +05:30
Raj Nandan Sharma ae6c2c985f feat: enhance incident details formatting and add active status filter for triggers #370 2025-04-08 10:44:17 +05:30
Raj Nandan Sharma 940331b87f feat: update package description and enhance incident date formatting #371 2025-04-08 09:23:12 +05:30
Raj Nandan Sharma e6b5600a47 feat: add Umami analytics support and enhance capture functionality 2025-04-02 21:16:34 +05:30
Raj Nandan Sharma ce2a6ab756 feat: update version to 3.2.13 and refactor monitoring data calculations 2025-04-01 22:51:55 +05:30
Raj Nandan Sharma 6af91af639 chore: bump version to 3.2.12 in package.json 2025-04-01 21:22:43 +05:30
github-actions e1ff156b91 Auto-generate README.md with release versions 2025-03-31 12:16:53 +00:00
Raj Nandan Sharma b56d20ac35 Merge pull request #367 from rajnandan1/release/3.2.12
feat: add kenerTheme support and enhance footer HTML structure
2025-03-31 17:17:30 +05:30
Raj Nandan Sharma 9ee7a7b861 feat: add kenerTheme support and enhance footer HTML structure 2025-03-31 17:12:40 +05:30
Raj Nandan Sharma f73ffce3f0 feat: enhance documentation and styling for custom JS/CSS guide and incident comments 2025-03-30 12:58:19 +05:30
Raj Nandan Sharma 0997967787 docs: update environment variables documentation for CORS and reverse proxy setup
feat: modify analytics event naming convention for consistency

fix: adjust delete monitor confirmation form layout for better UX

style: refine positioning of monitor action buttons for improved alignment

refactor: streamline password validation imports in setup and forgot password routes

fix: remove unnecessary line breaks in layout server file
2025-03-29 22:33:17 +05:30
github-actions 3476ec2b29 Auto-generate README.md with release versions 2025-03-28 16:39:18 +00:00
Raj Nandan Sharma e0cf568aa9 feat: implement custom handle function to modify response headers 2025-03-28 21:50:07 +05:30
Raj Nandan Sharma c12d7f8345 feat: add null check for document before dispatching analytics event 2025-03-28 21:18:26 +05:30
Raj Nandan Sharma 9ba115d5ab Merge pull request #366 from rajnandan1/release/3.2.11
release 3.2.11
2025-03-28 20:56:59 +05:30
Raj Nandan Sharma 3756bbfd06 chore: bump version to 3.2.11 in package.json 2025-03-28 20:54:50 +05:30
Raj Nandan Sharma 9096bcc641 feat: import analytics event module for enhanced tracking in incident page 2025-03-28 20:53:44 +05:30
Raj Nandan Sharma c4d04854e5 feat: enhance monitor deletion confirmation UI and add analytics event for incident back button click 2025-03-28 20:52:59 +05:30
Raj Nandan Sharma d95fc0005b feat: indicate Markdown support in incident summary and comment sections 2025-03-28 20:40:24 +05:30
Raj Nandan Sharma 2b9093184a feat: add support for Markdown language in events and enhance SEO documentation as requested in #359 2025-03-28 20:34:19 +05:30
Raj Nandan Sharma 831376054b feat: add analytics capture snippets and enhance event tracking also added plausible and ms clarity #355 2025-03-28 15:07:45 +05:30
Raj Nandan Sharma 885ee0b6d6 feat: implement monitor deletion functionality with confirmation 2025-03-26 11:47:53 +05:30
Raj Nandan Sharma 511d0f26dd feat: add email validation in CreateUpdateTrigger function 2025-03-26 10:16:36 +05:30
github-actions 5263698625 Auto-generate README.md with release versions 2025-03-25 17:45:05 +00:00
Raj Nandan Sharma 7859f9564e fix: bump version to 3.2.10 in package.json 2025-03-25 22:58:04 +05:30
github-actions 149456682d Auto-generate README.md with release versions 2025-03-25 17:20:55 +00:00
Raj Nandan Sharma 8c040997da fix: refactor monitorTriggers initialization into a separate function 2025-03-25 22:47:05 +05:30
Raj Nandan Sharma 868fed0176 fix: update tzdata version in Dockerfile to use latest available 2025-03-25 22:38:13 +05:30
116 changed files with 8308 additions and 1096 deletions
+1
View File
@@ -9,6 +9,7 @@ config/server.yaml
/src/lib/.kener
/package
.env
.vscode
.env.*
!.env.example
vite.config.js.timestamp-*
+9 -9
View File
@@ -25,14 +25,14 @@ RUN apt-get update && apt-get install -y \
FROM node:${ALPINE_VERSION_TAG} AS builder-alpine
RUN apk add --no-cache --update \
build-base=0.5-r3 \
python3=3.12.9-r0 \
py3-pip=24.3.1-r0 \
python3 \
py3-pip \
make=4.4.1-r2 \
g++=14.2.0-r4 \
sqlite=3.48.0-r0 \
sqlite-dev=3.48.0-r0 \
tzdata=2025a-r0 \
iputils=20240905-r0
sqlite \
sqlite-dev \
tzdata \
iputils
FROM builder-${VARIANT} AS builder
@@ -83,9 +83,9 @@ RUN apt-get update && apt-get install -y \
FROM node:${ALPINE_VERSION_TAG} AS final-alpine
RUN apk add --no-cache --update \
iputils=20240905-r0 \
sqlite=3.48.0-r0 \
tzdata=2025a-r0
iputils \
sqlite \
tzdata
FROM final-${VARIANT} AS final
+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.8"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.8-blue" alt="Kener latest stable version: 3.2.8" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.13"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.13-blue" alt="Kener latest stable version: 3.2.13" /></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.8)</td>
<td>Latest stable release (aka 3.2.13)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.8" target="_blank"><code>3.2.8</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.13" target="_blank"><code>3.2.13</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.8) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.13) 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.8) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.13) 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.8)</td>
<td>Latest stable release (aka 3.2.13)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.8-alpine" target="_blank"><code>3.2.8-alpine</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.13-alpine" target="_blank"><code>3.2.13-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.8) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.13) 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.8) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.13) release within that major version (3.x.x)</td>
</tr>
</table>
+22 -257
View File
@@ -7,6 +7,28 @@ description: Changelogs for Kener
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
## v3.2.14
### ✨ Features
- **Gamedig Monitor Functionality:** Introduced a new monitor type using Gamedig to monitor over 320+ games and services. Includes options for host, port, timeout, game/service selection, and documentation updates. ([#389](https://github.com/rajnandan1/kener/pull/389) by ToxykAuBleu)
- **Event Subscription:** Added functionality for event subscriptions, involving new database tables (`subscribers`, `subscriptions`, `subscription_triggers`), UI enhancements, a comprehensive game list, and package version update (3.2.13 to 3.2.14). ([#385](https://github.com/rajnandan1/kener/pull/385) by rajnandan1)
- **CRUD API for Monitors:** Implemented API endpoints for creating, reading, updating, and deleting monitors (`GET /api/monitor`, `POST /api/monitor`, `GET /api/monitor/[monitor_id]`, `PUT /api/monitor/[monitor_id]`, `DELETE /api/monitor/[monitor_id]`). Includes OpenAPI spec updates. ([#384](https://github.com/rajnandan1/kener/pull/384) by jensvandenreyt)
- **Modify Monitor Data:** Added the ability for users to modify historical monitoring data via a new UI option and backend API, useful for correcting past statuses (e.g., false positives). ([#380](https://github.com/rajnandan1/kener/pull/380) by mcorbin)
- **Site Status Banner:** Now you can choose to show a site status banner in the main home page. The setting has to be turned on in the Home section of the kener portal.
- **Upcoming Maintenance**: Dedicated Page for upcoming maintenance events.
- **Event Page**: Dedicated page for each event (incident/maintenance)
- **Admin Portal UI Update**: Revamped kener management portal for better accessibility.
### 🐛 Bug Fixes
- **SMTP Secure Variable Fix:** Corrected the evaluation of the `SMTP_SECURE` environment variable to properly handle values like '0' or empty strings, preventing SSL errors with STARTTLS. ([#392](https://github.com/rajnandan1/kener/pull/392) by myned)
- **Group Monitor Fix:** Fix group monitor using queues
### 🌍 Internationalization
- **Polish Translation:** Added Polish language support (`pl.json`) and integrated it into the application's localization framework. ([#386](https://github.com/rajnandan1/kener/pull/386) by lolen)
## v3.2.5
<picture>
@@ -85,260 +107,3 @@ This update is fully backward compatible with existing installations. User accou
- **Documentation**
- Added comprehensive documentation for Heartbeat Monitors
- Updated API documentation with new endpoints
## v3.2.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **Improved Monitor Evaluation Functions**
- Replaced unsafe `eval()` with secure `Function()` constructors across all monitor types
- Added support for using modules (like cheerio) directly in evaluation functions
- **Enhanced API Monitors**
- Raw response data is now passed directly to eval functions instead of base64 encoded
- Added `modules` parameter with access to cheerio for HTML parsing
- Updated default evaluation function to use the new parameter structure
- **Improved TCP & Ping Monitors**
- Simplified evaluation functions with direct access to ping/TCP data
- Removed unnecessary base64 encoding/decoding steps
- Better error handling for invalid evaluation functions
- **Documentation Updates**
- Updated all examples and documentation for the new evaluation function signatures
- Added more detailed explanations of input parameters
- Improved examples showing usage with the new parameter structure
### Breaking Changes
- **Monitor Evaluation Functions**
- Custom evaluation functions will need to be updated to the new parameter structure
- API monitors: `(statusCode, responseTime, responseRaw, modules)` instead of `(statusCode, responseTime, responseDataBase64)`
- TCP/Ping monitors: `(arrayOfPings)` instead of `(responseDataBase64)`
### Fixes
- Fixed "cheerio is undefined" errors in API monitor evaluations
- Improved error handling and logging for monitor evaluation failures
- Security enhancements by removing `eval()` usage
### Migration
If you're using custom evaluation functions in your monitors, you'll need to update them to the new format:
#### API Monitors
```javascript
// Old format
;(async function (statusCode, responseTime, responseDataBase64) {
const resp = atob(responseDataBase64)
// Your logic here
})
```
```javascript
// New format
;(async function (statusCode, responseTime, responseRaw, modules) {
// responseRaw is the direct response - no need to decode
// Access cheerio with modules.cheerio
// Your logic here
})
```
#### TCP/Ping Monitors
```javascript
// Old format
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
// Your logic here
})
```
```javascript
// New format
;(async function (arrayOfPings) {
// arrayOfPings is directly available - no need to decode
// Your logic here
})
```
## v3.1.8
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **Timezone Support & UI Toggle**
- Added timezone support using `date-fns-tz` for improved date formatting.
- Introduced a UI toggle in settings to switch between different timezones.
- **Enhanced Incident Management**
- Improved incident filtering to prevent duplicate auto incidents when creating manual incidents.
- Added support for incident sources to refine incident handling.
- **Dynamic Cron Job Scheduling**
- Cron jobs are now dynamically added and removed based on active monitors.
- Ensures jobs are triggered in the correct order and prevents duplicate incidents.
- **Monitor Component Improvements**
- Refactored monitor component for better data display and interaction.
- Improved uptime calculations.
- Added a dropdown to select time ranges for better visibility.
### Fixes & Improvements
- **Refactored Incident Handling & Scheduling** for better reliability and performance.
- **UI Responsiveness Fixes** to improve the experience on smaller screens.
- **Dependency Updates** to support new timezone functionality and ensure stability.
## v3.0.10
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- Added TCP monitors
### Breaking Changes
- Ping monitors will break.
### Fixes
- Bug fixes in the UI
## v3.0.9
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- Support of SMTP for email notifications. Read more [here](/docs/triggers/#email-smtp)
- Introduction of event type `MAINTENANCE` for incidents.
- You can write eval function for ping now in monitors. Read more [here](/docs/monitors-ping/#eval)
- Added category filter for monitor management.
### Fixes
- Support longer TLD in `siteURL` example `https://example.network`
- Remove googleapis preconnect and preload
- Fixed wrong action url in webhook.
## v3.0.1
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- Support for i18n in dates.
- Support of i18n in monitor embeds. Read more [here](/docs/embed#javascript-parameters)
## v3.0.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- New APIs for creating incidents and pushing updates. Read more [here](/docs/kener-apis)
- Incident management is now part of the admin UI and removed from the config file.
- The UI colors have been updated to be more muted.
- Email Notifications for incidents using [resend](https://resend.com).
- New Kener management portal. No monitors.yaml or site.yaml needed anymore
- Login Page and Setup Page
- Remove Github dependency
- Options to disable square or dot pattern
- Support for new languages
- Multiple DB support (mysql, postgres, sqlite3)
- New API reference
- New documentation site
## v2.0.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- Added support for sqlite3 and removed dependency on file system
- Added support for postgres database. Read more [here](/docs/database)
- Added support for alerting. Read more [here](/docs/alerting)
- Added color customization. Read more [here](/docs/customize-site#color)
- Added three new customizations for home page. Read more [here](/docs/customize-site#barstyle)
- `barStyle`
- `barRoundness`
- `summaryStyle`
### Migration
Kener will automatically migrate your data from file system to sqlite3. If you are using a custom domain, you need to update the `site.yaml` file with the new `siteURL` field. Read more [here](/docs/customize-site#siteURL)
## v0.0.16
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- Added support for `hideURLForGet` in monitors. Read more [here](/docs/monitors)
- New SVG badges for LIVE status. Read more [here](/docs/status-badges#live)
- `[Breaking Change]` Removed dependency on Environment variable `PUBLIC_KENER_FOLDER`. Read more [here](#v0-0-16-migration)
- Simplified build and deploy process
- Added support for fonts. Read more [here](/docs/customize-site#font)
- Added support for home page pattern. Read more [here](/docs/customize-site#pattern)
- Added support for adding your analytics provider. Read more [here](/docs/site-analytics)
- New Documentation Site
- Addes support for `sqaures` pattern in home page. Read more [here](/docs/customize-site#pattern)
- Redesigned the UI for better consistency
- Embed now supports background color using a parameter `bgc`. Read more [here](/docs/embed#javascript-parameters)
- Now title in `site.yaml` is `<title>` and `siteName` is actually the name of the site. Read more [here](/docs/customize-site#siteName)
### Migration
#### Source
- Move data from `PUBLIC_KENER_FOLDER` to `/database` file.
- Move `site.yaml` to `/config` folder
- Move `monitors.yaml` to `/config` folder
#### Docker
- Use `-v $(pwd)/database:/app/database` and `-v $(pwd)/config:/app/config` in your docker run command
+44 -1
View File
@@ -33,10 +33,53 @@ To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS the
```css
.my-class {
color: red;
color: red;
}
```
<div class="note danger">
Do not include &#x3C;style&#x3E; tags.
</div>
## Grid Layout
To change from a column layout to a grid layout in your Kener instance, you can use the following CSS:
```css
@media (min-width: 1330px) {
.section-monitors,
.section-categories,
.section-hero,
.section-back,
.section-events,
.section-legend,
.section-categories,
.section-browser-events {
width: 1330px !important;
max-width: 1330px !important;
}
.section-monitors .monitor-root {
background-color: transparent;
border: none !important;
box-shadow: none;
}
.section-monitors .monitor-root .monitors-card {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}
.section-monitors .monitor-root .monitors-card .monitor {
grid-column: span 1;
border-radius: 0.375rem;
background-color: hsl(var(--card) / var(--tw-bg-opacity, 1));
border-width: 1px;
}
.section-monitors .monitor-root .monitors-card .monitor:last-child {
border-bottom: inherit;
border-bottom-width: 1px !important;
}
}
```
+9 -2
View File
@@ -25,6 +25,13 @@ Set this to the origin of your website(protocl + hostname + port if there). This
export ORIGIN=http://localhost:3000
```
If your host is at let us say `https://example.com`, then set the ORIGIN to `https://example.com`. This is required for CORS.
If you are using a reverse proxy like nginx, make sure to set the `ORIGIN` to the domain name of your website.
```bash
export ORIGIN=https://example.com
```
## PORT
Defaults to 3000 if not specified
@@ -37,8 +44,8 @@ export PORT=4242
By default kener runs on `/` but you can change it to `/status` or any other path.
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
- Important: This env variable should be present during both build and run time
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
- Important: This env variable should be present during both build and run time
```bash
export KENER_BASE_PATH=/status
+68
View File
@@ -0,0 +1,68 @@
---
title: Gamedig Monitors | Kener
description: Monitor games and services
---
# Gamedig Monitors
Gamedig monitors are used to monitor games and services. Currently, 320+ games and some services are available. You can view the entire list [here](https://github.com/gamedig/node-gamedig/blob/HEAD/GAMES_LIST.md). More than just the state of the game/service, it provides information about your game/service, such as players list, world name, connection method, and so on.
<div class="border rounded-md">
![Monitors Gamedig](/documentation/m_gamedig.png)
</div>
## Host
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The host is the domain name, IPv4 or IPv6 of the game/service you want to monitor. The host should be valid.
## Port
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The port is the query port of the game/service you want to monitor. Some games use a port, different from the one to connect, to accept queries, such as Minecraft with `query-port`.
If you do not know the query port, you can let the monitor guess the port by checking the option `Guess port`.
## Timeout
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The timeout is used to define the time in milliseconds after which the monitor should timeout. If the monitor does not respond within the timeout period, the monitor will be marked as down. For example, 10000 will set the timeout to 10 seconds. It is required and has to be a number greater than 2000 (or 2 seconds).
## Request additional rules
Valve games can provide additional 'rules' to Gamedig monitors. If checked, they will be available in `reponseRaw.raw` (see [Eval](#eval)), beware that it may increase query time.
## Eval
The eval is used to define the JavaScript code that should be used to evaluate the response. It is optional and has be a valid JavaScript code.
This is an anonymous JS function, it should return a **Promise**, that resolves or rejects to `{status, latency}`, by default it looks like this.
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and latency(number)
> `{status: "DEGRADED", latency: 200}`.
```javascript
(async function (responseTime, responseRaw) {
return {
status: "UP",
latency: responseTime
}
})
```
- `responseTime` **REQUIRED** is a number. It is the latency in milliseconds
- `responseRaw` **REQUIRED** is the raw response of the game/service. It varies from game by game, we recommend first printing responseRaw with a simple `console.log`.
## Additional notes
Some games/services need more configuration on their part to accept correctly any query of the monitor. You can consult these notes [here](https://github.com/gamedig/node-gamedig/blob/HEAD/GAMES_LIST.md#games-with-additional-notes).
+4
View File
@@ -103,6 +103,10 @@ To monitor the DNS records, you need to provide the domain name and the record t
To monitor the ping, you need to provide the IP address or the domain name. If the ping is not successful, the monitor will be marked as down. You can read more about PING monitoring [here](/docs/monitors-ping).
### GAMEDIG
To monitor game/service, you need to provide the game or service to monitor, the IP address or the domain name and the port. If the query is not successful, the monitor will be marked as down. You can read more about GAMEDIG monitoring [here](/docs/monitors-gamedig).
---
## Triggers
+11 -1
View File
@@ -21,7 +21,17 @@ Add API key of your Amplitude account
### Mixpanel
Add token of your Mixpanel account.
Add project token of your Mixpanel account.
### Plausible
- Add domain of your Plausible account. Example: `example.com`
- Add API key of your Plausible account. Default is `https://plausible.io/api/event`
- Add Script Source of your Plausible account. Default is `https://plausible.io/js/script.pageview-props.tagged-events.js`
### Microsoft Clarity
Add Project ID of your Microsoft Clarity account.
## Search Engine Optimization
+5
View File
@@ -155,6 +155,11 @@
"title": "Heartbeat Monitor",
"link": "/docs/monitors-heartbeat",
"file": "/monitors-heartbeat.md"
},
{
"title": "Gamedig Monitor",
"link": "/docs/monitors-gamedig",
"file": "/monitors-gamedig.md"
}
]
},
+2 -1
View File
@@ -9,7 +9,8 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"noImplicitAny": false
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
@@ -0,0 +1,49 @@
export function up(knex) {
return knex.schema
.createTable("subscribers", (table) => {
table.increments("id").primary();
table.string("subscriber_send").notNullable();
table.text("subscriber_meta").nullable();
table.string("subscriber_type").notNullable();
table.string("subscriber_status").notNullable();
table.datetime("created_at").defaultTo(knex.fn.now());
table.datetime("updated_at").defaultTo(knex.fn.now());
// Add unique constraint on subscriber_send and subscriber_type
table.unique(["subscriber_send", "subscriber_type"]);
// Add index on subscriber_send for better query performance
table.index(["subscriber_send"]);
})
.createTable("subscriptions", (table) => {
table.increments("id").primary();
table.integer("subscriber_id").unsigned().notNullable();
table.string("subscriptions_status").notNullable();
table.string("subscriptions_monitors").notNullable();
table.text("subscriptions_meta").nullable();
table.datetime("created_at").defaultTo(knex.fn.now());
table.datetime("updated_at").defaultTo(knex.fn.now());
// Add unique constraint on subscriber_id and subscriptions_monitors
// This constraint also creates an index that will be used for queries
table.unique(["subscriber_id", "subscriptions_monitors"]);
// Add index to optimize queries filtering by status and monitors
table.index(["subscriptions_status", "subscriptions_monitors"]);
})
.createTable("subscription_triggers", (table) => {
table.increments("id").primary();
table.string("subscription_trigger_type").notNullable().unique();
table.string("subscription_trigger_status").notNullable();
table.text("config").nullable();
table.datetime("created_at").defaultTo(knex.fn.now());
table.datetime("updated_at").defaultTo(knex.fn.now());
});
}
export function down(knex) {
return knex.schema
.dropTableIfExists("subscription_triggers")
.dropTableIfExists("subscriptions")
.dropTableIfExists("subscribers");
}
+901 -3
View File
@@ -25,6 +25,10 @@
"name": "Monitors",
"description": "APIs to interact with monitors"
},
{
"name": "MonitorStatus",
"description": "APIs to interact with monitor status"
},
{
"name": "Incidents",
"description": "APIs to integrate incidents"
@@ -457,7 +461,7 @@
"operationId": "updateMonitorStatus",
"summary": "Update status of a monitor",
"description": "Update status of a monitor at a given timestamp UTC",
"tags": ["Monitors"],
"tags": ["MonitorStatus"],
"security": [
{
"bearerAuth": []
@@ -516,7 +520,7 @@
"operationId": "getMonitorStatus",
"summary": "Get status of a monitor",
"description": "get status of a monitor at timestamp",
"tags": ["Monitors"],
"tags": ["MonitorStatus"],
"security": [
{
"bearerAuth": []
@@ -1097,6 +1101,900 @@
}
]
}
}
},
"/api/monitor": {
"get": {
"summary": "Search Monitor",
"description": "Search Monitor",
"operationId": "searchMonitor",
"tags": ["Monitors"],
"parameters": [
{
"name": "tag",
"in": "query",
"schema": {
"type": "string",
"example": "{{tag}}"
},
"description": "Optional tag to filter results"
}
],
"responses": {
"200": {
"description": "Response200",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T11:14:49.822Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 15
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "SSL"
},
"name": {
"type": "string",
"example": "Github SSL Certificate"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "GithubSSL"
},
"type_data": {
"type": "string",
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T12:05:01.941Z"
}
}
},
"example": [
{
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T12:05:01.941Z"
}
]
},
"examples": {
"Response200": {
"value": [
{
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T12:05:01.941Z"
}
]
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"post": {
"summary": "Create Monitor",
"description": "API to create monitors",
"operationId": "createMonitor",
"tags": ["Monitors"],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-22T10:10:00.546Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 15
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "API"
},
"name": {
"type": "string",
"example": "Github"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "Github"
},
"type_data": {
"type": "string",
"example": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}"
},
"updated_at": {
"type": "string",
"example": "2025-04-22T10:10:00.546Z"
}
}
},
"examples": {
"Create Monitor": {
"value": {
"category_name": "Home",
"created_at": "2025-04-22T10:10:00.546Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "API",
"name": "Github",
"status": "ACTIVE",
"tag": "Github",
"type_data": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}",
"updated_at": "2025-04-22T10:10:00.546Z"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Response201",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T12:35:21.030Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 16
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "API"
},
"name": {
"type": "string",
"example": "Github"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "Github"
},
"type_data": {
"type": "string",
"example": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T12:35:21.030Z"
}
}
},
"examples": {
"Response201": {
"value": {
"category_name": "Home",
"created_at": "2025-04-23T12:35:21.030Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 16,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "API",
"name": "Github",
"status": "ACTIVE",
"tag": "Github",
"type_data": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}",
"updated_at": "2025-04-23T12:35:21.030Z"
}
}
}
}
}
},
"400": {
"description": "Response400",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "monitor id must be empty or 0"
}
}
},
"examples": {
"Response400": {
"value": {
"error": "monitor id must be empty or 0"
}
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/monitor/{monitorId}": {
"get": {
"summary": "Get Monitor",
"description": "API to get a specified monitor",
"operationId": "getMonitor",
"tags": ["Monitors"],
"responses": {
"200": {
"description": "Response200",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T11:14:49.822Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 15
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "SSL"
},
"name": {
"type": "string",
"example": "Github SSL Certificate"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "GithubSSL"
},
"type_data": {
"type": "string",
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T12:05:01.941Z"
}
}
},
"examples": {
"Response200": {
"value": {
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T12:05:01.941Z"
}
}
}
}
}
},
"404": {
"description": "Response404",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "monitor does not exist"
}
}
},
"examples": {
"Response404": {
"value": {
"error": "monitor does not exist"
}
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"put": {
"summary": "Update Monitor",
"description": "API to update a specified monitor",
"operationId": "updateMonitor",
"tags": ["Monitors"],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T11:14:49.822Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 20
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "SSL"
},
"name": {
"type": "string",
"example": "Github SSL Certificate"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "GithubSSL"
},
"type_data": {
"type": "string",
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T12:05:01.941Z"
}
}
},
"examples": {
"Update Monitor": {
"value": {
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 20,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T12:05:01.941Z"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Response200",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T11:14:49.822Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 15
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "SSL"
},
"name": {
"type": "string",
"example": "Github SSL Certificate"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "GithubSSL"
},
"type_data": {
"type": "string",
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T13:38:22.965Z"
}
}
},
"examples": {
"Response200": {
"value": {
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T13:38:22.965Z"
}
}
}
}
}
},
"400": {
"description": "Response400",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "id mismatch: payload: 15, url: 14"
}
}
},
"examples": {
"Response400": {
"value": {
"error": "id mismatch: payload: 15, url: 14"
}
}
}
}
}
},
"404": {
"description": "Response404",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "monitor does not exist"
}
}
},
"examples": {
"Response404": {
"value": {
"error": "monitor does not exist"
}
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"delete": {
"summary": "Delete Monitor",
"description": "API to delete a specified monitor",
"operationId": "deleteMonitor",
"tags": ["Monitors"],
"responses": {
"200": {
"description": "Response200",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
},
"examples": {
"Response200": {
"value": {}
}
}
}
}
},
"404": {
"description": "Response404",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "monitor does not exist"
}
}
},
"examples": {
"Response404": {
"value": {
"error": "monitor does not exist"
}
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"parameters": [
{
"name": "monitorId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
]
}
}
}
+544 -5
View File
@@ -1,17 +1,20 @@
{
"name": "kener",
"version": "3.2.5",
"version": "3.2.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kener",
"version": "3.2.5",
"version": "3.2.14",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
@@ -32,6 +35,7 @@
"express": "^4.18.2",
"figlet": "^1.8.0",
"fs-extra": "^11.1.1",
"gamedig": "^5.3.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
@@ -218,6 +222,36 @@
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.0"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz",
@@ -243,6 +277,21 @@
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-markdown": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.2.tgz",
"integrity": "sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz",
@@ -852,6 +901,17 @@
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/css": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.11.tgz",
"integrity": "sha512-FuAnusbLBl1SEAtfN8NdShxYJiESKw9LAFysfea1T96jD3ydBn12oYjaSG1a04BQRIUd93/0D8e5CV1cUMkmQg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
@@ -861,6 +921,17 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
"version": "1.4.21",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
@@ -892,6 +963,16 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.2.tgz",
"integrity": "sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@@ -1184,6 +1265,18 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sindresorhus/is": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
"integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sveltejs/adapter-auto": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.1.1.tgz",
@@ -1296,6 +1389,18 @@
"tslib": "^2.8.0"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
"license": "MIT",
"dependencies": {
"defer-to-connect": "^2.0.1"
},
"engines": {
"node": ">=14.16"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
@@ -1332,6 +1437,12 @@
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"license": "MIT"
},
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@@ -1740,6 +1851,39 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/barse": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz",
"integrity": "sha512-UEpvriJqAn8zuVinYICuKoPttZy3XxXEoqX/V2uYAL4zzJRuNzCK3+20nAu3YUIa2U7G53kf90wfBIp9/A+Odw==",
"license": "MIT",
"dependencies": {
"readable-stream": "~1.0.2"
}
},
"node_modules/barse/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"license": "MIT"
},
"node_modules/barse/node_modules/readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/barse/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -1984,6 +2128,45 @@
"node": ">= 0.8"
}
},
"node_modules/cacheable-lookup": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
"integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
"license": "MIT",
"engines": {
"node": ">=14.16"
}
},
"node_modules/cacheable-request": {
"version": "10.2.14",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz",
"integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==",
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "^4.0.2",
"get-stream": "^6.0.1",
"http-cache-semantics": "^4.1.1",
"keyv": "^4.5.3",
"mimic-response": "^4.0.0",
"normalize-url": "^8.0.0",
"responselike": "^3.0.0"
},
"engines": {
"node": ">=14.16"
}
},
"node_modules/cacheable-request/node_modules/mimic-response": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -2456,6 +2639,12 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
@@ -2709,6 +2898,15 @@
"node": ">=0.10.0"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -3277,6 +3475,13 @@
"node": ">= 0.6"
}
},
"node_modules/event-to-promise": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/event-to-promise/-/event-to-promise-0.7.0.tgz",
"integrity": "sha512-VOBBfyaADfe378ZzG0tgkzmsvzUyeU5arehrFzNRt5yaASUDshgctTwSrPI17ocAwR3+YftsxRClHF+GBKFByQ==",
"deprecated": "Use promise-toolbox/fromEvent instead",
"license": "MIT"
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -3378,6 +3583,24 @@
"node": ">=8.6.0"
}
},
"node_modules/fast-xml-parser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.1.tgz",
"integrity": "sha512-Kqq/ewnRACQ20e0BlQ5KqHRYWRBp7yv+jttK4Yj2yY+2ldgCoxJkrP1NHUhjypsJ+eQXlGJ/jebM3wa60s1rbQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^2.0.5"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastq": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
@@ -3536,6 +3759,15 @@
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
"integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==",
"license": "MIT",
"engines": {
"node": ">= 14.17"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -3670,6 +3902,40 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gamedig": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.0.tgz",
"integrity": "sha512-4TlnM7cWQKNOYOiyY4gUlauwYB5PInig40wZE+9qRWa78YB/G2cynT7vHb1A6vy3iygYa2Dol0cOavrjRGzTRA==",
"license": "MIT",
"dependencies": {
"fast-xml-parser": "5.2.1",
"gbxremote": "0.2.1",
"got": "13.0.0",
"iconv-lite": "0.6.3",
"long": "5.3.2",
"minimist": "1.2.8",
"seek-bzip": "2.0.0",
"varint": "6.0.0"
},
"bin": {
"gamedig": "bin/gamedig.js"
},
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/gamedig/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
@@ -3691,6 +3957,21 @@
"node": ">=10"
}
},
"node_modules/gbxremote": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.2.1.tgz",
"integrity": "sha512-SMehu6Y6ndq2Qgp9VxAb8Np3f+UUD+RWoW2SAMaxzGS96rWXyr4T1GGkecO0HHtxeH1m7pEh4FJWB8a/6aM2XQ==",
"dependencies": {
"any-promise": "^1.1.0",
"barse": "~0.4.2",
"event-to-promise": "^0.7.0",
"string-to-stream": "^1.0.1",
"xmlrpc": "^1.3.1"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -3756,6 +4037,18 @@
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-symbol-description": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
@@ -3860,6 +4153,31 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/got": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
"integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^5.2.0",
"@szmarczak/http-timer": "^5.0.1",
"cacheable-lookup": "^7.0.0",
"cacheable-request": "^10.2.8",
"decompress-response": "^6.0.0",
"form-data-encoder": "^2.1.2",
"get-stream": "^6.0.1",
"http2-wrapper": "^2.1.10",
"lowercase-keys": "^3.0.0",
"p-cancelable": "^3.0.0",
"responselike": "^3.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -4007,6 +4325,12 @@
"entities": "^4.4.0"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"license": "BSD-2-Clause"
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -4023,6 +4347,19 @@
"node": ">= 0.8"
}
},
"node_modules/http2-wrapper": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
"integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.2.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -4687,6 +5024,12 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@@ -4748,6 +5091,15 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -4943,11 +5295,23 @@
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.0.tgz",
"integrity": "sha512-5vvY5yF1zF/kXk+L94FRiTDa1Znom46UjPCH6/XbSvS8zBKMFBHTJk8KDMqJ+2J6QezQFi7k1k8v21ClJYHPaw==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/lowercase-keys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
@@ -5520,6 +5884,18 @@
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz",
"integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-all": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
@@ -5827,6 +6203,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/p-cancelable": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -6538,6 +6923,12 @@
"node": ">=6"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -6614,6 +7005,18 @@
],
"license": "MIT"
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -6881,6 +7284,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"license": "MIT"
},
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -6890,6 +7299,21 @@
"node": ">=8"
}
},
"node_modules/responselike": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
"integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
"license": "MIT",
"dependencies": {
"lowercase-keys": "^3.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -7172,6 +7596,12 @@
"rimraf": "bin.js"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"license": "ISC"
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@@ -7179,6 +7609,28 @@
"license": "MIT",
"peer": true
},
"node_modules/seek-bzip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz",
"integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==",
"license": "MIT",
"dependencies": {
"commander": "^6.0.0"
},
"bin": {
"seek-bunzip": "bin/seek-bunzip",
"seek-table": "bin/seek-bzip-table"
}
},
"node_modules/seek-bzip/node_modules/commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/selderee": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
@@ -7616,6 +8068,52 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-to-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz",
"integrity": "sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.1",
"readable-stream": "^2.1.0"
}
},
"node_modules/string-to-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/string-to-stream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/string-to-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/string-to-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -7775,6 +8273,18 @@
"node": ">=0.10.0"
}
},
"node_modules/strnum": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",
"integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
@@ -8613,6 +9123,12 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
"license": "MIT"
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -8906,6 +9422,29 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xmlbuilder": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
"integrity": "sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xmlrpc": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz",
"integrity": "sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==",
"license": "MIT",
"dependencies": {
"sax": "1.2.x",
"xmlbuilder": "8.2.x"
},
"engines": {
"node": ">=0.8",
"npm": ">=1.0.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+21 -17
View File
@@ -1,26 +1,26 @@
{
"name": "kener",
"version": "3.2.9",
"version": "3.2.14",
"private": false,
"license": "MIT",
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
"description": "Kener is a modern, open-source status page application built with Node.js. It provides real-time monitoring, uptime tracking, incident management, and beautiful dashboards. Perfect for DevOps teams, SaaS providers, and businesses needing reliable service status communication with minimal setup.",
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
"keywords": [
"Node.js application",
"Open-source status page",
"Service monitoring tool",
"Real-time incident management",
"Customizable reporting",
"Service outage tracker",
"User-friendly dashboard",
"Incident communication platform",
"Scalable monitoring solution",
"Community-driven software",
"Website status tracker",
"Incident response tool",
"System status monitoring",
"Service reliability management",
"Incident alert system"
"status page",
"uptime monitoring",
"incident management",
"DevOps tools",
"service reliability",
"open source",
"Node.js",
"dashboard",
"system monitoring",
"status alerts",
"outage communication",
"API monitoring",
"SaaS status",
"performance metrics",
"status reporting"
],
"repository": {
"type": "git",
@@ -68,8 +68,11 @@
"type": "module",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
@@ -90,6 +93,7 @@
"express": "^4.18.2",
"figlet": "^1.8.0",
"fs-extra": "^11.1.1",
"gamedig": "^5.3.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
-4
View File
@@ -1,7 +1,3 @@
code:not([class^="language-"]) {
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs dark:bg-gray-800;
}
.sidebar-item.active,
.sidebar-item:hover {
color: #ed702d;
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event);
response.headers.delete("Link");
return response;
}
+95 -98
View File
@@ -1,108 +1,10 @@
/*one is the class for dotted background*/
.dots-pattern {
position: absolute;
top: 0px;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background: linear-gradient(177deg, rgba(255, 137, 131, 0.5) 0%, rgba(35, 136, 224, 0.05) 60%);
clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%);
}
.dots-pattern::after {
content: "";
position: absolute;
background-image: radial-gradient(rgba(0, 0, 0, 0) 1.5px, var(--background-kener) 1px);
background-size: 14px 14px;
width: 100%;
height: 100vh;
top: 0;
transform: blur(3px);
left: 0;
}
.squares-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
/* clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%); */
}
.squares-pattern::after {
content: "";
position: absolute;
background-image:
linear-gradient(#444cf7 1px, transparent 1px),
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
-webkit-mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.8) 20%,
rgba(0, 0, 0, 0.6) 40%,
rgba(0, 0, 0, 0.4) 60%,
rgba(0, 0, 0, 0.2) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0.1) 20%,
rgba(0, 0, 0, 0.05) 40%,
rgba(0, 0, 0, 0.025) 60%,
rgba(0, 0, 0, 0.0125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-size: 32px 32px;
width: 100%;
height: 100vh;
top: 0;
left: 0;
}
.dark .squares-pattern::after {
-webkit-mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-image:
linear-gradient(#616fbf 1px, transparent 1px),
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
}
/*Needed to overlay content on top of dotted bg*/
section {
position: relative;
z-index: 1;
}
/*Needed overlay content on top of dotted bg*/
.blurry-bg {
/* background-color: var(--background-kener-rgba);
box-shadow: 0 0 64px 64px var(--background-kener-rgba); */
}
:root {
--up-color: #4ead94;
@@ -939,3 +841,98 @@ textarea::placeholder {
.dark .like-input {
background-color: rgba(0, 0, 0, 0.1) !important;
}
.ͼo .cm-gutters {
border-top-left-radius: var(--radius);
border-bottom-left-radius: var(--radius);
}
.no-gutter .ͼo .cm-gutters {
display: none;
}
.kener-md {
@apply text-sm;
}
p code:not([class^="language-"]) {
@apply rounded-sm bg-yellow-100 px-1 py-0.5 font-mono text-xs dark:bg-neutral-700 dark:text-neutral-100;
}
.kener-theme-mono .section-hero .h1,
.kener-theme-mono-hero-h1 {
@apply text-foreground;
}
.kener-theme-default .section-hero .h1,
.kener-theme-default-hero-h1 {
@apply bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-transparent;
}
.kener-theme-sunset .section-hero .h1,
.kener-theme-sunset-hero-h1 {
@apply bg-gradient-to-r from-yellow-500 via-red-500 to-pink-500 bg-clip-text text-transparent;
}
.kener-theme-forest .section-hero .h1,
.kener-theme-forest-hero-h1 {
@apply bg-gradient-to-l from-blue-500 via-teal-500 to-green-500 bg-clip-text text-transparent;
}
.kener-theme-ocean .section-hero .h1,
.kener-theme-ocean-hero-h1 {
@apply bg-gradient-to-l from-cyan-500 via-blue-500 to-indigo-500 bg-clip-text text-transparent;
}
.prose :where(hr):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1.3em;
margin-bottom: 1.3em;
}
input[type="checkbox"]:disabled + .peer {
cursor: not-allowed;
opacity: 0.5;
}
/* Select any element whose class begins with "lucid-chevron" */
.hide-chevron [data-bits-accordion-header] .lucide-chevron-down,
.hide-chevron [data-bits-accordion-header] .lucide-chevron-up {
/* add your styles here */
display: none; /* Hides the chevron when the class 'hide-chevron' is applied */
}
.hide-chevron [data-bits-accordion-header] button:hover {
background-color: transparent;
}
.copybtn .copy-btn {
transform: scale(1);
transition: transform 0.15s;
}
.copybtn .check-btn {
transform: scale(0);
transition: transform 0.15s;
}
.copybtn:focus .copy-btn {
transform: scale(0);
}
.copybtn:focus .check-btn {
transform: scale(1);
}
.dash-after::after {
content: " ";
display: inline-block;
position: absolute;
width: 1px;
height: 24px;
border-radius: 2px;
background-color: hsl(var(--accent) / var(--tw-bg-opacity, 1));
right: -1px;
}
.dash-after:hover::after {
display: none;
}
.status-color-bar {
transition: all 0.2s ease-in-out;
}
.status-color-banner:hover .status-color-bar {
bottom: -0.75rem;
}
.status-color-banner:hover > div {
@apply bg-secondary;
}
+356
View File
@@ -0,0 +1,356 @@
[
{ "id": "sdtd", "name": "7 Days to Die", "isValve": true },
{ "id": "arma2", "name": "ARMA 2", "isValve": true },
{ "id": "a2oa", "name": "ARMA 2: Operation Arrowhead", "isValve": true },
{ "id": "arma3", "name": "ARMA 3", "isValve": true },
{ "id": "aaa", "name": "ARMA: Armed Assault" },
{ "id": "acwa", "name": "ARMA: Cold War Assault" },
{ "id": "armareforger", "name": "ARMA: Reforger", "isValve": true },
{ "id": "armaresistance", "name": "ARMA: Resistance" },
{ "id": "abioticfactor", "name": "Abiotic Factor", "isValve": true },
{ "id": "aosc", "name": "Ace of Spades Classic" },
{ "id": "ahl", "name": "Action Half-Life", "isValve": true },
{ "id": "actionsource", "name": "Action: Source", "isValve": true },
{ "id": "aoc", "name": "Age of Chivalry", "isValve": true },
{ "id": "aoe2", "name": "Age of Empires 2" },
{ "id": "alienarena", "name": "Alien Arena" },
{ "id": "alienswarm", "name": "Alien Swarm", "isValve": true },
{ "id": "avp2", "name": "Aliens versus Predator 2" },
{ "id": "avp2010", "name": "Aliens vs. Predator 2010", "isValve": true },
{ "id": "americasarmy", "name": "America's Army" },
{ "id": "americasarmy2", "name": "America's Army 2" },
{ "id": "americasarmy3", "name": "America's Army 3", "isValve": true },
{ "id": "aapg", "name": "America's Army: Proving Grounds", "isValve": true },
{ "id": "asr08", "name": "Arca Sim Racing '08" },
{ "id": "asa", "name": "Ark: Survival Ascended" },
{ "id": "ase", "name": "Ark: Survival Evolved", "isValve": true },
{ "id": "armagetronadvanced", "name": "Armagetron Advanced" },
{ "id": "assettocorsa", "name": "Assetto Corsa" },
{ "id": "atlas", "name": "Atlas", "isValve": true },
{ "id": "avorion", "name": "Avorion", "isValve": true },
{ "id": "brokeprotocol", "name": "BROKE PROTOCOL" },
{ "id": "baldursgate", "name": "Baldur's Gate" },
{ "id": "ballisticoverkill", "name": "Ballistic Overkill", "isValve": true },
{ "id": "barotrauma", "name": "Barotrauma", "isValve": true },
{ "id": "basedefense", "name": "Base Defense", "isValve": true },
{ "id": "battalion1944", "name": "Battalion 1944", "isValve": true },
{ "id": "battlefield1942", "name": "Battlefield 1942" },
{ "id": "battlefield2", "name": "Battlefield 2" },
{ "id": "battlefield2142", "name": "Battlefield 2142" },
{ "id": "battlefield3", "name": "Battlefield 3" },
{ "id": "battlefield4", "name": "Battlefield 4" },
{ "id": "battlefieldhardline", "name": "Battlefield Hardline" },
{ "id": "battlefieldvietnam", "name": "Battlefield Vietnam" },
{ "id": "bbc2", "name": "Battlefield: Bad Company 2" },
{ "id": "beammp", "name": "BeamMP (2021)" },
{ "id": "blackmesa", "name": "Black Mesa", "isValve": true },
{ "id": "bladesymphony", "name": "Blade Symphony", "isValve": true },
{ "id": "brainbread", "name": "BrainBread", "isValve": true },
{ "id": "brainbread2", "name": "BrainBread 2", "isValve": true },
{ "id": "breach", "name": "Breach", "isValve": true },
{ "id": "breed", "name": "Breed" },
{ "id": "brink", "name": "Brink", "isValve": true },
{ "id": "bas", "name": "Build and Shoot" },
{ "id": "c2d", "name": "CS2D" },
{ "id": "cod", "name": "Call of Duty" },
{ "id": "cod2", "name": "Call of Duty 2" },
{ "id": "cod3", "name": "Call of Duty 3" },
{ "id": "cod4mw", "name": "Call of Duty 4: Modern Warfare" },
{ "id": "codbo3", "name": "Call of Duty: Black Ops 3", "isValve": true },
{ "id": "codmw2", "name": "Call of Duty: Modern Warfare 2" },
{ "id": "codmw3", "name": "Call of Duty: Modern Warfare 3", "isValve": true },
{ "id": "coduo", "name": "Call of Duty: United Offensive" },
{ "id": "codwaw", "name": "Call of Duty: World at War" },
{ "id": "coj", "name": "Call of Juarez" },
{ "id": "chaser", "name": "Chaser" },
{ "id": "cmw", "name": "Chivalry: Medieval Warfare", "isValve": true },
{ "id": "chrome", "name": "Chrome" },
{ "id": "codenamecure", "name": "Codename CURE", "isValve": true },
{ "id": "codenameeagle", "name": "Codename Eagle" },
{ "id": "colonysurvival", "name": "Colony Survival", "isValve": true },
{ "id": "cacr", "name": "Command and Conquer: Renegade" },
{ "id": "c3db", "name": "Commandos 3: Destination Berlin" },
{ "id": "conanexiles", "name": "Conan Exiles", "isValve": true },
{ "id": "contagion", "name": "Contagion", "isValve": true },
{ "id": "contractjack", "name": "Contract J.A.C.K." },
{ "id": "corekeeper", "name": "Core Keeper", "isValve": true },
{ "id": "counterstrike15", "name": "Counter-Strike 1.5" },
{ "id": "counterstrike16", "name": "Counter-Strike 1.6", "isValve": true },
{ "id": "counterstrike2", "name": "Counter-Strike 2", "isValve": true },
{ "id": "cscz", "name": "Counter-Strike: Condition Zero", "isValve": true },
{ "id": "csgo", "name": "Counter-Strike: Global Offensive", "isValve": true },
{ "id": "css", "name": "Counter-Strike: Source", "isValve": true },
{ "id": "creativerse", "name": "Creativerse", "isValve": true },
{ "id": "crce", "name": "Cross Racing Championship Extreme" },
{ "id": "crysis", "name": "Crysis" },
{ "id": "crysis2", "name": "Crysis 2" },
{ "id": "crysiswars", "name": "Crysis Wars" },
{ "id": "daikatana", "name": "Daikatana" },
{ "id": "dmomam", "name": "Dark Messiah of Might and Magic", "isValve": true },
{ "id": "dal", "name": "Dark and Light", "isValve": true },
{ "id": "dhe4445", "name": "Darkest Hour: Europe '44-'45" },
{ "id": "dod", "name": "Day of Defeat", "isValve": true },
{ "id": "dods", "name": "Day of Defeat: Source", "isValve": true },
{ "id": "dayofdragons", "name": "Day of Dragons", "isValve": true },
{ "id": "doi", "name": "Day of Infamy", "isValve": true },
{ "id": "dayz", "name": "DayZ", "isValve": true },
{ "id": "dayzmod", "name": "DayZ Mod", "isValve": true },
{ "id": "dow", "name": "Days of War", "isValve": true },
{ "id": "ddpt", "name": "Deadly Dozen: Pacific Theater" },
{ "id": "deathmatchclassic", "name": "Deathmatch Classic", "isValve": true },
{ "id": "deerhunter2005", "name": "Deer Hunter 2005" },
{ "id": "descent3", "name": "Descent 3" },
{ "id": "deusex", "name": "Deus Ex" },
{ "id": "devastation", "name": "Devastation" },
{ "id": "ddd", "name": "Dino D-Day", "isValve": true },
{ "id": "dtr2", "name": "Dirt Track Racing 2" },
{ "id": "discord", "name": "Discord" },
{ "id": "dst", "name": "Don't Starve Together", "isValve": true },
{ "id": "doom3", "name": "Doom 3" },
{ "id": "dota2", "name": "Dota 2", "isValve": true },
{ "id": "dab", "name": "Double Action: Boogaloo", "isValve": true },
{ "id": "dootf", "name": "Drakan: Order of the Flame" },
{ "id": "dnf2001", "name": "Duke Nukem Forever 2001" },
{ "id": "dystopia", "name": "Dystopia", "isValve": true },
{ "id": "eco", "name": "Eco" },
{ "id": "empiresmod", "name": "Empires Mod", "isValve": true },
{ "id": "egs", "name": "Empyrion - Galactic Survival", "isValve": true },
{ "id": "etqw", "name": "Enemy Territory: Quake Wars" },
{ "id": "ets2", "name": "Euro Truck Simulator 2", "isValve": true },
{ "id": "exfil", "name": "Exfil", "isValve": true },
{ "id": "fear", "name": "F.E.A.R." },
{ "id": "f1c9902", "name": "F1 Challenge '99-'02" },
{ "id": "foundry", "name": "FOUNDRY", "isValve": true },
{ "id": "factorio", "name": "Factorio" },
{ "id": "farcry", "name": "Far Cry" },
{ "id": "farcry2", "name": "Far Cry 2" },
{ "id": "farmingsimulator19", "name": "Farming Simulator 19" },
{ "id": "farmingsimulator22", "name": "Farming Simulator 22" },
{ "id": "farmingsimulator25", "name": "Farming Simulator 25" },
{ "id": "fof", "name": "Fistful of Frags", "isValve": true },
{ "id": "formulaone2002", "name": "Formula One 2002" },
{ "id": "fortressforever", "name": "Fortress Forever", "isValve": true },
{ "id": "ffow", "name": "Frontlines: Fuel of War" },
{ "id": "garrysmod", "name": "Garry's Mod", "isValve": true },
{ "id": "geneshift", "name": "Geneshift" },
{ "id": "gck", "name": "Giants: Citizen Kabuto" },
{ "id": "globaloperations", "name": "Global Operations" },
{ "id": "goldeneyesource", "name": "GoldenEye: Source", "isValve": true },
{ "id": "gus", "name": "Gore: Ultimate Soldier" },
{ "id": "gta5f", "name": "Grand Theft Auto V - FiveM" },
{ "id": "gta5r", "name": "Grand Theft Auto V - RageMP" },
{ "id": "gta5am", "name": "Grand Theft Auto V - alt:V Multiplayer" },
{ "id": "gtasamta", "name": "Grand Theft Auto: San Andreas - Multi Theft Auto" },
{ "id": "gtasam", "name": "Grand Theft Auto: San Andreas Multiplayer" },
{ "id": "gtasao", "name": "Grand Theft Auto: San Andreas OpenMP" },
{ "id": "gtavcmta", "name": "Grand Theft Auto: Vice City - Multi Theft Auto" },
{ "id": "groundbreach", "name": "Ground Breach", "isValve": true },
{ "id": "gunmanchronicles", "name": "Gunman Chronicles", "isValve": true },
{ "id": "hl2d", "name": "Half-Life 2: Deathmatch", "isValve": true },
{ "id": "hld", "name": "Half-Life Deathmatch", "isValve": true },
{ "id": "hlds", "name": "Half-Life Deathmatch: Source", "isValve": true },
{ "id": "hlof", "name": "Half-Life: Opposing Force", "isValve": true },
{ "id": "halo", "name": "Halo" },
{ "id": "halo2", "name": "Halo 2" },
{ "id": "eldewrito", "name": "Halo Online - ElDewrito" },
{ "id": "hawakening", "name": "Hawakening" },
{ "id": "hll", "name": "Hell Let Loose", "isValve": true },
{ "id": "heretic2", "name": "Heretic II" },
{ "id": "hexen2", "name": "Hexen II" },
{ "id": "hiddendangerous2", "name": "Hidden & Dangerous 2" },
{ "id": "homefront", "name": "Homefront", "isValve": true },
{ "id": "homeworld2", "name": "Homeworld 2" },
{ "id": "hurtworld", "name": "Hurtworld", "isValve": true },
{ "id": "i2cs", "name": "IGI 2: Covert Strike" },
{ "id": "i2s", "name": "IL-2 Sturmovik" },
{ "id": "icarus", "name": "Icarus", "isValve": true },
{ "id": "insurgency", "name": "Insurgency", "isValve": true },
{ "id": "imic", "name": "Insurgency: Modern Infantry Combat", "isValve": true },
{ "id": "insurgencysandstorm", "name": "Insurgency: Sandstorm", "isValve": true },
{ "id": "ironstorm", "name": "Iron Storm" },
{ "id": "jb0n", "name": "James Bond 007: Nightfire" },
{ "id": "jc2m", "name": "Just Cause 2 - Multiplayer" },
{ "id": "jc3m", "name": "Just Cause 3 - Multiplayer", "isValve": true },
{ "id": "kspd", "name": "Kerbal Space Program - DMP" },
{ "id": "killingfloor", "name": "Killing Floor" },
{ "id": "killingfloor2", "name": "Killing Floor 2", "isValve": true },
{ "id": "kloc", "name": "Kingpin: Life of Crime" },
{ "id": "kpctnc", "name": "Kiss: Psycho Circus: The Nightmare Child" },
{ "id": "kreedzclimbing", "name": "Kreedz Climbing", "isValve": true },
{ "id": "l4d", "name": "Left 4 Dead", "isValve": true },
{ "id": "l4d2", "name": "Left 4 Dead 2", "isValve": true },
{ "id": "m2m", "name": "Mafia II - Multiplayer" },
{ "id": "m2o", "name": "Mafia II - Online" },
{ "id": "moh", "name": "Medal of Honor" },
{ "id": "moha", "name": "Medal of Honor: Airborne" },
{ "id": "mohaa", "name": "Medal of Honor: Allied Assault" },
{ "id": "mohaab", "name": "Medal of Honor: Allied Assault Breakthrough" },
{ "id": "mohaas", "name": "Medal of Honor: Allied Assault Spearhead" },
{ "id": "mohpa", "name": "Medal of Honor: Pacific Assault" },
{ "id": "mohw", "name": "Medal of Honor: Warfighter" },
{ "id": "medievalengineers", "name": "Medieval Engineers", "isValve": true },
{ "id": "minecraft", "name": "Minecraft" },
{ "id": "mbe", "name": "Minecraft: Bedrock Edition" },
{ "id": "minetest", "name": "Minetest" },
{ "id": "mnc", "name": "Monday Night Combat", "isValve": true },
{ "id": "mordhau", "name": "Mordhau", "isValve": true },
{ "id": "mumble", "name": "Mumble" },
{ "id": "mgm", "name": "Mumble - GT Murmur" },
{ "id": "mutantfactions", "name": "Mutant Factions" },
{ "id": "moe", "name": "Myth of Empires", "isValve": true },
{ "id": "nascarthunder2004", "name": "NASCAR Thunder 2004" },
{ "id": "naturalselection", "name": "Natural Selection", "isValve": true },
{ "id": "naturalselection2", "name": "Natural Selection 2", "isValve": true },
{ "id": "nfshp2", "name": "Need for Speed: Hot Pursuit 2" },
{ "id": "nab", "name": "Nerf Arena Blast" },
{ "id": "neverwinternights", "name": "Neverwinter Nights" },
{ "id": "neverwinternights2", "name": "Neverwinter Nights 2" },
{ "id": "nexuiz", "name": "Nexuiz" },
{ "id": "nitrofamily", "name": "Nitro Family" },
{ "id": "nmrih", "name": "No More Room in Hell", "isValve": true },
{ "id": "nolf2asihw", "name": "No One Lives Forever 2: A Spy in H.A.R.M.'s Way" },
{ "id": "nla", "name": "Nova-Life: Amboise", "isValve": true },
{ "id": "nucleardawn", "name": "Nuclear Dawn", "isValve": true },
{ "id": "onset", "name": "Onset", "isValve": true },
{ "id": "openarena", "name": "OpenArena" },
{ "id": "openttd", "name": "OpenTTD" },
{ "id": "ofcwc", "name": "Operation Flashpoint: Cold War Crisis" },
{ "id": "ofr", "name": "Operation Flashpoint: Resistance" },
{ "id": "ohd", "name": "Operation: Harsh Doorstop", "isValve": true },
{ "id": "painkiller", "name": "Painkiller" },
{ "id": "palworld", "name": "Palworld" },
{ "id": "pvak2", "name": "Pirates, Vikings, and Knights II", "isValve": true },
{ "id": "pixark", "name": "PixARK", "isValve": true },
{ "id": "postscriptum", "name": "Post Scriptum", "isValve": true },
{ "id": "postal2", "name": "Postal 2" },
{ "id": "prey", "name": "Prey" },
{ "id": "pce", "name": "Primal Carnage: Extinction", "isValve": true },
{ "id": "projectcars", "name": "Project Cars", "isValve": true },
{ "id": "projectcars2", "name": "Project Cars 2", "isValve": true },
{ "id": "prb2", "name": "Project Reality: Battlefield 2" },
{ "id": "projectzomboid", "name": "Project Zomboid", "isValve": true },
{ "id": "quake", "name": "Quake" },
{ "id": "quake2", "name": "Quake 2" },
{ "id": "q3a", "name": "Quake 3: Arena" },
{ "id": "quake4", "name": "Quake 4" },
{ "id": "quakelive", "name": "Quake Live", "isValve": true },
{ "id": "rdkf", "name": "Rag Doll Kung Fu", "isValve": true },
{ "id": "rainbowsix", "name": "Rainbow Six" },
{ "id": "rs2rs", "name": "Rainbow Six 2: Rogue Spear" },
{ "id": "rs3rs", "name": "Rainbow Six 3: Raven Shield" },
{ "id": "rallisportchallenge", "name": "RalliSport Challenge" },
{ "id": "rallymasters", "name": "Rally Masters" },
{ "id": "rdr2r", "name": "Red Dead Redemption 2 - RedM" },
{ "id": "redorchestra", "name": "Red Orchestra" },
{ "id": "redorchestra2", "name": "Red Orchestra 2", "isValve": true },
{ "id": "roo4145", "name": "Red Orchestra: Ostfront 41-45" },
{ "id": "redline", "name": "Redline" },
{ "id": "renegade10", "name": "Renegade X" },
{ "id": "renown", "name": "Renown" },
{ "id": "rtcw", "name": "Return to Castle Wolfenstein" },
{ "id": "ricochet", "name": "Ricochet", "isValve": true },
{ "id": "ron", "name": "Rise of Nations" },
{ "id": "rs2v", "name": "Rising Storm 2: Vietnam", "isValve": true },
{ "id": "risingworld", "name": "Rising World", "isValve": true },
{ "id": "ror2", "name": "Risk of Rain 2", "isValve": true },
{ "id": "rune", "name": "Rune" },
{ "id": "rust", "name": "Rust", "isValve": true },
{ "id": "stalker", "name": "S.T.A.L.K.E.R." },
{ "id": "swat4", "name": "SWAT 4" },
{ "id": "satisfactory", "name": "Satisfactory" },
{ "id": "s2ats", "name": "Savage 2: A Tortured Soul" },
{ "id": "serioussam", "name": "Serious Sam" },
{ "id": "serioussam2", "name": "Serious Sam 2" },
{ "id": "sstse", "name": "Serious Sam: The Second Encounter" },
{ "id": "shatteredhorizon", "name": "Shattered Horizon", "isValve": true },
{ "id": "shogo", "name": "Shogo" },
{ "id": "shootmania", "name": "Shootmania" },
{ "id": "sin", "name": "SiN" },
{ "id": "sinepisodes", "name": "SiN Episodes", "isValve": true },
{ "id": "soldat", "name": "Soldat" },
{ "id": "sof", "name": "Soldier of Fortune" },
{ "id": "sof2", "name": "Soldier of Fortune 2" },
{ "id": "sotf", "name": "Sons Of The Forest", "isValve": true },
{ "id": "soulmask", "name": "Soulmask", "isValve": true },
{ "id": "spaceengineers", "name": "Space Engineers", "isValve": true },
{ "id": "squad", "name": "Squad", "isValve": true },
{ "id": "stbc", "name": "Star Trek: Bridge Commander" },
{ "id": "stvef", "name": "Star Trek: Voyager - Elite Force" },
{ "id": "stvef2", "name": "Star Trek: Voyager - Elite Force 2" },
{ "id": "swjk2jo", "name": "Star Wars Jedi Knight II: Jedi Outcast" },
{ "id": "swjkja", "name": "Star Wars Jedi Knight: Jedi Academy" },
{ "id": "swb", "name": "Star Wars: Battlefront" },
{ "id": "swb2", "name": "Star Wars: Battlefront 2" },
{ "id": "swrc", "name": "Star Wars: Republic Commando" },
{ "id": "starmade", "name": "StarMade" },
{ "id": "starbound", "name": "Starbound", "isValve": true },
{ "id": "starsiege", "name": "Starsiege" },
{ "id": "suicidesurvival", "name": "Suicide Survival", "isValve": true },
{ "id": "stn", "name": "Survive the Nights", "isValve": true },
{ "id": "svencoop", "name": "Sven Coop", "isValve": true },
{ "id": "synergy", "name": "Synergy", "isValve": true },
{ "id": "toxikk", "name": "TOXIKK" },
{ "id": "tacticalops", "name": "Tactical Ops" },
{ "id": "toh", "name": "Take On Helicopters" },
{ "id": "teamfactor", "name": "Team Factor" },
{ "id": "teamfortress2", "name": "Team Fortress 2", "isValve": true },
{ "id": "tfc", "name": "Team Fortress Classic", "isValve": true },
{ "id": "teamspeak2", "name": "Teamspeak 2" },
{ "id": "teamspeak3", "name": "Teamspeak 3" },
{ "id": "terminus", "name": "Terminus" },
{ "id": "terrariatshock", "name": "Terraria - TShock" },
{ "id": "theforest", "name": "The Forest", "isValve": true },
{ "id": "thefront", "name": "The Front", "isValve": true },
{ "id": "thehidden", "name": "The Hidden", "isValve": true },
{ "id": "theisle", "name": "The Isle", "isValve": true },
{ "id": "tie", "name": "The Isle Evrima" },
{ "id": "tonolf", "name": "The Operative: No One Lives Forever" },
{ "id": "theship", "name": "The Ship", "isValve": true },
{ "id": "thespecialists", "name": "The Specialists", "isValve": true },
{ "id": "tcgraw", "name": "Tom Clancy's Ghost Recon Advanced Warfighter" },
{ "id": "tcgraw2", "name": "Tom Clancy's Ghost Recon Advanced Warfighter 2" },
{ "id": "thps3", "name": "Tony Hawk's Pro Skater 3" },
{ "id": "thps4", "name": "Tony Hawk's Pro Skater 4" },
{ "id": "thu2", "name": "Tony Hawk's Underground 2" },
{ "id": "towerunite", "name": "Tower Unite", "isValve": true },
{ "id": "trackmania2", "name": "Trackmania 2" },
{ "id": "trackmaniaforever", "name": "Trackmania Forever" },
{ "id": "tremulous", "name": "Tremulous" },
{ "id": "t1s", "name": "Tribes 1: Starsiege" },
{ "id": "tribesvengeance", "name": "Tribes: Vengeance" },
{ "id": "tron20", "name": "Tron 2.0" },
{ "id": "turok2", "name": "Turok 2" },
{ "id": "universalcombat", "name": "Universal Combat" },
{ "id": "unreal", "name": "Unreal" },
{ "id": "u2tax", "name": "Unreal 2: The Awakening - XMP" },
{ "id": "unrealtournament", "name": "Unreal Tournament" },
{ "id": "unrealtournament2003", "name": "Unreal Tournament 2003" },
{ "id": "unrealtournament2004", "name": "Unreal Tournament 2004" },
{ "id": "unrealtournament3", "name": "Unreal Tournament 3" },
{ "id": "urbanterror", "name": "Urban Terror" },
{ "id": "vrising", "name": "V Rising", "isValve": true },
{ "id": "v8sc", "name": "V8 Supercar Challenge" },
{ "id": "valheim", "name": "Valheim", "isValve": true },
{ "id": "vampireslayer", "name": "Vampire Slayer", "isValve": true },
{ "id": "ventrilo", "name": "Ventrilo" },
{ "id": "vcm", "name": "Vice City Multiplayer" },
{ "id": "vietcong", "name": "Vietcong" },
{ "id": "vietcong2", "name": "Vietcong 2" },
{ "id": "vintagestory", "name": "Vintage Story" },
{ "id": "warfork", "name": "Warfork" },
{ "id": "warsow", "name": "Warsow" },
{ "id": "wot", "name": "Wheel of Time" },
{ "id": "wolfenstein", "name": "Wolfenstein" },
{ "id": "wet", "name": "Wolfenstein: Enemy Territory" },
{ "id": "wop", "name": "World Of Padman" },
{ "id": "wurmunlimited", "name": "Wurm Unlimited", "isValve": true },
{ "id": "xonotic", "name": "Xonotic" },
{ "id": "xpandrally", "name": "Xpand Rally" },
{ "id": "zombiemaster", "name": "Zombie Master", "isValve": true },
{ "id": "zps", "name": "Zombie Panic: Source", "isValve": true },
{ "id": "enshrouded", "name": "enshrouded", "isValve": true },
{ "id": "netpanzer", "name": "netPanzer" },
{ "id": "rfactor", "name": "rFactor" },
{ "id": "rfactor2", "name": "rFactor 2", "isValve": true },
{ "id": "unturned", "name": "unturned", "isValve": true }
]
+10 -1
View File
@@ -66,7 +66,7 @@ export const WebhookJSONTemplate = JSON.stringify(
actions: [
{
text: "{{action_text}}",
url: "{{action_url}}",
url: "{{{action_url}}}",
},
],
},
@@ -185,6 +185,15 @@ export const DefaultTCPEval = `(async function (arrayOfPings) {
}
})`;
export const DefaultGamedigEval = `(async function (responseTime, responseRaw) {
return {
status: 'UP',
latency: responseTime,
}
})`;
export const GAMEDIG_TIMEOUT = 10 * 1000; // 10 seconds
export const GAMEDIG_SOCKET_TIMEOUT = 2 * 1000; // 2 seconds
export const ErrorSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="60" viewBox="0 0 120 60" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="30" cy="24" r="10"/>
<path d="M26 27h8"/>
+14 -10
View File
@@ -1,13 +1,17 @@
// @ts-ignore
export const analyticsEvent = (event, data) => {
// Do something with the event and data
document.dispatchEvent(
new CustomEvent("analyticsEvent", {
bubbles: true,
detail: {
event,
data
}
})
);
// Do something with the event and data
if (typeof document !== "undefined") {
event = "kener_" + event;
document.dispatchEvent(
new CustomEvent("analyticsEvent", {
bubbles: true,
detail: {
event,
data,
},
}),
);
}
};
+15
View File
@@ -258,6 +258,10 @@ function isValidRange(value, min, max) {
const num = Number(value);
return !isNaN(num) && num >= min && num <= max;
}
function IsValidPort(port) {
return isValidRange(port, 1, 65535);
}
function SortMonitor(monitorSort, resp) {
let monitors = [];
if (!!monitorSort && monitorSort.length > 0) {
@@ -277,6 +281,15 @@ function RandomString(length) {
return result;
}
/**
* Retreive game's data from its id.
* @param {string} id Id
* @return Returns game's data if found, undefined instead.
*/
function GetGameFromId(list, id) {
return list.find((game) => game.id === id);
}
export {
siteDataExtractFromDb,
storeSiteData,
@@ -285,7 +298,9 @@ export {
IsValidHost,
IsValidNameServer,
IsValidURL,
IsValidPort,
ValidateCronExpression,
SortMonitor,
RandomString,
GetGameFromId,
};
+74 -28
View File
@@ -2,6 +2,9 @@
import { formatDistanceToNow, formatDistance } from "date-fns";
import Settings from "lucide-svelte/icons/settings";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import Copy from "lucide-svelte/icons/copy";
import Check from "lucide-svelte/icons/check";
import { analyticsEvent } from "$lib/boringOne";
import * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
@@ -9,9 +12,12 @@
import { Tooltip } from "bits-ui";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { marked } from "marked";
import { onMount } from "svelte";
export let incident;
export let index;
export let lang;
export let allowCollapse = true;
let startTime = new Date(incident.start_date_time * 1000);
let endTime = new Date();
let nowTime = new Date();
@@ -33,7 +39,7 @@
}
let accordionValue = "incident-0";
if ($page.data.site.incidentGroupView == "COLLAPSED") {
if ($page.data.site.incidentGroupView == "COLLAPSED" && allowCollapse) {
accordionValue = "incident-collapse";
} else if ($page.data.site.incidentGroupView == "EXPANDED") {
accordionValue = index;
@@ -73,34 +79,66 @@
maintenanceBadge = "Maintenance Completed";
maintenanceBadgeColor = "text-maintenance-completed";
}
let pathMonitorLink = "";
onMount(async () => {
let protocol = window.location.protocol;
let domain = window.location.host;
pathMonitorLink = `${protocol}//${domain}${base}/view/events/${incident.incident_type}-${incident.id}`;
});
</script>
<div class="newincident relative grid w-full grid-cols-12 gap-2 px-0 py-0 last:border-b-0">
<div class="col-span-12">
<Accordion.Root bind:value={index} class="accor">
<Accordion.Root
bind:value={index}
class="accor {allowCollapse === false ? 'hide-chevron' : ''}"
disabled={!allowCollapse}
>
<Accordion.Item value={accordionValue}>
<Accordion.Trigger class="rounded-md px-4 hover:bg-muted hover:no-underline">
<Accordion.Trigger
class="rounded-md px-4 hover:bg-muted hover:no-underline"
on:click={() => analyticsEvent("incident_open", { incident_title: incident.title })}
>
<div class="w-full text-left hover:no-underline">
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
<span class="badge-{incident.state}">
{l($page.data.lang, incident.state)}
</span>
{:else if incidentType == "MAINTENANCE"}
<span class="{maintenanceBadgeColor} ">
{l($page.data.lang, maintenanceBadge)}
</span>
{/if}
{#if $page.data.isLoggedIn}
<Button
href="{base}/manage/app/events#{incident.id}"
class=" rotate-once h-5 p-0 text-muted-foreground hover:text-primary"
variant="link"
>
<Settings class="h-4 w-4 " />
</Button>
{/if}
</p>
<div class="flex justify-start gap-x-2">
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
<span class="badge-{incident.state}">
{l($page.data.lang, incident.state)}
</span>
{:else if incidentType == "MAINTENANCE"}
<span class="{maintenanceBadgeColor} ">
{l($page.data.lang, maintenanceBadge)}
</span>
{/if}
</p>
<div class="flex justify-end gap-x-2">
{#if $page.data.isLoggedIn}
<Button
href="{base}/manage/app/events#{incident.id}"
class="rotate-once h-5 p-0 text-muted-foreground hover:text-primary"
variant="link"
>
<Settings class="h-4 w-4 " />
</Button>
{/if}
{#if !!pathMonitorLink}
<Button
size="icon"
variant="link"
class="copybtn relative h-5 p-0 text-muted-foreground hover:text-primary"
on:click={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(pathMonitorLink);
analyticsEvent("incident_copy_link", { incident_title: incident.title });
}}
>
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
</Button>
{/if}
</div>
</div>
<p class="font-medium">
{incident.title}
@@ -185,7 +223,11 @@
</time>
<div class="mb-4 text-sm font-normal">
{@html comment.comment}
<div
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
{@html marked.parse(comment.comment)}
</div>
</div>
</li>
{/each}
@@ -203,9 +245,13 @@
)}
</time>
<p class="mb-2 text-sm font-normal">
{comment.comment}
</p>
<div class="mb-2 text-sm font-normal">
<div
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
{@html marked.parse(comment.comment)}
</div>
</div>
</li>
{/each}
</ol>
+128 -50
View File
@@ -8,6 +8,7 @@
import * as Select from "$lib/components/ui/select";
import locales from "$lib/locales/locales.json?raw";
import GMI from "$lib/components/gmi.svelte";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import Loader from "lucide-svelte/icons/loader";
import X from "lucide-svelte/icons/x";
@@ -16,6 +17,11 @@
import Play from "lucide-svelte/icons/play";
import { Tooltip } from "bits-ui";
import CodeMirror from "svelte-codemirror-editor";
import { html } from "@codemirror/lang-html";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
export let data;
let formStateHero = "idle";
@@ -31,9 +37,19 @@
};
let footerHTML = "";
let tzToggle = "NO";
let showSiteStatus = "NO";
let homeIncidentCount = 10;
let homeIncidentStartTimeWithin = 30;
let incidentGroupView = "EXPAND_FIRST";
let kenerTheme = "default";
let availableThemes = [
{ name: "Default", value: "default" },
{ name: "Mono", value: "mono" },
{ name: "Sunset", value: "sunset" },
{ name: "Forest", value: "forest" },
{ name: "Ocean", value: "ocean" }
];
let nav = [];
let categories = [];
@@ -74,9 +90,16 @@
if (data.siteData.footerHTML) {
footerHTML = data.siteData.footerHTML;
}
if (data.siteData.kenerTheme) {
kenerTheme = data.siteData.kenerTheme;
}
if (data.siteData.tzToggle) {
tzToggle = data.siteData.tzToggle;
}
if (data.siteData.showSiteStatus) {
showSiteStatus = data.siteData.showSiteStatus;
}
if (data.siteData.i18n) {
i18n = data.siteData.i18n;
//add locales that are not present in the data
@@ -105,6 +128,20 @@
heroError = data.error;
return;
}
saveTheme();
}
async function saveTheme() {
let resp = await storeSiteData({
kenerTheme: kenerTheme,
showSiteStatus: showSiteStatus
});
//print data
let data = await resp.json();
if (data.error) {
alert(data.error);
return;
}
}
let navErrorMessage = "";
@@ -258,44 +295,16 @@
<Card.Description>Configure the hero section of your site.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitHero}>
<form class="mx-auto mt-4 flex flex-col gap-y-4" on:submit|preventDefault={formSubmitHero}>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="hero_title">
Title
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
You can set the title of the hero section here. This will be displayed on the top of the page.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Label for="hero_title">Title</Label>
<Input bind:value={hero.title} class="mt-2" type="text" id="hero_title" placeholder="Status Page for Kener" />
</div>
</div>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="hero_subtitle">
Subtitle
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
You can set the subtitle of the hero section here. This will be displayed below the title.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Label for="hero_subtitle">Subtitle</Label>
<Input
bind:value={hero.subtitle}
class="mt-2"
@@ -305,12 +314,63 @@
/>
</div>
</div>
<div class="flex flex-col gap-y-2">
<div>
<Label>Color</Label>
</div>
<div class="flex flex-row gap-x-2 rounded-md border p-2">
<div class="w-3/4 pt-2">
<span class="kener-theme-{kenerTheme}-hero-h1 font-medium">{hero.title}</span>
</div>
<div class="flex w-1/4 justify-end">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline">
{availableThemes.find((el) => el.value === kenerTheme)?.name || "Mono"}
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{#each availableThemes as theme}
<DropdownMenu.Group>
<DropdownMenu.Item>
<Button
variant="ghost"
class="h-6 w-full justify-start text-xs"
on:click={() => {
kenerTheme = theme.value;
}}
>
{theme.name}
</Button>
</DropdownMenu.Item>
</DropdownMenu.Group>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
</div>
<div>
<p class=" font-medium">
<label>
<input
on:change={(e) => {
showSiteStatus = e.target.checked === true ? "YES" : "NO";
}}
type="checkbox"
checked={showSiteStatus === "YES"}
/>
Show Site Status Banner
</label>
</p>
</div>
<div class="flex w-full justify-end gap-x-2">
{#if !!heroError}
<div class="py-2 text-sm font-medium text-destructive">{heroError}</div>
{/if}
<Button type="submit" disabled={formStateHero === "loading"}>
Save
Save Hero Section
{#if formStateHero === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -370,7 +430,7 @@
</div>
<div class="pt-8">
<Button type="submit" disabled={formStateIncident === "loading"}>
Save
Save Incidents Settings
{#if formStateIncident === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -495,7 +555,7 @@
<span class="text-sm font-medium text-destructive"> {navErrorMessage} </span>
</p>
<Button type="submit" disabled={formStateHero === "loading"}>
Save
Save Navigation
{#if formStateHero === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -574,7 +634,7 @@
<div class="py-2 text-sm font-medium text-destructive">{i18nErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStatei18n === "loading"}>
Save
Save Languages
{#if formStatei18n === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -637,7 +697,7 @@
<div class="py-2 text-sm font-medium text-destructive">{categoriesErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStateCategories === "loading"}>
Save
Save Categories
{#if formStateCategories === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -656,24 +716,42 @@
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitFooter}>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<textarea
bind:value={footerHTML}
class="mt-2 h-48 w-full rounded-sm border p-2"
placeholder="eg. <p>Powered by Kener</p>"
></textarea>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={footerHTML}
lang={html()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "320px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "0.375rem"
}
}}
/>
</div>
</div>
</div>
<div class="flex w-full justify-end 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"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
<div class="flex w-full justify-between gap-x-2">
<div>
<Button variant="outline" class="mt-2" on:click={() => (footerHTML = data.defaultFooterHTML)}>
<span class="ml-2">Reset to default</span>
</Button>
</div>
<div class="flex flex-row justify-end gap-x-2">
{#if !!footerErrorMessage}
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
{/if}
</Button>
<Button type="submit" disabled={formStateFooter === "loading"}>
Save Custom Footer
{#if formStateFooter === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
</form>
</Card.Content>
+221 -9
View File
@@ -19,16 +19,20 @@
import { javascript } from "@codemirror/lang-javascript";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
import AllGamesListRaw from "$lib/all-games-list.json?raw";
import {
allRecordTypes,
ValidateIpAddress,
ValidateCronExpression,
IsValidHost,
IsValidNameServer
IsValidNameServer,
GetGameFromId,
IsValidPort
} from "$lib/clientTools.js";
import { GAMEDIG_SOCKET_TIMEOUT } from "$lib/anywhere";
const dispatch = createEventDispatcher();
let AllGamesList = JSON.parse(AllGamesListRaw);
const defaultEval = `(async function (statusCode, responseTime, responseData) {
let statusCodeShort = Math.floor(statusCode/100);
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
@@ -240,7 +244,7 @@
return;
}
//validating port
if (hosts[i].port < 1 || hosts[i].port > 65535) {
if (!IsValidPort(hosts[i].port)) {
invalidFormMessage = "Port should be valid";
return;
}
@@ -314,12 +318,7 @@
return;
}
//validating port
if (
!!!newMonitor.sslConfig.port ||
isNaN(newMonitor.sslConfig.port) ||
newMonitor.sslConfig.port < 1 ||
newMonitor.sslConfig.port > 65535
) {
if (!IsValidPort(newMonitor.sslConfig.port)) {
invalidFormMessage = "Port should be valid";
return;
}
@@ -402,6 +401,54 @@
}
newMonitor.type_data = JSON.stringify(newMonitor.heartbeatConfig);
} else if (newMonitor.monitor_type === "GAMEDIG") {
// validating host
if (!newMonitor.gamedigConfig.host) {
invalidFormMessage = "Host is required";
return;
}
if (ValidateIpAddress(newMonitor.gamedigConfig.host) === "Invalid") {
invalidFormMessage = "Invalid host";
return;
}
// validating port
if (!IsValidPort(newMonitor.gamedigConfig.port)) {
invalidFormMessage = "Port should be valid";
return;
}
// validating game
const game = GetGameFromId(AllGamesList, newMonitor.gamedigConfig.gameId);
if (!game) {
invalidFormMessage = "Invalid game/service";
return;
}
if (!game.isValve && newMonitor.gamedigConfig.requestRules) {
newMonitor.gamedigConfig.requestRules = false;
}
//validating timeout
if (!!newMonitor.gamedigConfig.timeout && isNaN(newMonitor.gamedigConfig.timeout)) {
invalidFormMessage = "Timeout should be a number";
return;
}
newMonitor.gamedigConfig.timeout = Number(newMonitor.gamedigConfig.timeout);
if (newMonitor.gamedigConfig.timeout < GAMEDIG_SOCKET_TIMEOUT) {
invalidFormMessage = `Timeout should be greater than ${GAMEDIG_SOCKET_TIMEOUT}`;
return;
}
// validating eval
if (!!newMonitor.gamedigConfig.eval) {
newMonitor.gamedigConfig.eval = newMonitor.gamedigConfig.eval.trim();
if (!(await isValidEval(newMonitor.gamedigConfig.eval))) {
invalidFormMessage = invalidFormMessage + ". Invalid eval";
return;
}
}
newMonitor.type_data = JSON.stringify(newMonitor.gamedigConfig);
}
formState = "loading";
@@ -431,6 +478,35 @@
pg: "PostgreSQL",
mysql2: "MySQL"
};
let deleteMonitorConfirmText = "";
let deletingMonitor = false;
async function deleteMonitor() {
if (deleteMonitorConfirmText != `delete ${newMonitor.tag}`) {
invalidFormMessage = "Please type the correct text to delete the monitor";
return;
}
deletingMonitor = true;
try {
let data = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "deleteMonitor", data: { tag: newMonitor.tag } })
});
let resp = await data.json();
if (resp.error) {
invalidFormMessage = resp.error;
} else {
dispatch("closeModal", {});
}
} catch (error) {
invalidFormMessage = "Error while deleting monitor";
} finally {
deletingMonitor = false;
}
}
</script>
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
@@ -655,6 +731,7 @@
<Select.Item value="SSL" label="SSL" class="text-sm font-medium">SSL</Select.Item>
<Select.Item value="SQL" label="SQL" class="text-sm font-medium">SQL</Select.Item>
<Select.Item value="HEARTBEAT" label="HEARTBEAT" class="text-sm font-medium">HEARTBEAT</Select.Item>
<Select.Item value="GAMEDIG" label="GAMEDIG" class="text-sm font-medium">GAMEDIG</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
@@ -1275,6 +1352,141 @@
</span>
</div>
</div>
{:else if newMonitor.monitor_type == "GAMEDIG"}
<div class="mt-4 grid grid-cols-6 gap-2">
<div class="col-span-4">
<Label for="gameId">
Game <span class="text-red-500">*</span>
</Label>
<Select.Root portal={null} onSelectedChange={(e) => (newMonitor.gamedigConfig.gameId = e.value)}>
<Select.Trigger id="gameId">
<Select.Value placeholder={GetGameFromId(AllGamesList, newMonitor.gamedigConfig.gameId).name || ""} />
</Select.Trigger>
<Select.Content class="max-h-56 overflow-y-auto">
<Select.Group>
<Select.Label>Game</Select.Label>
{#each AllGamesList as game}
<Select.Item value={game.id} label={game.name} class="text-sm font-medium">{game.name}</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="col-span-2 col-start-1">
<Label for="host">
Host <span class="text-red-500">*</span>
</Label>
<Input bind:value={newMonitor.gamedigConfig.host} id="host" placeholder="172.12.14.42" />
</div>
<div class="col-span-2">
<Label for="port">
Port <span class="text-red-500">*</span>
</Label>
<Input bind:value={newMonitor.gamedigConfig.port} id="port" placeholder="port number ex 8080" />
</div>
<div class="col-span-2">
<Label for="timeout">
Timeout(ms)
<span class="text-red-500">*</span>
</Label>
<Input bind:value={newMonitor.gamedigConfig.timeout} id="timeout" />
</div>
<div class="col-span-6">
<label class="cursor-pointer">
<input
type="checkbox"
on:change={(e) => {
newMonitor.gamedigConfig.guessPort = e.target.checked;
}}
checked={newMonitor.gamedigConfig.guessPort}
/>
<span class="ml-2 text-sm">Guess port</span>
<p class="my-1 text-xs text-muted-foreground">
Used port can be different from client port, depending on queried game. Try this if you have
unsuccessful responses.
</p>
</label>
</div>
{#if newMonitor.gamedigConfig.gameId && GetGameFromId(AllGamesList, newMonitor.gamedigConfig.gameId).isValve}
<div class="col-span-6">
<label class="cursor-pointer">
<input
type="checkbox"
on:change={(e) => {
newMonitor.gamedigConfig.requestRules = e.target.checked;
}}
checked={newMonitor.gamedigConfig.requestRules}
/>
<span class="ml-2 text-sm">Request additional rules</span>
<p class="my-1 text-xs text-muted-foreground">
Valve games can provide additional 'rules' to Gamedig monitors. If checked, they will be available in
`reponseRaw.raw`, beware that it may increase query time.
</p>
</label>
</div>
{/if}
<div class="col-span-6 mt-2">
<Label for="eval">Eval</Label>
<p class="my-1 text-xs text-muted-foreground">
You can write a custom eval function to evaluate the response. The function should return a promise that
resolves to an object with status and latency. <a
target="_blank"
class="font-medium text-primary"
href="https://kener.ing/docs/monitors-gamedig#eval">Read the docs</a
> to learn
</p>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={newMonitor.gamedigConfig.eval}
lang={javascript()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "18rem",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "0.375rem"
}
}}
/>
</div>
</div>
</div>
{/if}
{#if !!newMonitor.id}
<div class="mt-4 flex">
<div class="flex w-full flex-col rounded-md border border-destructive p-2">
<div class="w-full">
<h2 class="flex justify-between text-lg">
<span class="text-destructive">Danger Zone</span>
</h2>
<p class="text-sm">Deleting a monitor is irreversible. Please be sure before deleting.</p>
</div>
<form class="flex gap-x-2" on:submit|preventDefault={deleteMonitor}>
<div class="mt-1 flex flex-col gap-y-2">
<Label for="deleteMonitor">
Type <i class="text-destructive">delete {newMonitor.tag}</i> to confirm
</Label>
<Input bind:value={deleteMonitorConfirmText} id="deleteMonitor" required />
</div>
<Button
variant="destructive"
type="submit"
class="mt-7"
disabled={deleteMonitorConfirmText != `delete ${newMonitor.tag}`}
>
Delete
{#if deletingMonitor}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</form>
</div>
</div>
{/if}
</div>
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
+216 -25
View File
@@ -5,6 +5,7 @@
import Settings from "lucide-svelte/icons/settings";
import Bell from "lucide-svelte/icons/bell";
import Loader from "lucide-svelte/icons/loader";
import DatabaseZap from "lucide-svelte/icons/database-zap";
import ArrowDownUp from "lucide-svelte/icons/arrow-down-up";
import Grip from "lucide-svelte/icons/grip";
import ExternalLink from "lucide-svelte/icons/external-link";
@@ -15,22 +16,32 @@
import { onMount } from "svelte";
import * as Card from "$lib/components/ui/card";
import * as Select from "$lib/components/ui/select";
import AllGamesListRaw from "$lib/all-games-list.json?raw";
import { storeSiteData, SortMonitor, RandomString } from "$lib/clientTools.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { DateInput } from "date-picker-svelte";
import { dndzone } from "svelte-dnd-action";
import { flip } from "svelte/animate";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { DefaultAPIEval, DefaultTCPEval, DefaultPingEval } from "$lib/anywhere.js";
import {
DefaultAPIEval,
DefaultTCPEval,
DefaultPingEval,
DefaultGamedigEval,
GAMEDIG_TIMEOUT
} from "$lib/anywhere.js";
export let categories = [];
export let colorDown = "#777";
export let colorDegraded = "#777";
export let monitorSort = [];
let AllGamesList = JSON.parse(AllGamesListRaw);
let monitors = [];
let status = "ACTIVE";
let showAddMonitor = false;
let formState = "idle";
let modifyMonitoringDataFormState = "idle";
let loadingData = false;
let triggers = [];
let selectedCategory = "All Categories";
@@ -119,6 +130,15 @@
degradedRemainingMinutes: 1,
downRemainingMinutes: 2,
secretString: RandomString(32)
},
gamedigConfig: {
gameId: AllGamesList[0].id,
host: "",
port: "",
timeout: GAMEDIG_TIMEOUT,
guessPort: false,
requestRules: false,
eval: DefaultGamedigEval
}
};
}
@@ -157,6 +177,8 @@
newMonitor.sqlConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "HEARTBEAT") {
newMonitor.heartbeatConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "GAMEDIG") {
newMonitor.gamedigConfig = JSON.parse(newMonitor.type_data);
}
showAddMonitor = true;
}
@@ -196,7 +218,12 @@
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "getTriggers", data: {} })
body: JSON.stringify({
action: "getTriggers",
data: {
status: "ACTIVE"
}
})
});
triggers = await apiResp.json();
} catch (error) {
@@ -228,30 +255,105 @@
};
});
let shareMenusToggle = false;
let monitorTriggers = {
down_trigger: {
failureThreshold: 1,
trigger_type: "DOWN",
successThreshold: 1,
description: "The monitor is down",
createIncident: "NO",
active: false,
triggers: [],
severity: "critical"
},
degraded_trigger: {
failureThreshold: 1,
trigger_type: "DEGRADED",
successThreshold: 1,
active: false,
description: "The monitor is degraded",
createIncident: "NO",
triggers: [],
severity: "warning"
}
let monitoringDataMenusToggle = false;
let monitorDataSelected;
let monitoringDataRequest = {
newStatus: "UP"
};
function initMonitorTriggers() {
monitorTriggers = {
down_trigger: {
failureThreshold: 1,
trigger_type: "DOWN",
successThreshold: 1,
description: "The monitor is down",
createIncident: "NO",
active: false,
triggers: [],
severity: "critical"
},
degraded_trigger: {
failureThreshold: 1,
trigger_type: "DEGRADED",
successThreshold: 1,
active: false,
description: "The monitor is degraded",
createIncident: "NO",
triggers: [],
severity: "warning"
}
};
}
let modifyMonitoringDataError = "";
async function ModifyMonitoringDataTriggers() {
modifyMonitoringDataError = "";
if (!monitoringDataRequest.start) {
modifyMonitoringDataError = "Invalid start date";
return;
}
if (!monitoringDataRequest.end) {
modifyMonitoringDataError = "Invalid end date";
return;
}
if (!monitorDataSelected.tag) {
modifyMonitoringDataError = "Invalid monitor";
return;
}
if (
!monitoringDataRequest.newStatus ||
(monitoringDataRequest.newStatus != "UP" &&
monitoringDataRequest.newStatus != "DEGRADED" &&
monitoringDataRequest.newStatus != "DOWN")
) {
modifyMonitoringDataError = "Invalid status";
return;
}
let startTimestamp = parseInt(new Date(monitoringDataRequest.start).getTime() / 1000);
let endTimestamp = parseInt(new Date(monitoringDataRequest.end).getTime() / 1000);
if (startTimestamp >= endTimestamp) {
modifyMonitoringDataError = "The start date must be before the end date";
return;
}
let data = {
monitor_tag: monitorDataSelected.tag,
start: startTimestamp,
end: endTimestamp,
newStatus: monitoringDataRequest.newStatus
};
modifyMonitoringDataFormState = "loading";
try {
let apiResp = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "updateMonitoringData", data })
});
let resp = await apiResp.json();
if (resp.error) {
modifyMonitoringDataError = resp.error;
} else {
monitoringDataMenusToggle = false;
monitoringDataRequest = {
newStatus: "UP"
};
}
} catch (error) {
modifyMonitoringDataError = "Error while modifying monitoring data";
} finally {
modifyMonitoringDataFormState = "idle";
}
}
let monitorTriggers;
initMonitorTriggers();
let saveTriggerError = "";
async function saveTriggers() {
saveTriggerError = "";
@@ -286,6 +388,7 @@
}
let currentAlertMonitor;
function openAlertMenu(m) {
initMonitorTriggers();
currentAlertMonitor = m;
if (m.down_trigger) {
monitorTriggers.down_trigger = m.down_trigger;
@@ -296,7 +399,10 @@
shareMenusToggle = true;
}
const flipDurationMs = 200;
function openMonitoringDataMenu(m) {
monitoringDataMenusToggle = true;
monitorDataSelected = m;
}
let orderErrorMessage = "";
function handleSort(e) {
dropTargetStyle = {
@@ -527,6 +633,9 @@
>
<Bell class="inline h-4 w-4" />
</Button>
<Button variant="secondary" class="h-8 w-8 p-2" on:click={() => openMonitoringDataMenu(monitor)}>
<DatabaseZap class="inline h-4 w-4" />
</Button>
{/if}
<Button variant="secondary" class="h-8 w-8 p-2" rel="external" href="{base}/?monitor={monitor.tag}">
<ExternalLink class="inline h-4 w-4" />
@@ -570,6 +679,88 @@
{/each}
</div>
{#if monitoringDataMenusToggle}
<div class="moldal-container fixed left-0 top-0 z-50 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm">
<div
class="absolute left-1/2 top-1/2 h-fit w-full max-w-2xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
>
<Button
variant="ghost"
on:click={() => {
monitoringDataMenusToggle = false;
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
<X class="h-4 w-4 text-muted-foreground" />
</Button>
<div class="content px-4 py-4">
<h2 class="text-lg font-semibold">Modify monitors data</h2>
<p class="text-xs text-muted-foreground">You can modify the monitor results for a given time range.</p>
<div class="grid grid-cols-3 gap-2">
<div class="col-span-1">
<Label for="start_date_time">
Start date
<span class="text-red-500">*</span>
</Label>
<DateInput
bind:value={monitoringDataRequest.start}
id="start_date_time"
timePrecision="minute"
class=" text-sm"
/>
</div>
<div class="col-span-1">
<Label for="end_date_time">
End date
<span class="text-red-500">*</span>
</Label>
<DateInput
bind:value={monitoringDataRequest.end}
id="end_date_time"
timePrecision="minute"
class="text-sm"
min={monitoringDataRequest.start}
/>
</div>
<div class="col-span-1">
<Label for="monitorDataNewStatus">New status</Label>
<Select.Root portal={null} value="UP" onSelectedChange={(e) => (monitoringDataRequest.newStatus = e.value)}>
<Select.Trigger id="monitorDataNewStatus">
<Select.Value bind:value={monitoringDataRequest.newStatus} placeholder="UP" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Item value="UP" label="UP" class="text-sm font-medium">UP</Select.Item>
<Select.Item value="DEGRADED" label="DEGRADED" class="text-sm font-medium">DEGRADED</Select.Item>
<Select.Item value="DOWN" label="DOWN" class="text-sm font-medium">DOWN</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</div>
</div>
<div class="mt-4 flex justify-end gap-x-2">
{#if !!modifyMonitoringDataError}
<div class="overflow-hidden text-ellipsis whitespace-nowrap py-2 text-sm font-medium text-destructive">
{modifyMonitoringDataError}
</div>
{/if}
<Button
class=""
on:click={ModifyMonitoringDataTriggers}
disabled={modifyMonitoringDataFormState === "loading"}
>
Save
{#if modifyMonitoringDataFormState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
</div>
</div>
{/if}
{#if shareMenusToggle}
<div class="moldal-container fixed left-0 top-0 z-50 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm">
<div
+216 -74
View File
@@ -5,9 +5,11 @@
import { Label } from "$lib/components/ui/label";
import autoAnimate from "@formkit/auto-animate";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { base } from "$app/paths";
import Plus from "lucide-svelte/icons/plus";
import X from "lucide-svelte/icons/x";
import Check from "lucide-svelte/icons/check";
import Loader from "lucide-svelte/icons/loader";
import Info from "lucide-svelte/icons/info";
import { Tooltip } from "bits-ui";
@@ -24,26 +26,7 @@
}
];
}
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);
}
@@ -52,19 +35,6 @@
metaTags: JSON.stringify(metaTags)
}).metaTags;
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;
});
let formState = "idle";
let formStateAnalytics = "idle";
@@ -87,17 +57,150 @@
async function formSubmitAnalytics() {
seoError = "";
formStateAnalytics = "loading";
let resp = await storeSiteData({
analytics: JSON.stringify(analytics)
let dataToSave = {};
selectedAnalytics.requirements.forEach((req) => {
dataToSave[req.label] = req.value;
});
let finalData = {};
finalData[selectedAnalytics.key] = JSON.stringify({
requirements: dataToSave,
isEnabled: selectedAnalytics.isEnabled
});
let resp = await storeSiteData(finalData);
//print data
let data = await resp.json();
formStateAnalytics = "idle";
if (data.error) {
seoError = data.error;
return;
} else {
selectedAnalytics.activeInSite = selectedAnalytics.isEnabled;
//update activeInSite in analyticsDropdownItems also
let index = analyticsDropdownItems.findIndex((item) => item.label === selectedAnalytics.label);
analyticsDropdownItems[index].activeInSite = true;
}
}
let selectedAnalytics;
let analyticsDropdownItems = [
{
label: "Google Analytics",
logo: `/ga.png`,
key: "analytics.googleTagManager",
isEnabled: false,
requirements: [
{
label: "Measurement ID",
type: "text",
placeholder: "G-S05E5E5E5E5",
required: true,
value: ""
}
]
},
{
label: "Plausible",
logo: `/plausible.png`,
key: "analytics.plausible",
isEnabled: false,
requirements: [
{
label: "Domain",
type: "text",
placeholder: "kener.ing",
required: true,
value: new URL(data.siteData.siteURL).hostname
},
{
label: "API",
type: "text",
placeholder: "https://plausible.io/api/event",
required: true,
value: "https://plausible.io/api/event"
},
{
label: "Script Source",
type: "text",
placeholder: "https://plausible.io/js/script.pageview-props.tagged-events.js",
required: true,
value: "https://plausible.io/js/script.pageview-props.tagged-events.js"
}
]
},
{
label: "MixPanel",
logo: `/mixpanel.png`,
key: "analytics.mixpanel",
isEnabled: false,
requirements: [
{
label: "Project Token",
type: "text",
placeholder: "YOUR_PROJECT_TOKEN",
required: true,
value: ""
}
]
},
{
label: "Amplitude",
logo: `/amplitude.png`,
key: "analytics.amplitude",
isEnabled: false,
requirements: [
{
label: "Amplitude API Key",
type: "text",
placeholder: "API key for your Amplitude project",
required: true,
value: ""
}
]
},
{
label: "Microsoft Clarity",
logo: `/clarity.png`,
key: "analytics.clarity",
isEnabled: false,
requirements: [
{
label: "Project ID",
type: "text",
placeholder: "Project ID for your Microsoft Clarity project",
required: true,
value: ""
}
]
},
{
label: "Umami",
logo: `/umami.png`,
key: "analytics.umami",
isEnabled: false,
requirements: [
{
label: "Website ID",
type: "text",
placeholder: "Website ID for your umami script and domain",
required: true,
value: ""
}
]
}
];
analyticsDropdownItems = analyticsDropdownItems.map((item) => {
let dbData = data.siteData[item.key];
if (!!dbData) {
item.isEnabled = dbData.isEnabled;
item.requirements.forEach((req) => {
req.value = dbData.requirements[req.label];
});
item.activeInSite = true;
}
return item;
});
selectedAnalytics = analyticsDropdownItems[0];
</script>
<Card.Root class="mt-4">
@@ -106,48 +209,87 @@
<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>
<div class="mt-4 grid grid-cols-4 overflow-hidden rounded-md border">
<div class="col-span-1 flex flex-col border-r">
{#each analyticsDropdownItems as item}
<Button
variant={!!selectedAnalytics && selectedAnalytics.label === item.label ? "primary" : "ghost"}
class="flex items-center justify-between gap-x-2 rounded-none border-b text-sm last:border-none"
on:click={() => (selectedAnalytics = item)}
>
<div class="flex flex-row gap-x-3">
<img src={base + item.logo} class="w-5" alt={item.label} />
<span>{item.label}</span>
</div>
{#if item.activeInSite && item.isEnabled}
<div class="rounded-sm">
<span class="relative flex size-2">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
></span>
<span class="relative inline-flex size-2 rounded-full bg-green-500"></span>
</span>
</div>
{/if}
</Button>
{/each}
</div>
<div class="flex w-full justify-end 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" />
<form on:submit|preventDefault={formSubmitAnalytics} class="col-span-3 flex flex-col justify-between p-3 py-2">
{#if !!selectedAnalytics}
<div>
<div class="mb-2 text-sm font-medium text-muted-foreground">
Add details for your {selectedAnalytics.label} account
</div>
</div>
<div class="grid grid-cols-4 gap-4">
{#each selectedAnalytics.requirements as req}
<div class="col-span-2 flex flex-col gap-y-2">
<Label for={req.label}>{req.label}</Label>
<Input
bind:value={req.value}
class=""
type={req.type}
placeholder={req.placeholder}
bind:required={req.required}
/>
</div>
{/each}
</div>
<div class="mt-2 flex flex-row justify-between">
<div class="flex flex-row gap-x-2">
<label class="inline-flex cursor-pointer items-center">
<input
type="checkbox"
class="peer sr-only"
checked={selectedAnalytics.isEnabled}
on:change={() => {
selectedAnalytics.isEnabled = !selectedAnalytics.isEnabled;
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
<div class="pt-2.5 text-sm font-medium text-muted-foreground">
{selectedAnalytics.label} is
<i class="underline">{!selectedAnalytics.isEnabled ? "disabled" : "enabled"}</i> for your site
</div>
</div>
<div>
<Button type="submit" disabled={formStateAnalytics === "loading"}>
Save Changes for {selectedAnalytics.label}
{#if formStateAnalytics === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
{#if !!seoError}
<div class="py-2 text-sm font-medium text-destructive">{seoError}</div>
{/if}
</Button>
</div>
</form>
{/if}
</form>
</div>
</Card.Content>
</Card.Root>
<Card.Root class="mt-4">
@@ -202,7 +344,7 @@
<div class="py-2 text-sm font-medium text-destructive">{metaError}</div>
{/if}
<Button type="submit" disabled={formState === "loading"}>
Save
Save Meta Tags
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
+103 -37
View File
@@ -14,6 +14,11 @@
import { Tooltip } from "bits-ui";
import ColorPicker from "svelte-awesome-color-picker";
import CodeMirror from "svelte-codemirror-editor";
import { css } from "@codemirror/lang-css";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
export let data;
let formState = "idle";
@@ -26,6 +31,7 @@
barStyle: "PARTIAL",
barRoundness: "ROUNDED",
summaryStyle: "CURRENT",
kenerTheme: "default",
colorsJ: {
UP: "#4ead94",
DOWN: "#ca3038",
@@ -95,16 +101,64 @@
</p>
<div class="flex">
<RadioGroup.Root bind:value={themeData.pattern} onValueChange={(e) => (themeData.pattern = e)}>
<div class="flex items-center space-x-2">
<RadioGroup.Root
class="flex flex-1 flex-wrap justify-start gap-4"
bind:value={themeData.pattern}
onValueChange={(e) => (themeData.pattern = e)}
>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="squares" id="rsq" />
<Label for="rsq">Squares</Label>
</div>
<div class="flex items-center space-x-2">
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="dots" id="rdot" />
<Label for="rdot">Dots</Label>
</div>
<div class="flex items-center space-x-2">
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="tiles" id="tiles" />
<Label for="tiles">Tiles</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="radial-blue" id="radial-blue" />
<Label for="radial-blue">Radial Blue</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="radial-mono" id="radial-mono" />
<Label for="radial-mono">Radial Mono</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="radial-midnight" id="radial-midnight" />
<Label for="radial-midnight">Radial Midnight</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="circle-mono" id="circle-mono" />
<Label for="circle-mono">Circle Mono</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="carbon-fibre" id="carbon-fibre" />
<Label for="carbon-fibre">Carbon Fibre</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="texture-sky" id="texture-sky" />
<Label for="texture-sky">Texture Sky</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="angular-mono" id="angular-mono" />
<Label for="angular-mono">Angular Mono</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="angular-spring" id="angular-spring" />
<Label for="angular-spring">Angular Spring</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="angular-bloom" id="angular-bloom" />
<Label for="angular-bloom">Angular Bloom</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="pets" id="pets" />
<Label for="pets">Pets</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="none" id="rnone" />
<Label for="rnone">None</Label>
</div>
@@ -239,53 +293,51 @@
</RadioGroup.Root>
</div>
<hr />
<p class="font-medium">
Colors
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the colors for the monitor. You can choose between UP, DEGRADED, and DOWN.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="relative w-full">
<div>
<p class="font-medium">Status Colors</p>
<p class=" text-xs font-semibold text-muted-foreground">
Choose the colors for the monitor. You can choose between UP, DEGRADED, and DOWN.
</p>
</div>
<div class="flex w-full flex-row justify-start gap-x-8">
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.UP}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="UP"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.UP = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.DEGRADED}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="DEGRADED"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DEGRADED = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.DOWN}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="DOWN"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DOWN = event.detail.hex;
}}
@@ -338,14 +390,28 @@
<div class="flex w-full flex-row justify-start gap-2">
<div class="w-full">
<Label for="customCSS" class="text-sm text-muted-foreground">
You can add custom CSS to style your site. Do not include style tags.
You can add custom CSS to style your site. Do not include style tags. Go to <a
href="https://kener.ing/docs/custom-js-css-guide#grid-layout"
class="text-card-foreground"
target="_blank">this link</a
> to learn more about it.
</Label>
<textarea
bind:value={themeData.customCSS}
id="customCSS"
class="mt-1 h-48 w-full rounded-sm border p-2"
placeholder=".className&#123;color: red;&#125;"
></textarea>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={themeData.customCSS}
lang={css()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "320px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "0.375rem"
}
}}
/>
</div>
</div>
</div>
<hr />
@@ -354,7 +420,7 @@
<div class="py-2 text-sm font-medium text-destructive">{themeError}</div>
{/if}
<Button type="submit" disabled={formState === "loading"}>
Save
Save Theme Settings
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
+18 -8
View File
@@ -8,7 +8,7 @@
import { sub, startOfDay, getUnixTime } from "date-fns";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { analyticsEvent } from "$lib/boringOne";
import Share2 from "lucide-svelte/icons/share-2";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import Settings from "lucide-svelte/icons/settings";
@@ -94,33 +94,33 @@
let uptimesRollers = [
{
text: `${l(lang, "90 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 90 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 89,
endTs: monitor.pageData.maxDateTodayTimestamp,
value: uptime90Day
},
{
text: `${l(lang, "60 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 59 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 59,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "30 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 29 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 29,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "14 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 13 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 13,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "7 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 }))),
startTs: monitor.pageData.startOfTheDay - 86400 * 6,
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: l(lang, "Today"),
startTs: getUnixTime(startOfDay(new Date())),
startTs: monitor.pageData.startOfTheDay,
endTs: monitor.pageData.maxDateTodayTimestamp
}
];
@@ -130,6 +130,10 @@
let rollerLoading = false;
async function rollSummary(r) {
let newRolledAt = r;
analyticsEvent("monitor_interval_switch", {
tag: monitor.tag,
interval: uptimesRollers[newRolledAt].text
});
if (uptimesRollers[newRolledAt].value === undefined) {
rollerLoading = true;
@@ -179,12 +183,18 @@
if (embed) {
return;
}
let incidentIDs = incidentObj?.ids || [];
dayUptime = "NA";
dateFetchedFor = f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang, $page.data.localTz);
showDailyDataModal = true;
loadingDayData = true;
dayIncidentsFull = [];
analyticsEvent("monitor_day_data", {
tag: monitor.tag,
data_date: dateFetchedFor
});
setTimeout(() => {
getToday(bar.timestamp, incidentIDs);
}, 50);
@@ -213,7 +223,7 @@
{@html monitor.description}
{/if}
</p>
<div class="absolute right-14 top-5 flex gap-x-2">
<div class="absolute right-4 top-5 flex gap-x-2 md:right-14">
{#if $page.data.isLoggedIn}
<Button
href="{base}/manage/app/monitors#{monitor.tag}"
+57 -3
View File
@@ -8,11 +8,62 @@
import GMI from "$lib/components/gmi.svelte";
export let data;
let defaultPattern = data.site?.pattern || "squares";
let allPets = [
{
url: base + "/chicken.gif",
bottom: "-5"
},
{
url: base + "/dog.gif",
bottom: "-17"
},
{
url: base + "/cockatiel.gif",
bottom: "-10"
},
{
url: base + "/crab.gif",
bottom: "-20"
},
{
url: base + "/fox.gif",
bottom: "-9"
},
{
url: base + "/horse.gif",
bottom: "-11"
},
{
url: base + "/panda.gif",
bottom: "0"
},
{
url: base + "/totoro.gif",
bottom: "-27"
},
{
url: base + "/rabbit.gif",
bottom: "0"
},
{
url: base + "/duck.gif",
bottom: "-5"
},
{
url: base + "/snake.gif",
bottom: "0"
}
];
let randomPet = allPets[Math.floor(Math.random() * allPets.length)];
</script>
<div class="{defaultPattern}-pattern"></div>
{#if defaultPattern == "pets" && !!randomPet}
<div class="pets-pattern" style="background-image: url({randomPet.url});bottom: {randomPet.bottom}px"></div>
{:else}
<div class="{defaultPattern}-pattern"></div>
{/if}
<header class="blurry-bg sticky top-0 z-50 mx-auto md:mt-2">
<header class="sticky top-0 z-50 mx-auto md:mt-2">
<div class="container flex h-14 max-w-[820px] items-center border bg-card px-3 md:rounded-md">
<a rel="external" href={data.site.home ? data.site.home : base} class="mr-6 flex items-center space-x-2">
{#if data.site.logo}
@@ -32,7 +83,10 @@
rel="external"
href={navItem.url}
class="flex rounded-md px-3 py-2 text-card-foreground transition-all ease-linear hover:bg-background"
on:click={() => analyticsEvent("nav", navItem.name)}
on:click={() =>
analyticsEvent("navigation", {
name: navItem.name
})}
>
{#if navItem.iconURL}
<GMI src={navItem.iconURL} classList="mr-1.5 mt-0.5 inline h-4" alt={navItem.name} />
+366
View File
@@ -0,0 +1,366 @@
<script>
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import { page } from "$app/stores";
import { base } from "$app/paths";
import { onMount } from "svelte";
import GMI from "$lib/components/gmi.svelte";
import autoAnimate from "@formkit/auto-animate";
import Loader from "lucide-svelte/icons/loader";
import SquareChartGantt from "lucide-svelte/icons/square-chart-gantt";
import AlarmClockCheck from "lucide-svelte/icons/alarm-clock-check";
import Logout from "lucide-svelte/icons/log-out";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { l } from "$lib/i18n/client";
let apiError = "";
let view = "loading";
let localStorageKey = "user_subscription";
let login = {
userEmail: "",
verifyWithToken: "",
verificationCode: ""
};
let subscribedMonitors = [];
let subsInfo = {
allMonitors: false,
monitors: [],
hasActiveSubscription: false
};
let subscribableMonitors = $page.data.subscribableMonitors;
let callingAPI = false;
let alreadySubscribed = false;
let subscriberData = null;
onMount(async () => {
// Try to fetch stored subscription data from localStorage
await fetchStoredSubscription();
//
});
async function createNewUserSubscription() {
callingAPI = true;
apiError = "";
let action = "subscribe";
try {
const response = await callAPI(action, {
token: localStorage.getItem(localStorageKey),
monitors: subsInfo.monitors,
allMonitors: subsInfo.allMonitors
});
await fetchStoredSubscription();
} catch (e) {
apiError = e.message;
} finally {
callingAPI = false;
}
}
async function removeSubscription() {
//use confirm
if (!confirm("Are you sure you want to unsubscribe?")) {
return;
}
callingAPI = true;
apiError = "";
let action = "unsubscribe";
try {
await callAPI(action, {
token: localStorage.getItem(localStorageKey)
});
await fetchStoredSubscription();
} catch (e) {
apiError = e.message;
} finally {
callingAPI = false;
}
}
async function callAPI(action, data) {
try {
const response = await fetch(`${base}/api/subscriptions/${action}`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
if (response.ok) {
return await response.json();
} else {
let error = await response.json();
throw new Error(error.message || "An error occurred while processing your request.");
}
} catch (error) {
console.error("Error calling API:", error);
throw error;
}
}
async function doLogin() {
let action = "login";
callingAPI = true;
apiError = "";
try {
const response = await callAPI(action, {
userEmail: login.userEmail
});
if (response) {
login.verifyWithToken = response.token;
login.verificationCode = "";
view = "verify_login";
}
} catch (e) {
apiError = e.message;
} finally {
callingAPI = false;
}
}
async function verifyLogin() {
let action = "verify";
callingAPI = true;
apiError = "";
try {
const response = await callAPI(action, {
token: login.verifyWithToken,
code: login.verificationCode
});
if (response) {
localStorage.setItem(localStorageKey, response.token);
await fetchStoredSubscription();
}
} catch (e) {
apiError = e.message;
} finally {
callingAPI = false;
}
}
async function fetchStoredSubscription() {
let action = "fetch";
view = "loading";
let token = localStorage.getItem(localStorageKey);
if (!!!token) {
view = "login";
return;
}
try {
const response = await callAPI(action, {
token: token
});
if (response) {
subsInfo.email = response.email;
view = "subscribe";
subsInfo.monitors = response.monitors;
if (subsInfo.monitors && subsInfo.monitors.length > 0) {
subsInfo.hasActiveSubscription = true;
//if parsedData.monitors contains "_", set allMonitors to true
const allMonitorsIncluded = subsInfo.monitors.includes("_");
if (allMonitorsIncluded) {
subsInfo.allMonitors = true;
} else {
subsInfo.allMonitors = false;
subsInfo.monitors = subsInfo.monitors.filter((m) => m !== "_");
}
} else {
subsInfo.allMonitors = false;
subsInfo.monitors = [];
subsInfo.hasActiveSubscription = false; // Updated to set hasActiveSubscription
}
}
} catch (e) {
view = "login";
}
}
function doLogout() {
localStorage.removeItem(localStorageKey);
login.userEmail = "";
login.verificationCode = "";
login.verifyWithToken = "";
view = "login";
}
</script>
<div class="flex flex-col gap-2 pb-4" use:autoAnimate>
<div class="relative mb-2 scroll-m-20 px-4 pt-4 text-xl font-medium tracking-tight">
<p>
{l($page.data.lang, "Manage Subscription")}
</p>
</div>
{#if view === "login"}
<div class="flex flex-col gap-2 px-4">
<div>
<p class="mb-2 text-xs font-semibold text-muted-foreground">
{l(
$page.data.lang,
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe."
)}
</p>
<div class="relative">
<Input type="email" placeholder={l($page.data.lang, "Enter your email")} bind:value={login.userEmail} />
</div>
</div>
</div>
<div class="mt-2 flex flex-col gap-2 px-4">
{#if apiError}
<p class="text-xs font-semibold text-destructive">{apiError}</p>
{/if}
<Button class="w-full" disabled={callingAPI || !login.userEmail} on:click={doLogin}>
{l($page.data.lang, "Login")}
{#if callingAPI}
<Loader class="ml-2 h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
{/if}
{#if view === "loading"}
<div class="flex h-20 justify-center align-middle">
<Loader class="mt-5 h-10 w-10 animate-spin" />
</div>
{/if}
{#if view === "subscribe"}
<div class="flex flex-col gap-2 px-4">
{#if subsInfo.email}
<div
class="flex justify-between rounded-md stroke-secondary-foreground py-2 text-xs font-medium text-muted-foreground"
>
<span class="mt-1 text-xs font-semibold text-muted-foreground">
{l($page.data.lang, "You are logged in as %email", { email: subsInfo.email })}
</span>
<div>
<Button
variant="ghost"
size="icon"
class=" h-6 w-6 justify-center text-xs font-semibold text-muted-foreground"
on:click={doLogout}
>
<Logout class=" h-4 w-4" />
</Button>
</div>
</div>
{/if}
<div class="mt-2">
<label class="flex w-full cursor-pointer items-center justify-between">
<span class="text-sm font-medium">{l($page.data.lang, "Subscribe to all monitors")}</span>
<input
type="checkbox"
value=""
class="peer sr-only"
checked={subsInfo.allMonitors}
on:change={(e) => {
subsInfo.allMonitors = e.target.checked;
if (e.target.checked) {
subsInfo.monitors = [];
}
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</div>
</div>
{#if subscribableMonitors.length > 0 && !subsInfo.allMonitors}
<div class="flex flex-col gap-2">
<p class="px-4">
<span class="text-xs font-semibold text-muted-foreground">
{l($page.data.lang, "Please select specific monitors to receive updates from.")}
</span>
</p>
<div class="flex max-h-96 flex-col gap-2 overflow-y-auto px-4">
{#each subscribableMonitors as monitor}
<div class="flex items-center justify-between gap-2 border-b pb-2 last:border-0">
<label for={monitor.tag} class="flex gap-x-2 text-sm font-medium text-gray-900 dark:text-gray-300">
{#if !!monitor.image}
<GMI src={monitor.image} classList="mt-1 h-4 w-4" />
{:else}
<SquareChartGantt class="mt-1 h-4 w-4" />
{/if}
{monitor.name}
</label>
<input
type="checkbox"
on:change={(e) => {
if (e.target.checked) {
subsInfo.monitors.push(monitor.tag);
} else {
subsInfo.monitors = subsInfo.monitors.filter((tag) => tag !== monitor.tag);
}
}}
id={monitor.tag}
value={monitor.tag}
bind:group={subsInfo.monitors}
class="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
/>
</div>
{/each}
</div>
</div>
{/if}
<div class="mt-2 flex flex-col gap-2 px-4">
{#if apiError}
<p class="text-xs font-semibold text-destructive">{apiError}</p>
{/if}
<Button class="w-full" disabled={callingAPI} on:click={createNewUserSubscription}>
{#if subsInfo.monitors.length > 0}
{l($page.data.lang, "Update Subscription")}
{:else}
{l($page.data.lang, "Subscribe")}
{/if}
{#if callingAPI}
<Loader class="ml-2 h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
{/if}
{#if view === "verify_login"}
<div class="flex flex-col gap-2 px-4">
<div>
<Button
variant="outline"
class="bounce-left mb-2 h-8 justify-start pl-1.5 text-xs text-muted-foreground"
on:click={() => (view = "login")}
>
<ChevronLeft class="arrow mr-1 h-5 w-5" />
{l($page.data.lang, "Change Email")}
</Button>
<p class="mb-2 text-xs text-muted-foreground">
{l($page.data.lang, "We have sent a code to your email. Please enter it below to confirm your login")}
</p>
<Input type="text" placeholder={l($page.data.lang, "Enter the code")} bind:value={login.verificationCode} />
</div>
</div>
<div class="mt-2 flex flex-col gap-2 px-4">
{#if apiError}
<p class="text-xs font-semibold text-destructive">{apiError}</p>
{/if}
<Button class="w-full" disabled={callingAPI || !login.verificationCode} on:click={verifyLogin}>
{l($page.data.lang, "Confirm Login")}
{#if callingAPI}
<Loader class="ml-2 h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
{/if}
</div>
+2 -1
View File
@@ -1,7 +1,7 @@
// @ts-nocheck
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr, nb } from "date-fns/locale";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr, nb, pl } from "date-fns/locale";
import { formatInTimeZone } from "date-fns-tz";
const locales = {
@@ -19,6 +19,7 @@ const locales = {
"pt-BR": ptBR,
tr,
"nb-NO": nb,
pl,
};
const f = function (date, formatStr, locale, tz) {
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 Tage",
"90 Days": "90 Tage",
"Availability per Component": "Verfügbarkeit pro Komponente",
"All Systems are Degraded": "Alle Systeme sind eingeschränkt",
"All Systems are Down": "Alle Systeme sind ausgefallen",
"All Systems are Operational": "Alle Systeme sind betriebsbereit",
"Back": "Zurück",
"Badge Copied": "Abzeichen kopiert",
"Badge": "Abzeichen",
"Browse Events": "Ereignisse durchsuchen",
"Change Email": "E-Mail ändern",
"Code Copied": "Code kopiert",
"Confirm Login": "Login bestätigen",
"Copy Code": "Code kopieren",
"Copy Link": "Link kopieren",
"DEGRADED": "EINGESCHRÄNKT",
@@ -19,6 +24,9 @@
"Days": "Tage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
"Embed": "Einbetten",
"Enter the code": "Code eingeben",
"Enter your email": "E-Mail-Adresse eingeben",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Geben Sie Ihre E-Mail-Adresse ein, um sich anzumelden. Wenn Sie nicht abonniert haben, werden Sie zum Abonnieren aufgefordert.",
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
"IDENTIFIED": "IDENTIFIZIERT",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "Dauerte etwa %lastedFor",
"Light": "Hell",
"Link Copied": "Link kopiert",
"Login": "Anmelden",
"MAINTENANCE": "WARTUNG",
"MONITORING": "ÜBERWACHUNG",
"Maintenance Completed": "Wartung abgeschlossen",
"Maintenance in Progress": "Wartung läuft",
"Manage Subscription": "Abonnement verwalten",
"Mode": "Modus",
"No Data": "Keine Daten",
"No Incident in %date": "Kein Vorfall am %date",
@@ -40,11 +50,15 @@
"No Updates Yet": "Noch keine Updates",
"Ongoing Incidents": "Laufende Vorfälle",
"Pinging": "Pingen",
"Please select specific monitors to receive updates from.": "Bitte wählen Sie bestimmte Monitore aus, von denen Sie Updates erhalten möchten.",
"RESOLVED": "GELÖST",
"Recent Incidents": "Kürzliche Vorfälle",
"Recent Maintenances": "Kürzliche Wartungen",
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
"Share": "Teilen",
"Some Systems are not working as expected": "Einige Systeme funktionieren nicht wie erwartet",
"Some Systems Degraded": "Einige Systeme eingeschränkt",
"Some Systems Down": "Einige Systeme ausgefallen",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
@@ -53,13 +67,21 @@
"Starts %startedAt": "Beginnt in %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Abonnieren",
"Subscribe to Updates": "Updates abonnieren",
"Subscribe to all monitors": "Alle Monitore abonnieren",
"Switch to your timezone": "Wechseln Sie zu Ihrer Zeitzone",
"Theme": "Thema",
"There are no upcoming maintenances": "Es gibt keine bevorstehenden Wartungen",
"Timezone set to %tz": "Zeitzone auf %tz gesetzt",
"Today": "Heute",
"UP": "VERFÜGBAR",
"Update Subscription": "Abonnement aktualisieren",
"Upcoming Maintenance": "Bevorstehende Wartung",
"Upcoming Maintenances": "Bevorstehende Wartungen",
"Updates": "Updates",
"Uptime": "Verfügbarkeit",
"View in detail": "Details anzeigen"
"View in detail": "Details anzeigen",
"We have sent a code to your email. Please enter it below to confirm your login": "Wir haben einen Code an Ihre E-Mail-Adresse gesendet. Bitte geben Sie ihn unten ein, um Ihren Login zu bestätigen",
"You are logged in as %email": "Sie sind angemeldet als %email"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 Dage",
"90 Days": "90 Dage",
"Availability per Component": "Tilgængelighed pr. Komponent",
"All Systems are Degraded": "Alle Systemer er Forringet",
"All Systems are Down": "Alle Systemer er Nede",
"All Systems are Operational": "Alle Systemer er Operationelle",
"Back": "Tilbage",
"Badge Copied": "Mærke Kopieret",
"Badge": "Mærke",
"Browse Events": "Gennemse begivenheder",
"Change Email": "Skift Email",
"Code Copied": "Kode Kopieret",
"Confirm Login": "Bekræft Login",
"Copy Code": "Kopier Kode",
"Copy Link": "Kopier Link",
"DEGRADED": "FORRINGET",
@@ -19,6 +24,9 @@
"Days": "Dage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
"Embed": "Integrer",
"Enter the code": "Indtast koden",
"Enter your email": "Indtast din email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Indtast din email for at logge ind. Hvis du ikke er abonnent, vil du blive bedt om at abonnere.",
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
"IDENTIFIED": "IDENTIFICERET",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "Varede i cirka %lastedFor",
"Light": "Lys",
"Link Copied": "Link Kopieret",
"Login": "Log ind",
"MAINTENANCE": "VEDLIGEHOLDELSE",
"MONITORING": "OVERVÅGNING",
"Maintenance Completed": "Vedligeholdelse afsluttet",
"Maintenance in Progress": "Vedligeholdelse i gang",
"Manage Subscription": "Administrer Abonnement",
"Mode": "Tilstand",
"No Data": "Ingen Data",
"No Incident in %date": "Ingen Hændelse Den %date",
@@ -40,11 +50,15 @@
"No Updates Yet": "Ingen Opdateringer Endnu",
"Ongoing Incidents": "Igangværende Hændelser",
"Pinging": "Pinger",
"Please select specific monitors to receive updates from.": "Vælg venligst specifikke monitorer for at modtage opdateringer fra.",
"RESOLVED": "LØST",
"Recent Incidents": "Seneste Hændelser",
"Recent Maintenances": "Seneste Vedligeholdelser",
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
"Share": "Del",
"Some Systems are not working as expected": "Nogle Systemer fungerer ikke som forventet",
"Some Systems Degraded": "Nogle Systemer er Forringet",
"Some Systems Down": "Nogle Systemer er Nede",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
@@ -53,13 +67,21 @@
"Starts %startedAt": "Starter om %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Abonner",
"Subscribe to Updates": "Abonner på opdateringer",
"Subscribe to all monitors": "Abonner på alle monitorer",
"Switch to your timezone": "Skift til din tidszone",
"Theme": "Tema",
"There are no upcoming maintenances": "Der er ingen kommende vedligeholdelser",
"Timezone set to %tz": "Tidszone sat til %tz",
"Today": "I dag",
"UP": "OPPE",
"Update Subscription": "Opdater Abonnement",
"Upcoming Maintenance": "Kommende vedligeholdelse",
"Upcoming Maintenances": "Kommende vedligeholdelser",
"Updates": "Opdateringer",
"Uptime": "Oppetid",
"View in detail": "Vis i detaljer"
"View in detail": "Vis i detaljer",
"We have sent a code to your email. Please enter it below to confirm your login": "Vi har sendt en kode til din email. Indtast den venligst nedenfor for at bekræfte dit login",
"You are logged in as %email": "Du er logget ind som %email"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 Days",
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"All Systems are Degraded": "All Systems are Degraded",
"All Systems are Down": "All Systems are Down",
"All Systems are Operational": "All Systems are Operational",
"Back": "Back",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Change Email": "Change Email",
"Code Copied": "Code Copied",
"Confirm Login": "Confirm Login",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"DEGRADED": "DEGRADED",
@@ -19,6 +24,9 @@
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Enter the code": "Enter the code",
"Enter your email": "Enter your email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"Login": "Login",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Manage Subscription": "Manage Subscription",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
@@ -40,11 +50,15 @@
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"Please select specific monitors to receive updates from.": "Please select specific monitors to receive updates from.",
"RESOLVED": "RESOLVED",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Some Systems are not working as expected": "Some Systems are not working as expected",
"Some Systems Degraded": "Some Systems Degraded",
"Some Systems Down": "Some Systems Down",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
@@ -53,13 +67,21 @@
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Subscribe",
"Subscribe to Updates": "Subscribe to Updates",
"Subscribe to all monitors": "Subscribe to all monitors",
"Switch to your timezone": "Switch to your timezone",
"Theme": "Theme",
"There are no upcoming maintenances": "There are no upcoming maintenances",
"Timezone set to %tz": "Timezone set to %tz",
"Today": "Today",
"UP": "UP",
"Update Subscription": "Update Subscription",
"Upcoming Maintenance": "Upcoming Maintenance",
"Upcoming Maintenances": "Upcoming Maintenances",
"Updates": "Updates",
"Uptime": "Uptime",
"View in detail": "View in detail"
"View in detail": "View in detail",
"We have sent a code to your email. Please enter it below to confirm your login": "We have sent a code to your email. Please enter it below to confirm your login",
"You are logged in as %email": "You are logged in as %email"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 jours",
"90 Days": "90 jours",
"Availability per Component": "Disponibilité par composant",
"All Systems are Degraded": "Tous les systèmes sont dégradés",
"All Systems are Down": "Tous les systèmes sont hors ligne",
"All Systems are Operational": "Tous les systèmes sont opérationnels",
"Back": "Retour",
"Badge Copied": "Badge copié",
"Badge": "Badge",
"Browse Events": "Parcourir les événements",
"Change Email": "Changer l'e-mail",
"Code Copied": "Code copié",
"Confirm Login": "Confirmer la connexion",
"Copy Code": "Copier le code",
"Copy Link": "Copier le lien",
"DEGRADED": "Dégradé",
@@ -19,6 +24,9 @@
"Days": "Jours",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
"Embed": "Intégrer",
"Enter the code": "Entrez le code",
"Enter your email": "Entrez votre e-mail",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Entrez votre e-mail pour vous connecter. Si vous n'êtes pas abonné, vous serez invité à vous abonner.",
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
"IDENTIFIED": "Identifié",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "A duré environ %lastedFor",
"Light": "Clair",
"Link Copied": "Lien copié",
"Login": "Connexion",
"MAINTENANCE": "Maintenance",
"MONITORING": "En surveillance",
"Maintenance Completed": "Maintenance terminée",
"Maintenance in Progress": "Maintenance en cours",
"Manage Subscription": "Gérer l'abonnement",
"Mode": "Mode",
"No Data": "Pas de données",
"No Incident in %date": "Pas d'incident le %date",
@@ -40,11 +50,15 @@
"No Updates Yet": "Pas encore de mises à jour",
"Ongoing Incidents": "Incidents en cours",
"Pinging": "Ping en cours",
"Please select specific monitors to receive updates from.": "Veuillez sélectionner les moniteurs spécifiques dont vous souhaitez recevoir des mises à jour.",
"RESOLVED": "Résolu",
"Recent Incidents": "Incidents récents",
"Recent Maintenances": "Maintenances récentes",
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
"Share": "Partager",
"Some Systems are not working as expected": "Certains systèmes ne fonctionnent pas comme prévu",
"Some Systems Degraded": "Certains systèmes sont dégradés",
"Some Systems Down": "Certains systèmes sont hors ligne",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
@@ -53,13 +67,21 @@
"Starts %startedAt": "Commence dans %startedAt",
"Status OK": "Statut OK",
"Status": "Statut",
"Subscribe": "S'abonner",
"Subscribe to Updates": "S'abonner aux mises à jour",
"Subscribe to all monitors": "S'abonner à tous les moniteurs",
"Switch to your timezone": "Passer à votre fuseau horaire",
"Theme": "Thème",
"There are no upcoming maintenances": "Il n'y a pas de maintenances à venir",
"Timezone set to %tz": "Fuseau horaire défini sur %tz",
"Today": "Aujourd'hui",
"UP": "En ligne",
"Update Subscription": "Mettre à jour l'abonnement",
"Upcoming Maintenance": "Maintenance à venir",
"Upcoming Maintenances": "Maintenances à venir",
"Updates": "Mises à jour",
"Uptime": "Temps de fonctionnement",
"View in detail": "Voir en détail"
"View in detail": "Voir en détail",
"We have sent a code to your email. Please enter it below to confirm your login": "Nous avons envoyé un code à votre e-mail. Veuillez l'entrer ci-dessous pour confirmer votre connexion",
"You are logged in as %email": "Vous êtes connecté en tant que %email"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 दिन",
"90 Days": "90 दिन",
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
"All Systems are Degraded": "सभी सिस्टम क्षीण हैं",
"All Systems are Down": "सभी सिस्टम डाउन हैं",
"All Systems are Operational": "सभी सिस्टम कार्यरत हैं",
"Back": "वापस",
"Badge Copied": "बैज कॉपी किया गया",
"Badge": "बैज",
"Browse Events": "ईवेंट ब्राउज़ करें",
"Change Email": "ईमेल बदलें",
"Code Copied": "कोड कॉपी किया गया",
"Confirm Login": "लॉगिन पुष्टि करें",
"Copy Code": "कोड कॉपी करें",
"Copy Link": "लिंक कॉपी करें",
"DEGRADED": "क्षीण",
@@ -19,6 +24,9 @@
"Days": "दिन",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
"Embed": "एम्बेड",
"Enter the code": "कोड दर्ज करें",
"Enter your email": "अपना ईमेल दर्ज करें",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "लॉगिन करने के लिए अपना ईमेल दर्ज करें। यदि आप सब्सक्राइब नहीं हैं, तो आपको सब्सक्राइब करने के लिए कहा जाएगा।",
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
"IDENTIFIED": "पहचाना गया",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "%lastedFor तक चला",
"Light": "लाइट",
"Link Copied": "लिंक कॉपी किया गया",
"Login": "लॉगिन",
"MAINTENANCE": "मेंटेनेंस",
"MONITORING": "निगरानी",
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
"Maintenance in Progress": "मेंटेनेंस जारी है",
"Manage Subscription": "सब्सक्रिप्शन प्रबंधित करें",
"Mode": "मोड",
"No Data": "कोई डेटा नहीं",
"No Incident in %date": "%date को कोई घटना नहीं",
@@ -40,11 +50,15 @@
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
"Ongoing Incidents": "चल रही घटनाएँ",
"Pinging": "पिंगिंग",
"Please select specific monitors to receive updates from.": "अपडेट प्राप्त करने के लिए विशिष्ट मॉनिटर चुनें।",
"RESOLVED": "सुलझाया गया",
"Recent Incidents": "हाल की घटनाएँ",
"Recent Maintenances": "हाल की मेंटेनेंस",
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
"Share": "साझा करें",
"Some Systems are not working as expected": "कुछ सिस्टम अपेक्षित रूप से काम नहीं कर रहे हैं",
"Some Systems Degraded": "कुछ सिस्टम क्षीण हैं",
"Some Systems Down": "कुछ सिस्टम डाउन हैं",
"Standard": "मानक",
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
@@ -53,13 +67,21 @@
"Starts %startedAt": "%startedAt में शुरू होगा",
"Status OK": "स्थिति ठीक है",
"Status": "स्थिति",
"Subscribe": "सब्सक्राइब करें",
"Subscribe to Updates": "अपडेट्स के लिए सब्सक्राइब करें",
"Subscribe to all monitors": "सभी मॉनिटर को सब्सक्राइब करें",
"Switch to your timezone": "अपने समय क्षेत्र में स्विच करें",
"Theme": "थीम",
"There are no upcoming maintenances": "कोई आगामी मेंटेनेंस नहीं है",
"Timezone set to %tz": "समय क्षेत्र सेट किया गया %tz",
"Today": "आज",
"UP": "अप",
"Update Subscription": "सब्सक्रिप्शन अपडेट करें",
"Upcoming Maintenance": "आगामी मेंटेनेंस",
"Upcoming Maintenances": "आगामी मेंटेनेंस सूची",
"Updates": "अपडेट्स",
"Uptime": "अपटाइम",
"View in detail": "विस्तार में देखें"
"View in detail": "विस्तार में देखें",
"We have sent a code to your email. Please enter it below to confirm your login": "हमने आपके ईमेल पर एक कोड भेजा है। लॉगिन की पुष्टि करने के लिए इसे नीचे दर्ज करें",
"You are logged in as %email": "आप %email के रूप में लॉग इन हैं"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7日間",
"90 Days": "90日間",
"Availability per Component": "コンポーネントごとの可用性",
"All Systems are Degraded": "すべてのシステムが劣化しています",
"All Systems are Down": "すべてのシステムがダウンしています",
"All Systems are Operational": "すべてのシステムが正常に稼働しています",
"Back": "戻る",
"Badge Copied": "バッジがコピーされました",
"Badge": "バッジ",
"Browse Events": "イベントを閲覧",
"Change Email": "メールを変更",
"Code Copied": "コードがコピーされました",
"Confirm Login": "ログインを確認",
"Copy Code": "コードをコピー",
"Copy Link": "リンクをコピー",
"DEGRADED": "劣化",
@@ -19,6 +24,9 @@
"Days": "日間",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "このモニターをアプリに <script> または <iframe> を使用して埋め込む。",
"Embed": "埋め込み",
"Enter the code": "コードを入力してください",
"Enter your email": "メールアドレスを入力してください",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "ログインするにはメールアドレスを入力してください。登録されていない場合は、購読を促されます。",
"Get SVG badge for this monitor": "このモニターのSVGバッジを取得",
"Get a LIVE Status for this monitor": "このモニターのライブステータスを取得",
"IDENTIFIED": "識別済み",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "約 %lastedFor 続きました",
"Light": "ライト",
"Link Copied": "リンクがコピーされました",
"Login": "ログイン",
"MAINTENANCE": "メンテナンス",
"MONITORING": "モニタリング中",
"Maintenance Completed": "メンテナンス完了",
"Maintenance in Progress": "メンテナンス中",
"Manage Subscription": "購読を管理",
"Mode": "モード",
"No Data": "データなし",
"No Incident in %date": "%date にインシデントなし",
@@ -40,11 +50,15 @@
"No Updates Yet": "まだ更新はありません",
"Ongoing Incidents": "進行中のインシデント",
"Pinging": "ピング中",
"Please select specific monitors to receive updates from.": "更新を受け取る特定のモニターを選択してください。",
"RESOLVED": "解決済み",
"Recent Incidents": "最近のインシデント",
"Recent Maintenances": "最近のメンテナンス",
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
"Share": "共有",
"Some Systems are not working as expected": "一部のシステムが期待通りに動作していません",
"Some Systems Degraded": "一部のシステムが劣化しています",
"Some Systems Down": "一部のシステムがダウンしています",
"Standard": "標準",
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
@@ -53,13 +67,21 @@
"Starts %startedAt": "%startedAt 後に開始",
"Status OK": "ステータス正常",
"Status": "ステータス",
"Subscribe": "購読する",
"Subscribe to Updates": "更新を購読する",
"Subscribe to all monitors": "すべてのモニターを購読する",
"Switch to your timezone": "あなたのタイムゾーンに切り替え",
"Theme": "テーマ",
"There are no upcoming maintenances": "予定されているメンテナンスはありません",
"Timezone set to %tz": "タイムゾーンを %tz に設定",
"Today": "今日",
"UP": "稼働中",
"Update Subscription": "購読を更新する",
"Upcoming Maintenance": "今後のメンテナンス",
"Upcoming Maintenances": "今後のメンテナンス一覧",
"Updates": "更新",
"Uptime": "稼働時間",
"View in detail": "詳細を表示"
"View in detail": "詳細を表示",
"We have sent a code to your email. Please enter it below to confirm your login": "メールにコードを送信しました。ログインを確認するには、以下に入力してください",
"You are logged in as %email": "%email としてログインしています"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 일",
"90 Days": "90 일",
"Availability per Component": "항목당 가용성",
"All Systems are Degraded": "모든 시스템이 성능 저하 상태입니다",
"All Systems are Down": "모든 시스템이 중단되었습니다",
"All Systems are Operational": "모든 시스템이 정상 작동 중입니다",
"Back": "뒤로",
"Badge Copied": "배지 복사됨",
"Badge": "배지",
"Browse Events": "이벤트 찾아보기",
"Change Email": "이메일 변경",
"Code Copied": "소스코드 복사됨",
"Confirm Login": "로그인 확인",
"Copy Code": "소스코드 복사",
"Copy Link": "링크 복사",
"DEGRADED": "성능 저하됨",
@@ -19,6 +24,9 @@
"Days": "일",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "<script> 나 <iframe> 태그를 사용하여 내 앱에 이 모니터링을 임베딩 해보세요.",
"Embed": "임베드",
"Enter the code": "코드를 입력하세요",
"Enter your email": "이메일을 입력하세요",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "로그인하려면 이메일을 입력하세요. 구독하지 않은 경우 구독 요청이 표시됩니다.",
"Get SVG badge for this monitor": "이 모니터링 항목에 대한 SVG 배지를 구해보세요.",
"Get a LIVE Status for this monitor": "이 모니터링 항목에 대한 실시간 상태를 구해보세요.",
"IDENTIFIED": "확인됨",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "%lastedFor 동안 지속",
"Light": "라이트",
"Link Copied": "링크 복사됨",
"Login": "로그인",
"MAINTENANCE": "유지보수",
"MONITORING": "감시 중",
"Maintenance Completed": "유지보수 완료",
"Maintenance in Progress": "유지보수 진행 중",
"Manage Subscription": "구독 관리",
"Mode": "모드",
"No Data": "데이터 없음",
"No Incident in %date": "%date 에는 아무 사건도 없었음",
@@ -40,11 +50,15 @@
"No Updates Yet": "아직 아무 업데이트도 없음",
"Ongoing Incidents": "진행중 사건",
"Pinging": "핑 요청",
"Please select specific monitors to receive updates from.": "업데이트를 받을 특정 모니터를 선택하세요.",
"RESOLVED": "해결됨",
"Recent Incidents": "최근 사건",
"Recent Maintenances": "최근 유지보수",
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
"Share": "공유",
"Some Systems are not working as expected": "일부 시스템이 예상대로 작동하지 않습니다",
"Some Systems Degraded": "일부 시스템이 성능 저하 상태입니다",
"Some Systems Down": "일부 시스템이 중단되었습니다",
"Standard": "기본",
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
@@ -53,13 +67,21 @@
"Starts %startedAt": "%startedAt 에 시작되었습니다",
"Status OK": "양호",
"Status": "상태",
"Subscribe": "구독",
"Subscribe to Updates": "업데이트 구독하기",
"Subscribe to all monitors": "모든 모니터 구독",
"Switch to your timezone": "귀하의 시간대로 변경",
"Theme": "테마",
"There are no upcoming maintenances": "예정된 유지보수가 없습니다",
"Timezone set to %tz": "시간대 설정 %tz",
"Today": "오늘",
"UP": "작동 중",
"Update Subscription": "구독 업데이트",
"Upcoming Maintenance": "예정된 유지보수",
"Upcoming Maintenances": "예정된 유지보수 목록",
"Updates": "업데이트",
"Uptime": "업타임",
"View in detail": "자세히 보기"
"View in detail": "자세히 보기",
"We have sent a code to your email. Please enter it below to confirm your login": "이메일로 코드를 보냈습니다. 로그인을 확인하려면 아래에 입력하세요",
"You are logged in as %email": "%email로 로그인했습니다"
}
+4
View File
@@ -54,5 +54,9 @@
{
"code": "nb-NO",
"name": "Norsk Bokmål"
},
{
"code": "pl",
"name": "Polski"
}
]
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 dager",
"90 Days": "90 dager",
"Availability per Component": "Tilgjengelighet per komponent",
"All Systems are Degraded": "Alle systemer har nedsatt ytelse",
"All Systems are Down": "Alle systemer er nede",
"All Systems are Operational": "Alle systemer er operative",
"Back": "Tilbake",
"Badge Copied": "Merke kopiert",
"Badge": "Merke",
"Browse Events": "Bla gjennom hendelser",
"Change Email": "Endre e-post",
"Code Copied": "Kode kopiert",
"Confirm Login": "Bekreft pålogging",
"Copy Code": "Kopier kode",
"Copy Link": "Kopier lenke",
"DEGRADED": "NEDSATT",
@@ -19,6 +24,9 @@
"Days": "Dager",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Bygg inn denne monitoren ved å bruke <script> eller <iframe> i appen din.",
"Embed": "Bygg inn",
"Enter the code": "Skriv inn koden",
"Enter your email": "Skriv inn din e-post",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Skriv inn din e-post for å logge inn. Hvis du ikke er abonnent, vil du bli bedt om å abonnere.",
"Get SVG badge for this monitor": "Hent SVG-merke for denne monitoren",
"Get a LIVE Status for this monitor": "Få en LIVE-status for denne monitoren",
"IDENTIFIED": "IDENTIFISERT",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "Varte i omtrent %lastedFor",
"Light": "Lys",
"Link Copied": "Lenke kopiert",
"Login": "Logg inn",
"MAINTENANCE": "VEDLIKEHOLD",
"MONITORING": "OVERVÅKER",
"Maintenance Completed": "Vedlikehold fullført",
"Maintenance in Progress": "Vedlikehold pågår",
"Manage Subscription": "Administrer abonnement",
"Mode": "Modus",
"No Data": "Ingen data",
"No Incident in %date": "Ingen hendelser på %date",
@@ -40,11 +50,15 @@
"No Updates Yet": "Ingen oppdateringer ennå",
"Ongoing Incidents": "Pågående hendelser",
"Pinging": "Pinger",
"Please select specific monitors to receive updates from.": "Vennligst velg spesifikke monitorer å motta oppdateringer fra.",
"RESOLVED": "LØST",
"Recent Incidents": "Nylige hendelser",
"Recent Maintenances": "Nylige vedlikehold",
"Share this monitor using a link with others": "Del denne monitoren med andre via lenke",
"Share": "Del",
"Some Systems are not working as expected": "Noen systemer fungerer ikke som forventet",
"Some Systems Degraded": "Noen systemer har nedsatt ytelse",
"Some Systems Down": "Noen systemer er nede",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
@@ -53,13 +67,21 @@
"Starts %startedAt": "Starter %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Abonner",
"Subscribe to Updates": "Abonner på oppdateringer",
"Subscribe to all monitors": "Abonner på alle monitorer",
"Switch to your timezone": "Bytt til din tidssone",
"Theme": "Tema",
"There are no upcoming maintenances": "Det er ingen kommende vedlikeholdsperioder",
"Timezone set to %tz": "Tidssone satt til %tz",
"Today": "I dag",
"UP": "OPPE",
"Update Subscription": "Oppdater abonnement",
"Upcoming Maintenance": "Kommende vedlikehold",
"Upcoming Maintenances": "Kommende vedlikeholdsperioder",
"Updates": "Oppdateringer",
"Uptime": "Oppetid",
"View in detail": "Vis detaljer"
"View in detail": "Vis detaljer",
"We have sent a code to your email. Please enter it below to confirm your login": "Vi har sendt en kode til din e-post. Vennligst skriv den inn nedenfor for å bekrefte påloggingen",
"You are logged in as %email": "Du er logget inn som %email"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 dagen",
"90 Days": "90 dagen",
"Availability per Component": "Beschikbaarheid per component",
"All Systems are Degraded": "Alle systemen werken met beperkte functionaliteit",
"All Systems are Down": "Alle systemen zijn niet bereikbaar",
"All Systems are Operational": "Alle systemen zijn operationeel",
"Back": "Terug",
"Badge Copied": "Badge gekopieerd",
"Badge": "Badge",
"Browse Events": "Evenementen bekijken",
"Change Email": "E-mail wijzigen",
"Code Copied": "Code gekopieerd",
"Confirm Login": "Inloggen bevestigen",
"Copy Code": "Kopieer code",
"Copy Link": "Kopieer link",
"DEGRADED": "BEPERKT",
@@ -19,6 +24,9 @@
"Days": "Dagen",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Sluit deze monitor in met behulp van <script> of <iframe> in je app.",
"Embed": "Insluiten",
"Enter the code": "Voer de code in",
"Enter your email": "Voer je e-mailadres in",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Voer je e-mailadres in om in te loggen. Als je nog niet geabonneerd bent, word je gevraagd om je te abonneren.",
"Get SVG badge for this monitor": "SVG-badge ophalen voor deze monitor",
"Get a LIVE Status for this monitor": "LIVE-status ophalen voor deze monitor",
"IDENTIFIED": "GEIDENTIFICEERD",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "Duurde ongeveer %lastedFor",
"Light": "Licht",
"Link Copied": "Link gekopieerd",
"Login": "Inloggen",
"MAINTENANCE": "ONDERHOUD",
"MONITORING": "MONITOREN",
"Maintenance Completed": "Onderhoud voltooid",
"Maintenance in Progress": "Onderhoud bezig",
"Manage Subscription": "Abonnement beheren",
"Mode": "Modus",
"No Data": "Geen gegevens",
"No Incident in %date": "Geen incident op %date",
@@ -40,11 +50,15 @@
"No Updates Yet": "Nog geen updates",
"Ongoing Incidents": "Lopende incidenten",
"Pinging": "Pingen",
"Please select specific monitors to receive updates from.": "Selecteer specifieke monitoren waarvan je updates wilt ontvangen.",
"RESOLVED": "OPGELOST",
"Recent Incidents": "Recente incidenten",
"Recent Maintenances": "Recente onderhoudsbeurten",
"Share this monitor using a link with others": "Deel deze monitor met anderen via een link",
"Share": "Delen",
"Some Systems are not working as expected": "Sommige systemen werken niet zoals verwacht",
"Some Systems Degraded": "Sommige systemen werken met beperkte functionaliteit",
"Some Systems Down": "Sommige systemen zijn niet bereikbaar",
"Standard": "Standaard",
"Started %startedAt, lasted for %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
"Started %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
@@ -53,13 +67,21 @@
"Starts %startedAt": "Begint over %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Abonneren",
"Subscribe to Updates": "Abonneren op updates",
"Subscribe to all monitors": "Abonneren op alle monitoren",
"Switch to your timezone": "Schakel over naar je tijdzone",
"Theme": "Thema",
"There are no upcoming maintenances": "Er zijn geen geplande onderhoudsperiodes",
"Timezone set to %tz": "Tijdzone ingesteld op %tz",
"Today": "Vandaag",
"UP": "BEREIKBAAR",
"Update Subscription": "Abonnement bijwerken",
"Upcoming Maintenance": "Aankomend onderhoud",
"Upcoming Maintenances": "Aankomende onderhoudsperiodes",
"Updates": "Updates",
"Uptime": "Uptime",
"View in detail": "Bekijk in detail"
"View in detail": "Bekijk in detail",
"We have sent a code to your email. Please enter it below to confirm your login": "We hebben een code naar je e-mailadres gestuurd. Voer deze hieronder in om je login te bevestigen",
"You are logged in as %email": "Je bent ingelogd als %email"
}
+87
View File
@@ -0,0 +1,87 @@
{
"%status for %duration": "%status przez %duration",
"14 Days": "14 dni",
"30 Days": "30 dni",
"60 Days": "60 dni",
"7 Days": "7 dni",
"90 Days": "90 dni",
"Availability per Component": "Dostępność według komponentu",
"All Systems are Degraded": "Wszystkie systemy działają z ograniczeniami",
"All Systems are Down": "Wszystkie systemy są niedostępne",
"All Systems are Operational": "Wszystkie systemy działają poprawnie",
"Back": "Wstecz",
"Badge Copied": "Odznaka skopiowana",
"Badge": "Odznaka",
"Browse Events": "Przeglądaj zdarzenia",
"Change Email": "Zmień email",
"Code Copied": "Kod skopiowany",
"Confirm Login": "Potwierdź logowanie",
"Copy Code": "Skopiuj kod",
"Copy Link": "Skopiuj link",
"DEGRADED": "OGRANICZONA",
"DOWN": "NIEDOSTĘPNY",
"Dark": "Ciemny",
"Days": "Dni",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Osadź ten monitor za pomocą <script> lub <iframe> w swojej aplikacji.",
"Embed": "Osadź",
"Enter the code": "Wprowadź kod",
"Enter your email": "Wprowadź swój email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Wprowadź swój email, aby się zalogować. Jeśli nie masz subskrypcji, zostaniesz poproszony o jej utworzenie.",
"Get SVG badge for this monitor": "Pobierz odznakę SVG dla tego monitora",
"Get a LIVE Status for this monitor": "Uzyskaj STATUS NA ŻYWO tego monitora",
"IDENTIFIED": "ZIDENTYFIKOWANE",
"INVESTIGATING": "BADANE",
"Incident Updates": "Aktualizacje incydentu",
"LIVE Status": "Status NA ŻYWO",
"Lasted for about %lastedFor": "Trwało około %lastedFor",
"Light": "Jasny",
"Link Copied": "Link skopiowany",
"Login": "Zaloguj się",
"MAINTENANCE": "KONSERWACJA",
"MONITORING": "MONITOROWANE",
"Maintenance Completed": "Konserwacja zakończona",
"Maintenance in Progress": "Trwa konserwacja",
"Manage Subscription": "Zarządzaj subskrypcją",
"Mode": "Tryb",
"No Data": "Brak danych",
"No Incident in %date": "Brak incydentów w dniu %date",
"No Incidents": "Brak incydentów",
"No Monitor Found": "Nie znaleziono monitora",
"No Updates Yet": "Brak aktualizacji",
"Ongoing Incidents": "Trwające incydenty",
"Pinging": "Pingowanie",
"Please select specific monitors to receive updates from.": "Wybierz konkretne monitory, z których chcesz otrzymywać aktualizacje.",
"RESOLVED": "ROZWIĄZANE",
"Recent Incidents": "Ostatnie incydenty",
"Recent Maintenances": "Ostatnie konserwacje",
"Share this monitor using a link with others": "Udostępnij ten monitor za pomocą linku innym osobom",
"Share": "Udostępnij",
"Some Systems are not working as expected": "Niektóre systemy nie działają zgodnie z oczekiwaniami",
"Some Systems Degraded": "Niektóre systemy działają z ograniczeniami",
"Some Systems Down": "Niektóre systemy są niedostępne",
"Standard": "Standardowy",
"Started %startedAt, lasted for %lastedFor": "Rozpoczęto %startedAt, trwało %lastedFor",
"Started %startedAt, still ongoing": "Rozpoczęto %startedAt, nadal trwa",
"Started %startedAt, will last for %lastedFor more": "Rozpoczęto %startedAt, potrwa jeszcze %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Rozpocznie się %startedAt, potrwa %lastedFor",
"Starts %startedAt": "Rozpocznie się %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Subskrybuj",
"Subscribe to Updates": "Subskrybuj aktualizacje",
"Subscribe to all monitors": "Subskrybuj wszystkie monitory",
"Switch to your timezone": "Przełącz na swoją strefę czasową",
"Theme": "Motyw",
"There are no upcoming maintenances": "Nie ma nadchodzących konserwacji",
"Timezone set to %tz": "Strefa czasowa ustawiona na %tz",
"Today": "Dzisiaj",
"UP": "DOSTĘPNY",
"Update Subscription": "Aktualizuj subskrypcję",
"Upcoming Maintenance": "Nadchodząca konserwacja",
"Upcoming Maintenances": "Nadchodzące konserwacje",
"Updates": "Aktualizacje",
"Uptime": "Czas działania",
"View in detail": "Zobacz szczegóły",
"We have sent a code to your email. Please enter it below to confirm your login": "Wysłaliśmy kod na Twój adres email. Wprowadź go poniżej, aby potwierdzić logowanie",
"You are logged in as %email": "Jesteś zalogowany jako %email"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 Dias",
"90 Days": "90 Dias",
"Availability per Component": "Disponibilidade por Componente",
"All Systems are Degraded": "Todos os Sistemas estão com Falhas",
"All Systems are Down": "Todos os Sistemas estão Indisponíveis",
"All Systems are Operational": "Todos os Sistemas estão Operacionais",
"Back": "Voltar",
"Badge Copied": "Emblema Copiado",
"Badge": "Emblema",
"Browse Events": "Navegar Eventos",
"Change Email": "Alterar E-mail",
"Code Copied": "Código Copiado",
"Confirm Login": "Confirmar Login",
"Copy Code": "Copiar Código",
"Copy Link": "Copiar Link",
"DEGRADED": "FALHA",
@@ -19,6 +24,9 @@
"Days": "Dias",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
"Embed": "Incorporar",
"Enter the code": "Digite o código",
"Enter your email": "Digite seu e-mail",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Digite seu e-mail para fazer login. Se você não estiver inscrito, você será solicitado a se inscrever.",
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
"IDENTIFIED": "IDENTIFICADO",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
"Light": "Claro",
"Link Copied": "Link Copiado",
"Login": "Entrar",
"MAINTENANCE": "MANUTENÇÃO",
"MONITORING": "MONITORANDO",
"Maintenance Completed": "Manutenção Concluída",
"Maintenance in Progress": "Manutenção em Progresso",
"Manage Subscription": "Gerenciar Inscrição",
"Mode": "Modo",
"No Data": "Sem dados",
"No Incident in %date": "Nenhum incidente em %date",
@@ -40,11 +50,15 @@
"No Updates Yet": "Nenhuma atualização ainda",
"Ongoing Incidents": "Incidentes em Andamento",
"Pinging": "Animado",
"Please select specific monitors to receive updates from.": "Por favor, selecione monitores específicos para receber atualizações.",
"RESOLVED": "RESOLVIDO",
"Recent Incidents": "Incidentes Recentes",
"Recent Maintenances": "Manutenções Recentes",
"Share this monitor using a link with others": "Compartilhar este monitor através do link",
"Share": "Compartilhar",
"Some Systems are not working as expected": "Alguns Sistemas não estão funcionando como esperado",
"Some Systems Degraded": "Alguns Sistemas estão com Falhas",
"Some Systems Down": "Alguns Sistemas estão Indisponíveis",
"Standard": "Padrão",
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
@@ -53,13 +67,21 @@
"Starts %startedAt": "Inicia em %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Inscrever-se",
"Subscribe to Updates": "Inscrever-se para atualizações",
"Subscribe to all monitors": "Inscrever-se em todos os monitores",
"Switch to your timezone": "Mudar para o seu fuso horário",
"Theme": "Tema",
"There are no upcoming maintenances": "Não há manutenções agendadas",
"Timezone set to %tz": "Fuso horário definido para %tz",
"Today": "Hoje",
"UP": "OPERACIONAL",
"Update Subscription": "Atualizar Inscrição",
"Upcoming Maintenance": "Manutenção Agendada",
"Upcoming Maintenances": "Manutenções Agendadas",
"Updates": "Atualizações",
"Uptime": "Tempo de Atividade",
"View in detail": "Ver em detalhes"
"View in detail": "Ver em detalhes",
"We have sent a code to your email. Please enter it below to confirm your login": "Enviamos um código para seu e-mail. Por favor, digite-o abaixo para confirmar seu login",
"You are logged in as %email": "Você está logado como %email"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 Дней",
"90 Days": "90 Дней",
"Availability per Component": "Доступность по компонентам",
"All Systems are Degraded": "Все системы работают с ухудшенной производительностью",
"All Systems are Down": "Все системы недоступны",
"All Systems are Operational": "Все системы работают нормально",
"Back": "Назад",
"Badge Copied": "Значок скопирован",
"Badge": "Значок",
"Browse Events": "Просмотр событий",
"Change Email": "Изменить email",
"Code Copied": "Код скопирован",
"Confirm Login": "Подтвердить вход",
"Copy Code": "Копировать код",
"Copy Link": "Копировать ссылку",
"DEGRADED": "УХУДШЕНО",
@@ -19,6 +24,9 @@
"Days": "Дней",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Встройте этот монитор используя <script> или <iframe> в ваше приложение.",
"Embed": "Встроить",
"Enter the code": "Введите код",
"Enter your email": "Введите ваш email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Введите ваш email для входа. Если вы не подписаны, вам будет предложено подписаться.",
"Get SVG badge for this monitor": "Получить SVG значок для этого монитора",
"Get a LIVE Status for this monitor": "Получить ЖИВОЙ статус для этого монитора",
"IDENTIFIED": "ВЫЯВЛЕНО",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "Длилось около %lastedFor",
"Light": "Светлая",
"Link Copied": "Ссылка скопирована",
"Login": "Войти",
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
"MONITORING": "МОНИТОРИНГ",
"Maintenance Completed": "Техническое обслуживание завершено",
"Maintenance in Progress": "Техническое обслуживание",
"Manage Subscription": "Управление подпиской",
"Mode": "Режим",
"No Data": "Нет данных",
"No Incident in %date": "Нет инцидентов на %date",
@@ -40,11 +50,15 @@
"No Updates Yet": "Пока нет обновлений",
"Ongoing Incidents": "Текущие инциденты",
"Pinging": "Пингуется",
"Please select specific monitors to receive updates from.": "Выберите конкретные мониторы для получения обновлений.",
"RESOLVED": "РЕШЕНО",
"Recent Incidents": "Недавние инциденты",
"Recent Maintenances": "Последние обслуживания",
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
"Share": "Поделиться",
"Some Systems are not working as expected": "Некоторые системы работают не так, как ожидалось",
"Some Systems Degraded": "Некоторые системы работают с ухудшенной производительностью",
"Some Systems Down": "Некоторые системы недоступны",
"Standard": "Стандартная",
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
@@ -53,13 +67,21 @@
"Starts %startedAt": "Начнется через %startedAt",
"Status OK": "Статус ОК",
"Status": "Статус",
"Subscribe": "Подписаться",
"Subscribe to Updates": "Подписаться на обновления",
"Subscribe to all monitors": "Подписаться на все мониторы",
"Switch to your timezone": "Переключиться на ваш часовой пояс",
"Theme": "Тема",
"There are no upcoming maintenances": "Нет предстоящих технических обслуживаний",
"Timezone set to %tz": "Часовой пояс установлен на %tz",
"Today": "Сегодня",
"UP": "РАБОТАЕТ",
"Update Subscription": "Обновить подписку",
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
"Upcoming Maintenances": "Предстоящие технические обслуживания",
"Updates": "Обновления",
"Uptime": "Время работы",
"View in detail": "Подробный просмотр"
"View in detail": "Подробный просмотр",
"We have sent a code to your email. Please enter it below to confirm your login": "Мы отправили код на ваш email. Пожалуйста, введите его ниже для подтверждения входа",
"You are logged in as %email": "Вы вошли как %email"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 Gün",
"90 Days": "90 Gün",
"Availability per Component": "Bileşen Kullanılabilirliği",
"All Systems are Degraded": "Tüm Sistemler Bozuk Durumda",
"All Systems are Down": "Tüm Sistemler Çalışmıyor",
"All Systems are Operational": "Tüm Sistemler Çalışıyor",
"Back": "Geri",
"Badge Copied": "Rozet Kopyalandı",
"Badge": "Rozet",
"Browse Events": "Etkinliklere Göz At",
"Change Email": "E-postayı Değiştir",
"Code Copied": "Kod Kopyalandı",
"Confirm Login": "Girişi Onayla",
"Copy Code": "Kodu Kopyala",
"Copy Link": "Bağlantıyı Kopyala",
"DEGRADED": "BOZUK",
@@ -19,6 +24,9 @@
"Days": "Günler",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
"Embed": "Yerleştir",
"Enter the code": "Kodu girin",
"Enter your email": "E-posta adresinizi girin",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Giriş yapmak için e-posta adresinizi girin. Abone değilseniz, abone olmanız istenecektir.",
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
"IDENTIFIED": "TANIMLANDI",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "%lastedFor sürdü",
"Light": "Açık",
"Link Copied": "Bağlantı Kopyalandı",
"Login": "Giriş Yap",
"MAINTENANCE": "BAKIM",
"MONITORING": "İZLEME",
"Maintenance Completed": "Bakım Tamamlandı",
"Maintenance in Progress": "Bakım Devam Ediyor",
"Manage Subscription": "Aboneliği Yönet",
"Mode": "Mod",
"No Data": "Veri Yok",
"No Incident in %date": "%date Tarihinde Olay Yok",
@@ -40,11 +50,15 @@
"No Updates Yet": "Henüz Güncelleme Yok",
"Ongoing Incidents": "Devam Eden Olaylar",
"Pinging": "Pinging",
"Please select specific monitors to receive updates from.": "Lütfen güncellemeleri almak istediğiniz belirli servisleri seçin.",
"RESOLVED": "ÇÖZÜLDÜ",
"Recent Incidents": "Son Olaylar",
"Recent Maintenances": "Son Bakımlar",
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
"Share": "Paylaş",
"Some Systems are not working as expected": "Bazı Sistemler Beklendiği Gibi Çalışmıyor",
"Some Systems Degraded": "Bazı Sistemler Bozuk Durumda",
"Some Systems Down": "Bazı Sistemler Çalışmıyor",
"Standard": "Standart",
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
@@ -53,13 +67,21 @@
"Starts %startedAt": "%startedAt başlıyor",
"Status OK": "Durum UYGUN",
"Status": "Durum",
"Subscribe": "Abone Ol",
"Subscribe to Updates": "Güncellemelere Abone Ol",
"Subscribe to all monitors": "Tüm servislere abone ol",
"Switch to your timezone": "Saat diliminize geçin",
"Theme": "Tema",
"There are no upcoming maintenances": "Yaklaşan bakım bulunmamaktadır",
"Timezone set to %tz": "Saat dilimi %tz olarak ayarlandı",
"Today": "Bugün",
"UP": "ÇALIŞIYOR",
"Update Subscription": "Aboneliği Güncelle",
"Upcoming Maintenance": "Yaklaşan Bakım",
"Upcoming Maintenances": "Yaklaşan Bakımlar",
"Updates": "Güncellemeler",
"Uptime": "Çalışma Süresi",
"View in detail": "Detaylı Görüntüle"
"View in detail": "Detaylı Görüntüle",
"We have sent a code to your email. Please enter it below to confirm your login": "E-posta adresinize bir kod gönderdik. Girişinizi onaylamak için lütfen aşağıya girin",
"You are logged in as %email": "%email olarak giriş yaptınız"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7 ngày",
"90 Days": "90 ngày",
"Availability per Component": "Tính khả dụng theo thành phần",
"All Systems are Degraded": "Tất cả hệ thống đang suy giảm hiệu suất",
"All Systems are Down": "Tất cả hệ thống đang ngừng hoạt động",
"All Systems are Operational": "Tất cả hệ thống đang hoạt động bình thường",
"Back": "Quay lại",
"Badge Copied": "Huy hiệu đã được sao chép",
"Badge": "Huy hiệu",
"Browse Events": "Duyệt sự kiện",
"Change Email": "Thay đổi email",
"Code Copied": "Mã đã được sao chép",
"Confirm Login": "Xác nhận đăng nhập",
"Copy Code": "Sao chép mã",
"Copy Link": "Sao chép liên kết",
"DEGRADED": "Suy giảm",
@@ -19,6 +24,9 @@
"Days": "Ngày",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Nhúng trình theo dõi này vào ứng dụng của bạn bằng <script> hoặc <iframe>.",
"Embed": "Nhúng",
"Enter the code": "Nhập mã",
"Enter your email": "Nhập email của bạn",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Nhập email của bạn để đăng nhập. Nếu bạn chưa đăng ký, bạn sẽ được nhắc đăng ký.",
"Get SVG badge for this monitor": "Lấy huy hiệu SVG cho trình theo dõi này",
"Get a LIVE Status for this monitor": "Lấy trạng thái TRỰC TIẾP cho trình theo dõi này",
"IDENTIFIED": "Đã xác định",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "Kéo dài khoảng %lastedFor",
"Light": "Sáng",
"Link Copied": "Liên kết đã được sao chép",
"Login": "Đăng nhập",
"MAINTENANCE": "Bảo trì",
"MONITORING": "Đang giám sát",
"Maintenance Completed": "Bảo trì đã hoàn thành",
"Maintenance in Progress": "Bảo trì đang diễn ra",
"Manage Subscription": "Quản lý đăng ký",
"Mode": "Chế độ",
"No Data": "Không có dữ liệu",
"No Incident in %date": "Không có sự cố vào %date",
@@ -40,11 +50,15 @@
"No Updates Yet": "Chưa có cập nhật",
"Ongoing Incidents": "Sự cố đang diễn ra",
"Pinging": "Đang kiểm tra",
"Please select specific monitors to receive updates from.": "Vui lòng chọn các trình theo dõi cụ thể để nhận cập nhật.",
"RESOLVED": "Đã giải quyết",
"Recent Incidents": "Sự cố gần đây",
"Recent Maintenances": "Bảo trì gần đây",
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
"Share": "Chia sẻ",
"Some Systems are not working as expected": "Một số hệ thống không hoạt động như mong đợi",
"Some Systems Degraded": "Một số hệ thống đang suy giảm hiệu suất",
"Some Systems Down": "Một số hệ thống đang ngừng hoạt động",
"Standard": "Tiêu chuẩn",
"Started %startedAt, lasted for %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
"Started %startedAt, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
@@ -53,13 +67,21 @@
"Starts %startedAt": "Bắt đầu trong %startedAt",
"Status OK": "Trạng thái OK",
"Status": "Trạng thái",
"Subscribe": "Đăng ký",
"Subscribe to Updates": "Đăng ký nhận cập nhật",
"Subscribe to all monitors": "Đăng ký tất cả trình theo dõi",
"Switch to your timezone": "Chuyển sang múi giờ của bạn",
"Theme": "Giao diện",
"There are no upcoming maintenances": "Không có bảo trì sắp tới nào",
"Timezone set to %tz": "Đã đặt múi giờ %tz",
"Today": "Hôm nay",
"UP": "Hoạt động",
"Update Subscription": "Cập nhật đăng ký",
"Upcoming Maintenance": "Bảo trì sắp tới",
"Upcoming Maintenances": "Các bảo trì sắp tới",
"Updates": "Cập nhật",
"Uptime": "Thời gian hoạt động",
"View in detail": "Xem chi tiết"
"View in detail": "Xem chi tiết",
"We have sent a code to your email. Please enter it below to confirm your login": "Chúng tôi đã gửi một mã đến email của bạn. Vui lòng nhập mã đó dưới đây để xác nhận đăng nhập",
"You are logged in as %email": "Bạn đã đăng nhập với tư cách %email"
}
+23 -1
View File
@@ -6,11 +6,16 @@
"7 Days": "7天",
"90 Days": "90天",
"Availability per Component": "每个组件的可用性",
"All Systems are Degraded": "所有系统性能下降",
"All Systems are Down": "所有系统不可用",
"All Systems are Operational": "所有系统运行正常",
"Back": "返回",
"Badge Copied": "徽章已复制",
"Badge": "徽章",
"Browse Events": "浏览事件",
"Change Email": "更改邮箱",
"Code Copied": "代码已复制",
"Confirm Login": "确认登录",
"Copy Code": "复制代码",
"Copy Link": "复制链接",
"DEGRADED": "性能下降",
@@ -19,6 +24,9 @@
"Days": "天",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "使用 <script> 或 <iframe> 将此监控嵌入您的应用。",
"Embed": "嵌入",
"Enter the code": "输入验证码",
"Enter your email": "输入您的邮箱",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "输入您的邮箱进行登录。如果您尚未订阅,将会提示您进行订阅。",
"Get SVG badge for this monitor": "获取此监控的SVG徽章",
"Get a LIVE Status for this monitor": "获取此监控的实时状态",
"IDENTIFIED": "已识别",
@@ -28,10 +36,12 @@
"Lasted for about %lastedFor": "持续了约 %lastedFor",
"Light": "浅色",
"Link Copied": "链接已复制",
"Login": "登录",
"MAINTENANCE": "维护",
"MONITORING": "正在监控",
"Maintenance Completed": "维护已完成",
"Maintenance in Progress": "维护进行中",
"Manage Subscription": "管理订阅",
"Mode": "模式",
"No Data": "无数据",
"No Incident in %date": "%date 无事件",
@@ -40,11 +50,15 @@
"No Updates Yet": "尚无更新",
"Ongoing Incidents": "正在处理的事件",
"Pinging": "正在测试",
"Please select specific monitors to receive updates from.": "请选择您希望接收更新的特定监控。",
"RESOLVED": "已解决",
"Recent Incidents": "最近的事件",
"Recent Maintenances": "最近的维护",
"Share this monitor using a link with others": "通过链接与他人分享此监控",
"Share": "分享",
"Some Systems are not working as expected": "部分系统运行异常",
"Some Systems Degraded": "部分系统性能下降",
"Some Systems Down": "部分系统不可用",
"Standard": "标准",
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
@@ -53,13 +67,21 @@
"Starts %startedAt": "%startedAt 后开始",
"Status OK": "状态正常",
"Status": "状态",
"Subscribe": "订阅",
"Subscribe to Updates": "订阅更新",
"Subscribe to all monitors": "订阅所有监控",
"Switch to your timezone": "切换到您的时区",
"Theme": "主题",
"There are no upcoming maintenances": "没有即将进行的维护",
"Timezone set to %tz": "时区设置为 %tz",
"Today": "今天",
"UP": "可用",
"Update Subscription": "更新订阅",
"Upcoming Maintenance": "即将进行的维护",
"Upcoming Maintenances": "即将进行的维护列表",
"Updates": "更新",
"Uptime": "正常运行时间",
"View in detail": "详细查看"
"View in detail": "详细查看",
"We have sent a code to your email. Please enter it below to confirm your login": "我们已向您的邮箱发送了验证码。请在下方输入以确认您的登录",
"You are logged in as %email": "您已作为 %email 登录"
}
+29 -13
View File
@@ -8,6 +8,7 @@ import {
CreateIncident,
AddIncidentComment,
AddIncidentMonitor,
GetIncidentByIDDashboard,
InsertNewAlert,
} from "./controllers/controller.js";
@@ -15,6 +16,7 @@ import db from "./db/db.js";
const TRIGGERED = "TRIGGERED";
const RESOLVED = "RESOLVED";
const CLOSED = "CLOSED";
async function createJSONCommonAlert(monitor, config, alert, severity) {
let siteData = await GetAllSiteData();
@@ -58,17 +60,22 @@ async function createNewIncident(monitor, alert, commonData) {
};
let update = commonData.description;
update =
update +
`<div style="line-height: 1.5;">
<p><strong>Monitor Name:</strong> ${monitor.name}</p>
<p><strong>Incident Status:</strong> ${commonData.status}</p>
<p><strong>Severity:</strong> ${commonData.severity}</p>
<p><strong>Monitor Status:</strong> ${alert.monitor_status}</p>
<p><strong>Monitor Health Checks:</strong> ${alert.health_checks}</p>
<p><strong>Monitor Failure Threshold:</strong> ${commonData.details.threshold}</p>
<p><strong>Visit:</strong> <a href="${commonData.actions[0].url}">${commonData.actions[0].url}</a></p>
</div>`;
update = `#### ${update}
---
**Monitor Name:** ${monitor.name}
**Incident Status:** ${commonData.status}
**Severity:** ${commonData.severity}
**Monitor Status:** ${alert.monitor_status}
**Monitor Health Checks:** ${alert.health_checks}
**Monitor Failure Threshold:** ${commonData.details.threshold}
**Visit:** [${commonData.actions[0].url}](${commonData.actions[0].url})
`;
let newIncident = await CreateIncident(payload);
@@ -82,13 +89,22 @@ async function createNewIncident(monitor, alert, commonData) {
}
async function closeIncident(alert, comment) {
//check if incident is already resolved
let incident = await GetIncidentByIDDashboard({
incident_id: alert.incident_number,
});
if (incident.status === CLOSED || incident.state === RESOLVED) {
console.log("Incident is already resolved");
return;
}
let incident_id = alert.incident_number;
return await AddIncidentComment(incident_id, comment, "RESOLVED", moment(alert.updated_at).unix());
return await AddIncidentComment(incident_id, comment, RESOLVED, moment(alert.updated_at).unix());
}
function createClosureComment(alert, commonJSON) {
let comment = "The incident has been auto resolved";
let downtimeDuration = moment(alert.updated_at).diff(moment(alert.created_at), "minutes");
let downtimeDuration = Math.round(moment(alert.updated_at).diff(moment(alert.created_at), "seconds") / 60);
comment = comment + `, Total downtime: ` + downtimeDuration + ` minutes`;
return comment;
}
+570 -159
View File
@@ -1,175 +1,115 @@
// @ts-nocheck
// @ts-nocheck
import {
IsValidURL,
IsValidGHObject,
IsValidObject,
IsValidNav,
IsValidHero,
IsValidI18n,
IsValidAnalytics,
IsValidColors,
IsValidJSONString,
IsValidHero,
IsValidI18n,
IsValidJSONArray,
IsValidJSONString,
IsValidNav,
IsValidURL,
} from "./validators.js";
import { siteDataKeys } from "./siteDataKeys.js";
import db from "../db/db.js";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { Resend } from "resend";
import Queue from "queue";
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, ReplaceAllOccurrences } from "../tool.js";
import { addMonths, format, startOfMonth, subMonths } from "date-fns";
import { DEGRADED, DOWN, NO_DATA, SIGNAL, UP, REALTIME } from "../constants.js";
import {
GetMinuteStartNowTimestampUTC,
GetMinuteStartTimestampUTC,
GetNowTimestampUTC,
ReplaceAllOccurrences,
ValidateEmail,
} from "../tool.js";
import getSMTPTransport from "../notification/smtps.js";
import fs from "fs-extra";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const saltRounds = 10;
const DUMMY_SECRET = "DUMMY_SECRET";
const siteDataKeys = [
{
key: "title",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "siteName",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "siteURL",
isValid: IsValidURL,
data_type: "string",
},
{
key: "home",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "favicon",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "logo",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "metaTags",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "nav",
isValid: IsValidNav,
data_type: "object",
},
{
key: "hero",
isValid: IsValidHero,
data_type: "object",
},
{
key: "footerHTML",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "customCSS",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "i18n",
isValid: IsValidI18n,
data_type: "object",
},
{
key: "pattern",
//string dots or sqaures or circ
isValid: (value) => typeof value === "string" && ["dots", "squares", "none"].includes(value),
data_type: "string",
},
{
key: "analytics",
isValid: IsValidAnalytics,
data_type: "object",
},
{
key: "theme",
//light dark system none
isValid: (value) => typeof value === "string" && ["light", "dark", "system", "none"].includes(value),
data_type: "string",
},
{
key: "themeToggle",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "tzToggle",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "barStyle",
//PARTIAL or FULL
isValid: (value) => typeof value === "string" && ["PARTIAL", "FULL"].includes(value),
data_type: "string",
},
{
key: "barRoundness",
//SHARP or ROUNDED
isValid: (value) => typeof value === "string" && ["SHARP", "ROUNDED"].includes(value),
data_type: "string",
},
{
key: "summaryStyle",
//CURRENT or DAY
isValid: (value) => typeof value === "string" && ["CURRENT", "DAY"].includes(value),
data_type: "string",
},
{
key: "colors",
isValid: IsValidColors,
data_type: "object",
},
{
key: "font",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "monitorSort",
isValid: IsValidJSONArray,
data_type: "object",
},
{
key: "categories",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "homeIncidentCount",
isValid: (value) => parseInt(value) >= 0,
data_type: "string",
},
{
key: "homeIncidentStartTimeWithin",
isValid: (value) => parseInt(value) >= 1,
data_type: "string",
},
{
key: "incidentGroupView",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
];
const eventQueue = new Queue({
concurrency: 10, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true, // Automatically start the queue (optional)
});
const insertStatusQueue = new Queue({
concurrency: 1, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true, // Automatically start the queue (optional)
});
export const PushDataToQueue = async (eventID, eventName, eventData) => {
//fetch subscription trigger config from db of email type
let subscription = await db.getSubscriptionTriggerByType("email");
if (!subscription) {
return;
}
let config;
try {
config = JSON.parse(subscription.config);
} catch (e) {
return;
}
if (!config[eventName]) {
return;
}
//get incident data from db using incident id
let tags = ["_"];
let monitors = await db.getIncidentMonitorsByIncidentID(eventID); //get email template
if (monitors) {
for (let i = 0; i < monitors.length; i++) {
const monitor = monitors[i];
tags.push(monitor.monitor_tag);
}
}
//get all the eligible emails that are there in subscription table
//get email template
const emailTemplate = fs.readFileSync(path.join(__dirname, "../templates/event_update.html"), "utf8");
let siteData = await GetAllSiteData();
let base = !!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "";
let emailData = {
brand_name: siteData.siteName,
logo_url: await GetSiteLogoURL(siteData.siteURL, siteData.logo, base),
incident_url: await GetSiteLogoURL(
siteData.siteURL,
`/view/events/${eventData.incident_type.toLowerCase()}-${eventID}`,
base,
),
update_message: eventData.message,
title: `[${eventData.incident_type}] ` + eventData.title,
};
let eligibleEmails = await db.getSubscriberEmails(tags);
if (eligibleEmails) {
for (let i = 0; i < eligibleEmails.length; i++) {
let email = eligibleEmails[i];
eventQueue.push(async (cb) => {
await SendEmailWithTemplate(
emailTemplate,
emailData,
email.subscriber_send,
`[${eventData.incident_type}] ${eventData.title}`,
eventData.message,
);
cb(eventData);
});
}
}
};
export function InsertKeyValue(key, value) {
let f = siteDataKeys.find((k) => k.key === key);
@@ -203,6 +143,20 @@ export async function GetAllSiteData() {
return transformedData;
}
//get all analytics data as json
export async function GetAllAnalyticsData() {
let data = await db.getAllSiteDataAnalytics();
//return all data as key value pairs, transform using data_type
let transformedData = [];
for (const d of data) {
transformedData.push({
key: d.key,
value: JSON.parse(d.value),
});
}
return transformedData;
}
export const CreateUpdateMonitor = async (monitor) => {
let monitorData = { ...monitor };
if (monitorData.id) {
@@ -212,9 +166,112 @@ export const CreateUpdateMonitor = async (monitor) => {
}
};
export const UpdateMonitoringData = async (data) => {
let queryData = { ...data };
return await db.updateMonitoringData(
queryData.monitor_tag,
GetMinuteStartTimestampUTC(queryData.start),
GetMinuteStartTimestampUTC(queryData.end),
queryData.newStatus,
queryData.type,
);
};
export const CreateMonitor = async (monitor) => {
let monitorData = { ...monitor };
if (monitorData.id) {
throw new Error("monitor id must be empty or 0");
}
return await db.insertMonitor(monitorData);
};
export const UpdateMonitor = async (monitor) => {
let monitorData = { ...monitor };
if (!!!monitorData.id || monitorData.id === 0) {
throw new Error("monitor id cannot be empty or 0");
}
return await db.updateMonitor(monitorData);
};
export const GetMonitors = async (data) => {
return await db.getMonitors(data);
};
export const SystemDataMessage = async () => {
//get all active monitors
let monitors = await db.getMonitors({ status: "ACTIVE" });
//get last status using getLatestMonitoringData(monitor_tag)
let monitorTags = monitors.map((monitor) => monitor.tag);
let upsCount = 0;
let degradedCount = 0;
let downCount = 0;
for (let i = 0; i < monitors.length; i++) {
let status = await db.getLatestMonitoringData(monitors[i].tag);
if (status) {
if (status.status === "UP") {
upsCount++;
} else if (status.status === "DEGRADED") {
degradedCount++;
} else if (status.status === "DOWN") {
downCount++;
}
}
}
const total = upsCount + degradedCount + downCount;
if (total === 0) {
return {
text: "No Systems",
upsPercentage: 0,
degradedPercentage: 0,
downsPercentage: 0,
};
}
let upsPercentage = Math.round((upsCount / total) * 100);
let degradedPercentage = Math.round((degradedCount / total) * 100);
let downsPercentage = Math.round((downCount / total) * 100);
let message = "";
// Determine message based on the combination of system states
if (upsCount > 0 && degradedCount === 0 && downCount === 0) {
// UP=1|DOWN=0|DEGRADED=0
message = "All Systems are Operational";
} else if (upsCount === 0 && degradedCount > 0 && downCount === 0) {
// UP=0|DOWN=0|DEGRADED=1
message = "All Systems are Degraded";
} else if (upsCount === 0 && degradedCount === 0 && downCount > 0) {
// UP=0|DOWN=1|DEGRADED=0
message = "All Systems are Down";
} else if (
(upsCount > 0 && degradedCount > 0 && downCount > 0) ||
(upsCount === 0 && degradedCount > 0 && downCount > 0)
) {
// UP=1|DOWN=1|DEGRADED=1 or UP=0|DOWN=1|DEGRADED=1
message = "Some Systems are not working as expected";
} else if (upsCount > 0 && degradedCount === 0 && downCount > 0) {
// UP=1|DOWN=1|DEGRADED=0
message = "Some Systems Down";
} else if (upsCount > 0 && degradedCount > 0 && downCount === 0) {
// UP=1|DOWN=0|DEGRADED=1
message = "Some Systems Degraded";
}
//if percentage is not 100 sum, then add remaining to up
if (upsPercentage + degradedPercentage + downsPercentage < 100) {
upsPercentage = 100 - (degradedPercentage + downsPercentage);
}
return {
text: message,
upsPercentage,
degradedPercentage,
downsPercentage,
};
};
export const GetMonitorsParsed = async (query) => {
// Retrieve monitors from the database based on the provided query
const rawMonitors = await db.getMonitors(query);
@@ -242,6 +299,16 @@ export const GetMonitorsParsed = async (query) => {
export const CreateUpdateTrigger = async (alert) => {
let alertData = { ...alert };
let alertMetaJSON = JSON.parse(alertData.trigger_meta);
if (alertData.trigger_type === "email") {
let emailsArray = alertMetaJSON.to.split(",").map((email) => email.trim());
for (let i = 0; i < emailsArray.length; i++) {
if (!ValidateEmail(emailsArray[i])) {
throw new Error(`Invalid email: ${emailsArray[i]}`);
}
}
}
if (alertData.id) {
return await db.updateTrigger(alertData);
} else {
@@ -326,6 +393,100 @@ export const GetLastHeartbeat = async (monitor_tag) => {
return await db.getLastHeartbeat(monitor_tag);
};
export const ProcessGroupUpdate = async (data) => {
//find all active monitor that are of type group
let groupActiveMonitors = await db.getMonitors({ status: "ACTIVE", monitor_type: "GROUP" });
let validMonitorTags = [];
for (let i = 0; i < groupActiveMonitors.length; i++) {
let groupActiveMonitor = groupActiveMonitors[i];
let typeData = JSON.parse(groupActiveMonitor.type_data);
let monitorsInGroup = typeData.monitors;
let selectedMonitorTags = monitorsInGroup
.filter((monitor) => {
if (!!monitor.selected) {
return monitor.tag;
}
})
.map((monitor) => monitor.tag);
validMonitorTags.push({
groupTag: groupActiveMonitor.tag,
selectedMonitorTags: selectedMonitorTags,
});
}
for (let i = 0; i < validMonitorTags.length; i++) {
let groupActiveMonitor = validMonitorTags[i];
if (groupActiveMonitor.selectedMonitorTags.indexOf(data.monitor_tag) !== -1) {
//do db insert
//get last status by tag for the group tag
let updateData = {};
let lastStatus = await db.getMonitoringDataAt(groupActiveMonitor.groupTag, data.timestamp);
if (!!lastStatus) {
let status = lastStatus.status;
let timestamp = lastStatus.timestamp;
let receivedStatus = data.status;
let receivedTimestamp = data.timestamp;
if (receivedStatus === DOWN) {
updateData = {
monitor_tag: groupActiveMonitor.groupTag,
timestamp: receivedTimestamp,
status: DOWN,
type: REALTIME,
latency: data.latency,
};
} else if (receivedStatus === DEGRADED && status !== DOWN) {
updateData = {
monitor_tag: groupActiveMonitor.groupTag,
timestamp: receivedTimestamp,
status: DEGRADED,
type: REALTIME,
latency: data.latency,
};
} else if (receivedStatus === UP && status !== DOWN && status !== DEGRADED) {
updateData = {
monitor_tag: groupActiveMonitor.groupTag,
timestamp: receivedTimestamp,
status: UP,
type: REALTIME,
latency: data.latency,
};
}
} else {
//if no last status then insert the new status
updateData = {
monitor_tag: groupActiveMonitor.groupTag,
timestamp: data.timestamp,
status: data.status,
type: REALTIME,
latency: data.latency,
};
}
if (!!updateData.status) {
await db.insertMonitoringData(updateData);
}
}
}
};
export const InsertMonitoringData = async (data) => {
//do validation if present all fields below
if (!data.monitor_tag || !data.timestamp || !data.status || !data.type) {
throw new Error("Invalid data");
}
insertStatusQueue.push(async (cb) => {
await ProcessGroupUpdate(data);
cb();
});
return await db.insertMonitoringData({
monitor_tag: data.monitor_tag,
timestamp: data.timestamp,
status: data.status,
latency: data.latency || 0,
type: data.type,
});
};
export const RegisterHeartbeat = async (tag, secret) => {
let monitor = await db.getMonitorByTag(tag);
if (!monitor) {
@@ -339,7 +500,7 @@ export const RegisterHeartbeat = async (tag, secret) => {
let heartbeatConfig = JSON.parse(typeData);
let heartbeatSecret = heartbeatConfig.secretString;
if (heartbeatSecret === secret) {
return await db.insertMonitoringData({
return InsertMonitoringData({
monitor_tag: monitor.tag,
timestamp: GetMinuteStartNowTimestampUTC(GetNowTimestampUTC()),
status: UP,
@@ -365,6 +526,19 @@ export const GenerateToken = async (data) => {
}
};
//generate token with expiry
export const GenerateTokenWithExpiry = async (data, expiry) => {
try {
const token = jwt.sign(data, process.env.KENER_SECRET_KEY || DUMMY_SECRET, {
expiresIn: expiry,
});
return token;
} catch (err) {
console.error("Error generating token with expiry:", err);
throw err;
}
};
export const ForgotPasswordJWT = async (data) => {
try {
const token = jwt.sign(data, process.env.KENER_SECRET_KEY || DUMMY_SECRET, {
@@ -382,7 +556,6 @@ export const VerifyToken = async (token) => {
const decoded = jwt.verify(token, process.env.KENER_SECRET_KEY || DUMMY_SECRET);
return decoded; // Returns the decoded payload if the token is valid
} catch (err) {
console.error("Error verifying token:", err);
return undefined; // Returns null if the token is invalid
}
};
@@ -393,11 +566,13 @@ export const GetAllAlertsPaginated = async (data) => {
total: await db.getMonitorAlertsCount(),
};
};
function generateApiKey() {
const prefix = "kener_";
const randomKey = crypto.randomBytes(32).toString("hex"); // 64-character hexadecimal string
return prefix + randomKey;
}
function createHash(apiKey) {
return crypto
.createHmac("sha256", process.env.KENER_SECRET_KEY || DUMMY_SECRET)
@@ -596,6 +771,10 @@ export const CreateIncident = async (data) => {
}
let newIncident = await db.createIncident(incident);
PushDataToQueue(newIncident.id, "createIncident", {
message: `${incident.incident_type} Created`,
...incident,
});
return {
incident_id: newIncident.id,
};
@@ -621,6 +800,26 @@ export const UpdateIncident = async (incident_id, data) => {
state: data.state || incidentExists.state,
end_date_time: data.end_date_time || incidentExists.end_date_time,
};
//check if updateObject same as incidentExists
if (
JSON.stringify(updateObject) ===
JSON.stringify({
id: incidentExists.id,
title: incidentExists.title,
start_date_time: incidentExists.start_date_time,
status: incidentExists.status,
state: incidentExists.state,
end_date_time: incidentExists.end_date_time,
})
) {
PushDataToQueue(incident_id, "updateIncident", {
message: `${incidentExists.incident_type} has been updated to ${updateObject.state}`,
...incidentExists,
...updateObject,
});
}
return await db.updateIncident(updateObject);
};
@@ -681,6 +880,11 @@ export const AddIncidentMonitor = async (incident_id, monitor_tag, monitor_impac
throw new Error(`Incident with id ${incident_id} does not exist`);
}
PushDataToQueue(incident_id, "insertIncidentMonitor", {
title: incidentExists.title,
message: `Monitor ${monitor_tag} added to ${incidentExists.incident_type}. Impact is ${monitor_impact}`,
...incidentExists,
});
return await db.insertIncidentMonitor(incident_id, monitor_tag, monitor_impact);
};
@@ -708,6 +912,11 @@ export const UpdateCommentByID = async (incident_id, comment_id, comment, state,
if (!commentExists) {
throw new Error(`Comment with id ${comment_id} does not exist`);
}
PushDataToQueue(incident_id, "updateIncidentComment", {
title: incidentExists.title,
message: `${comment}`,
...incidentExists,
});
let c = await db.updateIncidentCommentByID(comment_id, comment, state, commented_at);
if (c) {
let incidentUpdate = {
@@ -733,7 +942,11 @@ export const AddIncidentComment = async (incident_id, comment, state, commented_
if (!!!state) {
state = incidentExists.state;
}
PushDataToQueue(incident_id, "insertIncidentComment", {
title: incidentExists.title,
message: `${comment}`,
...incidentExists,
});
let c = await db.insertIncidentComment(incident_id, comment, state, commented_at);
let incidentType = incidentExists.incident_type;
//update incident state
@@ -787,6 +1000,45 @@ export const GetIncidentByIDDashboard = async (data) => {
return incident;
};
export const GetIncidentsPaginated = async (page, limit, filter, direction) => {
let incidents = await db.getIncidentsPaginated(page, limit, filter, direction);
let allMonitors = {};
for (let i = 0; i < incidents.length; i++) {
let incidentMonitors = await GetIncidentMonitors(incidents[i].id);
incidents[i].monitors = incidentMonitors;
}
//for each monitor tag, in monitorsTagAndImpact for every incident, call get monitor by tag
for (let i = 0; i < incidents.length; i++) {
for (let j = 0; j < incidents[i].monitors.length; j++) {
let monitorTag = incidents[i].monitors[j].monitor_tag;
let monitorImpact = incidents[i].monitors[j].monitor_impact;
if (!allMonitors[monitorTag]) {
let monitor = await db.getMonitorByTag(monitorTag);
if (monitor) {
allMonitors[monitorTag] = {
id: monitor.id,
tag: monitor.tag,
name: monitor.name,
image: monitor.image,
impact_type: monitorImpact,
};
}
}
incidents[i].monitors[j] = allMonitors[monitorTag];
}
}
//get comments
for (let i = 0; i < incidents.length; i++) {
incidents[i].comments = await GetIncidentActiveComments(incidents[i].id);
}
return incidents;
};
export const GetIncidentsOpenHome = async (homeIncidentCount, start, end) => {
homeIncidentCount = parseInt(homeIncidentCount);
@@ -928,7 +1180,7 @@ export const GetSMTPFromENV = () => {
smtp_user: process.env.SMTP_USER,
smtp_from_email: process.env.SMTP_FROM_EMAIL,
smtp_pass: process.env.SMTP_PASS,
smtp_secure: !!process.env.SMTP_SECURE,
smtp_secure: !!Number(process.env.SMTP_SECURE),
};
};
@@ -1176,6 +1428,81 @@ export const CreateNewUser = async (currentUser, data) => {
return await db.insertUser(user);
};
export const DeleteMonitorCompletelyUsingTag = async (tag) => {
await db.deleteMonitorDataByTag(tag);
await db.deleteIncidentMonitorsByTag(tag);
await db.deleteMonitorAlertsByTag(tag);
return await db.deleteMonitorsByTag(tag);
};
export const CreateNewSubscriber = async (data) => {
await db.insertSubscriber(data);
return await GetSubscriberByEmailAndType(data.subscriber_send, data.subscriber_type);
};
export const GetSubscriberByEmailAndType = async (email, type) => {
return await db.getSubscriberByDetails(email, type);
};
//get subscriber by id
export const GetSubscriberByID = async (id) => {
return await db.getSubscriberById(id);
};
//remove all subscriptions for a subscriber
export const RemoveAllSubscriptions = async (subscriber_id) => {
return await db.removeAllDataFromSubscriptions(subscriber_id);
};
export const CreateNewSubscription = async (subscriber_id, monitors) => {
if (!monitors || monitors.length === 0) {
throw new Error("No monitors found");
}
await db.removeAllDataFromSubscriptions(subscriber_id);
for (let i = 0; i < monitors.length; i++) {
let tag = monitors[i];
let subscription = {
subscriber_id: subscriber_id,
subscriptions_status: "ACTIVE",
subscriptions_monitors: tag,
subscriptions_meta: "",
};
await db.insertSubscription(subscription);
}
return {
message: "Subscriptions created successfully",
count: monitors.length,
};
};
//updateSubscriberMeta given id
export const UpdateSubscriberMeta = async (id, meta) => {
return await db.updateSubscriberMeta(id, meta);
};
//updateSubscriberStatus
export const UpdateSubscriberStatus = async (id, status) => {
return await db.updateSubscriberStatus(id, status);
};
//delete subscriber by id
export const DeleteSubscriberByID = async (id) => {
return await db.deleteSubscriberById(id);
};
//get subscriptions by subscriber id
export const GetSubscriptionsBySubscriberID = async (subscriber_id) => {
return await db.getSubscriptionsBySubscriberId(subscriber_id);
};
//getMonitorsByTag
export const GetMonitorsByTag = async (tag) => {
return await db.getMonitorsByTag(tag);
};
export const GetSiteMap = async (cookies) => {
let siteMapData = [];
let siteURLData = await GetSiteDataByKey("siteURL");
@@ -1265,3 +1592,87 @@ export const GetSiteMap = async (cookies) => {
.join("")}
</urlset>`;
};
// fetch the single subscription_trigger (only type=email supported)
export const GetSubscriptionTriggerByEmail = async () => {
return await db.getSubscriptionTriggerByType("email");
};
// create a subscription_trigger record for email type
export const CreateSubscriptionTrigger = async (data) => {
// only email supported
if (data.subscription_trigger_type !== "email") {
throw new Error("Only email trigger type is supported");
}
//update subscription_trigger_status and subscription_trigger_id given subscription_trigger_type, if not present insert otherwise update
let subscriptionTrigger = await db.getSubscriptionTriggerByType(data.subscription_trigger_type);
if (!subscriptionTrigger) {
await db.insertSubscriptionTrigger({
subscription_trigger_type: data.subscription_trigger_type,
subscription_trigger_status: "ACTIVE",
config: data.config,
});
} else {
await db.updateSubscriptionTrigger({
id: subscriptionTrigger.id,
subscription_trigger_status: "ACTIVE",
subscription_trigger_type: subscriptionTrigger.subscription_trigger_type,
config: data.config,
});
}
return {
subscription_trigger_type: data.subscription_trigger_type,
subscription_trigger_status: data.subscription_trigger_status,
config: data.config,
};
};
// Get subscribers paginated
export const GetSubscribersPaginated = async (data) => {
const page = parseInt(data.page) || 1;
const limit = parseInt(data.limit) || 10;
const subscriptions = await db.getSubscriptionsPaginated(page, limit);
const total = await db.getTotalSubscriptionCount();
//all monitor tags
let allTags = subscriptions.map((subscription) => subscription.subscriptions_monitors);
//get all monitors by tags
let monitors = await db.getMonitorsByTags(allTags);
let tagMonitor = {};
//convert monitors to map in tagMonitor
for (let i = 0; i < monitors.length; i++) {
let m = monitors[i];
tagMonitor[monitors[i].tag] = {
name: m.name,
tag: m.tag,
image: m.image,
};
}
let subscriberIDObj = {};
//for each subscription get subscriber details
for (let i = 0; i < subscriptions.length; i++) {
let subsID = subscriptions[i].subscriber_id;
if (!subscriberIDObj[subsID]) {
const subscriber = await db.getSubscriberById(subscriptions[i].subscriber_id);
subscriberIDObj[subsID] = {
id: subscriber.id,
email: subscriber.subscriber_send,
status: subscriber.subscriber_status,
};
}
subscriptions[i].subscriber = subscriberIDObj[subsID];
subscriptions[i].monitor = tagMonitor[subscriptions[i].subscriptions_monitors];
}
return {
subscriptions: subscriptions,
total: total,
};
};
//updateSubscriptionStatus
export const UpdateSubscriptionStatus = async (subscription_id, status) => {
return await db.updateSubscriptionStatus(subscription_id, status);
};
+214
View File
@@ -0,0 +1,214 @@
// @ts-nocheck
import {
IsValidAnalytics,
IsValidColors,
IsValidHero,
IsValidI18n,
IsValidJSONArray,
IsValidJSONString,
IsValidNav,
IsValidURL,
} from "./validators.js";
export const siteDataKeys = [
{
key: "title",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "siteName",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "siteURL",
isValid: IsValidURL,
data_type: "string",
},
{
key: "home",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "favicon",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "logo",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "metaTags",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "nav",
isValid: IsValidNav,
data_type: "object",
},
{
key: "hero",
isValid: IsValidHero,
data_type: "object",
},
{
key: "footerHTML",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "kenerTheme",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "customCSS",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "i18n",
isValid: IsValidI18n,
data_type: "object",
},
{
key: "pattern",
//string dots or squares or circle
isValid: (value) =>
typeof value === "string" &&
[
"dots",
"squares",
"tiles",
"none",
"radial-blue",
"radial-mono",
"radial-midnight",
"circle-mono",
"carbon-fibre",
"texture-sky",
"angular-mono",
"angular-spring",
"angular-bloom",
"pets",
].includes(value),
data_type: "string",
},
{
key: "analytics",
isValid: IsValidAnalytics,
data_type: "object",
},
{
key: "theme",
//light dark system none
isValid: (value) => typeof value === "string" && ["light", "dark", "system", "none"].includes(value),
data_type: "string",
},
{
key: "themeToggle",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "tzToggle",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "showSiteStatus",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "barStyle",
//PARTIAL or FULL
isValid: (value) => typeof value === "string" && ["PARTIAL", "FULL"].includes(value),
data_type: "string",
},
{
key: "barRoundness",
//SHARP or ROUNDED
isValid: (value) => typeof value === "string" && ["SHARP", "ROUNDED"].includes(value),
data_type: "string",
},
{
key: "summaryStyle",
//CURRENT or DAY
isValid: (value) => typeof value === "string" && ["CURRENT", "DAY"].includes(value),
data_type: "string",
},
{
key: "colors",
isValid: IsValidColors,
data_type: "object",
},
{
key: "font",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "monitorSort",
isValid: IsValidJSONArray,
data_type: "object",
},
{
key: "categories",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "homeIncidentCount",
isValid: (value) => parseInt(value) >= 0,
data_type: "string",
},
{
key: "homeIncidentStartTimeWithin",
isValid: (value) => parseInt(value) >= 1,
data_type: "string",
},
{
key: "incidentGroupView",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "analytics.googleTagManager",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.plausible",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.mixpanel",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.amplitude",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.clarity",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.umami",
isValid: IsValidJSONString,
data_type: "object",
},
];
+5 -3
View File
@@ -12,6 +12,7 @@ import path from "path";
import db from "./db/db.js";
import notification from "./notification/notif.js";
import DNSResolver from "./dns.js";
import { InsertMonitoringData } from "./controllers/controller.js";
dotenv.config();
@@ -83,7 +84,7 @@ const Minuter = async (monitor) => {
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({
await InsertMonitoringData({
monitor_tag: monitor.tag,
timestamp: startOfMinute,
status: data.status,
@@ -101,7 +102,8 @@ const Minuter = async (monitor) => {
} else if (monitor.monitor_type === "DNS") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "GROUP") {
realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
// realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
realTimeData[startOfMinute] = null;
} else if (monitor.monitor_type === "SSL") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "SQL") {
@@ -139,7 +141,7 @@ const Minuter = async (monitor) => {
for (const timestamp in mergedData) {
const element = mergedData[timestamp];
db.insertMonitoringData({
await InsertMonitoringData({
monitor_tag: monitor.tag,
timestamp: parseInt(timestamp),
status: element.status,
+354
View File
@@ -48,6 +48,16 @@ class DbImpl {
.first();
}
//get data at time stamp
async getMonitoringDataAt(monitor_tag, timestamp) {
return await this.knex("monitoring_data")
.where("monitor_tag", monitor_tag)
.where("timestamp", timestamp)
.orderBy("timestamp", "desc")
.limit(1)
.first();
}
//get latest data for all active monitors
async getLatestMonitoringDataAllActive(monitor_tags) {
// Find the latest timestamp for each provided monitor tag
@@ -278,6 +288,61 @@ class DbImpl {
return await this.knex("monitor_alerts").count("* as count").first();
}
//getMonitorsByTags array of string
async getMonitorsByTags(tags) {
return await this.knex("monitors").whereIn("tag", tags);
}
async getMonitorsByTag(tag) {
return await this.knex("monitors").where("tag", tag).first();
}
//get incidents paginated and direction, given a start timestamp, also might have filter on incident_type, incident_source
async getIncidentsPaginated(page, limit, filter, direction = "after") {
let query = this.knex("incidents").select("*").whereRaw("1=1");
if (filter && filter.status) {
query = query.andWhere("status", filter.status);
}
if (filter && filter.start && direction === "after") {
query = query.andWhere("start_date_time", ">=", filter.start);
}
if (filter && filter.start && direction === "before") {
query = query.andWhere("start_date_time", "<=", filter.start);
}
if (filter && filter.end && direction === "after") {
query = query.andWhere("start_date_time", ">=", filter.end);
}
if (filter && filter.end && direction === "before") {
query = query.andWhere("start_date_time", "<=", filter.end);
}
if (filter && filter.state) {
query = query.andWhere("state", filter.state);
}
if (filter && filter.id) {
query = query.andWhere("id", filter.id);
}
if (filter && filter.incident_type) {
query = query.andWhere("incident_type", filter.incident_type);
}
if (filter && filter.incident_source) {
query = query.andWhere("incident_source", filter.incident_source);
}
if (direction === "after") {
query = query
.orderBy("start_date_time", "asc")
.limit(limit)
.offset((page - 1) * limit);
} else {
query = query
.orderBy("start_date_time", "desc")
.limit(limit)
.offset((page - 1) * limit);
}
return await query;
}
//update alert to inactive given monitor_tag, monitor_status, given id
async updateAlertStatus(id, alert_status) {
return await this.knex("monitor_alerts").where({ id }).update({
@@ -324,6 +389,11 @@ class DbImpl {
return await this.knex("site_data").where("key", key).first();
}
//get site data that starts with analytics.
async getAllSiteDataAnalytics() {
return await this.knex("site_data").where("key", "like", "analytics.%");
}
//insert into monitors
async insertMonitor(data) {
return await this.knex("monitors").insert({
@@ -365,6 +435,39 @@ class DbImpl {
});
}
//given monitor_tag, start and end timestamp and a status, update all monitoring data with this status
async updateMonitoringData(monitor_tag, start, end, newStatus, type) {
const count = Math.floor((end - start) / 60) + 1;
const timestamps = Array.from({ length: count }, (_, i) => start + i * 60);
const records = timestamps.map((ts) => ({
monitor_tag,
timestamp: ts,
status: newStatus,
type,
latency: 0,
}));
// Use transaction for better performance with batches
const batchSize = 500;
return await this.knex.transaction(async (trx) => {
const results = [];
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);
const result = await trx("monitoring_data")
.insert(batch)
.onConflict(["monitor_tag", "timestamp"])
.merge({ status: newStatus });
results.push(result);
}
return results;
});
}
async updateMonitorTrigger(data) {
return await this.knex("monitors").where({ id: data.id }).update({
down_trigger: data.down_trigger,
@@ -385,6 +488,10 @@ class DbImpl {
if (!!data.id) {
query = query.andWhere("id", data.id);
}
//monitor_type
if (!!data.monitor_type) {
query = query.andWhere("monitor_type", data.monitor_type);
}
if (!!data.tag) {
query = query.andWhere("tag", data.tag);
}
@@ -598,6 +705,10 @@ class DbImpl {
query = query.andWhere("state", filter.state);
}
if (filter && filter.id) {
query = query.andWhere("id", filter.id);
}
query = query
.orderBy("id", "desc")
.limit(limit)
@@ -918,6 +1029,249 @@ class DbImpl {
.andWhere("invitation_expiry", ">", this.knex.fn.now())
.first();
}
//delete from monitor_data using tag
async deleteMonitorDataByTag(tag) {
return await this.knex("monitoring_data").where("monitor_tag", tag).del();
}
//delete from incident monitors using tag
async deleteIncidentMonitorsByTag(tag) {
return await this.knex("incident_monitors").where("monitor_tag", tag).del();
}
//delete from monitor alerts using tag
async deleteMonitorAlertsByTag(tag) {
return await this.knex("monitor_alerts").where("monitor_tag", tag).del();
}
//delete from monitors using tag
async deleteMonitorsByTag(tag) {
return await this.knex("monitors").where("tag", tag).del();
}
//insert into subscribers
async insertSubscriber(data) {
return await this.knex("subscribers").insert({
subscriber_send: data.subscriber_send,
subscriber_meta: data.subscriber_meta,
subscriber_type: data.subscriber_type,
subscriber_status: data.subscriber_status,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
});
}
//update subscriber_meta given id
async updateSubscriberMeta(id, subscriber_meta) {
return await this.knex("subscribers").where({ id }).update({
subscriber_meta,
updated_at: this.knex.fn.now(),
});
}
//update subscriber_status given id
async updateSubscriberStatus(id, subscriber_status) {
return await this.knex("subscribers").where({ id }).update({
subscriber_status,
updated_at: this.knex.fn.now(),
});
}
//delete subscriber by id
async deleteSubscriberById(id) {
return await this.knex("subscribers").where({ id }).del();
}
//insert into subscriptions table
async insertSubscription(data) {
return await this.knex("subscriptions").insert({
subscriber_id: data.subscriber_id,
subscriptions_status: data.subscriptions_status,
subscriptions_monitors: data.subscriptions_monitors,
subscriptions_meta: data.subscriptions_meta,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
});
}
//given subscriber_id remove all data from subscriptions
async removeAllDataFromSubscriptions(subscriber_id) {
return await this.knex("subscriptions").where({ subscriber_id }).del();
}
//get subscriptions by subscriber_id
async getSubscriptionsBySubscriberId(subscriber_id) {
return await this.knex("subscriptions").where("subscriber_id", subscriber_id).orderBy("id", "desc");
}
//update subscription status
async updateSubscriptionStatus(id, subscriptions_status) {
return await this.knex("subscriptions").where({ id }).update({
subscriptions_status,
updated_at: this.knex.fn.now(),
});
}
//get all subscribers with active status
async getAllActiveSubscribers() {
return await this.knex("subscribers").where("subscriber_status", "ACTIVE");
}
//get subscriptions for a monitor tag
async getSubscriptionsForMonitor(monitor_tag) {
return await this.knex("subscriptions as s")
.join("subscribers as sub", "s.subscriber_id", "sub.id")
.where("s.subscriptions_status", "ACTIVE")
.where("sub.subscriber_status", "ACTIVE")
.whereRaw("s.subscriptions_monitors = ? OR s.subscriptions_monitors = 'ALL'", [monitor_tag])
.select("sub.subscriber_send", "sub.subscriber_type", "sub.subscriber_meta", "s.subscriptions_meta");
}
//get subscriber by subscriber_send and subscriber_type
async getSubscriberByDetails(subscriber_send, subscriber_type) {
return await this.knex("subscribers")
.where({
subscriber_send,
subscriber_type,
})
.first();
}
//get all subscribers by type
async getSubscribersByType(subscriber_type) {
return await this.knex("subscribers").where("subscriber_type", subscriber_type).orderBy("id", "desc");
}
//get subscriptions paginated
async getSubscriptionsPaginated(page, limit) {
return await this.knex("subscriptions")
.select(
"id",
"subscriber_id",
"subscriptions_status",
"subscriptions_monitors",
"subscriptions_meta",
"created_at",
)
.orderBy("id", "desc")
.limit(limit)
.offset((page - 1) * limit);
}
//get total subscription count
async getTotalSubscriptionCount() {
const result = await this.knex("subscriptions").count("* as count").first();
return result.count || 0;
}
//get all subscriber emails from subscriptions table that are of given tag or = _
async getSubscriberEmails(monitor_tags) {
return await this.knex("subscriptions")
.join("subscribers", "subscribers.id", "subscriptions.subscriber_id")
.distinct("subscribers.subscriber_send as subscriber_send")
.where("subscriptions.subscriptions_status", "ACTIVE")
.whereIn("subscriptions.subscriptions_monitors", monitor_tags)
.orderBy("subscriber_send");
}
//getSubscriberById
async getSubscriberById(id) {
return await this.knex("subscribers")
.select("id", "subscriber_send", "subscriber_meta", "subscriber_type", "subscriber_status", "created_at")
.where("id", id)
.first();
}
// Get subscribers paginated
async getSubscribersPaginated(page, limit) {
// Use a subquery for pagination to apply limit/offset before joining
const subquery = this.knex("subscribers")
.select("id")
.orderBy("created_at", "desc")
.limit(limit)
.offset((page - 1) * limit)
.as("paginated_subscribers");
const dbType = GetDbType(); // Get the database type
const aggregationFunction = dbType === "postgresql" ? "STRING_AGG" : "GROUP_CONCAT";
const results = await this.knex("subscribers as s")
.select(
"s.id",
"s.subscriber_send",
"s.subscriber_status",
"s.created_at",
// Aggregate monitor tags based on DB type
this.knex.raw(`${aggregationFunction}(sub.subscriptions_monitors, ',') as monitors_agg`),
)
.innerJoin(subquery, "s.id", "paginated_subscribers.id") // Join with paginated IDs
.leftJoin("subscriptions as sub", "s.id", "sub.subscriber_id") // Left join to include subscribers with no subscriptions
.groupBy("s.id", "s.subscriber_send", "s.subscriber_status", "s.created_at") // Group by subscriber details
.orderBy("s.created_at", "desc"); // Ensure final order
// Process results to split the aggregated string into an array
return results.map((row) => ({
...row,
subscriptions_monitors: row.monitors_agg ? row.monitors_agg.split(",") : [], // Split string into array, handle null
monitors_agg: undefined, // Remove the temporary aggregation field
}));
}
// Get total count of subscribers
async getSubscribersCount() {
const result = await this.knex("subscribers").count("* as count").first();
return result.count || 0; // Return count or 0 if no subscribers
}
// CRUD operations for subscription_triggers
async insertSubscriptionTrigger(data) {
return await this.knex("subscription_triggers").insert({
subscription_trigger_type: data.subscription_trigger_type,
subscription_trigger_status: data.subscription_trigger_status,
config: data.config,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
});
}
async getSubscriptionTriggerById(id) {
return await this.knex("subscription_triggers").where({ id }).first();
}
async getAllSubscriptionTriggers() {
return await this.knex("subscription_triggers").orderBy("id", "desc");
}
// get a single subscription_trigger by type
async getSubscriptionTriggerByType(subscription_trigger_type) {
return await this.knex("subscription_triggers")
.where("subscription_trigger_type", subscription_trigger_type)
.first();
}
async updateSubscriptionTrigger(data) {
return await this.knex("subscription_triggers").where({ id: data.id }).update({
subscription_trigger_type: data.subscription_trigger_type,
subscription_trigger_status: data.subscription_trigger_status,
config: data.config,
updated_at: this.knex.fn.now(),
});
}
async updateSubscriptionTriggerStatus(id, subscription_trigger_status) {
return await this.knex("subscription_triggers").where({ id }).update({
subscription_trigger_status,
updated_at: this.knex.fn.now(),
});
}
//deleteSubscriptionTriggerByType
async deleteSubscriptionTriggerByType(subscription_trigger_type) {
return await this.knex("subscription_triggers").where({ subscription_trigger_type }).del();
}
async deleteSubscriptionTriggerById(id) {
return await this.knex("subscription_triggers").where({ id }).del();
}
}
export default DbImpl;
+16 -2
View File
@@ -43,8 +43,20 @@ const seedSiteData = {
title: "Build stunning status pages in minutes",
subtitle: "Let your users know what's going on.",
},
footerHTML:
'<p class="text-center">\nMade using \n<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">\n Kener\n</a>\nan open source status page system built with Svelte and TailwindCSS.<br/>\nCreated with ❤️ by <a href="https://rajnandan.com" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.\n</p>\n',
footerHTML: `<div
class="container relative mt-4 max-w-[655px]"
>
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
<p class="text-center text-sm leading-loose text-muted-foreground ">
Made using
<a href="https://github.com/rajnandan1/kener" target="_blank" class="font-medium underline underline-offset-4">
Kener
</a>
an open source status page system built with Svelte and TailwindCSS.<br/>
Created with by <a href="https://rajnandan.com" target="_blank" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.
</p>
</div>
</div>`,
i18n: {
defaultLocale: "en",
locales: [{ code: "en", name: "English", selected: true, disabled: false }],
@@ -88,6 +100,8 @@ const seedSiteData = {
categories: [{ name: "Home", description: "Monitors for Home Page" }],
homeIncidentCount: 5,
homeIncidentStartTimeWithin: 30,
kenerTheme: "default",
showSiteStatus: "NO",
};
export default seedSiteData;
+1
View File
@@ -197,6 +197,7 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
barRoundness: site.barRoundness,
midnight90DaysAgo: midnight90DaysAgoTz,
maxDateTodayTimestamp: maxDateTodayTimestampTz,
startOfTheDay: midnightTz,
};
};
export { FetchData };
+67
View File
@@ -0,0 +1,67 @@
// @ts-nocheck
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import { GameDig } from 'gamedig';
import { DefaultGamedigEval, GAMEDIG_SOCKET_TIMEOUT, GAMEDIG_TIMEOUT } from "../../anywhere.js";
class GamedigCall {
monitor;
constructor(monitor) {
this.monitor = monitor;
}
async execute() {
const tag = this.monitor.tag;
const host = this.monitor.type_data.host;
const port = this.monitor.type_data.port;
const timeout = !!this.monitor.type_data.timeout ? this.monitor.type_data.timeout : GAMEDIG_TIMEOUT;
const gamedigEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : DefaultGamedigEval;
// Query
let responseTime, responseRaw;
try {
const response = await GameDig.query({
type: this.monitor.type_data.gameId,
host: host,
port: port,
socketTimeout: GAMEDIG_SOCKET_TIMEOUT,
attemptTimeout: timeout,
givenPortOnly: !this.monitor.type_data.guessPort,
requestRules: this.monitor.type_data.requestRules,
});
responseTime = response.ping;
responseRaw = response.raw;
} catch (error) {
console.log(`Error while requesting game's information for ${tag}: `, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
// Eval
let evalResp = undefined;
const evalFunction = new Function("responseTime", "responseRaw", `return (${gamedigEval})(responseTime, responseRaw);`);
try {
evalResp = await evalFunction(responseTime, responseRaw);
} catch (error) {
console.log(`Error in gamedigEval for ${tag}: `, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
// Result
return {
status: evalResp.status,
latency: evalResp.latency,
type: REALTIME,
};
}
}
export default GamedigCall;
+8 -18
View File
@@ -4,26 +4,16 @@ import { GetRequiredSecrets, ReplaceAllOccurrences, Wait, GetMinuteStartNowTimes
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import db from "../db/db.js";
async function waitForDataAndReturn(arr, ts, d, maxTime) {
await Wait(d);
let data = await db.getLastStatusBeforeCombined(arr, ts);
if (data) {
data.type = REALTIME;
return data;
} else if (d > maxTime) {
data = await db.getLastStatusBeforeCombined(arr, ts, ts - 86400);
if (data) {
return data;
}
async function waitForDataAndReturn(tag) {
let res = await db.getLatestMonitoringData(tag);
if (!!res) {
return {
status: DOWN,
latency: 0,
type: TIMEOUT,
status: res.status,
latency: res.latency,
type: REALTIME,
};
} else {
return await waitForDataAndReturn(arr, ts, d * 2, maxTime);
}
return null;
}
class GroupCall {
@@ -38,7 +28,7 @@ class GroupCall {
startOfMinute = GetMinuteStartNowTimestampUTC();
}
let tagArr = this.monitor.type_data.monitors.map((m) => m.tag);
return await waitForDataAndReturn(tagArr, startOfMinute, 500, this.monitor.type_data.timeout);
return await waitForDataAndReturn(this.monitor.tag);
}
}
+3
View File
@@ -7,6 +7,7 @@ import GroupCall from "./groupCall.js";
import SSLCall from "./sslCall.js";
import SqlCall from "./sqlCall.js";
import HeartbeatCall from "./heartbeatCall.js";
import GamedigCall from "./gamedigCall.js";
class Service {
service;
@@ -30,6 +31,8 @@ class Service {
this.service = new SqlCall(monitor);
} else if (monitor.monitor_type === "HEARTBEAT") {
this.service = new HeartbeatCall(monitor);
} else if (monitor.monitor_type === "GAMEDIG") {
this.service = new GamedigCall(monitor);
} else {
console.log("Invalid monitor.monitor_type ", monitor.monitor_type);
process.exit(1);
+178
View File
@@ -0,0 +1,178 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<link rel="preload" as="image" href="{{logo_url}}" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<body
style="
background-color: rgb(243, 244, 246);
font-family:
ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;,
&quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;
padding-top: 40px;
padding-bottom: 40px;
"
>
<!--$-->
<div
style="
display: none;
overflow: hidden;
line-height: 1px;
opacity: 0;
max-height: 0;
max-width: 0;
"
>
Your verification code: {{email_code}}
</div>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
background-color: rgb(255, 255, 255);
border-radius: 8px;
margin-left: auto;
margin-right: auto;
margin-top: 0px;
margin-bottom: 0px;
padding: 24px;
max-width: 600px;
"
>
<tbody>
<tr style="width: 100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-top: 8px; margin-bottom: 32px; text-align: center"
>
<tbody>
<tr>
<td>
<img
alt="{{brand_name}}"
height="40"
src="{{logo_url}}"
style="
margin-left: auto;
margin-right: auto;
display: block;
outline: none;
border: none;
text-decoration: none;
"
width="120"
/>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tbody>
<tr>
<td>
<h1
style="
font-size: 24px;
font-weight: 700;
color: rgb(31, 41, 55);
margin-bottom: 16px;
text-align: center;
"
>
Verification Code
</h1>
<p
style="
font-size: 16px;
color: rgb(75, 85, 99);
margin-bottom: 24px;
line-height: 24px;
margin-top: 16px;
"
>
To complete your {{action}} for {{brand_name}} updates, please use the
verification code below:
</p>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
background-color: rgb(249, 250, 251);
border-width: 1px;
border-color: rgb(229, 231, 235);
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
text-align: center;
"
>
<tbody>
<tr>
<td>
<p
style="
font-size: 32px;
font-weight: 700;
letter-spacing: 4px;
color: rgb(31, 41, 55);
margin: 0px;
line-height: 24px;
margin-bottom: 0px;
margin-top: 0px;
margin-left: 0px;
margin-right: 0px;
"
>
{{email_code}}
</p>
</td>
</tr>
</tbody>
</table>
<p
style="
font-size: 16px;
color: rgb(75, 85, 99);
margin-bottom: 24px;
line-height: 24px;
margin-top: 16px;
"
>
This code will expire in 5 minutes. If you didn&#x27;t request this code, you
can safely ignore this email.
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--7--><!--/$-->
</body>
</html>
+248
View File
@@ -0,0 +1,248 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<link rel="preload" as="image" href="{{logo_url}}" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<body
style="
background-color: rgb(243, 244, 246);
font-family:
ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;,
&quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;
padding-top: 40px;
padding-bottom: 40px;
"
>
<!--$-->
<div
style="
display: none;
overflow: hidden;
line-height: 1px;
opacity: 0;
max-height: 0;
max-width: 0;
"
>
{{title}}
</div>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
max-width: 600px;
margin-left: auto;
margin-right: auto;
background-color: rgb(255, 255, 255);
border-radius: 8px;
overflow: hidden;
"
>
<tbody>
<tr style="width: 100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
background-color: #e4e5ec;
padding-left: 24px;
padding-right: 24px;
padding-top: 24px;
padding-bottom: 24px;
"
>
<tbody>
<tr>
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tbody style="width: 100%">
<tr style="width: 100%">
<td data-id="__react-email-column" style="text-align: center">
<img
alt="{{brand_name}}"
src="{{logo_url}}"
style="
height: auto;
display: block;
outline: none;
border: none;
text-decoration: none;
margin-left: auto;
margin-right: auto;
"
width="80"
/>
<p
style="
font-size: 20px;
font-weight: bold;
color: #191919;
margin: 8px 0 0 0;
text-align: center;
"
>
{{brand_name}}
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
padding-left: 24px;
padding-right: 24px;
padding-top: 32px;
padding-bottom: 32px;
"
>
<tbody>
<tr>
<td>
<h1
style="
font-size: 24px;
font-weight: 700;
color: rgb(31, 41, 55);
margin-bottom: 16px;
"
>
{{title}}
</h1>
<p
style="
font-size: 16px;
color: rgb(55, 65, 81);
margin-bottom: 16px;
line-height: 24px;
margin-top: 16px;
"
>
Dear Valued Customer,
</p>
<p
style="
font-size: 16px;
color: rgb(55, 65, 81);
margin-bottom: 16px;
line-height: 24px;
margin-top: 16px;
"
>
We would like to provide you with an update regarding the current system
status.
</p>
<p
style="
font-size: 16px;
color: rgb(55, 65, 81);
margin-bottom: 16px;
line-height: 24px;
margin-top: 16px;
"
>
<strong>Update:</strong>
{{update_message}}
</p>
<a
href="{{incident_url}}"
style="
background-color: rgb(22, 163, 74);
color: rgb(255, 255, 255);
font-weight: 700;
padding-top: 12px;
padding-bottom: 12px;
padding-left: 24px;
padding-right: 24px;
border-radius: 4px;
font-size: 16px;
text-decoration-line: none;
text-align: center;
display: block;
box-sizing: border-box;
line-height: 100%;
text-decoration: none;
max-width: 100%;
mso-padding-alt: 0px;
padding: 12px 24px 12px 24px;
"
target="_blank"
><span
><!--[if mso
]><i style="mso-font-width: 400%; mso-text-raise: 18" hidden
>&#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;
"
>View Incident Details</span
><span
><!--[if mso
]><i style="mso-font-width: 400%" hidden
>&#8202;&#8202;&#8202;&#8203;</i
><!
[endif]--></span
></a
>
</td>
</tr>
</tbody>
</table>
<hr
style="
border-top-width: 1px;
border-color: rgb(209, 213, 219);
margin-top: 8px;
margin-bottom: 8px;
width: 100%;
border: none;
border-top: 1px solid #eaeaea;
"
/>
<footer>
<p style="text-align: center; color: #6b7280; font-size: 16px; margin: 24px 0 0 0">
Thank you,<br />The {{brand_name}} Team
</p>
</footer>
<div style="height: 32px"></div>
</td>
</tr>
</tbody>
</table>
<!--7--><!--/$-->
</body>
</html>
+19
View File
@@ -280,6 +280,9 @@ function Wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function MaskString(str) {
if (str.length <= 4) {
return "*".repeat(str.length);
}
return "*".repeat(str.length - 4) + str.slice(-4);
}
@@ -293,6 +296,20 @@ function HashString(str) {
return hash.digest("hex");
}
//function to validate email address
function ValidateEmail(email) {
const regex =
/^(?:(?:[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+\.)*[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+)@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\[(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?:\.(?!$)|$)){4}\]))$/;
return regex.test(email);
}
//function to generate 6 digit random number, number of digits as argument
function GenerateRandomNumber(digits) {
const min = Math.pow(10, digits - 1);
const max = Math.pow(10, digits) - 1;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export {
IsValidURL,
IsValidHTTPMethod,
@@ -322,4 +339,6 @@ export {
MaskString,
GetDbType,
HashString,
ValidateEmail,
GenerateRandomNumber,
};
+2 -2
View File
@@ -9,7 +9,7 @@ import {
import db from "./db/db.js";
import { WEBHOOK } from "./constants.js";
import { GetMonitors, VerifyAPIKey } from "./controllers/controller.js";
import { GetMonitors, VerifyAPIKey, InsertMonitoringData } from "./controllers/controller.js";
const GetAllTags = async function () {
let tags = [];
@@ -94,7 +94,7 @@ const store = async function (data) {
//get the monitor object matching the tag
await db.insertMonitoringData({
await InsertMonitoringData({
monitor_tag: tag,
timestamp: data.timestampInSeconds,
status: resp.status,
+42
View File
@@ -0,0 +1,42 @@
/**
* ***************************************************
* * *
* * Amplitude Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.amplitude.add(window.sessionReplay.plugin({ sampleRate: 1 }));
window.amplitude.init("{{api_key}}", { fetchRemoteConfig: true, autocapture: true });
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.amplitude.track(eventName, eventData);
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://cdn.amplitude.com/script/{{api_key}}.js", initJS, document.getElementsByTagName("head")[0]);
});
})();
+20
View File
@@ -0,0 +1,20 @@
/**
* ***************************************************
* * *
* * Clarity Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function (c, l, a, r, i, t, y) {
c[a] =
c[a] ||
function () {
(c[a].q = c[a].q || []).push(arguments);
};
t = l.createElement(r);
t.async = 1;
t.src = "https://www.clarity.ms/tag/" + i;
y = l.getElementsByTagName(r)[0];
y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "{{project_id}}");
@@ -0,0 +1,48 @@
/**
* ***************************************************
* * *
* * Google Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "{{id}}");
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
// Send the event to Google Tag Manager
if (window.dataLayer && eventName) {
gtag("event", eventName, eventData);
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://www.googletagmanager.com/gtag/js?id={{id}}", initJS, document.getElementsByTagName("head")[0]);
});
})();
+78
View File
@@ -0,0 +1,78 @@
/**
* ***************************************************
* * *
* * Mixpanel Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function (f, b) {
if (!b.__SV) {
var e, g, i, h;
window.mixpanel = b;
b._i = [];
b.init = function (e, f, c) {
function g(a, d) {
var b = d.split(".");
2 == b.length && ((a = a[b[0]]), (d = b[1]));
a[d] = function () {
a.push([d].concat(Array.prototype.slice.call(arguments, 0)));
};
}
var a = b;
"undefined" !== typeof c ? (a = b[c] = []) : (c = "mixpanel");
a.people = a.people || [];
a.toString = function (a) {
var d = "mixpanel";
"mixpanel" !== c && (d += "." + c);
a || (d += " (stub)");
return d;
};
a.people.toString = function () {
return a.toString(1) + ".people (stub)";
};
i =
"disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(
" ",
);
for (h = 0; h < i.length; h++) g(a, i[h]);
var j = "set set_once union unset remove delete".split(" ");
a.get_group = function () {
function b(c) {
d[c] = function () {
call2_args = arguments;
call2 = [c].concat(Array.prototype.slice.call(call2_args, 0));
a.push([e, call2]);
};
}
for (var d = {}, e = ["get_group"].concat(Array.prototype.slice.call(arguments, 0)), c = 0; c < j.length; c++)
b(j[c]);
return d;
};
b._i.push([e, f, c]);
};
b.__SV = 1.2;
e = f.createElement("script");
e.type = "text/javascript";
e.async = !0;
e.src =
"undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL
? MIXPANEL_CUSTOM_LIB_URL
: "file:" === f.location.protocol && "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)
? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"
: "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";
g = f.getElementsByTagName("script")[0];
g.parentNode.insertBefore(e, g);
}
})(document, window.mixpanel || []);
window.mixpanel.init("{{YOUR_PROJECT_TOKEN}}", { track_pageview: true, autocapture: true });
window.addEventListener("analyticsEvent", function (e) {
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.mixpanel.track(eventName, eventData);
}
});
+47
View File
@@ -0,0 +1,47 @@
/**
* ***************************************************
* * *
* * Plausible Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.defer = true;
scriptTag.dataset.domain = "{{domain}}";
scriptTag.dataset.api = "{{api}}";
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.plausible =
window.plausible ||
function () {
(window.plausible.q = window.plausible.q || []).push(arguments);
};
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.plausible(eventName, { props: eventData });
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("{{script_src}}", initJS, document.getElementsByTagName("head")[0]);
});
})();
+38
View File
@@ -0,0 +1,38 @@
/**
* ***************************************************
* * *
* * Umami Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.dataset.websiteId = "{{website_id}}";
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
window.umami.track(eventName, eventData);
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://cloud.umami.is/script.js", initJS, document.getElementsByTagName("head")[0]);
});
})();
-14
View File
@@ -1,20 +1,6 @@
.tabs [data-state="active"] {
border-color: orange;
}
.copybtn .copy-btn {
transform: scale(1);
transition: transform 0.15s;
}
.copybtn .check-btn {
transform: scale(0);
transition: transform 0.15s;
}
.copybtn:focus .copy-btn {
transform: scale(0);
}
.copybtn:focus .check-btn {
transform: scale(1);
}
input,
[data-bits-select-trigger],
+1 -1
View File
@@ -217,7 +217,7 @@
</div>
</div>
<div
class="prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
<slot />
</div>
+39 -37
View File
@@ -3,50 +3,52 @@ import i18n from "$lib/i18n/server";
import { redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import {
GetAllSiteData,
IsSetupComplete,
IsLoggedInSession,
GetLocaleFromCookie
GetAllSiteData,
IsSetupComplete,
IsLoggedInSession,
GetLocaleFromCookie,
IsEmailSetup,
} from "$lib/server/controllers/controller.js";
export async function load({ params, route, url, cookies, request }) {
let isSetupComplete = await IsSetupComplete();
if (!isSetupComplete) {
throw redirect(302, base + "/manage/setup");
}
let isSetupComplete = await IsSetupComplete();
if (!isSetupComplete) {
throw redirect(302, base + "/manage/setup");
}
if (process.env.KENER_SECRET_KEY === undefined || process.env.ORIGIN === undefined) {
throw redirect(302, base + "/manage/setup");
}
if (process.env.KENER_SECRET_KEY === undefined || process.env.ORIGIN === undefined) {
throw redirect(302, base + "/manage/setup");
}
let isLoggedIn = await IsLoggedInSession(cookies);
let isLoggedIn = await IsLoggedInSession(cookies);
let site = await GetAllSiteData();
const headers = request.headers;
const userAgent = headers.get("user-agent");
let localTz = "UTC";
const localTzCookie = cookies.get("localTz");
if (!!localTzCookie) {
localTz = localTzCookie;
}
let site = await GetAllSiteData();
const headers = request.headers;
const userAgent = headers.get("user-agent");
let localTz = "UTC";
const localTzCookie = cookies.get("localTz");
if (!!localTzCookie) {
localTz = localTzCookie;
}
// if the user agent is lighthouse, then we are running a lighthouse test
//if bot also set localTz to -1 to avoid reload
let isBot = false;
if (userAgent?.includes("Chrome-Lighthouse") || userAgent?.includes("bot")) {
isBot = true;
}
// if the user agent is lighthouse, then we are running a lighthouse test
//if bot also set localTz to -1 to avoid reload
let isBot = false;
if (userAgent?.includes("Chrome-Lighthouse") || userAgent?.includes("bot")) {
isBot = true;
}
let selectedLang = GetLocaleFromCookie(site, cookies);
let selectedLang = GetLocaleFromCookie(site, cookies);
const query = url.searchParams;
const bgc = query.get("bgc") ? "#" + query.get("bgc") : "";
return {
site: site,
localTz: localTz,
isBot,
lang: i18n(String(selectedLang)),
selectedLang: selectedLang,
isLoggedIn: !!isLoggedIn.user
};
const query = url.searchParams;
const bgc = query.get("bgc") ? "#" + query.get("bgc") : "";
return {
site: site,
localTz: localTz,
isBot,
lang: i18n(String(selectedLang)),
selectedLang: selectedLang,
isLoggedIn: !!isLoggedIn.user,
canSendEmail: IsEmailSetup(),
};
}
+14 -76
View File
@@ -1,6 +1,7 @@
<script>
import "../../app.postcss";
import "../../kener.css";
import "../../theme.css";
import Nav from "$lib/components/nav.svelte";
import { onMount } from "svelte";
import { Input } from "$lib/components/ui/input";
@@ -22,8 +23,6 @@
let searchTzValue = "";
let defaultTheme = data.site.theme;
const allLocales = data.site.i18n?.locales.filter((locale) => locale.selected === true);
let hasConfiguredAnalytics =
!!data.site.analytics && data.site.analytics.filter((provider) => !!provider.id).length > 0;
function toggleMode() {
if ($mode === "light") {
@@ -61,10 +60,12 @@
//set timezone
function setTz(tz) {
document.cookie = `localTz=${tz};max-age=${60 * 60 * 24 * 365 * 30};path=${base ? "base" : "/"};`;
analyticsEvent("timezone_changed", {
time_zone: tz
});
location.reload();
}
let Analytics;
let myTimezone = data.localTz;
onMount(async () => {
myTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -76,70 +77,20 @@
if (defaultTheme != "none") {
setMode(defaultTheme);
}
//
const providers = data.site.analytics;
const analyticsPlugins = [];
if (hasConfiguredAnalytics) {
//loop object
for (let i = 0; i < providers.length; i++) {
const provider = providers[i];
if (!!!provider.id) {
continue;
}
if (provider.type == "GA") {
analyticsPlugins.push(
analyticsGa.default({
measurementIds: provider.id
})
);
} else if (provider.type == "AMPLITUDE") {
analyticsPlugins.push(
analyticsAmplitude({
apiKey: provider.measurementIds[0],
options: {
trackingOptions: {
ip_address: false
}
}
})
);
} else if (provider.type == "MIXPANEL") {
analyticsPlugins.push(
analyticsMixpanel({
token: provider.measurementIds[0]
})
);
}
}
}
if (hasConfiguredAnalytics) {
Analytics = _analytics.init({
app: "kener",
debug: true,
version: 100,
plugins: analyticsPlugins
});
Analytics.page();
}
if (!!data.bgc && data.bgc[0] == "#") {
document.body.style.backgroundColor = data.bgc;
}
});
function captureAnalytics(e) {
if (hasConfiguredAnalytics) {
const { event, data } = e.detail;
Analytics.track(event, data);
}
}
let customCSS = `<style>${data.site.customCSS}</style>`;
if (!!data.site.favicon && !data.site.favicon.startsWith("http")) data.site.favicon = `${base}${data.site.favicon}`;
let kenerTheme = data.site.kenerTheme || "default";
</script>
<svelte:window on:analyticsEvent={captureAnalytics} />
<svelte:head>
<script src="{base}/api/js/capture.js"></script>
{#if data.site.favicon}
<link rel="icon" id="kener-app-favicon" href={data.site.favicon} />
{/if}
@@ -147,14 +98,6 @@
<link href={data.site.font.cssSrc} rel="stylesheet" />
{/if}
{#if hasConfiguredAnalytics}
<script src="https://unpkg.com/analytics/dist/analytics.min.js"></script>
{#each data.site.analytics as { id, type, script }}
{#if !!id}
<script data-type={type} src={script}></script>
{/if}
{/each}
{/if}
{#if !!data.site.customCSS}
{@html customCSS}
{/if}
@@ -168,6 +111,7 @@
--down-color: {data.site.colors.DOWN};
--degraded-color: {data.site.colors.DEGRADED}
"
class="kener-theme-{kenerTheme}"
>
<Nav {data} />
<div class="main-content min-h-[70vh]">
@@ -175,16 +119,8 @@
</div>
{#if !!data.site.footerHTML}
<footer class="z-10 py-6 md:px-8 md:py-0">
<div
class="container relative flex max-w-[655px] flex-col items-center justify-center gap-4 pl-0 md:h-24 md:flex-row"
>
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
<p class="text-center text-sm leading-loose text-muted-foreground md:text-left">
{@html data.site.footerHTML}
</p>
</div>
</div>
<footer class="kener-footer z-10 py-6 md:px-8 md:py-0">
{@html data.site.footerHTML}
</footer>
{/if}
<div class="fixed bottom-4 right-4 z-20 flex flex-col rounded-md bg-background">
@@ -222,7 +158,9 @@
<div class="text-xs font-semibold">
<Button
variant={tz === data.localTz ? "primary" : "ghost"}
class="h-8 w-full justify-start rounded-none text-xs text-muted-foreground"
class="h-8 w-full justify-start rounded-none text-xs {tz === data.localTz
? 'text-background'
: 'text-muted-foreground'}"
on:click={() => setTz(tz)}
>
{tz}
+18 -7
View File
@@ -1,6 +1,6 @@
// @ts-nocheck
import { FetchData } from "$lib/server/page";
import { GetMonitors, GetIncidentsOpenHome } from "$lib/server/controllers/controller.js";
import { GetMonitors, GetIncidentsOpenHome, SystemDataMessage } from "$lib/server/controllers/controller.js";
import { SortMonitor } from "$lib/clientTools.js";
import moment from "moment";
@@ -30,7 +30,6 @@ 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) {
@@ -42,20 +41,23 @@ 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();
let subscribableMonitors = await GetMonitors({ status: "ACTIVE" });
subscribableMonitors = subscribableMonitors.map((monitor) => {
return {
tag: monitor.tag,
name: monitor.name,
image: monitor.image,
};
});
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") {
@@ -194,8 +196,16 @@ export async function load({ parent, url }) {
let allRecentIncidents = allOpenIncidents.filter((incident) => incident.incident_type == "INCIDENT");
let allRecentMaintenances = allOpenIncidents.filter((incident) => incident.incident_type == "MAINTENANCE");
let systemDataMessage;
if (!!siteData.showSiteStatus && siteData.showSiteStatus == "YES") {
systemDataMessage = await SystemDataMessage();
}
return {
monitors: monitorsActive,
subscribableMonitors,
allRecentIncidents,
allRecentMaintenances,
pageType,
@@ -204,5 +214,6 @@ export async function load({ parent, url }) {
hero,
categoryName: query.get("category"),
canonical,
systemDataMessage,
};
}
+111 -23
View File
@@ -12,6 +12,8 @@
import { hotKeyAction, clickOutsideAction } from "svelte-legos";
import { onMount } from "svelte";
import ShareMenu from "$lib/components/shareMenu.svelte";
import SubscribeMenu from "$lib/components/subscribeMenu.svelte";
import { analyticsEvent } from "$lib/boringOne";
import { scale } from "svelte/transition";
import { format } from "date-fns";
import GMI from "$lib/components/gmi.svelte";
@@ -26,7 +28,7 @@
let pageLoaded = false;
$: {
if (pageLoaded) {
if (shareMenusToggle) {
if (shareMenusToggle || subscribeMenuToggle) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "auto";
@@ -39,12 +41,30 @@
});
let kindFilter = "INCIDENT";
function kindOfIncidents(kind) {
analyticsEvent("monitor_incident_kind_filter", {
kind: kind
});
kindFilter = kind;
}
if (data.allRecentIncidents.length == 0) {
kindOfIncidents("MAINTENANCE");
}
function closeShareMenu() {
shareMenusToggle = false;
analyticsEvent("monitor_share_menu_close", {
tag: activeMonitor.tag
});
}
let subscribeMenuToggle = false;
function openSubscribeMenu() {
subscribeMenuToggle = true;
}
function closeSubscribeMenu() {
subscribeMenuToggle = false;
}
</script>
<svelte:head>
@@ -66,27 +86,60 @@
<div class="mt-12"></div>
{#if data.hero}
<section class="section-hero mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center">
{#if data.hero.image}
<GMI src={data.hero.image} classList="m-auto mb-2 h-14 w-14" alt="" srcset="" />
{/if}
{#if data.hero.title}
<h1
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-tight text-transparent"
>
{@html data.hero.title}
</h1>
{/if}
{#if data.hero.subtitle}
<h2 class="mx-auto mt-4 max-w-xl sm:text-xl">
{@html data.hero.subtitle}
</h2>
{/if}
</div>
<div class="mx-auto max-w-3xl text-center">
{#if data.hero.image}
<GMI src={data.hero.image} classList="m-auto mb-2 h-14 w-14" alt="" srcset="" />
{/if}
{#if data.hero.title}
<h1 class="h1 text-5xl font-extrabold leading-tight">
{@html data.hero.title}
</h1>
{/if}
{#if data.hero.subtitle}
<h2 class="mx-auto mt-4 max-w-xl sm:text-xl">
{@html data.hero.subtitle}
</h2>
{/if}
</div>
</section>
{/if}
{#if !!data.systemDataMessage}
<div
class="section-actions status-color-banner relative mx-auto mb-10 flex w-full max-w-[655px] flex-1 flex-col items-center justify-center"
>
<div class="z-20 flex w-full flex-wrap gap-x-2 rounded-md border bg-card px-3 py-2 font-medium shadow-sm">
{l(data.lang, data.systemDataMessage.text)}
</div>
<div
class="status-color-bar absolute -bottom-1.5 left-[2%] z-10 mx-auto flex h-4 w-[98%] -translate-x-[1%] overflow-hidden rounded-md rounded-tl-none rounded-tr-none shadow-sm"
>
<div class="bg-api-up h-4" style="width: {data.systemDataMessage.upsPercentage}%;"></div>
<div class="bg-api-degraded h-4" style="width: {data.systemDataMessage.degradedPercentage}%;"></div>
<div class="bg-api-down h-4" style="width: {data.systemDataMessage.downsPercentage}%;"></div>
</div>
</div>
{/if}
<div class="section-actions mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-center justify-center">
<div class="flex w-full flex-wrap rounded-md border shadow-sm">
{#if data.canSendEmail}
<Button
variant="ghost"
class="dash-after relative h-10 rounded-md text-sm font-medium"
on:click={openSubscribeMenu}
>
{l(data.lang, "Subscribe to Updates")}
</Button>
{/if}
<Button
rel="external"
href="{base}/view/events?incident_type=MAINTENANCE"
variant="ghost"
class="h-10 rounded-md text-sm font-medium"
>
{l(data.lang, "Upcoming Maintenances")}
</Button>
</div>
</div>
{#if data.pageType != "home"}
<section class="section-back mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<Button
@@ -94,10 +147,13 @@
class="bounce-left h-8 justify-start pl-1.5"
on:click={() => {
if (data.pageType == "category") {
analyticsEvent("category_back_button_click");
return window.history.back();
} else if (data.pageType == "monitor") {
analyticsEvent("monitor_back_button_click");
return (window.location.href = `${base}/`);
} else if (data.pageType == "group") {
analyticsEvent("group_back_button_click");
return (window.location.href = `${base}/`);
}
}}
@@ -109,7 +165,7 @@
{/if}
{#if data.allRecentIncidents.length + data.allRecentMaintenances.length > 0}
<section
class="section-events mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
class="section-events mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center"
id=""
>
<div class="mb-2 grid w-full grid-cols-2 gap-4">
@@ -256,21 +312,21 @@
transition:scale={{ duration: 100 }}
use:hotKeyAction={{
code: "Escape",
cb: () => (shareMenusToggle = false)
cb: () => closeShareMenu()
}}
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
>
<div
use:clickOutsideAction
on:clickoutside={() => {
shareMenusToggle = false;
closeShareMenu();
}}
class="absolute left-1/2 top-1/2 h-fit w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg md:w-[568px]"
>
<Button
variant="ghost"
on:click={() => {
shareMenusToggle = false;
closeShareMenu();
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
@@ -282,3 +338,35 @@
</div>
</div>
{/if}
{#if subscribeMenuToggle}
<div
transition:scale={{ duration: 100 }}
use:hotKeyAction={{
code: "Escape",
cb: () => closeSubscribeMenu()
}}
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
>
<div
use:clickOutsideAction
on:clickoutside={() => {
closeSubscribeMenu();
}}
class="absolute left-1/2 top-1/2 h-fit w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg md:w-[568px]"
>
<Button
variant="ghost"
on:click={() => {
closeSubscribeMenu();
}}
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">
<SubscribeMenu lang={data.lang} selectedLang={data.selectedLang} />
</div>
</div>
</div>
{/if}
@@ -0,0 +1,65 @@
// @ts-nocheck
import gtm from "$lib/snippets/capture/google-tag-manager.js?raw";
import mx from "$lib/snippets/capture/mixpanel.js?raw";
import am from "$lib/snippets/capture/amplitude.js?raw";
import pl from "$lib/snippets/capture/plausible.js?raw";
import mscl from "$lib/snippets/capture/clarity.js?raw";
import um from "$lib/snippets/capture/umami.js?raw";
import { GetAllAnalyticsData } from "$lib/server/controllers/controller.js";
export async function GET({ params, setHeaders, url }) {
let analyticsData = await GetAllAnalyticsData();
analyticsData = analyticsData.filter((item) => item.value.isEnabled == true);
if (analyticsData.length == 0) {
return new Response("//no data", {
headers: {
"Content-Type": "application/javascript",
},
});
}
//reduce to key value
analyticsData = analyticsData.reduce((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {});
let captureScript = "";
if (!!analyticsData["analytics.googleTagManager"]) {
let id = analyticsData["analytics.googleTagManager"].requirements["Measurement ID"];
captureScript = captureScript + ";\n" + gtm.replaceAll("{{id}}", id);
}
if (!!analyticsData["analytics.mixpanel"]) {
let projectToken = analyticsData["analytics.mixpanel"].requirements["Project Token"];
captureScript = captureScript + ";\n" + mx.replaceAll("{{YOUR_PROJECT_TOKEN}}", projectToken);
}
if (!!analyticsData["analytics.amplitude"]) {
let api_key = analyticsData["analytics.amplitude"].requirements["Amplitude API Key"];
captureScript = captureScript + ";\n" + am.replaceAll("{{api_key}}", api_key);
}
if (!!analyticsData["analytics.plausible"]) {
let domain = analyticsData["analytics.plausible"].requirements["Domain"];
let api = analyticsData["analytics.plausible"].requirements["API"];
let script_src = analyticsData["analytics.plausible"].requirements["Script Source"];
captureScript =
captureScript +
";\n" +
pl.replaceAll("{{domain}}", domain).replaceAll("{{api}}", api).replaceAll("{{script_src}}", script_src);
}
if (!!analyticsData["analytics.clarity"]) {
let project_id = analyticsData["analytics.clarity"].requirements["Project ID"];
captureScript = captureScript + ";\n" + mscl.replaceAll("{{project_id}}", project_id);
}
if (!!analyticsData["analytics.umami"]) {
let website_id = analyticsData["analytics.umami"].requirements["Website ID"];
captureScript = captureScript + ";\n" + um.replaceAll("{{website_id}}", website_id);
}
return new Response(captureScript, {
headers: {
"Content-Type": "application/javascript",
},
});
}
+57
View File
@@ -0,0 +1,57 @@
// @ts-nocheck
// @ts-ignore
import { json } from "@sveltejs/kit";
import { auth } from "$lib/server/webhook";
import { CreateMonitor, GetMonitors } from "$lib/server/controllers/controller.js";
export async function GET({ request, url }) {
const authError = await auth(request);
if (authError !== null) {
return json({ error: authError.message }, { status: 401 });
}
let data = {};
const query = url.searchParams;
const tag = query.get("tag");
if (tag) {
data = {
tag: tag,
};
}
let resp;
try {
resp = await GetMonitors(data);
} catch (error) {
resp = { error: error.message };
return json(resp, { status: 500 });
}
return json(resp, { status: 200 });
}
export async function POST({ request }) {
const payload = await request.json();
const authError = await auth(request);
if (authError !== null) {
return json({ error: authError.message }, { status: 401 });
}
if (payload.id) {
return json({ error: 'monitor id must be empty or 0' }, { status: 400 });
}
try {
await CreateMonitor(payload);
} catch (error) {
return json({ error: error.message }, { status: 500 });
}
let resp;
try {
resp = await GetMonitors({ tag: payload.tag });
} catch (error) {
resp = { error: error.message };
return json(resp, { status: 500 });
}
return json(resp[0], { status: 201 });
}
@@ -0,0 +1,105 @@
// @ts-nocheck
// @ts-ignore
import { json } from "@sveltejs/kit";
import { auth } from "$lib/server/webhook";
import { DeleteMonitorCompletelyUsingTag, GetMonitors, UpdateMonitor } from "$lib/server/controllers/controller";
export async function GET({ request, params }) {
const authError = await auth(request);
if (authError !== null) {
return json({ error: authError.message }, { status: 401 });
}
const id = params.monitor_id;
if (!!!id) {
return json({ error: "id missing" }, { status: 400 });
}
let resp;
try {
resp = await GetMonitors({ id: id });
} catch (error) {
resp = { error: error.message };
return json(resp, { status: 500 });
}
if (resp.length === 0) {
return json({ error: "monitor does not exist" }, { status: 404 });
}
return json(resp[0], { status: 200 });
}
export async function PUT({ request, params }) {
const payload = await request.json();
const authError = await auth(request);
if (authError !== null) {
return json({ error: authError.message }, { status: 401 });
}
const id = params.monitor_id;
if (!!!id) {
return json({ error: "id missing" }, { status: 400 });
}
if (`${payload.id}` !== `${id}`) {
return json({ error: `id mismatch: payload: ${payload.id}, url: ${id}` }, { status: 400 });
}
let resp;
try {
resp = await GetMonitors({ id: id });
} catch (error) {
resp = { error: error.message };
return json(resp, { status: 500 });
}
if (resp.length === 0) {
return json({ error: "monitor does not exist" }, { status: 404 });
}
try {
await UpdateMonitor(payload);
} catch (error) {
return json({ error: error.message }, { status: 500 });
}
try {
resp = await GetMonitors({ tag: payload.tag });
} catch (error) {
resp = { error: error.message };
return json(resp, { status: 500 });
}
return json(resp[0], { status: 200 });
}
export async function DELETE({ request, params }) {
const authError = await auth(request);
if (authError !== null) {
return json({ error: authError.message }, { status: 401 });
}
const id = params.monitor_id;
if (!!!id) {
return json({ error: "id missing" }, { status: 400 });
}
let resp;
try {
resp = await GetMonitors({ id: id });
} catch (error) {
resp = { error: error.message };
return json(resp, { status: 500 });
}
if (resp.length === 0) {
return json({ error: "monitor does not exist" }, { status: 404 });
}
try {
resp = await DeleteMonitorCompletelyUsingTag(resp[0].tag);
} catch (error) {
resp = { error: error.message };
return json(resp, { status: 500 });
}
return json({}, { status: 200 });
}
@@ -0,0 +1,240 @@
// @ts-nocheck
// @ts-ignore
import { json } from "@sveltejs/kit";
import emailCodeTemplate from "$lib/server/templates/email_code.html?raw";
import { GetNowTimestampUTC, GenerateRandomNumber, ValidateEmail } from "$lib/server/tool.js";
import {
GetSiteLogoURL,
SendEmailWithTemplate,
GetAllSiteData,
CreateNewSubscriber,
GetSubscriberByEmailAndType,
CreateNewSubscription,
UpdateSubscriberMeta,
RemoveAllSubscriptions,
GetSubscriptionsBySubscriberID,
UpdateSubscriberStatus,
DeleteSubscriberByID,
GenerateTokenWithExpiry,
GetSubscriberByID,
VerifyToken,
} from "$lib/server/controllers/controller.js";
import { base } from "$app/paths";
async function subscribe(data) {
const decoded = await VerifyToken(data.token);
if (!!!decoded) {
return json({ message: "Invalid token" }, { status: 400 });
}
let email = decoded.email;
if (!!!email) {
return json({ message: "Invalid email" }, { status: 400 });
}
//subscriber_id
let subscriber_id = decoded.subscriber_id;
if (!!!subscriber_id) {
return json({ message: "Invalid subscriber_id" }, { status: 400 });
}
//get subscriber by id
let existingSubscriber = await GetSubscriberByID(subscriber_id);
if (!!!existingSubscriber) {
return json({ message: "No active subscription found" }, { status: 400 });
}
let monitors = data.monitors;
if (data.allMonitors) {
monitors = ["_"];
}
if (!!!monitors || monitors.length == 0) {
//remove all subscriptions
await RemoveAllSubscriptions(existingSubscriber.id);
return json({ message: "Unsubscribed successfully" }, { status: 200 });
}
try {
await CreateNewSubscription(subscriber_id, monitors);
} catch (e) {
console.error("Error in CreateNewSubscription: ", e);
return json({ message: "Error in CreateNewSubscription" }, { status: 500 });
}
return json({ message: "Subscription created successfully" }, { status: 200 });
}
async function unsubscribe(token) {
//decode token
const decoded = await VerifyToken(token);
if (!!!decoded) {
return json({ message: "Invalid token" }, { status: 400 });
}
let email = decoded.email;
if (!!!email) {
return json({ message: "Invalid email" }, { status: 400 });
}
//subscriber_id
let subscriber_id = decoded.subscriber_id;
if (!!!subscriber_id) {
return json({ message: "Invalid subscriber_id" }, { status: 400 });
}
//get subscriber by id
let existingSubscriber = await GetSubscriberByID(subscriber_id);
if (!!!existingSubscriber) {
return json({ message: "No active subscription found" }, { status: 400 });
}
await RemoveAllSubscriptions(existingSubscriber.id);
return json({ message: "Unsubscribed successfully" }, { status: 200 });
}
async function fetchData(token) {
//decode token
const decoded = await VerifyToken(token);
if (!!!decoded) {
return json({ message: "Invalid token" }, { status: 400 });
}
let email = decoded.email;
if (!!!email) {
return json({ message: "Invalid email" }, { status: 400 });
}
//subscriber_id
let subscriber_id = decoded.subscriber_id;
if (!!!subscriber_id) {
return json({ message: "Invalid subscriber_id" }, { status: 400 });
}
//get subscriber by id
let existingSubscriber = await GetSubscriberByID(subscriber_id);
if (!!!existingSubscriber) {
return json({ message: "No active subscription found" }, { status: 400 });
}
let monitors = [];
let allSubscriptions = await GetSubscriptionsBySubscriberID(subscriber_id);
if (!!allSubscriptions) {
monitors = allSubscriptions.map((item) => {
return item.subscriptions_monitors;
});
}
return json({ monitors, email: existingSubscriber.subscriber_send }, { status: 200 });
}
async function login(email) {
//if the email does not exists then create the user, return 200
//if the email exists then return 200
//validate email
if (!ValidateEmail(email)) {
return json({ message: "Invalid email address" }, { status: 400 });
}
let subscriberMeta = {
email_code: GenerateRandomNumber(6),
};
const existingUser = await GetSubscriberByEmailAndType(email, "email");
if (!!!existingUser) {
let newSubscriberData = {
subscriber_send: email,
subscriber_type: "email",
subscriber_status: "PENDING",
subscriber_meta: JSON.stringify(subscriberMeta),
};
await CreateNewSubscriber(newSubscriberData);
} else {
await UpdateSubscriberMeta(existingUser.id, JSON.stringify(subscriberMeta));
}
//send email with code
let siteData = await GetAllSiteData();
let emailData = {
brand_name: siteData.siteName,
logo_url: await GetSiteLogoURL(siteData.siteURL, siteData.logo, base),
email_code: subscriberMeta.email_code,
action: "login",
};
let resp = await SendEmailWithTemplate(
emailCodeTemplate,
emailData,
email,
`[Important] Verify code to login to ${emailData.brand_name}`,
`Your verification code is: ${emailData.email_code}`,
);
if (resp.error) {
console.error("Error in SendEmailWithTemplate: ", resp.error);
return json({ message: "Error sending email" }, { status: 500 });
}
return json({ newUser: false, token: await GenerateTokenWithExpiry({ email }, "1m") }, { status: 200 });
}
//verify token from jwt
async function verifyToken(token, code) {
const decoded = await VerifyToken(token);
if (!!!decoded) {
return json({ message: "Invalid token" }, { status: 400 });
}
let email = decoded.email;
if (!!!email) {
return json({ message: "Invalid email" }, { status: 400 });
}
let existingSubscriber = await GetSubscriberByEmailAndType(email, "email");
if (!!!existingSubscriber) {
return json({ message: "Invalid User" }, { status: 400 });
}
let subscriberMeta = JSON.parse(existingSubscriber.subscriber_meta);
let storeCode = subscriberMeta.email_code;
if (!!!storeCode) {
return json({ message: "Invalid Data" }, { status: 400 });
}
if (storeCode != code) {
return json({ message: "Invalid Code" }, { status: 400 });
}
//update the subscriber status to active
await UpdateSubscriberStatus(existingSubscriber.id, "ACTIVE");
await UpdateSubscriberMeta(existingSubscriber.id, JSON.stringify({}));
//create a new token
return json(
{
token: await GenerateTokenWithExpiry(
{
email: email,
subscriber_id: existingSubscriber.id,
},
"1y",
),
},
{ status: 200 },
);
}
export async function POST({ request, params }) {
const data = await request.json();
const action = params.action;
const type = data.type;
try {
if (action == "login") {
return await login(data.userEmail);
} else if (action == "verify") {
return await verifyToken(data.token, data.code);
} else if (action == "subscribe") {
return await subscribe(data);
} else if (action == "unsubscribe") {
return await unsubscribe(data.token);
} else if (action == "fetch") {
return await fetchData(data.token);
}
} catch (e) {
return json({ message: "Error in creating email subscription" }, { status: 500 });
}
return json({ message: "Invalid action" }, { status: 400 });
}
+20 -14
View File
@@ -29,14 +29,28 @@ export async function POST({ request }) {
};
}
let dayData = await db.getMonitoringData(monitor.tag, payload.startTs, payload.endTs);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
dayData = InterpolateData(dayData, payload.startTs, anchorStatus, payload.endTs);
let siteData = await GetAllSiteData();
let ups = 0;
let downs = 0;
let degradeds = 0;
let dayData = await db.getMonitoringData(monitor.tag, start, end);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
dayData = InterpolateData(dayData, start, anchorStatus, end);
let aggregatedData = dayData.reduce(
(acc, row) => {
acc[row.status] = (acc[row.status] || 0) + 1;
return acc;
},
{
UP: 0,
DOWN: 0,
DEGRADED: 0,
},
);
let ups = Number(aggregatedData.UP);
let downs = Number(aggregatedData.DOWN);
let degradeds = Number(aggregatedData.DEGRADED);
for (let i = 0; i < dayData.length; i++) {
let row = dayData[i];
@@ -54,14 +68,6 @@ export async function POST({ request }) {
_0Day[timestamp].status = status;
_0Day[timestamp].cssClass = StatusObj[status];
}
if (status == "UP") {
ups++;
} else if (status == "DOWN") {
downs++;
} else if (status == "DEGRADED") {
degradeds++;
}
}
let total = ups + downs + degradeds;
@@ -1,11 +1,6 @@
// @ts-nocheck
import {
GetMonitors,
GetIncidentsOpenHome,
GetIncidentsPage,
GetLocaleFromCookie,
} from "$lib/server/controllers/controller.js";
import { GetMonitors, GetIncidentsPage, GetLocaleFromCookie } from "$lib/server/controllers/controller.js";
import { BeginningOfDay } from "$lib/server/tool.js";
import db from "$lib/server/db/db.js";
import { format, addDays, subDays, parse, getUnixTime, startOfMonth, endOfMonth } from "date-fns";
@@ -2,6 +2,7 @@
import { Badge } from "$lib/components/ui/badge";
import Incident from "$lib/components/IncidentNew.svelte";
import { Button } from "$lib/components/ui/button";
import { analyticsEvent } from "$lib/boringOne";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import ArrowLeft from "lucide-svelte/icons/arrow-left";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
@@ -17,10 +18,6 @@
let incidentSmartDates = {};
function tsToDate(mnt) {
return mnt.unix();
}
incidents.forEach((incident) => {
let startTime = incident.start_date_time;
@@ -76,6 +73,7 @@
variant="outline"
class="bounce-left h-8 justify-start pl-1.5"
on:click={() => {
analyticsEvent("incident_back_button_click");
return (window.location.href = `${base}/`);
}}
>
@@ -109,7 +107,12 @@
<h1 class=" text-xl font-semibold leading-tight">
{l(data.lang, "No Incident in %date", {
date: f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)
date: f(
new Date(data.midnightMonthStartUTCTimestamp * 1000),
"MMMM, yyyy",
selectedLang,
$page.data.localTz
)
})}
</h1>
</div>
@@ -0,0 +1,19 @@
// @ts-nocheck
import { GetIncidentsPaginated } from "$lib/server/controllers/controller.js";
import { GetMinuteStartNowTimestampUTC } from "$lib/server/tool.js";
export async function load({ parent, url, params, cookies }) {
let page = url.searchParams.get("page") || 1;
let limit = url.searchParams.get("limit") || 100;
let direction = url.searchParams.get("direction") || "after";
let filter = {
start: url.searchParams.get("start") || GetMinuteStartNowTimestampUTC(),
status: url.searchParams.get("status") || "OPEN",
};
if (!!url.searchParams.get("incident_type")) {
filter.incident_type = url.searchParams.get("incident_type");
}
let incidents = await GetIncidentsPaginated(page, limit, filter, direction);
return { incidents };
}
@@ -0,0 +1,94 @@
<script>
import { l, f } from "$lib/i18n/client";
import { startOfDay, addDays, subDays, getUnixTime, parse } from "date-fns";
import { page } from "$app/stores";
import { Button } from "$lib/components/ui/button";
import Incident from "$lib/components/IncidentNew.svelte";
import { analyticsEvent } from "$lib/boringOne";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { base } from "$app/paths";
export let data;
let incidents = data.incidents;
let incidentSmartDates = {};
incidents.forEach((incident) => {
let startTime = incident.start_date_time;
const today = getUnixTime(startOfDay(new Date(startTime * 1000)));
if (!incidentSmartDates[today]) {
incidentSmartDates[today] = [];
}
incidentSmartDates[today].push(incident);
});
let sortedIncidentSmartDates = Object.keys(incidentSmartDates).sort((a, b) => a - b);
// Additional logic can be added here
</script>
<svelte:head>
<title>{l(data.lang, "Upcoming Maintenances")}</title>
<meta name="description" content={l(data.lang, "Upcoming Maintenances")} />
<link rel="canonical" href={`${base}/view/events`} />
</svelte:head>
<div class="mt-10"></div>
<section class="section-back mx-auto flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<div class=" mx-auto min-w-full max-w-[655px] rounded-md px-4 py-12 lg:flex lg:items-center">
<div class=" mx-auto text-center">
<h1 class=" text-3xl font-extrabold leading-tight">
{l(data.lang, "Upcoming Maintenances")}
</h1>
</div>
</div>
<Button
variant="outline"
class="bounce-left h-8 justify-start pl-1.5"
on:click={() => {
analyticsEvent("incident_back_button_click");
return (window.location.href = `${base}/`);
}}
>
<ChevronLeft class="arrow mr-1 h-5 w-5" />
{l(data.lang, "Back")}
</Button>
</section>
<section class="section-view-event mx-auto mb-8 flex max-w-[820px] flex-1 flex-col items-start justify-center">
<div
class="section-month-events mx-auto mb-2 mt-4 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
>
{#if sortedIncidentSmartDates.length == 0}
<div class="mx-auto w-full rounded-md bg-clip-text p-12 text-center">
<div class="mx-auto mb-4 h-[32px] w-[32px] text-primary">
<picture>
<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="32" height="32" />
</picture>
</div>
<h1 class=" text-xl font-semibold leading-tight">
{l(data.lang, "There are no upcoming maintenances")}
</h1>
</div>
{/if}
{#each sortedIncidentSmartDates as date}
<div class="mb-4 grid w-full grid-cols-2 gap-x-4 rounded-md border bg-card">
<div class="text-md col-span-2 border-b p-2 px-4 font-medium">
{f(new Date(date * 1000), "EEEE, MMMM do", data.selectedLang, $page.data.localTz)}
</div>
{#if incidentSmartDates[date].length === 0}
<div class="col-span-2 p-2 px-4 text-sm font-medium text-muted-foreground">
{l(data.lang, "No Incidents")}
</div>
{/if}
{#each incidentSmartDates[date] as incident, index}
<div class="newincidents col-span-2">
<Incident {incident} lang={data.lang} index="incident-{index}" selectedLang={$page.data.selectedLang} />
</div>
{/each}
</div>
{/each}
</div>
</section>
@@ -0,0 +1,53 @@
// @ts-nocheck
import { GetIncidentsByIDS, GetIncidentMonitors, GetMonitorsByTag } from "$lib/server/controllers/controller.js";
export async function load({ parent, url, params, cookies }) {
const parentData = await parent();
const siteData = parentData.site;
let typeID = params.type_ID;
if (!!!typeID) {
return {
error: "Unknown event id",
title: "Unknown event id",
};
}
//check array length 2
if (typeID.split("-").length !== 2) {
return {
error: "Unknown event id",
title: "Unknown event id",
};
}
let eventID = typeID.split("-")[1];
let incident = await GetIncidentsByIDS([eventID]);
if (incident.length === 0) {
return {
error: "Unknown event id",
};
}
incident = incident[0];
let monitors = await GetIncidentMonitors(eventID);
for (let i = 0; i < monitors.length; i++) {
const element = monitors[i];
const tag = element.monitor_tag;
const monitor = await GetMonitorsByTag(tag);
if (!!monitor) {
monitors[i] = {
tag: monitor.tag,
name: monitor.name,
description: monitor.description,
image: monitor.image,
impact_type: element.monitor_impact,
id: monitor.id,
};
}
}
incident.monitors = monitors;
return { incident, title: incident.title };
}
@@ -0,0 +1,48 @@
<script>
import Incident from "$lib/components/IncidentNew.svelte";
import { l, f } from "$lib/i18n/client";
import { base } from "$app/paths";
import { page } from "$app/stores";
import { Button } from "$lib/components/ui/button";
import { analyticsEvent } from "$lib/boringOne";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
export let data;
let incident = data.incident;
</script>
<svelte:head>
<title>
{data.title}
</title>
<meta name="description" content={data.title} />
</svelte:head>
<section class="section-view-event-id mx-auto mb-8 mt-20 flex max-w-[820px] flex-1 flex-col items-start justify-center">
<Button
variant="outline"
class="bounce-left mb-2 h-8 justify-start pl-1.5"
on:click={() => {
analyticsEvent("incident_back_button_click");
return (window.location.href = `${base}/`);
}}
>
<ChevronLeft class="arrow mr-1 h-5 w-5" />
{l(data.lang, "Back")}
</Button>
{#if !!incident}
<div class=" w-full rounded-md border bg-card">
<Incident
{incident}
lang={$page.data.lang}
index={`incident-0`}
selectedLang={$page.data.selectedLang}
allowCollapse={false}
/>
</div>
{/if}
{#if !!data.error}
<div class="mt-20 w-full rounded-md border bg-background">
<p class="py-3 text-center text-lg font-medium text-destructive">{data.error}</p>
</div>
{/if}
</section>
@@ -5,13 +5,7 @@ import db from "$lib/server/db/db.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,
} from "$lib/server/controllers/controller.js";
import { HashPassword, GenerateToken, VerifyToken, GetSMTPFromENV } from "$lib/server/controllers/controller.js";
export async function POST({ request, cookies }) {
//read form post data email and password
@@ -2,23 +2,7 @@
import { json, redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import db from "$lib/server/db/db.js";
import {
HashPassword,
GenerateSalt,
GenerateToken,
CookieConfig,
ValidatePassword,
} from "$lib/server/controllers/controller.js";
//function to validate a strong password
/**
* Validates a password to ensure it meets the following criteria:
* - Contains at least one digit.
* - Contains at least one lowercase letter.
* - Contains at least one uppercase letter.
* - Contains at least one letter (either lowercase or uppercase).
* - Has a minimum length of 8 characters.
*/
import { HashPassword, GenerateToken, CookieConfig, ValidatePassword } from "$lib/server/controllers/controller.js";
export async function POST({ request, cookies }) {
//read form post data email and password
@@ -21,7 +21,6 @@ export async function load({ params, route, url, cookies, request }) {
}
userDB = isLoggedIn.user;
return {
siteData,
KENER_SECRET_KEY: !!process.env.KENER_SECRET_KEY ? MaskString(process.env.KENER_SECRET_KEY) : "",
@@ -64,6 +64,11 @@
url: `${base}/manage/app/users`,
id: "/(manage)/manage/(app)/app/users"
},
{
name: "Subscription",
url: `${base}/manage/app/subscriptions`,
id: "/(manage)/manage/(app)/app/subscriptions"
},
{
name: "Profile",
url: `${base}/manage/app/profile`,
@@ -96,7 +101,7 @@
<meta name="robots" content="noindex" />
<link rel="icon" href="{base}/logo96.png" />
</svelte:head>
<header class="sticky inset-x-0 top-0 z-50 mx-auto mt-4 flex max-w-6xl px-8">
<header class="fixed inset-x-0 top-0 z-50 mx-auto mt-2 flex px-2">
<div class=" flex w-full justify-between rounded-lg border bg-card px-5 py-4">
<div class="mt-2 flex gap-x-1.5">
<img src="{base}/logo.png" alt="Kener" class="inline h-6 w-6" />
@@ -133,25 +138,32 @@
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
<Button variant="secondary" class="ml-4" href="{base}/" rel="external">
<Play class="mr-2 h-4 w-4" />
<span>Preview</span>
</Button>
<div class="ml-3">
<a
type="button"
href="{base}/"
rel="external"
class=" flex gap-x-1 rounded-lg bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 px-3.5 py-2.5 text-center text-sm font-medium text-white hover:bg-gradient-to-br focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800"
>
<Play class="mr-1 mt-0.5 inline-block h-4 w-4" />
<span> Preview </span>
</a>
</div>
</div>
</div>
</header>
<main class="manage">
<div class="container my-4 max-w-6xl">
<nav class="flex justify-start gap-x-2 overflow-x-auto">
<div class="fixed left-2 top-[5.2rem] my-2 h-[calc(100vh-6.5rem)] w-56 rounded-lg border bg-card px-2 py-2 shadow-md">
<nav class="flex flex-col justify-start gap-x-2 overflow-x-auto">
{#each nav as item}
<Button variant={item.id.includes(activeTab) ? "secondary" : "ghost"} href={item.url} class="flex-1r"
<Button variant={item.id.includes(activeTab) ? "secondary" : "ghost"} href={item.url} class="justify-start "
>{item.name}</Button
>
{/each}
</nav>
</div>
<div class="container max-w-6xl pt-0">
<div class=" mx-auto my-2 w-full max-w-7xl pl-60 pr-2 pt-24">
<slot></slot>
</div>
</main>
@@ -9,11 +9,14 @@ import { format } from "date-fns";
import {
CreateUpdateMonitor,
UpdateMonitoringData,
InsertKeyValue,
GetMonitors,
CreateUpdateTrigger,
GetAllTriggers,
UpdateTriggerData,
GetSubscriptionTriggerByEmail,
CreateSubscriptionTrigger,
GetAllAlertsPaginated,
GetIncidentByIDDashboard,
GetMonitorsParsed,
@@ -43,9 +46,12 @@ import {
GetUserByID,
GetUserByEmail,
ManualUpdateUserData,
DeleteMonitorCompletelyUsingTag,
GetSubscribersPaginated,
UpdateSubscriptionStatus,
} from "$lib/server/controllers/controller.js";
import { INVITE_VERIFY_EMAIL } from "$lib/server/constants.js";
import { INVITE_VERIFY_EMAIL, MANUAL } from "$lib/server/constants.js";
import { GetNowTimestampUTC } from "$lib/server/tool.js";
function AdminCan(role) {
@@ -146,8 +152,14 @@ export async function POST({ request, cookies }) {
} else if (action == "storeMonitorData") {
AdminEditorCan(userDB.role);
resp = await CreateUpdateMonitor(data);
} else if (action == "updateMonitoringData") {
data.type = MANUAL;
resp = await UpdateMonitoringData(data);
} else if (action == "getMonitors") {
resp = await GetMonitors(data);
} else if (action == "deleteMonitor") {
AdminEditorCan(userDB.role);
resp = await DeleteMonitorCompletelyUsingTag(data.tag);
} else if (action == "createUpdateTrigger") {
AdminEditorCan(userDB.role);
resp = await CreateUpdateTrigger(data);
@@ -230,8 +242,17 @@ export async function POST({ request, cookies }) {
}
const serviceClient = new Service(monitor);
resp = await serviceClient.execute();
} else if (action == "createSubscriptionTrigger") {
resp = await CreateSubscriptionTrigger(data);
} else if (action == "getSubscriptionTrigger") {
resp = await GetSubscriptionTriggerByEmail();
} else if (action == "getSubscribers") {
resp = await GetSubscribersPaginated(data);
} else if (action == "updateSubscriptionStatus") {
resp = await UpdateSubscriptionStatus(data.id, data.status);
}
} catch (error) {
console.log(error);
resp = { error: error.message };
return json(resp, { status: 500 });
}
@@ -27,6 +27,12 @@
import MessageSquarePlus from "lucide-svelte/icons/message-square-plus";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import Trash from "lucide-svelte/icons/trash";
import CodeMirror from "svelte-codemirror-editor";
import { markdown } from "@codemirror/lang-markdown";
import { marked } from "marked";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
import * as Select from "$lib/components/ui/select";
export let data;
let status = "OPEN";
@@ -414,6 +420,7 @@
function setCommentEdit(c) {
newComment = { ...c };
newComment.commented_at = new Date(Number(c.commented_at) * 1000);
scrollToElementInsideContainer("incident-modal", "edit-comment-box");
}
$: {
//broadcast a custom event named blockScroll
@@ -429,6 +436,17 @@
showModal = false;
window.location.hash = "";
}
function scrollToElementInsideContainer(containerId, elementId) {
const container = document.getElementById(containerId);
const element = document.getElementById(elementId);
if (container && element) {
container.scrollTo({
top: element.offsetTop - container.offsetTop,
behavior: "smooth"
});
}
}
</script>
<div class="min-h-[70vh]">
@@ -674,7 +692,7 @@
on:clickoutside={closeModal}
class="absolute right-0 top-0 h-screen w-[800px] border-l bg-background p-4 shadow-xl"
>
<div class="mt-0 w-full overflow-y-auto p-3" style="height: 100vh;">
<div class="mt-0 w-full overflow-y-auto p-3" style="height: 100vh;" id="incident-modal">
<div class="rounded-md border p-4">
<div>
{#if newIncident.id}
@@ -735,12 +753,24 @@
<span class="capitalize">{newIncident.incident_type}</span>
Summary
<span class="text-red-500">*</span>
(supports Markdown)
</Label>
<Input
class="mt-2"
bind:value={newIncident.firstComment}
placeholder="We are facing degraded service in all servers"
/>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={newIncident.firstComment}
lang={markdown()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "320px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "var(--radius)"
}
}}
/>
</div>
</div>
</div>
{/if}
@@ -860,11 +890,23 @@
/>
</div>
<div class="mt-4">
<textarea
bind:value={newComment.comment}
class="h-24 w-full rounded-sm border p-2 text-sm"
placeholder="There is an outage in all server. Our best minds are on it. We will update you soon."
></textarea>
<p class="mb-1 text-right text-xs font-medium">Supports Markdown</p>
<div class="overflow-hidden rounded-md" id="edit-comment-box">
<CodeMirror
bind:value={newComment.comment}
lang={markdown()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "220px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "var(--radius)"
}
}}
/>
</div>
</div>
<div class="flex justify-end gap-x-2">
@@ -913,7 +955,14 @@
{#each comments as comment}
<div class="flex items-center justify-between gap-2 border-b py-2">
<div class="w-full rounded px-2 py-2 {newComment.id == comment.id ? 'bg-input' : ''}">
<p class="mb-2 text-xs font-medium">{@html comment.comment}</p>
<div class="mb-2 max-h-[400px] overflow-y-auto rounded-md border bg-card p-2">
<div
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
{@html marked.parse(comment.comment)}
</div>
</div>
<div class="flex w-full justify-between gap-x-2">
<div class="text-xs font-semibold text-muted-foreground">
{#if currentIncident.incident_type == "INCIDENT"}
@@ -0,0 +1,7 @@
import seedSiteData from "$lib/server/db/seedSiteData.js";
export async function load({ parent }) {
return {
defaultFooterHTML: seedSiteData.footerHTML,
};
}
@@ -4,7 +4,7 @@ import { IsEmailSetup, CheckInvitationExists } from "$lib/server/controllers/con
import { INVITE_VERIFY_EMAIL } from "$lib/server/constants.js";
export async function load({ parent }) {
let canSendEmail = await IsEmailSetup();
let canSendEmail = IsEmailSetup();
const parentData = await parent();
return {
canSendEmail,
@@ -0,0 +1,8 @@
import { IsEmailSetup } from "$lib/server/controllers/controller.js";
export async function load({ parent }) {
let canSendEmail = IsEmailSetup();
return {
canSendEmail,
};
}
@@ -0,0 +1,391 @@
<script>
import { onMount } from "svelte";
import * as Card from "$lib/components/ui/card";
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import GMI from "$lib/components/gmi.svelte";
import * as Select from "$lib/components/ui/select";
import { Label } from "$lib/components/ui/label";
import ExternalLink from "lucide-svelte/icons/external-link";
import Loader from "lucide-svelte/icons/loader";
export let data;
let canSendEmail = data.canSendEmail;
let page = 1;
let limit = 25; // Set a default limit value
let total = 0; // Initialize total to 0
let totalPages = 0; // Initialize totalPages
let subscriptionTrigger = {
subscription_trigger_id: null,
subscription_trigger_status: "INACTIVE",
subscription_trigger_type: "email",
config: {
createIncident: false,
updateIncident: false,
insertIncidentMonitor: false,
updateIncidentComment: false,
insertIncidentComment: false
}
};
let selectedTriggerId = null;
let subscriberListLoading = false;
let pageLoading = true;
let subscribers = [];
async function loadSubscriptionTrigger() {
try {
const res = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "getSubscriptionTrigger" })
});
let data = await res.json();
if (data) {
if (!!data.config) {
data.config = JSON.parse(data.config);
}
subscriptionTrigger = data;
}
} catch (error) {
console.log("Error fetching subscription trigger: " + error);
}
}
async function saveSubscriptionTrigger() {
try {
const res = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "createSubscriptionTrigger",
data: {
subscription_trigger_type: "email",
subscription_trigger_status: subscriptionTrigger.subscription_trigger_status,
config: JSON.stringify(subscriptionTrigger.config)
}
})
});
//subscriptionTrigger = await res.json();
} catch (error) {
console.console.log(error);
}
}
async function loadSubscribers() {
subscriberListLoading = true;
try {
const res = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "getSubscribers",
data: {
page: page,
limit: limit
}
})
});
const data = await res.json();
subscribers = data.subscriptions || [];
total = data.total || 0;
totalPages = Math.ceil(total / limit); // Calculate total pages based on total and limit
} catch (error) {
console.error("Error fetching subscribers:", error);
// Optionally show an error message to the user
alert("Error loading subscribers. Please try again.");
subscribers = [];
total = 0;
} finally {
subscriberListLoading = false;
}
}
onMount(async () => {
await loadSubscriptionTrigger();
await loadSubscribers(); // Load subscribers initially
pageLoading = false;
});
function updateSubscriptionStatus(id, status) {
// Call the API to update the subscription status
fetch(base + "/manage/app/api/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "updateSubscriptionStatus",
data: {
id: id,
status: status
}
})
});
}
// Reactive statement to calculate totalPages
</script>
<div class="min-h-[70vh]">
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title class="relative">
Events Subscription
{#if pageLoading}
<Loader size="16" class="absolute right-0 top-0 animate-spin text-gray-500" />
{/if}
</Card.Title>
<Card.Description>Configure and view who have subscribed to updates for your status page.</Card.Description>
</Card.Header>
<Card.Content class="py-2">
{#if !canSendEmail}
<div class="my-2 rounded-sm border border-destructive p-2 text-sm font-medium text-destructive">
You have not configured your email settings yet. Please configure your email settings to send updates to your
subscribers. You can read the documentation by visiting this link
<a
href="https://kener.ing/docs/environment-vars#smtp"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 underline"
>
here
</a>. You will either have to set up SMTP or use Resend.
</div>
{/if}
<div class="mt-5 grid grid-cols-2 gap-y-2">
<!-- Checkbox -->
<div class="border-b pb-3">
<label class="cursor-pointer">
<input
on:change={(e) => {
subscriptionTrigger.config.createIncident = e.target.checked;
saveSubscriptionTrigger(); // Save the subscription trigger after change
}}
class="mr-1"
type="checkbox"
checked={subscriptionTrigger.config.createIncident}
disabled={!canSendEmail}
/>
Event Triggered
</label>
<p class="pl-6 text-xs font-medium text-muted-foreground">
Send notification when a new event like incident or maintenance is created.
</p>
</div>
<div class="border-b pb-3">
<label class="cursor-pointer">
<input
on:change={(e) => {
subscriptionTrigger.config.updateIncident = e.target.checked;
saveSubscriptionTrigger(); // Save the subscription trigger after change
}}
class="mr-1"
type="checkbox"
checked={subscriptionTrigger.config.updateIncident}
disabled={!canSendEmail}
/>
Event Updated
</label>
<p class="pl-6 text-xs font-medium text-muted-foreground">Send notification when an event is updated.</p>
</div>
<div class="border-b pb-3">
<label class="cursor-pointer">
<input
on:change={(e) => {
subscriptionTrigger.config.insertIncidentMonitor = e.target.checked;
saveSubscriptionTrigger(); // Save the subscription trigger after change
}}
class="mr-1"
type="checkbox"
checked={subscriptionTrigger.config.insertIncidentMonitor}
disabled={!canSendEmail}
/>
Update Monitor in Event
</label>
<p class="pl-6 text-xs font-medium text-muted-foreground">
Send notification when a new monitor is added to an event.
</p>
</div>
<div class="border-b pb-3">
<label class="cursor-pointer">
<input
on:change={(e) => {
subscriptionTrigger.config.updateIncidentComment = e.target.checked;
saveSubscriptionTrigger(); // Save the subscription trigger after change
}}
class="mr-1"
type="checkbox"
checked={subscriptionTrigger.config.updateIncidentComment}
disabled={!canSendEmail}
/>
Update Incident Comment
</label>
<p class="pl-6 text-xs font-medium text-muted-foreground">
Send notification when an event comment is updated.
</p>
</div>
<div class=" pb-3">
<label class="cursor-pointer">
<input
on:change={(e) => {
subscriptionTrigger.config.insertIncidentComment = e.target.checked;
saveSubscriptionTrigger(); // Save the subscription trigger after change
}}
class="mr-1"
type="checkbox"
checked={subscriptionTrigger.config.insertIncidentComment}
disabled={!canSendEmail}
/>
Insert Incident Comment
</label>
<p class="pl-6 text-xs font-medium text-muted-foreground">
Send notification when a new comment is added to an event.
</p>
</div>
</div>
</Card.Content>
</Card.Root>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title class="relative">
Site Subscribers
<!-- Temporarily removed loader to debug -->
<!-- {#if subscriberListLoading}
<Loader size="16" class="absolute right-0 top-0 animate-spin text-gray-500" />
{/if} -->
</Card.Title>
<Card.Description>
View and manage subscribers who have signed up for updates on your status page.
</Card.Description>
</Card.Header>
<Card.Content class="py-4">
<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"
>
Email
</th>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>
Subscribed Monitors
</th>
<th
scope="col"
class="px-6 py-3 text-center text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Subscribed At</th
>
<th scope="col" class="flex w-[130px] gap-x-2 px-1 py-3 text-start text-xs font-medium uppercase">
<Select.Root
portal={null}
class="w-full"
onSelectedChange={(e) => {
if (e) {
// Check if e is not null/undefined
page = e.value;
loadSubscribers();
}
}}
selected={{
value: page,
label: "Page " + page
}}
>
<Select.Trigger id="page" class="h-6 w-full px-2 py-1">
<Select.Value placeholder="Page" class="text-xs" />
</Select.Trigger>
<Select.Content class="text-xs">
<Select.Group>
<Select.Label class="text-xs">Pages</Select.Label>
{#if totalPages > 0}
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as pageNum}
<Select.Item value={pageNum} class="text-xs font-medium uppercase"
>Page {pageNum}</Select.Item
>
{/each}
{:else}
<Select.Item value={1} disabled class="text-xs font-medium uppercase">Page 1</Select.Item>
{/if}
</Select.Group>
</Select.Content>
</Select.Root>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
{#if subscriberListLoading}
<tr>
<td colspan="5" class="py-4 text-center text-sm text-gray-500">
<Loader size="16" class="mx-auto animate-spin" /> Loading subscribers...
</td>
</tr>
{:else if subscribers.length === 0}
<tr>
<td colspan="5" class="py-4 text-center text-sm text-gray-500"> No subscribers found. </td>
</tr>
{:else}
{#each subscribers as user}
<!-- Add a unique key if available, e.g., user.id -->
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium">
{user.subscriber.email}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
<!-- Display monitor tags comma-separated -->
{#if !!user.monitor?.name}
<div class="items-left flex gap-x-2">
{#if !!user.monitor.image}
<GMI src={user.monitor.image} alt="" classList="h-4" />
{/if}
<span class="text-sm font-medium">{user.monitor.name}</span>
</div>
{:else}
<span class="text-sm font-medium uppercase italic text-cyan-500">All Monitors</span>
{/if}
</td>
<td class="whitespace-nowrap px-6 py-4 text-center text-xs font-semibold uppercase">
<!-- Format date nicely if needed -->
{new Date(user.created_at).toLocaleString()}
</td>
<td class="whitespace-nowrap px-6 py-4 text-center text-xs font-semibold">
<label class="flex w-full cursor-pointer items-center justify-between">
<input
type="checkbox"
value=""
disabled={!canSendEmail}
class="peer sr-only"
checked={user.subscriptions_status == "ACTIVE"}
on:change={(e) => {
user.subscriptions_status = e.target.checked ? "ACTIVE" : "INACTIVE";
// Call the API to update the subscription status
updateSubscriptionStatus(user.id, user.subscriptions_status);
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
</div>
+902
View File
@@ -0,0 +1,902 @@
.dots-pattern {
position: absolute;
top: 0px;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background: linear-gradient(177deg, rgba(255, 137, 131, 0.5) 0%, rgba(35, 136, 224, 0.05) 60%);
clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%);
}
.dots-pattern::after {
content: "";
position: absolute;
background-image: radial-gradient(rgba(0, 0, 0, 0) 1.5px, var(--background-kener) 1px);
background-size: 14px 14px;
width: 100%;
height: 100vh;
top: 0;
transform: blur(3px);
left: 0;
}
.squares-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
/* clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%); */
}
.squares-pattern::after {
content: "";
position: absolute;
background-image:
linear-gradient(#444cf7 1px, transparent 1px),
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
-webkit-mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.8) 20%,
rgba(0, 0, 0, 0.6) 40%,
rgba(0, 0, 0, 0.4) 60%,
rgba(0, 0, 0, 0.2) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0.1) 20%,
rgba(0, 0, 0, 0.05) 40%,
rgba(0, 0, 0, 0.025) 60%,
rgba(0, 0, 0, 0.0125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-size: 32px 32px;
width: 100%;
height: 100vh;
top: 0;
left: 0;
}
.dark .squares-pattern::after {
-webkit-mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-image:
linear-gradient(#616fbf 1px, transparent 1px),
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
}
.radial-blue-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at top right,
rgb(49, 157, 235) 0%,
rgb(49, 157, 235) 13%,
rgb(76, 166, 234) 13%,
rgb(76, 166, 234) 23%,
rgb(103, 176, 232) 23%,
rgb(103, 176, 232) 33%,
rgb(130, 185, 231) 33%,
rgb(130, 185, 231) 46%,
rgb(156, 194, 230) 46%,
rgb(156, 194, 230) 48%,
rgb(183, 203, 229) 48%,
rgb(183, 203, 229) 63%,
rgb(210, 213, 227) 63%,
rgb(210, 213, 227) 83%,
rgb(237, 222, 226) 83%,
rgb(237, 222, 226) 100%
);
}
.dark .radial-blue-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at bottom right,
rgb(42, 23, 131) 0%,
rgb(42, 23, 131) 20%,
rgb(42, 40, 158) 20%,
rgb(42, 40, 158) 40%,
rgb(29, 41, 133) 40%,
rgb(21, 34, 134) 60%,
rgb(29, 52, 145) 60%,
rgb(24, 47, 141) 80%,
rgb(25, 54, 140) 80%,
rgb(26, 58, 154) 100%
);
}
.radial-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at top left,
rgb(195, 195, 195) 0%,
rgb(195, 195, 195) 2%,
rgb(202, 202, 202) 2%,
rgb(202, 202, 202) 23%,
rgb(209, 209, 209) 23%,
rgb(209, 209, 209) 55%,
rgb(217, 217, 217) 55%,
rgb(217, 217, 217) 80%,
rgb(224, 224, 224) 80%,
rgb(224, 224, 224) 86%,
rgb(231, 231, 231) 86%,
rgb(231, 231, 231) 96%,
rgb(238, 238, 238) 96%,
rgb(238, 238, 238) 100%
);
}
.dark .radial-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at top left,
rgb(48, 48, 48) 0%,
rgb(48, 48, 48) 8%,
rgb(46, 46, 46) 8%,
rgb(46, 46, 46) 57%,
rgb(43, 43, 43) 57%,
rgb(43, 43, 43) 65%,
rgb(41, 41, 41) 65%,
rgb(41, 41, 41) 67%,
rgb(39, 39, 39) 67%,
rgb(39, 39, 39) 72%,
rgb(36, 36, 36) 72%,
rgb(36, 36, 36) 80%,
rgb(34, 34, 34) 80%,
rgb(34, 34, 34) 100%
);
}
.radial-midnight-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
radial-gradient(
circle at 11% 62%,
rgba(205, 205, 205, 0.04) 0%,
rgba(205, 205, 205, 0.04) 50%,
rgba(149, 149, 149, 0.04) 50%,
rgba(149, 149, 149, 0.04) 100%
),
radial-gradient(
circle at 78% 10%,
rgba(49, 49, 49, 0.04) 0%,
rgba(49, 49, 49, 0.04) 50%,
rgba(254, 254, 254, 0.04) 50%,
rgba(254, 254, 254, 0.04) 100%
),
radial-gradient(
circle at 92% 43%,
rgba(34, 34, 34, 0.04) 0%,
rgba(34, 34, 34, 0.04) 50%,
rgba(206, 206, 206, 0.04) 50%,
rgba(206, 206, 206, 0.04) 100%
),
linear-gradient(329deg, rgb(241, 77, 33), rgb(218, 227, 61));
}
.dark .radial-midnight-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
radial-gradient(
circle at 90% 24%,
rgba(209, 209, 209, 0.04) 0%,
rgba(209, 209, 209, 0.04) 50%,
rgba(160, 160, 160, 0.04) 50%,
rgba(160, 160, 160, 0.04) 100%
),
radial-gradient(
circle at 91% 63%,
rgba(45, 45, 45, 0.04) 0%,
rgba(45, 45, 45, 0.04) 50%,
rgba(87, 87, 87, 0.04) 50%,
rgba(87, 87, 87, 0.04) 100%
),
radial-gradient(
circle at 17% 2%,
rgba(124, 124, 124, 0.04) 0%,
rgba(124, 124, 124, 0.04) 50%,
rgba(117, 117, 117, 0.04) 50%,
rgba(117, 117, 117, 0.04) 100%
),
linear-gradient(88deg, rgb(33, 20, 105), rgb(1, 15, 13));
}
.circle-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(
circle at center center,
rgb(252, 252, 252),
rgb(129, 129, 129)
);
}
.dark .circle-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image: radial-gradient(circle at center center, rgb(71, 71, 71), rgb(8, 8, 8));
}
.carbon-fibre-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
radial-gradient(circle at center center, transparent, rgb(255, 255, 255)),
repeating-linear-gradient(
135deg,
rgb(255, 255, 255) 0px,
rgb(255, 255, 255) 12px,
transparent 12px,
transparent 19px,
rgb(255, 255, 255) 19px,
rgb(255, 255, 255) 24px,
transparent 24px,
transparent 28px
),
repeating-linear-gradient(
45deg,
rgb(245, 245, 245) 0px,
rgb(245, 245, 245) 7px,
transparent 7px,
transparent 14px
),
linear-gradient(90deg, rgb(255, 255, 255), rgb(255, 255, 255));
}
.dark .carbon-fibre-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
radial-gradient(circle at center center, rgba(33, 33, 33, 0), rgb(33, 33, 33)),
repeating-linear-gradient(
135deg,
rgb(33, 33, 33) 0px,
rgb(33, 33, 33) 1px,
transparent 1px,
transparent 4px
),
repeating-linear-gradient(
45deg,
rgb(56, 56, 56) 0px,
rgb(56, 56, 56) 5px,
transparent 5px,
transparent 6px
),
linear-gradient(90deg, rgb(33, 33, 33), rgb(33, 33, 33));
}
.texture-sky-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
repeating-linear-gradient(
45deg,
rgba(0, 0, 0, 0.04),
rgba(0, 0, 0, 0.08),
rgba(0, 0, 0, 0.05),
rgba(0, 0, 0, 0.04),
rgba(0, 0, 0, 0.01),
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.09),
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.03),
transparent,
rgba(0, 0, 0, 0.07),
rgba(0, 0, 0, 0.03),
rgba(0, 0, 0, 0.06) 4px
),
linear-gradient(90deg, rgb(193, 255, 253), rgb(59, 104, 247));
}
.dark .texture-sky-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
repeating-linear-gradient(
90deg,
transparent,
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.08),
rgba(0, 0, 0, 0.01),
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.05),
rgba(0, 0, 0, 0.08),
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.04),
rgba(0, 0, 0, 0.02),
rgba(0, 0, 0, 0.02),
rgba(0, 0, 0, 0.09),
rgba(0, 0, 0, 0.04),
rgba(0, 0, 0, 0.06),
rgba(0, 0, 0, 0.01) 3px
),
linear-gradient(0deg, rgb(28, 117, 231), rgb(72, 64, 87));
}
.tiles-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(45deg, rgb(255, 255, 255) 0%, transparent 36%),
repeating-linear-gradient(
135deg,
rgba(197, 197, 197, 0.1) 0px,
rgba(197, 197, 197, 0.1) 1px,
transparent 1px,
transparent 11px
),
repeating-linear-gradient(
45deg,
rgba(197, 197, 197, 0.1) 0px,
rgba(197, 197, 197, 0.1) 1px,
transparent 1px,
transparent 11px
),
linear-gradient(0deg, rgb(255, 255, 255), rgb(255, 255, 255));
}
.dark .tiles-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(90deg, rgb(33, 33, 33) 0%, transparent 59%),
repeating-linear-gradient(
45deg,
rgba(168, 168, 168, 0.1) 0px,
rgba(168, 168, 168, 0.1) 1px,
transparent 1px,
transparent 13px
),
repeating-linear-gradient(
135deg,
rgba(168, 168, 168, 0.1) 0px,
rgba(168, 168, 168, 0.1) 1px,
transparent 1px,
transparent 13px
),
linear-gradient(90deg, rgb(33, 33, 33), rgb(33, 33, 33));
}
.angular-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(
67.5deg,
rgb(215, 215, 215) 0%,
rgb(215, 215, 215) 46%,
rgb(198, 198, 198) 46%,
rgb(198, 198, 198) 49%,
rgb(181, 181, 181) 49%,
rgb(181, 181, 181) 56%,
rgb(164, 164, 164) 56%,
rgb(164, 164, 164) 61%,
rgb(146, 146, 146) 61%,
rgb(146, 146, 146) 75%,
rgb(129, 129, 129) 75%,
rgb(129, 129, 129) 84%,
rgb(112, 112, 112) 84%,
rgb(112, 112, 112) 100%
),
linear-gradient(
22.5deg,
rgb(215, 215, 215) 0%,
rgb(215, 215, 215) 46%,
rgb(198, 198, 198) 46%,
rgb(198, 198, 198) 49%,
rgb(181, 181, 181) 49%,
rgb(181, 181, 181) 56%,
rgb(164, 164, 164) 56%,
rgb(164, 164, 164) 61%,
rgb(146, 146, 146) 61%,
rgb(146, 146, 146) 75%,
rgb(129, 129, 129) 75%,
rgb(129, 129, 129) 84%,
rgb(112, 112, 112) 84%,
rgb(112, 112, 112) 100%
),
linear-gradient(
112.5deg,
rgb(215, 215, 215) 0%,
rgb(215, 215, 215) 46%,
rgb(198, 198, 198) 46%,
rgb(198, 198, 198) 49%,
rgb(181, 181, 181) 49%,
rgb(181, 181, 181) 56%,
rgb(164, 164, 164) 56%,
rgb(164, 164, 164) 61%,
rgb(146, 146, 146) 61%,
rgb(146, 146, 146) 75%,
rgb(129, 129, 129) 75%,
rgb(129, 129, 129) 84%,
rgb(112, 112, 112) 84%,
rgb(112, 112, 112) 100%
),
linear-gradient(90deg, rgb(231, 231, 231), rgb(195, 195, 195));
background-blend-mode: overlay, overlay, overlay, normal;
}
.dark .angular-mono-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(
45deg,
rgb(46, 46, 46) 0%,
rgb(46, 46, 46) 22%,
rgb(81, 81, 81) 22%,
rgb(81, 81, 81) 75%,
rgb(115, 115, 115) 75%,
rgb(115, 115, 115) 77%,
rgb(150, 150, 150) 77%,
rgb(150, 150, 150) 82%,
rgb(184, 184, 184) 82%,
rgb(184, 184, 184) 93%,
rgb(219, 219, 219) 93%,
rgb(219, 219, 219) 98%,
rgb(253, 253, 253) 98%,
rgb(253, 253, 253) 100%
),
linear-gradient(
135deg,
rgb(46, 46, 46) 0%,
rgb(46, 46, 46) 22%,
rgb(81, 81, 81) 22%,
rgb(81, 81, 81) 75%,
rgb(115, 115, 115) 75%,
rgb(115, 115, 115) 77%,
rgb(150, 150, 150) 77%,
rgb(150, 150, 150) 82%,
rgb(184, 184, 184) 82%,
rgb(184, 184, 184) 93%,
rgb(219, 219, 219) 93%,
rgb(219, 219, 219) 98%,
rgb(253, 253, 253) 98%,
rgb(253, 253, 253) 100%
),
linear-gradient(
22.5deg,
rgb(46, 46, 46) 0%,
rgb(46, 46, 46) 22%,
rgb(81, 81, 81) 22%,
rgb(81, 81, 81) 75%,
rgb(115, 115, 115) 75%,
rgb(115, 115, 115) 77%,
rgb(150, 150, 150) 77%,
rgb(150, 150, 150) 82%,
rgb(184, 184, 184) 82%,
rgb(184, 184, 184) 93%,
rgb(219, 219, 219) 93%,
rgb(219, 219, 219) 98%,
rgb(253, 253, 253) 98%,
rgb(253, 253, 253) 100%
),
linear-gradient(
157.5deg,
rgb(46, 46, 46) 0%,
rgb(46, 46, 46) 22%,
rgb(81, 81, 81) 22%,
rgb(81, 81, 81) 75%,
rgb(115, 115, 115) 75%,
rgb(115, 115, 115) 77%,
rgb(150, 150, 150) 77%,
rgb(150, 150, 150) 82%,
rgb(184, 184, 184) 82%,
rgb(184, 184, 184) 93%,
rgb(219, 219, 219) 93%,
rgb(219, 219, 219) 98%,
rgb(253, 253, 253) 98%,
rgb(253, 253, 253) 100%
),
linear-gradient(90deg, rgb(58, 58, 58), rgb(76, 76, 76));
background-blend-mode: overlay, overlay, overlay, overlay, normal;
}
.angular-spring-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(90deg, rgb(188, 188, 188), rgb(172, 172, 172)),
linear-gradient(
45deg,
rgb(164, 46, 188) 0%,
rgb(164, 46, 188) 14%,
rgb(165, 82, 189) 14%,
rgb(165, 82, 189) 20%,
rgb(165, 117, 190) 20%,
rgb(165, 117, 190) 23%,
rgb(166, 153, 191) 23%,
rgb(166, 153, 191) 30%,
rgb(166, 188, 192) 30%,
rgb(166, 188, 192) 37%,
rgb(167, 224, 193) 37%,
rgb(167, 224, 193) 100%
),
linear-gradient(
135deg,
rgb(164, 46, 188) 0%,
rgb(164, 46, 188) 14%,
rgb(165, 82, 189) 14%,
rgb(165, 82, 189) 20%,
rgb(165, 117, 190) 20%,
rgb(165, 117, 190) 23%,
rgb(166, 153, 191) 23%,
rgb(166, 153, 191) 30%,
rgb(166, 188, 192) 30%,
rgb(166, 188, 192) 37%,
rgb(167, 224, 193) 37%,
rgb(167, 224, 193) 100%
);
background-blend-mode: overlay, overlay, normal;
}
.dark .angular-spring-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(0deg, rgb(59, 59, 59), rgb(197, 197, 197)),
linear-gradient(
45deg,
rgb(159, 116, 214) 0%,
rgb(159, 116, 214) 25%,
rgb(130, 95, 173) 25%,
rgb(130, 95, 173) 45%,
rgb(101, 74, 132) 45%,
rgb(101, 74, 132) 50%,
rgb(72, 54, 90) 50%,
rgb(72, 54, 90) 52%,
rgb(43, 33, 49) 52%,
rgb(43, 33, 49) 61%,
rgb(14, 12, 8) 61%,
rgb(14, 12, 8) 100%
),
linear-gradient(
135deg,
rgb(159, 116, 214) 0%,
rgb(159, 116, 214) 25%,
rgb(130, 95, 173) 25%,
rgb(130, 95, 173) 45%,
rgb(101, 74, 132) 45%,
rgb(101, 74, 132) 50%,
rgb(72, 54, 90) 50%,
rgb(72, 54, 90) 52%,
rgb(43, 33, 49) 52%,
rgb(43, 33, 49) 61%,
rgb(14, 12, 8) 61%,
rgb(14, 12, 8) 100%
);
background-blend-mode: overlay, overlay, normal;
}
.angular-bloom-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(
157.5deg,
rgb(95, 97, 179) 0%,
rgb(95, 97, 179) 16%,
rgb(115, 120, 184) 16%,
rgb(115, 120, 184) 20%,
rgb(135, 142, 188) 20%,
rgb(135, 142, 188) 32%,
rgb(155, 165, 193) 32%,
rgb(155, 165, 193) 38%,
rgb(175, 187, 198) 38%,
rgb(175, 187, 198) 48%,
rgb(195, 210, 203) 48%,
rgb(195, 210, 203) 51%,
rgb(215, 232, 207) 51%,
rgb(215, 232, 207) 59%,
rgb(235, 255, 212) 59%,
rgb(235, 255, 212) 100%
),
linear-gradient(
135deg,
rgb(95, 97, 179) 0%,
rgb(95, 97, 179) 16%,
rgb(115, 120, 184) 16%,
rgb(115, 120, 184) 20%,
rgb(135, 142, 188) 20%,
rgb(135, 142, 188) 32%,
rgb(155, 165, 193) 32%,
rgb(155, 165, 193) 38%,
rgb(175, 187, 198) 38%,
rgb(175, 187, 198) 48%,
rgb(195, 210, 203) 48%,
rgb(195, 210, 203) 51%,
rgb(215, 232, 207) 51%,
rgb(215, 232, 207) 59%,
rgb(235, 255, 212) 59%,
rgb(235, 255, 212) 100%
),
linear-gradient(
112.5deg,
rgb(95, 97, 179) 0%,
rgb(95, 97, 179) 16%,
rgb(115, 120, 184) 16%,
rgb(115, 120, 184) 20%,
rgb(135, 142, 188) 20%,
rgb(135, 142, 188) 32%,
rgb(155, 165, 193) 32%,
rgb(155, 165, 193) 38%,
rgb(175, 187, 198) 38%,
rgb(175, 187, 198) 48%,
rgb(195, 210, 203) 48%,
rgb(195, 210, 203) 51%,
rgb(215, 232, 207) 51%,
rgb(215, 232, 207) 59%,
rgb(235, 255, 212) 59%,
rgb(235, 255, 212) 100%
),
linear-gradient(90deg, rgb(236, 151, 145), rgb(201, 243, 43));
background-blend-mode: overlay, overlay, overlay, normal;
}
.dark .angular-bloom-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background-image:
linear-gradient(
90deg,
rgb(168, 86, 201) 0%,
rgb(168, 86, 201) 3%,
rgb(140, 74, 173) 3%,
rgb(140, 74, 173) 11%,
rgb(112, 61, 145) 11%,
rgb(112, 61, 145) 20%,
rgb(85, 49, 118) 20%,
rgb(85, 49, 118) 49%,
rgb(57, 37, 90) 49%,
rgb(57, 37, 90) 71%,
rgb(29, 24, 62) 71%,
rgb(29, 24, 62) 72%,
rgb(1, 12, 34) 72%,
rgb(1, 12, 34) 100%
),
linear-gradient(
67.5deg,
rgb(168, 86, 201) 0%,
rgb(168, 86, 201) 3%,
rgb(140, 74, 173) 3%,
rgb(140, 74, 173) 11%,
rgb(112, 61, 145) 11%,
rgb(112, 61, 145) 20%,
rgb(85, 49, 118) 20%,
rgb(85, 49, 118) 49%,
rgb(57, 37, 90) 49%,
rgb(57, 37, 90) 71%,
rgb(29, 24, 62) 71%,
rgb(29, 24, 62) 72%,
rgb(1, 12, 34) 72%,
rgb(1, 12, 34) 100%
),
linear-gradient(
112.5deg,
rgb(168, 86, 201) 0%,
rgb(168, 86, 201) 3%,
rgb(140, 74, 173) 3%,
rgb(140, 74, 173) 11%,
rgb(112, 61, 145) 11%,
rgb(112, 61, 145) 20%,
rgb(85, 49, 118) 20%,
rgb(85, 49, 118) 49%,
rgb(57, 37, 90) 49%,
rgb(57, 37, 90) 71%,
rgb(29, 24, 62) 71%,
rgb(29, 24, 62) 72%,
rgb(1, 12, 34) 72%,
rgb(1, 12, 34) 100%
),
linear-gradient(
45deg,
rgb(168, 86, 201) 0%,
rgb(168, 86, 201) 3%,
rgb(140, 74, 173) 3%,
rgb(140, 74, 173) 11%,
rgb(112, 61, 145) 11%,
rgb(112, 61, 145) 20%,
rgb(85, 49, 118) 20%,
rgb(85, 49, 118) 49%,
rgb(57, 37, 90) 49%,
rgb(57, 37, 90) 71%,
rgb(29, 24, 62) 71%,
rgb(29, 24, 62) 72%,
rgb(1, 12, 34) 72%,
rgb(1, 12, 34) 100%
),
linear-gradient(90deg, rgb(95, 3, 74), rgb(35, 65, 96));
background-blend-mode: overlay, overlay, overlay, overlay, normal;
}
.tiles-pattern::after,
.radial-mono-pattern::after,
.angular-mono-pattern::after,
.angular-spring-pattern::after,
.angular-bloom-pattern::after,
.texture-sky-pattern::after,
.carbon-fibre-pattern::after,
.circle-mono-pattern::after,
.radial-midnight-pattern::after,
.radial-blue-pattern::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(
to bottom,
var(--background-kener-rgba) 0%,
var(--background-kener) 100%
);
}
.pets-pattern {
position: fixed;
left: -50px; /* Start offscreen */
width: 50px;
height: 50px;
z-index: 100;
background-repeat: no-repeat;
background-size: 100%;
animation: petWalking 45s linear infinite;
}
@keyframes petWalking {
0% {
left: -50px;
transform: scaleX(1); /* Dog facing right */
}
45% {
left: calc(100vw + 25px);
transform: scaleX(1); /* Still facing right */
}
50% {
left: calc(100vw + 50px);
transform: scaleX(-1); /* Flip to face left */
}
95% {
left: -25px;
transform: scaleX(-1); /* Still facing left */
}
100% {
left: -50px;
transform: scaleX(1); /* Flip back to face right */
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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