mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65f039b7bd | |||
| 5c7b62e9f7 | |||
| 5e6e891ea6 | |||
| 82c904608b | |||
| f5260ceb86 | |||
| abb32fdd6f | |||
| 97aef5c5f5 | |||
| c3eb8ae7f4 | |||
| 72f74f9164 | |||
| 958676e5a7 | |||
| 51134670e5 | |||
| ecea2b220d | |||
| 212f4d5e7a | |||
| 2edb09bd13 | |||
| 6b9f073cb7 | |||
| 9f933a0f1c | |||
| 8a6f721bb4 | |||
| eeadcca6ed | |||
| 8e89c10906 | |||
| 833d16351c | |||
| 0c10552fa7 | |||
| 443afe2f6e | |||
| db48d98da7 | |||
| bf3150434a | |||
| 71c4bd26d2 | |||
| ca4f7040e8 | |||
| 1cfa861d7e | |||
| 007baefe80 | |||
| 7d77f4d996 | |||
| 3d5a62c085 | |||
| 24ffef997b | |||
| 7d5275152c | |||
| 93167c2358 | |||
| 734d3ad280 | |||
| 753668dfa5 | |||
| 351817338d | |||
| 7c12218489 | |||
| 7c71298227 | |||
| 167db6d7e6 | |||
| 74834f3559 | |||
| e13ca5c639 | |||
| a51806d62b | |||
| 57238dfb72 | |||
| a2be642af0 | |||
| aec2d23c2f | |||
| 11a11162da | |||
| 12a7be497e | |||
| 9b69da66a5 | |||
| ee831f96bb | |||
| 5542790145 | |||
| 3b1091a34f | |||
| eebe6541c3 | |||
| e308089cfa | |||
| 5132d6894e | |||
| 006d1e1fa0 | |||
| 4c6dc24355 | |||
| acf227a947 | |||
| 3b05ca73e5 | |||
| 236d7ea585 | |||
| 87408d22c4 | |||
| ea6754775d | |||
| 3210f4d406 | |||
| 16ac97a9c9 | |||
| 8857a51dde | |||
| fc2abe7f16 | |||
| f04a930e13 | |||
| 1de1e2d6ba | |||
| 0208e4baf9 | |||
| 21af2a6d02 | |||
| 2fbe301a51 | |||
| 6d5382982c | |||
| a20b4ee570 | |||
| 909ff9e070 | |||
| 7d3e265ece | |||
| 78d8f311c0 | |||
| c52ba95fd0 | |||
| 9462a0331a | |||
| 4e3d1838c0 | |||
| aad7b5c6ea | |||
| abec24d65b | |||
| e0c3d1864c | |||
| bd25c6fe05 | |||
| 4ec5e14431 | |||
| c9b8c5bec3 | |||
| 223883adf7 | |||
| 83c2d7ff79 | |||
| cf33f5a2f6 | |||
| 92b4596924 | |||
| ce9b748570 | |||
| 87a9069ab4 | |||
| 3e6a76c8c3 | |||
| 3509955374 | |||
| 225711f95d | |||
| 5436647cd3 | |||
| fbd9da3b33 | |||
| edb6705ac8 | |||
| 382a2a5bcc | |||
| b5967607d2 | |||
| 03bbb6c3b6 | |||
| 6054be0ff1 | |||
| ff7355bcc2 | |||
| 23a9124e03 | |||
| ac41807cdd | |||
| d95bb13e6e | |||
| b9d58ba3c1 | |||
| a648b29f31 | |||
| e702feb04f | |||
| ce83355e58 | |||
| 683beed3b7 | |||
| c9095036eb | |||
| 29ec935d67 | |||
| 339015a093 | |||
| b8b5b4170f | |||
| 8a104e4480 | |||
| 89e1a5e88a | |||
| 8e5746d068 | |||
| 1df7c6560a | |||
| 0ff4546379 | |||
| ab036c968d | |||
| 625ece2e7b | |||
| 41842cc1b9 | |||
| e63992a7a5 | |||
| 7ae5d8f1b1 | |||
| 0a34f2138a | |||
| 38b326fe72 | |||
| 8785c609a9 | |||
| 1a67560137 | |||
| 1288b463f1 | |||
| 1aed6d0703 | |||
| 69f1e69140 | |||
| ab4ecd29c6 | |||
| 1d76c8a3d3 | |||
| b1c2cedbac |
+11
-11
@@ -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 \
|
||||
sqlite \
|
||||
sqlite-dev \
|
||||
tzdata \
|
||||
iputils=20240905-r0
|
||||
iputils
|
||||
|
||||
FROM builder-${VARIANT} AS builder
|
||||
|
||||
@@ -63,8 +63,10 @@ RUN rm -rf src/routes/\(docs\) \
|
||||
mkdir -p uploads database && \
|
||||
chmod -R 750 uploads database
|
||||
|
||||
# Build the application and remove `devDependencies`
|
||||
# Build the application and copy templates to build directory
|
||||
RUN npm run build && \
|
||||
mkdir -p build/server/templates && \
|
||||
cp -r src/lib/server/templates/* build/server/templates/ && \
|
||||
npm prune --omit=dev
|
||||
|
||||
#==========================================================#
|
||||
@@ -83,8 +85,8 @@ 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 \
|
||||
iputils \
|
||||
sqlite \
|
||||
tzdata
|
||||
|
||||
FROM final-${VARIANT} AS final
|
||||
@@ -108,6 +110,7 @@ COPY --chown=node:node --from=builder /app/src/lib/ ./src/lib/
|
||||
COPY --chown=node:node --from=builder /app/build ./build
|
||||
COPY --chown=node:node --from=builder /app/uploads ./uploads
|
||||
COPY --chown=node:node --from=builder /app/database ./database
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/templates ./build/server/templates
|
||||
# TODO: Consider changing from copying `node_modules` to instead letting `npm ci --omit=dev` handle production dependencies. Right now, copying `node_modules` is leading to a smaller image, whereas letting `npm ci` handle the install in final image is slightly faster, but leads to larger image size. IMO, having a slightly longer build time (e.g. ~10 sec.) is better in the end to have a smaller image.
|
||||
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
|
||||
COPY --chown=node:node --from=builder /app/migrations ./migrations
|
||||
@@ -120,9 +123,6 @@ COPY --chown=node:node --from=builder /app/openapi.json ./openapi.json
|
||||
COPY --chown=node:node --from=builder /app/openapi.yaml ./openapi.yaml
|
||||
COPY --chown=node:node --from=builder /app/package.json ./package.json
|
||||
|
||||
# Ensure necessary directories are writable
|
||||
VOLUME ["/uploads", "/database"]
|
||||
|
||||
# Set container timezone and make entrypoint script executable
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
|
||||
chmod +x ./entrypoint.sh
|
||||
|
||||
@@ -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.12"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.12-blue" alt="Kener latest stable version: 3.2.12" /></a>
|
||||
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.18"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.18-blue" alt="Kener latest stable version: 3.2.18" /></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.12)</td>
|
||||
<td>Latest stable release (aka 3.2.18)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.12" target="_blank"><code>3.2.12</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.18" target="_blank"><code>3.2.18</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.12) release within that minor version (3.2.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.18) 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.12) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.18) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 <strong><em>(smallest image size)</em></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
|
||||
<td>Latest stable release (aka 3.2.12)</td>
|
||||
<td>Latest stable release (aka 3.2.18)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.12-alpine" target="_blank"><code>3.2.12-alpine</code></a></td>
|
||||
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.18-alpine" target="_blank"><code>3.2.18-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.12) release within that minor version (3.2.x)</td>
|
||||
<td>Major-minor version tag pointing to the latest patch (3.2.18) 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.12) release within that major version (3.x.x)</td>
|
||||
<td>Major version tag pointing to the latest stable (3.2.18) release within that major version (3.x.x)</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
+3
-3
@@ -38,7 +38,7 @@ services:
|
||||
# Only use below section if you would like to utilize PostgreSQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=postgresql://db_user:db_password@localhost:5432/kener_db`)
|
||||
postgres:
|
||||
image: postgres:alpine
|
||||
name: kener_db
|
||||
container_name: postgres
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
# Only use below section if you would like to utilize MySQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=mysql://db_user:db_password@localhost:3306/kener_db`)
|
||||
mysql:
|
||||
image: mariadb:11
|
||||
name: kener_db
|
||||
container_name: mysql
|
||||
environment:
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
|
||||
@@ -58,4 +58,4 @@ services:
|
||||
|
||||
volumes:
|
||||
data:
|
||||
name: kener_db
|
||||
name: kener_db
|
||||
|
||||
+29
-257
@@ -7,6 +7,35 @@ description: Changelogs for Kener
|
||||
|
||||
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
|
||||
|
||||
## v3.2.16
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **Hidden Categories:** You can now create hidden categories that will not show up for users that are not logged in. All the monitors will also not show up. Some one however can directly get to see them if they visit the monitor link directly
|
||||
- **Configure data rage:** Now you can configure the data range in home page between 1 and 180 days. You can set different ranges for mobile and desktop devices.
|
||||
|
||||
## 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 +114,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
|
||||
|
||||
+12
-31
@@ -12,29 +12,24 @@ There are two ways to embed your monitor in your website
|
||||
You can embed your monitor in your website using javascript. We recommend using this method as it takes care of the height of the embedded monitor.
|
||||
|
||||
```html
|
||||
<script
|
||||
async
|
||||
src="http://[hostname]/embed/monitor-[tag]/js?theme=light&monitor=http://[hostname]/embed/monitor-[tag]"
|
||||
></script>
|
||||
<script async src="http://[hostname]/embed/monitor-[tag]/js?theme=light&monitor=http://[hostname]/embed/monitor-[tag]"></script>
|
||||
```
|
||||
|
||||
Here is an example
|
||||
|
||||
```html
|
||||
<script
|
||||
async
|
||||
src="https://kener.ing/embed/monitor-earth/js?theme=light&monitor=https://kener.ing/embed/monitor-earth"
|
||||
></script>
|
||||
<script async src="https://kener.ing/embed/monitor-earth/js?theme=light&monitor=https://kener.ing/embed/monitor-earth"></script>
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `monitor`: The monitor url
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `monitor`: The monitor url
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
- `tz`: Timezone. Example `Asia/Calcutta`, `Asia/Tokyo`
|
||||
|
||||
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
|
||||
@@ -49,27 +44,13 @@ Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
This is the simplest way to embed your monitor in your website. You can use the following code to embed your monitor in your website.
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="http://[hostname]/embed/monitor-[tag]?theme=light"
|
||||
width="100%"
|
||||
height="200"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allowpaymentrequest
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
<iframe src="http://[hostname]/embed/monitor-[tag]?theme=light" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
|
||||
```
|
||||
|
||||
Here is an example
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://kener.ing/embed/monitor-earth?theme=light"
|
||||
width="100%"
|
||||
height="100"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allowpaymentrequest
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
<iframe src="https://kener.ing/embed/monitor-earth?theme=light" width="100%" height="100" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
|
||||
```
|
||||
|
||||
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
@@ -78,9 +59,9 @@ Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
|
||||
You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
|
||||
### Demo
|
||||
|
||||
|
||||
+29
-1
@@ -29,17 +29,26 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
```json
|
||||
{
|
||||
"%status for %duration": "%status for %duration",
|
||||
"120 Days": "120 Days",
|
||||
"14 Days": "14 Days",
|
||||
"150 Days": "150 Days",
|
||||
"180 Days": "180 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"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",
|
||||
"All Systems are in Maintenance": "All Systems are in Maintenance",
|
||||
"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",
|
||||
@@ -48,6 +57,9 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
"Days": "Days",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"Embed": "Embed",
|
||||
"Enter the code": "Enter the code",
|
||||
"Enter your email": "Enter your email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.",
|
||||
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
|
||||
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
@@ -57,10 +69,12 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
"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",
|
||||
@@ -69,11 +83,16 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Some Systems in Maintenance",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
@@ -82,14 +101,23 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
"Starts %startedAt": "Starts %startedAt",
|
||||
"Status OK": "Status OK",
|
||||
"Status": "Status",
|
||||
"Subscribe": "Subscribe",
|
||||
"Subscribe to Updates": "Subscribe to Updates",
|
||||
"Subscribe to all monitors": "Subscribe to all monitors",
|
||||
"Switch to your timezone": "Switch to your timezone",
|
||||
"Theme": "Theme",
|
||||
"There are no upcoming maintenances": "There are no upcoming maintenances",
|
||||
"Timezone set to %tz": "Timezone set to %tz",
|
||||
"Today": "Today",
|
||||
"UP": "UP",
|
||||
"Update Subscription": "Update Subscription",
|
||||
"Upcoming Maintenance": "Upcoming Maintenance",
|
||||
"Upcoming Maintenances": "Upcoming Maintenances",
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime"
|
||||
"Uptime": "Uptime",
|
||||
"View in detail": "View in detail",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "We have sent a code to your email. Please enter it below to confirm your login",
|
||||
"You are logged in as %email": "You are logged in as %email"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ To close an incident, you need to add a message and set the status to `RESOLVED`
|
||||
|
||||
## Add Monitors
|
||||
|
||||
To add monitors to the incident, you need to add the monitor tag to the incident. This will automatically add the monitor to the incident. You will also get the status of the monitor in the incident. This can be `DEGRADED` or `DOWN`.
|
||||
To add monitors to the incident, you need to add the monitor tag to the incident. This will automatically add the monitor to the incident. You will also get the status of the monitor in the incident. For any event type, this can be `DEGRADED` or `DOWN`. For maintenance event type, this can also be `MAINTENANCE`.
|
||||
|
||||
## Maintenance
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Gamedig Monitors | Kener
|
||||
description: Monitor games and services
|
||||
---
|
||||
|
||||
# Gamedig Monitors
|
||||
|
||||
Gamedig monitors are used to monitor games and services. Currently, 320+ games and some services are available. You can view the entire list [here](https://github.com/gamedig/node-gamedig/blob/HEAD/GAMES_LIST.md). More than just the state of the game/service, it provides information about your game/service, such as players list, world name, connection method, and so on.
|
||||
|
||||
<div class="border rounded-md">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Host
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The host is the domain name, IPv4 or IPv6 of the game/service you want to monitor. The host should be valid.
|
||||
|
||||
## Port
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The port is the query port of the game/service you want to monitor. Some games use a port, different from the one to connect, to accept queries, such as Minecraft with `query-port`.
|
||||
If you do not know the query port, you can let the monitor guess the port by checking the option `Guess port`.
|
||||
|
||||
## Timeout
|
||||
|
||||
<span class="text-red-500 text-xs font-semibold">
|
||||
REQUIRED
|
||||
</span>
|
||||
|
||||
The timeout is used to define the time in milliseconds after which the monitor should timeout. If the monitor does not respond within the timeout period, the monitor will be marked as down. For example, 10000 will set the timeout to 10 seconds. It is required and has to be a number greater than 2000 (or 2 seconds).
|
||||
|
||||
## Request additional rules
|
||||
|
||||
Valve games can provide additional 'rules' to Gamedig monitors. If checked, they will be available in `reponseRaw.raw` (see [Eval](#eval)), beware that it may increase query time.
|
||||
|
||||
## Eval
|
||||
|
||||
The eval is used to define the JavaScript code that should be used to evaluate the response. It is optional and has be a valid JavaScript code.
|
||||
|
||||
This is an anonymous JS function, it should return a **Promise**, that resolves or rejects to `{status, latency}`, by default it looks like this.
|
||||
|
||||
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and latency(number)
|
||||
> `{status: "DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
(async function (responseTime, responseRaw) {
|
||||
return {
|
||||
status: "UP",
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- `responseTime` **REQUIRED** is a number. It is the latency in milliseconds
|
||||
- `responseRaw` **REQUIRED** is the raw response of the game/service. It varies from game by game, we recommend first printing responseRaw with a simple `console.log`.
|
||||
|
||||
## Additional notes
|
||||
|
||||
Some games/services need more configuration on their part to accept correctly any query of the monitor. You can consult these notes [here](https://github.com/gamedig/node-gamedig/blob/HEAD/GAMES_LIST.md#games-with-additional-notes).
|
||||
@@ -103,6 +103,10 @@ To monitor the DNS records, you need to provide the domain name and the record t
|
||||
|
||||
To monitor the ping, you need to provide the IP address or the domain name. If the ping is not successful, the monitor will be marked as down. You can read more about PING monitoring [here](/docs/monitors-ping).
|
||||
|
||||
### GAMEDIG
|
||||
|
||||
To monitor game/service, you need to provide the game or service to monitor, the IP address or the domain name and the port. If the query is not successful, the monitor will be marked as down. You can read more about GAMEDIG monitoring [here](/docs/monitors-gamedig).
|
||||
|
||||
---
|
||||
|
||||
## Triggers
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ Shows the day's impact. Example: If it was down at 10:00 AM and it is 11:00 AM,
|
||||
|
||||
## Colors
|
||||
|
||||
You can change the colors of UP/DEGRADED/DOWN states of the monitor. You show have a shade of green, yellow, and red respectively.
|
||||
You can change the colors of UP/DEGRADED/DOWN/MAINTENANCE states of the monitor. By default, it shows a shade of green, yellow, red and blue respectively.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
<body>
|
||||
<script
|
||||
async
|
||||
src="http://localhost:3000/embed/monitor-bitbuckettcp/js?bgc=212226&locale=en&theme=dark&monitor=http://localhost:3000/embed/monitor-bitbuckettcp"
|
||||
src="http://localhost:3000/embed/monitor-earth/js?theme=light&monitor=http://localhost:3000/embed/monitor-earth&locale=fr&tz=Asia/Tokyo"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"noImplicitAny": false
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
|
||||
@@ -15,6 +15,10 @@ const base = process.env.KENER_BASE_PATH || "";
|
||||
const app = express();
|
||||
const db = knex(knexOb);
|
||||
|
||||
if (process.env.ORIGIN) {
|
||||
process.env.SVELTEKIT_ORIGIN = process.env.ORIGIN;
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.startsWith("/embed")) {
|
||||
res.setHeader("Content-Security-Policy", "frame-ancestors *");
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
export function up(knex) {
|
||||
return knex.schema
|
||||
.createTable("subscribers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("subscriber_send").notNullable();
|
||||
table.text("subscriber_meta").nullable();
|
||||
table.string("subscriber_type").notNullable();
|
||||
table.string("subscriber_status").notNullable();
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Add unique constraint on subscriber_send and subscriber_type
|
||||
table.unique(["subscriber_send", "subscriber_type"]);
|
||||
|
||||
// Add index on subscriber_send for better query performance
|
||||
table.index(["subscriber_send"]);
|
||||
})
|
||||
.createTable("subscriptions", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("subscriber_id").unsigned().notNullable();
|
||||
table.string("subscriptions_status").notNullable();
|
||||
table.string("subscriptions_monitors").notNullable();
|
||||
table.text("subscriptions_meta").nullable();
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Add unique constraint on subscriber_id and subscriptions_monitors
|
||||
// This constraint also creates an index that will be used for queries
|
||||
table.unique(["subscriber_id", "subscriptions_monitors"]);
|
||||
|
||||
// Add index to optimize queries filtering by status and monitors
|
||||
table.index(["subscriptions_status", "subscriptions_monitors"]);
|
||||
})
|
||||
.createTable("subscription_triggers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("subscription_trigger_type").notNullable().unique();
|
||||
table.string("subscription_trigger_status").notNullable();
|
||||
table.text("config").nullable();
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
export function down(knex) {
|
||||
return knex.schema
|
||||
.dropTableIfExists("subscription_triggers")
|
||||
.dropTableIfExists("subscriptions")
|
||||
.dropTableIfExists("subscribers");
|
||||
}
|
||||
+901
-3
@@ -25,6 +25,10 @@
|
||||
"name": "Monitors",
|
||||
"description": "APIs to interact with monitors"
|
||||
},
|
||||
{
|
||||
"name": "MonitorStatus",
|
||||
"description": "APIs to interact with monitor status"
|
||||
},
|
||||
{
|
||||
"name": "Incidents",
|
||||
"description": "APIs to integrate incidents"
|
||||
@@ -457,7 +461,7 @@
|
||||
"operationId": "updateMonitorStatus",
|
||||
"summary": "Update status of a monitor",
|
||||
"description": "Update status of a monitor at a given timestamp UTC",
|
||||
"tags": ["Monitors"],
|
||||
"tags": ["MonitorStatus"],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
@@ -516,7 +520,7 @@
|
||||
"operationId": "getMonitorStatus",
|
||||
"summary": "Get status of a monitor",
|
||||
"description": "get status of a monitor at timestamp",
|
||||
"tags": ["Monitors"],
|
||||
"tags": ["MonitorStatus"],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
@@ -1097,6 +1101,900 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/monitor": {
|
||||
"get": {
|
||||
"summary": "Search Monitor",
|
||||
"description": "Search Monitor",
|
||||
"operationId": "searchMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tag",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"example": "{{tag}}"
|
||||
},
|
||||
"description": "Optional tag to filter results"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response200",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T11:14:49.822Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 15
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "SSL"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github SSL Certificate"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "GithubSSL"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": [
|
||||
{
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"Response200": {
|
||||
"value": [
|
||||
{
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create Monitor",
|
||||
"description": "API to create monitors",
|
||||
"operationId": "createMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-22T10:10:00.546Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 15
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "API"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "Github"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-22T10:10:00.546Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Create Monitor": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-22T10:10:00.546Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "API",
|
||||
"name": "Github",
|
||||
"status": "ACTIVE",
|
||||
"tag": "Github",
|
||||
"type_data": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}",
|
||||
"updated_at": "2025-04-22T10:10:00.546Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Response201",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:35:21.030Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 16
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "API"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "Github"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:35:21.030Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response201": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T12:35:21.030Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 16,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "API",
|
||||
"name": "Github",
|
||||
"status": "ACTIVE",
|
||||
"tag": "Github",
|
||||
"type_data": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}",
|
||||
"updated_at": "2025-04-23T12:35:21.030Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Response400",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "monitor id must be empty or 0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response400": {
|
||||
"value": {
|
||||
"error": "monitor id must be empty or 0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/monitor/{monitorId}": {
|
||||
"get": {
|
||||
"summary": "Get Monitor",
|
||||
"description": "API to get a specified monitor",
|
||||
"operationId": "getMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response200",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T11:14:49.822Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 15
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "SSL"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github SSL Certificate"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "GithubSSL"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response200": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Response404",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response404": {
|
||||
"value": {
|
||||
"error": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"summary": "Update Monitor",
|
||||
"description": "API to update a specified monitor",
|
||||
"operationId": "updateMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T11:14:49.822Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 20
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "SSL"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github SSL Certificate"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "GithubSSL"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Update Monitor": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 20,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T12:05:01.941Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response200",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category_name": {
|
||||
"type": "string",
|
||||
"example": "Home"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T11:14:49.822Z"
|
||||
},
|
||||
"cron": {
|
||||
"type": "string",
|
||||
"example": "* * * * *"
|
||||
},
|
||||
"day_degraded_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"day_down_minimum_count": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"default_status": {
|
||||
"type": "string",
|
||||
"example": "NONE"
|
||||
},
|
||||
"degraded_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"down_trigger": {
|
||||
"nullable": true,
|
||||
"example": null
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 15
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"example": ""
|
||||
},
|
||||
"include_degraded_in_downtime": {
|
||||
"type": "string",
|
||||
"example": "NO"
|
||||
},
|
||||
"monitor_type": {
|
||||
"type": "string",
|
||||
"example": "SSL"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Github SSL Certificate"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "ACTIVE"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"example": "GithubSSL"
|
||||
},
|
||||
"type_data": {
|
||||
"type": "string",
|
||||
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2025-04-23T13:38:22.965Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response200": {
|
||||
"value": {
|
||||
"category_name": "Home",
|
||||
"created_at": "2025-04-23T11:14:49.822Z",
|
||||
"cron": "* * * * *",
|
||||
"day_degraded_minimum_count": 1,
|
||||
"day_down_minimum_count": 1,
|
||||
"default_status": "NONE",
|
||||
"degraded_trigger": null,
|
||||
"description": "",
|
||||
"down_trigger": null,
|
||||
"id": 15,
|
||||
"image": "",
|
||||
"include_degraded_in_downtime": "NO",
|
||||
"monitor_type": "SSL",
|
||||
"name": "Github SSL Certificate",
|
||||
"status": "ACTIVE",
|
||||
"tag": "GithubSSL",
|
||||
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
|
||||
"updated_at": "2025-04-23T13:38:22.965Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Response400",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "id mismatch: payload: 15, url: 14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response400": {
|
||||
"value": {
|
||||
"error": "id mismatch: payload: 15, url: 14"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Response404",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response404": {
|
||||
"value": {
|
||||
"error": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Delete Monitor",
|
||||
"description": "API to delete a specified monitor",
|
||||
"operationId": "deleteMonitor",
|
||||
"tags": ["Monitors"],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Response200",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"examples": {
|
||||
"Response200": {
|
||||
"value": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Response404",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"example": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"Response404": {
|
||||
"value": {
|
||||
"error": "monitor does not exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "monitorId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+471
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.2.11",
|
||||
"version": "3.2.15",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kener",
|
||||
"version": "3.2.11",
|
||||
"version": "3.2.15",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10",
|
||||
@@ -35,11 +35,13 @@
|
||||
"express": "^4.18.2",
|
||||
"figlet": "^1.8.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"gamedig": "^5.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "^3.1.0",
|
||||
"lucide-svelte": "^0.483.0",
|
||||
"marked": "^11.1.1",
|
||||
"mobile-detect": "^1.4.5",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
@@ -1264,6 +1266,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",
|
||||
@@ -1376,6 +1390,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",
|
||||
@@ -1412,6 +1438,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",
|
||||
@@ -1820,6 +1852,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",
|
||||
@@ -2064,6 +2129,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",
|
||||
@@ -2536,6 +2640,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",
|
||||
@@ -2789,6 +2899,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",
|
||||
@@ -3357,6 +3476,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",
|
||||
@@ -3458,6 +3584,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",
|
||||
@@ -3616,6 +3760,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",
|
||||
@@ -3750,6 +3903,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",
|
||||
@@ -3771,6 +3958,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",
|
||||
@@ -3836,6 +4038,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",
|
||||
@@ -3940,6 +4154,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",
|
||||
@@ -4087,6 +4326,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",
|
||||
@@ -4103,6 +4348,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",
|
||||
@@ -4767,6 +5025,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",
|
||||
@@ -4828,6 +5092,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",
|
||||
@@ -5023,11 +5296,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",
|
||||
@@ -5320,6 +5605,12 @@
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mobile-detect": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/mobile-detect/-/mobile-detect-1.4.5.tgz",
|
||||
"integrity": "sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mode-watcher": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.4.1.tgz",
|
||||
@@ -5600,6 +5891,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",
|
||||
@@ -5907,6 +6210,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",
|
||||
@@ -6618,6 +6930,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",
|
||||
@@ -6694,6 +7012,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",
|
||||
@@ -6961,6 +7291,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",
|
||||
@@ -6970,6 +7306,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",
|
||||
@@ -7252,6 +7603,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",
|
||||
@@ -7259,6 +7616,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",
|
||||
@@ -7696,6 +8075,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",
|
||||
@@ -7855,6 +8280,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",
|
||||
@@ -8693,6 +9130,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",
|
||||
@@ -8986,6 +9429,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",
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.2.13",
|
||||
"version": "3.2.19",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"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.",
|
||||
@@ -93,11 +93,13 @@
|
||||
"express": "^4.18.2",
|
||||
"figlet": "^1.8.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"gamedig": "^5.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "^3.1.0",
|
||||
"lucide-svelte": "^0.483.0",
|
||||
"marked": "^11.1.1",
|
||||
"mobile-detect": "^1.4.5",
|
||||
"mode-watcher": "^0.4.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
|
||||
@@ -10,6 +10,7 @@ section {
|
||||
--up-color: #4ead94;
|
||||
--down-color: #ca3038;
|
||||
--degraded-color: #e6ca61;
|
||||
--maintenance-color: #6679cc;
|
||||
}
|
||||
|
||||
/*Colors for something UP*/
|
||||
@@ -82,6 +83,33 @@ section {
|
||||
background: linear-gradient(0deg, var(--degraded-color) 90%, var(--up-color) 1%);
|
||||
}
|
||||
|
||||
/*Colors for something in maintenance*/
|
||||
.bg-api-maintenance {
|
||||
background-color: var(--maintenance-color);
|
||||
}
|
||||
.text-api-maintenance {
|
||||
color: var(--maintenance-color);
|
||||
}
|
||||
|
||||
.bg-api-maintenance-10 {
|
||||
background: linear-gradient(0deg, var(--maintenance-color) 10%, var(--up-color) 1%);
|
||||
}
|
||||
.bg-api-maintenance-20 {
|
||||
background: linear-gradient(0deg, var(--maintenance-color) 20%, var(--up-color) 1%);
|
||||
}
|
||||
.bg-api-maintenance-40 {
|
||||
background: linear-gradient(0deg, var(--maintenance-color) 40%, var(--up-color) 1%);
|
||||
}
|
||||
.bg-api-maintenance-60 {
|
||||
background: linear-gradient(0deg, var(--maintenance-color) 60%, var(--up-color) 1%);
|
||||
}
|
||||
.bg-api-maintenance-80 {
|
||||
background: linear-gradient(0deg, var(--maintenance-color) 80%, var(--up-color) 1%);
|
||||
}
|
||||
.bg-api-maintenance-90 {
|
||||
background: linear-gradient(0deg, var(--maintenance-color) 90%, var(--up-color) 1%);
|
||||
}
|
||||
|
||||
/*Needed to show markdown properly*/
|
||||
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::before {
|
||||
content: "";
|
||||
@@ -881,3 +909,59 @@ p code:not([class^="language-"]) {
|
||||
margin-top: 1.3em;
|
||||
margin-bottom: 1.3em;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:disabled + .peer,
|
||||
label:has(input[type="checkbox"]:disabled) {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Select any element whose class begins with "lucid-chevron" */
|
||||
.hide-chevron [data-bits-accordion-header] .lucide-chevron-down,
|
||||
.hide-chevron [data-bits-accordion-header] .lucide-chevron-up {
|
||||
/* add your styles here */
|
||||
display: none; /* Hides the chevron when the class 'hide-chevron' is applied */
|
||||
}
|
||||
|
||||
.hide-chevron [data-bits-accordion-header] button:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.copybtn .copy-btn {
|
||||
transform: scale(1);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.copybtn .check-btn {
|
||||
transform: scale(0);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.copybtn:focus .copy-btn {
|
||||
transform: scale(0);
|
||||
}
|
||||
.copybtn:focus .check-btn {
|
||||
transform: scale(1);
|
||||
}
|
||||
.dash-after::after {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
background-color: hsl(var(--accent) / var(--tw-bg-opacity, 1));
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
.dash-after:hover::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-color-bar {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.status-color-banner:hover .status-color-bar {
|
||||
bottom: -0.75rem;
|
||||
}
|
||||
.status-color-banner:hover > div {
|
||||
@apply bg-secondary;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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 }
|
||||
]
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ async function StatusColor(status) {
|
||||
UP: site.colors?.UP || "#00dfa2",
|
||||
DEGRADED: site.colors?.DEGRADED || "#e6ca61",
|
||||
DOWN: site.colors?.DOWN || "#ca3038",
|
||||
MAINTENANCE: site.colors?.MAINTENANCE || "#6679cc",
|
||||
NO_DATA: "#f1f5f8"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
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";
|
||||
@@ -11,9 +13,11 @@
|
||||
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();
|
||||
@@ -35,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;
|
||||
@@ -75,37 +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"
|
||||
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}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import LaptopMinimal from "lucide-svelte/icons/laptop-minimal";
|
||||
import SmartPhone from "lucide-svelte/icons/smartphone";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import Plus from "lucide-svelte/icons/plus";
|
||||
import Info from "lucide-svelte/icons/info";
|
||||
@@ -37,10 +39,23 @@
|
||||
};
|
||||
let footerHTML = "";
|
||||
let tzToggle = "NO";
|
||||
let showSiteStatus = "NO";
|
||||
let homeIncidentCount = 10;
|
||||
let homeIncidentStartTimeWithin = 30;
|
||||
let incidentGroupView = "EXPAND_FIRST";
|
||||
let kenerTheme = "default";
|
||||
let homeDataMaxDays = {
|
||||
desktop: {
|
||||
maxDays: 90,
|
||||
selectableDays: [1, 7, 14, 30, 60, 90]
|
||||
},
|
||||
mobile: {
|
||||
maxDays: 90,
|
||||
selectableDays: [1, 7, 14, 30, 60, 90]
|
||||
}
|
||||
};
|
||||
|
||||
let selectableDaysList = [1, 7, 14, 30, 60, 90, 120, 150, 180];
|
||||
|
||||
let availableThemes = [
|
||||
{ name: "Default", value: "default" },
|
||||
@@ -92,9 +107,16 @@
|
||||
if (data.siteData.kenerTheme) {
|
||||
kenerTheme = data.siteData.kenerTheme;
|
||||
}
|
||||
if (data.siteData.homeDataMaxDays) {
|
||||
homeDataMaxDays = data.siteData.homeDataMaxDays;
|
||||
}
|
||||
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
|
||||
@@ -116,7 +138,6 @@
|
||||
let resp = await storeSiteData({
|
||||
hero: JSON.stringify(hero)
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formStateHero = "idle";
|
||||
if (data.error) {
|
||||
@@ -128,7 +149,8 @@
|
||||
|
||||
async function saveTheme() {
|
||||
let resp = await storeSiteData({
|
||||
kenerTheme: kenerTheme
|
||||
kenerTheme: kenerTheme,
|
||||
showSiteStatus: showSiteStatus
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
@@ -253,7 +275,8 @@
|
||||
...categories,
|
||||
{
|
||||
name: "",
|
||||
description: ""
|
||||
description: "",
|
||||
isHidden: false
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -281,6 +304,23 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let dataErrorMessage = "";
|
||||
let formStateData = "idle";
|
||||
async function formSubmitData() {
|
||||
dataErrorMessage = "";
|
||||
formStateData = "loading";
|
||||
let resp = await storeSiteData({
|
||||
homeDataMaxDays: JSON.stringify(homeDataMaxDays)
|
||||
});
|
||||
//print data
|
||||
let data = await resp.json();
|
||||
formStateData = "idle";
|
||||
if (data.error) {
|
||||
dataErrorMessage = data.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="mt-4">
|
||||
@@ -344,6 +384,20 @@
|
||||
</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}
|
||||
@@ -359,6 +413,173 @@
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Data</Card.Title>
|
||||
<Card.Description>
|
||||
Configure the data shown on the homepage. This includes the time range for incidents.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 flex flex-col space-y-4" on:submit|preventDefault={formSubmitData}>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<Label class="text-lg">
|
||||
<LaptopMinimal class="mr-2 inline h-4 w-4" />
|
||||
Desktop
|
||||
</Label>
|
||||
<div class="border-b pb-4">
|
||||
<Label for="desktopmaxdata" class="text-sm font-medium">Select Data Range</Label>
|
||||
<Select.Root
|
||||
portal={null}
|
||||
onSelectedChange={(e) => {
|
||||
homeDataMaxDays.desktop.maxDays = e.value;
|
||||
//uncheck all checkboxes greater than maxDays
|
||||
homeDataMaxDays.desktop.selectableDays = homeDataMaxDays.desktop.selectableDays.filter(
|
||||
(day) => day <= e.value
|
||||
);
|
||||
//push all days less than maxDays
|
||||
selectableDaysList.forEach((day) => {
|
||||
if (day <= e.value && !homeDataMaxDays.desktop.selectableDays.includes(day)) {
|
||||
homeDataMaxDays.desktop.selectableDays.push(day);
|
||||
}
|
||||
});
|
||||
}}
|
||||
selected={{
|
||||
value: homeDataMaxDays.desktop.maxDays,
|
||||
label: homeDataMaxDays.desktop.maxDays + " Days"
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class=" mt-2 w-[200px]" id="desktopmaxdata">
|
||||
<Select.Value bind:value={homeDataMaxDays.desktop.maxDays} placeholder="Select Range" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Max Days</Select.Label>
|
||||
{#each selectableDaysList as day}
|
||||
<Select.Item value={day} label={day + " Days"} class="text-sm font-medium">
|
||||
{day} Days
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="border-b pb-4">
|
||||
<Label for="desktopdropdown" class="text-sm font-medium">Select Dropdown Range</Label>
|
||||
<div class="mt-2 flex flex-row flex-wrap gap-x-2">
|
||||
{#each selectableDaysList as day}
|
||||
<label>
|
||||
<input
|
||||
on:change={(e) => {
|
||||
if (e.target.checked) {
|
||||
homeDataMaxDays.desktop.selectableDays.push(day);
|
||||
} else {
|
||||
homeDataMaxDays.desktop.selectableDays = homeDataMaxDays.desktop.selectableDays.filter(
|
||||
(d) => d !== day
|
||||
);
|
||||
}
|
||||
}}
|
||||
type="checkbox"
|
||||
disabled={day >= homeDataMaxDays.desktop.maxDays}
|
||||
checked={homeDataMaxDays.desktop.selectableDays.includes(day)}
|
||||
/>
|
||||
{day} Days
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<Label class="text-lg">
|
||||
<SmartPhone class="mr-2 inline h-4 w-4" />
|
||||
Mobile
|
||||
</Label>
|
||||
<div class="border-b pb-4">
|
||||
<Label for="mobilemaxdata" class="text-sm font-medium">Select Data Range</Label>
|
||||
<Select.Root
|
||||
portal={null}
|
||||
onSelectedChange={(e) => {
|
||||
homeDataMaxDays.mobile.maxDays = e.value;
|
||||
//uncheck all checkboxes greater than maxDays
|
||||
homeDataMaxDays.mobile.selectableDays = homeDataMaxDays.mobile.selectableDays.filter(
|
||||
(day) => day <= e.value
|
||||
);
|
||||
//push all days less than maxDays
|
||||
selectableDaysList.forEach((day) => {
|
||||
if (day <= e.value && !homeDataMaxDays.mobile.selectableDays.includes(day)) {
|
||||
homeDataMaxDays.mobile.selectableDays.push(day);
|
||||
}
|
||||
});
|
||||
}}
|
||||
selected={{
|
||||
value: homeDataMaxDays.mobile.maxDays,
|
||||
label: homeDataMaxDays.mobile.maxDays + " Days"
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class=" mt-2 w-[200px]" id="mobilemaxdata">
|
||||
<Select.Value bind:value={homeDataMaxDays.mobile.maxDays} placeholder="Select Range" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Max Days</Select.Label>
|
||||
{#each selectableDaysList as day}
|
||||
<Select.Item value={day} label={day + " Days"} class="text-sm font-medium">
|
||||
{day} Days
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="border-b pb-4">
|
||||
<Label for="mobiledropdown" class="text-sm font-medium">Select Dropdown Range</Label>
|
||||
<div class="mt-2 flex flex-row flex-wrap gap-x-2">
|
||||
{#each selectableDaysList as day}
|
||||
<label>
|
||||
<input
|
||||
on:change={(e) => {
|
||||
if (e.target.checked) {
|
||||
homeDataMaxDays.mobile.selectableDays.push(day);
|
||||
} else {
|
||||
homeDataMaxDays.mobile.selectableDays = homeDataMaxDays.mobile.selectableDays.filter(
|
||||
(d) => d !== day
|
||||
);
|
||||
}
|
||||
}}
|
||||
type="checkbox"
|
||||
disabled={day >= homeDataMaxDays.mobile.maxDays}
|
||||
checked={homeDataMaxDays.mobile.selectableDays.includes(day)}
|
||||
/>
|
||||
{day} Days
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<span class="text-xs font-medium text-muted-foreground">
|
||||
Select the maximum number of days to show data for. This will be used to filter the data shown on the
|
||||
homepage. The dropdown will show all the days that are less than or equal to the selected max days. The max
|
||||
value in the dropdown will be the entire value of data range it cannot be changed
|
||||
</span>
|
||||
</p>
|
||||
{#if !!dataErrorMessage}
|
||||
<p class="text-sm font-medium text-destructive">{dataErrorMessage}</p>
|
||||
{/if}
|
||||
<div class="flex w-full justify-end gap-x-2">
|
||||
<Button type="submit" disabled={formStateData === "loading"}>
|
||||
Save Data Settings
|
||||
{#if formStateData === "loading"}
|
||||
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header class="border-b">
|
||||
<Card.Title>Incidents</Card.Title>
|
||||
@@ -632,8 +853,8 @@
|
||||
<Card.Content>
|
||||
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitCategories}>
|
||||
{#each categories as cat, i}
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div class="col-span-1">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<Label for="{i}catkey" class="block w-full">Name</Label>
|
||||
<Input
|
||||
bind:value={cat.name}
|
||||
@@ -644,7 +865,7 @@
|
||||
disabled={i === 0}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative col-span-3 pr-8">
|
||||
<div class="flex-1">
|
||||
<Label for="{i}catdsc" class="block w-full">Description</Label>
|
||||
<Input
|
||||
bind:value={cat.description}
|
||||
@@ -654,9 +875,27 @@
|
||||
placeholder="Category Description"
|
||||
disabled={i === 0}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative flex flex-col items-center gap-y-2">
|
||||
<Label for="{i}vis" class="block w-full">Visibility</Label>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger disabled={i === 0} id="{i}vis">
|
||||
<Button variant="outline" class="mr-2" disabled={cat.name === "Home"}>
|
||||
{cat.isHidden ? "Private" : "Public"}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item on:click={() => (cat.isHidden = true)}>Private</DropdownMenu.Item>
|
||||
<DropdownMenu.Item on:click={() => (cat.isHidden = false)}>Public</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="absolute right-0 top-8 ml-2 h-6 w-6 p-1 "
|
||||
class="mt-4 h-6 w-6 p-1"
|
||||
disabled={i === 0}
|
||||
on:click={() => (categories = categories.filter((_, index) => index !== i))}
|
||||
>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -684,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>
|
||||
@@ -1304,6 +1352,110 @@
|
||||
</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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -233,6 +255,11 @@
|
||||
};
|
||||
});
|
||||
let shareMenusToggle = false;
|
||||
let monitoringDataMenusToggle = false;
|
||||
let monitorDataSelected;
|
||||
let monitoringDataRequest = {
|
||||
newStatus: "UP"
|
||||
};
|
||||
|
||||
function initMonitorTriggers() {
|
||||
monitorTriggers = {
|
||||
@@ -259,6 +286,71 @@
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -307,7 +399,10 @@
|
||||
shareMenusToggle = true;
|
||||
}
|
||||
const flipDurationMs = 200;
|
||||
|
||||
function openMonitoringDataMenu(m) {
|
||||
monitoringDataMenusToggle = true;
|
||||
monitorDataSelected = m;
|
||||
}
|
||||
let orderErrorMessage = "";
|
||||
function handleSort(e) {
|
||||
dropTargetStyle = {
|
||||
@@ -386,7 +481,7 @@
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Rearrange Monitors</h2>
|
||||
<div
|
||||
class="mt-4"
|
||||
class="mt-4 max-h-[80vh] overflow-y-auto"
|
||||
use:dndzone={{ items: monitors, flipDurationMs, dropTargetStyle }}
|
||||
on:consider={handleSort}
|
||||
on:finalize={handleSort}
|
||||
@@ -538,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" />
|
||||
@@ -581,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
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
colorsJ: {
|
||||
UP: "#4ead94",
|
||||
DOWN: "#ca3038",
|
||||
DEGRADED: "#e6ca61"
|
||||
DEGRADED: "#e6ca61",
|
||||
MAINTENANCE: "#6679cc"
|
||||
},
|
||||
fontJ: {
|
||||
cssSrc:
|
||||
@@ -50,7 +51,10 @@
|
||||
themeData = siteDataExtractFromDb(data.siteData, themeData);
|
||||
|
||||
if (data.siteData.colors) {
|
||||
themeData.colorsJ = data.siteData.colors;
|
||||
if (data.siteData.colors.MAINTENANCE === undefined) {
|
||||
data.siteData.colors.MAINTENANCE = "#6679cc"; // default value for MAINTENANCE
|
||||
}
|
||||
themeData.colorsJ = JSON.parse(JSON.stringify(data.siteData.colors));
|
||||
}
|
||||
if (data.siteData.font) {
|
||||
themeData.fontJ = data.siteData.font;
|
||||
@@ -296,7 +300,7 @@
|
||||
<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.
|
||||
Choose the colors for the monitor. You can choose between UP, DEGRADED, DOWN and MAINTENANCE.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -343,6 +347,20 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<ColorPicker
|
||||
bind:hex={themeData.colorsJ.MAINTENANCE}
|
||||
position="responsive"
|
||||
isAlpha={false}
|
||||
isDark={$mode == "dark"}
|
||||
--input-size="16px"
|
||||
isTextInput={true}
|
||||
label="MAINTENANCE"
|
||||
on:input={(event) => {
|
||||
themeData.colorsJ.MAINTENANCE = event.detail.hex;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<p class="font-medium">
|
||||
|
||||
@@ -275,10 +275,16 @@
|
||||
body: JSON.stringify({ action: "testTrigger", data: { trigger_id, status } })
|
||||
});
|
||||
let resp = await data.json();
|
||||
//check status code, if not 200 then show error
|
||||
if (resp.error) {
|
||||
triggers[i].testLoaders = "error";
|
||||
alert(resp.error);
|
||||
} else {
|
||||
triggers[i].testLoaders = "success";
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Error: " + error);
|
||||
} finally {
|
||||
triggers[i].testLoaders = "success";
|
||||
setTimeout(() => {
|
||||
triggers[i].testLoaders = "idle";
|
||||
}, 3000);
|
||||
@@ -426,6 +432,9 @@
|
||||
{#if trigger.testLoaders == "success"}
|
||||
<Check class="mr-1 inline h-3 w-3 text-green-500 " />
|
||||
{/if}
|
||||
{#if trigger.testLoaders == "error"}
|
||||
<X class="mr-1 inline h-3 w-3 text-red-500 " />
|
||||
{/if}
|
||||
<span class="h-4 text-xs font-medium"> Test Trigger </span>
|
||||
</Button>
|
||||
{#if $page.data.user.role != "member"}
|
||||
@@ -493,7 +502,7 @@
|
||||
newTrigger.trigger_type = "webhook";
|
||||
}}
|
||||
>
|
||||
<img src="{base}/webhooks.svg" title="webhook" class="mr-4 w-6" />
|
||||
<img src="{base}/webhooks.svg" title="webhook" class="mr-4 w-6" alt="Icon that represents webhooks" />
|
||||
Webhooks
|
||||
</Button>
|
||||
<Button
|
||||
@@ -503,7 +512,7 @@
|
||||
newTrigger.trigger_type = "discord";
|
||||
}}
|
||||
>
|
||||
<img src="{base}/discord.svg" title="discord" class="mr-4 w-6" />
|
||||
<img src="{base}/discord.svg" title="discord" class="mr-4 w-6" alt="Discord logo" />
|
||||
Discord
|
||||
</Button>
|
||||
<Button
|
||||
@@ -513,7 +522,7 @@
|
||||
newTrigger.trigger_type = "slack";
|
||||
}}
|
||||
>
|
||||
<img src="{base}/slack.svg" title="slack" class="mr-4 w-6" />
|
||||
<img src="{base}/slack.svg" title="slack" class="mr-4 w-6" alt="Slack logo" />
|
||||
Slack
|
||||
</Button>
|
||||
|
||||
@@ -524,7 +533,7 @@
|
||||
newTrigger.trigger_type = "email";
|
||||
}}
|
||||
>
|
||||
<img src="{base}/email.png" title="email" class="mr-4 w-6" />
|
||||
<img src="{base}/email.png" title="email" class="mr-4 w-6" alt="Icon that represents emails" />
|
||||
Email
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
let uptime90Day = monitor.pageData.uptime90Day;
|
||||
let incidents = {};
|
||||
let dayIncidentsFull = [];
|
||||
let homeDataMaxDays = monitor.pageData.homeDataMaxDays;
|
||||
|
||||
let dimension = {
|
||||
x1: 6,
|
||||
x2: 4
|
||||
};
|
||||
|
||||
dimension.x1 = ($page.data.isMobile ? 346 : 546) / homeDataMaxDays.maxDays;
|
||||
dimension.x2 = (4 / 6) * dimension.x1;
|
||||
|
||||
function loadIncidents() {
|
||||
axios
|
||||
@@ -91,39 +100,37 @@
|
||||
});
|
||||
}, 1000 * 0.2);
|
||||
}
|
||||
let uptimesRollers = [
|
||||
{
|
||||
text: `${l(lang, "90 Days")}`,
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 89,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp,
|
||||
value: uptime90Day
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "60 Days")}`,
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 59,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "30 Days")}`,
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 29,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "14 Days")}`,
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 13,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: `${l(lang, "7 Days")}`,
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * 6,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
},
|
||||
{
|
||||
text: l(lang, "Today"),
|
||||
startTs: monitor.pageData.startOfTheDay,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
|
||||
function returnUptimeRollers() {
|
||||
let rollers = homeDataMaxDays.selectableDays;
|
||||
//sort descending
|
||||
rollers.sort((a, b) => b - a);
|
||||
let ret = [];
|
||||
for (let i = 0; i < rollers.length; i++) {
|
||||
let roller = rollers[i];
|
||||
if (roller == 1) {
|
||||
ret.push({
|
||||
text: `${l(lang, "Today")}`,
|
||||
startTs: monitor.pageData.startOfTheDay,
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
});
|
||||
} else {
|
||||
ret.push({
|
||||
text: `${roller} ${l(lang, "Days")}`,
|
||||
startTs: monitor.pageData.startOfTheDay - 86400 * (roller - 1),
|
||||
endTs: monitor.pageData.maxDateTodayTimestamp
|
||||
});
|
||||
}
|
||||
//if last index
|
||||
if (i == 0) {
|
||||
ret[i].value = uptime90Day;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
let uptimesRollers = returnUptimeRollers();
|
||||
|
||||
//start of the week moment
|
||||
let rolledAt = 0;
|
||||
@@ -140,7 +147,10 @@
|
||||
uptimesRollers[newRolledAt].loading = true;
|
||||
|
||||
let resp = await axios.post(`${base}/api/today/aggregated`, {
|
||||
monitor: monitor,
|
||||
monitor: {
|
||||
tag: monitor.tag,
|
||||
include_degraded_in_downtime: monitor.include_degraded_in_downtime
|
||||
},
|
||||
startTs: uptimesRollers[newRolledAt].startTs,
|
||||
endTs: uptimesRollers[newRolledAt].endTs
|
||||
});
|
||||
@@ -201,7 +211,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="monitor relative grid w-full grid-cols-12 gap-2 pb-2 pt-0 md:w-[655px]">
|
||||
<div class="monitor relative grid w-full max-w-[655px] grid-cols-12 gap-2 pb-2 pt-0">
|
||||
{#if !!!embed}
|
||||
<div class="col-span-12 md:w-[546px]">
|
||||
<div class="pt-0">
|
||||
@@ -302,9 +312,9 @@
|
||||
showDailyDataModal = false;
|
||||
}}
|
||||
>
|
||||
<div class="daygrid90 flex min-h-[60px] justify-start overflow-x-auto overflow-y-hidden py-1">
|
||||
<div class="daygrid90 flex min-h-[60px] justify-between overflow-x-auto overflow-y-hidden py-1">
|
||||
{#each Object.entries(_90Day) as [ts, bar]}
|
||||
<a
|
||||
<button
|
||||
data-ts={ts}
|
||||
use:hoverAction
|
||||
on:hover={(e) => {
|
||||
@@ -313,24 +323,27 @@
|
||||
on:click={(e) => {
|
||||
dailyDataGetter(e, bar, incidents[ts]);
|
||||
}}
|
||||
href="#"
|
||||
class="oneline h-[34px] w-[6px]
|
||||
class="oneline h-[34px]
|
||||
{bar.border ? 'opacity-100' : 'opacity-20'} pb-1"
|
||||
style="width: {dimension.x1}px"
|
||||
>
|
||||
<div
|
||||
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto w-[4px] rounded-{monitor.pageData.barRoundness.toUpperCase() ==
|
||||
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto rounded-{monitor.pageData.barRoundness.toUpperCase() ==
|
||||
'SHARP'
|
||||
? 'none'
|
||||
: 'sm'}"
|
||||
style="width: {dimension.x2}px"
|
||||
></div>
|
||||
<!-- incident dot -->
|
||||
{#if !!incidents[ts]}
|
||||
<div
|
||||
class="bg-api-{incidents[
|
||||
ts
|
||||
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] left-[1px] h-[4px] w-[4px] rounded-full"
|
||||
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] h-[4px] w-[4px] rounded-full"
|
||||
style="left: {dimension.x1 / 2 - 2}px"
|
||||
></div>
|
||||
{/if}
|
||||
</a>
|
||||
</button>
|
||||
{#if bar.showDetails}
|
||||
<div class="show-hover absolute text-sm">
|
||||
<div class="text-{bar.textClass} text-xs font-semibold">
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
export let monitor;
|
||||
export let lang;
|
||||
export let selectedLang = "en";
|
||||
let theme = "light";
|
||||
let copiedLink = false;
|
||||
let embedType = "js";
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<script>
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { page } from "$app/stores";
|
||||
import { base } from "$app/paths";
|
||||
import { onMount } from "svelte";
|
||||
import GMI from "$lib/components/gmi.svelte";
|
||||
import autoAnimate from "@formkit/auto-animate";
|
||||
import Loader from "lucide-svelte/icons/loader";
|
||||
import SquareChartGantt from "lucide-svelte/icons/square-chart-gantt";
|
||||
import AlarmClockCheck from "lucide-svelte/icons/alarm-clock-check";
|
||||
import Logout from "lucide-svelte/icons/log-out";
|
||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
||||
import { l } from "$lib/i18n/client";
|
||||
|
||||
let apiError = "";
|
||||
let view = "loading";
|
||||
let localStorageKey = "user_subscription";
|
||||
|
||||
let login = {
|
||||
userEmail: "",
|
||||
verifyWithToken: "",
|
||||
verificationCode: ""
|
||||
};
|
||||
|
||||
let subscribedMonitors = [];
|
||||
|
||||
let subsInfo = {
|
||||
allMonitors: false,
|
||||
monitors: [],
|
||||
hasActiveSubscription: false
|
||||
};
|
||||
|
||||
let subscribableMonitors = $page.data.subscribableMonitors;
|
||||
let callingAPI = false;
|
||||
let alreadySubscribed = false;
|
||||
let subscriberData = null;
|
||||
|
||||
onMount(async () => {
|
||||
// Try to fetch stored subscription data from localStorage
|
||||
await fetchStoredSubscription();
|
||||
//
|
||||
});
|
||||
|
||||
async function createNewUserSubscription() {
|
||||
callingAPI = true;
|
||||
apiError = "";
|
||||
let action = "subscribe";
|
||||
|
||||
try {
|
||||
const response = await callAPI(action, {
|
||||
token: localStorage.getItem(localStorageKey),
|
||||
monitors: subsInfo.monitors,
|
||||
allMonitors: subsInfo.allMonitors
|
||||
});
|
||||
await fetchStoredSubscription();
|
||||
} catch (e) {
|
||||
apiError = e.message;
|
||||
} finally {
|
||||
callingAPI = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSubscription() {
|
||||
//use confirm
|
||||
if (!confirm("Are you sure you want to unsubscribe?")) {
|
||||
return;
|
||||
}
|
||||
callingAPI = true;
|
||||
apiError = "";
|
||||
let action = "unsubscribe";
|
||||
|
||||
try {
|
||||
await callAPI(action, {
|
||||
token: localStorage.getItem(localStorageKey)
|
||||
});
|
||||
await fetchStoredSubscription();
|
||||
} catch (e) {
|
||||
apiError = e.message;
|
||||
} finally {
|
||||
callingAPI = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function callAPI(action, data) {
|
||||
try {
|
||||
const response = await fetch(`${base}/api/subscriptions/${action}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
let error = await response.json();
|
||||
throw new Error(error.message || "An error occurred while processing your request.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error calling API:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function doLogin() {
|
||||
let action = "login";
|
||||
callingAPI = true;
|
||||
apiError = "";
|
||||
try {
|
||||
const response = await callAPI(action, {
|
||||
userEmail: login.userEmail
|
||||
});
|
||||
|
||||
if (response) {
|
||||
login.verifyWithToken = response.token;
|
||||
login.verificationCode = "";
|
||||
view = "verify_login";
|
||||
}
|
||||
} catch (e) {
|
||||
apiError = e.message;
|
||||
} finally {
|
||||
callingAPI = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyLogin() {
|
||||
let action = "verify";
|
||||
callingAPI = true;
|
||||
apiError = "";
|
||||
try {
|
||||
const response = await callAPI(action, {
|
||||
token: login.verifyWithToken,
|
||||
code: login.verificationCode
|
||||
});
|
||||
|
||||
if (response) {
|
||||
localStorage.setItem(localStorageKey, response.token);
|
||||
await fetchStoredSubscription();
|
||||
}
|
||||
} catch (e) {
|
||||
apiError = e.message;
|
||||
} finally {
|
||||
callingAPI = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStoredSubscription() {
|
||||
let action = "fetch";
|
||||
view = "loading";
|
||||
let token = localStorage.getItem(localStorageKey);
|
||||
if (!!!token) {
|
||||
view = "login";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await callAPI(action, {
|
||||
token: token
|
||||
});
|
||||
|
||||
if (response) {
|
||||
subsInfo.email = response.email;
|
||||
view = "subscribe";
|
||||
subsInfo.monitors = response.monitors;
|
||||
if (subsInfo.monitors && subsInfo.monitors.length > 0) {
|
||||
subsInfo.hasActiveSubscription = true;
|
||||
//if parsedData.monitors contains "_", set allMonitors to true
|
||||
const allMonitorsIncluded = subsInfo.monitors.includes("_");
|
||||
if (allMonitorsIncluded) {
|
||||
subsInfo.allMonitors = true;
|
||||
} else {
|
||||
subsInfo.allMonitors = false;
|
||||
subsInfo.monitors = subsInfo.monitors.filter((m) => m !== "_");
|
||||
}
|
||||
} else {
|
||||
subsInfo.allMonitors = false;
|
||||
subsInfo.monitors = [];
|
||||
subsInfo.hasActiveSubscription = false; // Updated to set hasActiveSubscription
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
view = "login";
|
||||
}
|
||||
}
|
||||
|
||||
function doLogout() {
|
||||
localStorage.removeItem(localStorageKey);
|
||||
login.userEmail = "";
|
||||
login.verificationCode = "";
|
||||
login.verifyWithToken = "";
|
||||
view = "login";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 pb-4" use:autoAnimate>
|
||||
<div class="relative mb-2 scroll-m-20 px-4 pt-4 text-xl font-medium tracking-tight">
|
||||
<p>
|
||||
{l($page.data.lang, "Manage Subscription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if view === "login"}
|
||||
<div class="flex flex-col gap-2 px-4">
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-semibold text-muted-foreground">
|
||||
{l(
|
||||
$page.data.lang,
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe."
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div class="relative">
|
||||
<Input type="email" placeholder={l($page.data.lang, "Enter your email")} bind:value={login.userEmail} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-col gap-2 px-4">
|
||||
{#if apiError}
|
||||
<p class="text-xs font-semibold text-destructive">{apiError}</p>
|
||||
{/if}
|
||||
<Button class="w-full" disabled={callingAPI || !login.userEmail} on:click={doLogin}>
|
||||
{l($page.data.lang, "Login")}
|
||||
{#if callingAPI}
|
||||
<Loader class="ml-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if view === "loading"}
|
||||
<div class="flex h-20 justify-center align-middle">
|
||||
<Loader class="mt-5 h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if view === "subscribe"}
|
||||
<div class="flex flex-col gap-2 px-4">
|
||||
{#if subsInfo.email}
|
||||
<div
|
||||
class="flex justify-between rounded-md stroke-secondary-foreground py-2 text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
<span class="mt-1 text-xs font-semibold text-muted-foreground">
|
||||
{l($page.data.lang, "You are logged in as %email", { email: subsInfo.email })}
|
||||
</span>
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class=" h-6 w-6 justify-center text-xs font-semibold text-muted-foreground"
|
||||
on:click={doLogout}
|
||||
>
|
||||
<Logout class=" h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-2">
|
||||
<label class="flex w-full cursor-pointer items-center justify-between">
|
||||
<span class="text-sm font-medium">{l($page.data.lang, "Subscribe to all monitors")}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
value=""
|
||||
class="peer sr-only"
|
||||
checked={subsInfo.allMonitors}
|
||||
on:change={(e) => {
|
||||
subsInfo.allMonitors = e.target.checked;
|
||||
if (e.target.checked) {
|
||||
subsInfo.monitors = [];
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
|
||||
></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{#if subscribableMonitors.length > 0 && !subsInfo.allMonitors}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="px-4">
|
||||
<span class="text-xs font-semibold text-muted-foreground">
|
||||
{l($page.data.lang, "Please select specific monitors to receive updates from.")}
|
||||
</span>
|
||||
</p>
|
||||
<div class="flex max-h-96 flex-col gap-2 overflow-y-auto px-4">
|
||||
{#each subscribableMonitors as monitor}
|
||||
<div class="flex items-center justify-between gap-2 border-b pb-2 last:border-0">
|
||||
<label for={monitor.tag} class="flex gap-x-2 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
{#if !!monitor.image}
|
||||
<GMI src={monitor.image} classList="mt-1 h-4 w-4" />
|
||||
{:else}
|
||||
<SquareChartGantt class="mt-1 h-4 w-4" />
|
||||
{/if}
|
||||
{monitor.name}
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
on:change={(e) => {
|
||||
if (e.target.checked) {
|
||||
subsInfo.monitors.push(monitor.tag);
|
||||
} else {
|
||||
subsInfo.monitors = subsInfo.monitors.filter((tag) => tag !== monitor.tag);
|
||||
}
|
||||
}}
|
||||
id={monitor.tag}
|
||||
value={monitor.tag}
|
||||
bind:group={subsInfo.monitors}
|
||||
class="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-2 flex flex-col gap-2 px-4">
|
||||
{#if apiError}
|
||||
<p class="text-xs font-semibold text-destructive">{apiError}</p>
|
||||
{/if}
|
||||
<Button class="w-full" disabled={callingAPI} on:click={createNewUserSubscription}>
|
||||
{#if subsInfo.monitors.length > 0}
|
||||
{l($page.data.lang, "Update Subscription")}
|
||||
{:else}
|
||||
{l($page.data.lang, "Subscribe")}
|
||||
{/if}
|
||||
|
||||
{#if callingAPI}
|
||||
<Loader class="ml-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if view === "verify_login"}
|
||||
<div class="flex flex-col gap-2 px-4">
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="bounce-left mb-2 h-8 justify-start pl-1.5 text-xs text-muted-foreground"
|
||||
on:click={() => (view = "login")}
|
||||
>
|
||||
<ChevronLeft class="arrow mr-1 h-5 w-5" />
|
||||
{l($page.data.lang, "Change Email")}
|
||||
</Button>
|
||||
<p class="mb-2 text-xs text-muted-foreground">
|
||||
{l($page.data.lang, "We have sent a code to your email. Please enter it below to confirm your login")}
|
||||
</p>
|
||||
|
||||
<Input type="text" placeholder={l($page.data.lang, "Enter the code")} bind:value={login.verificationCode} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-col gap-2 px-4">
|
||||
{#if apiError}
|
||||
<p class="text-xs font-semibold text-destructive">{apiError}</p>
|
||||
{/if}
|
||||
<Button class="w-full" disabled={callingAPI || !login.verificationCode} on:click={verifyLogin}>
|
||||
{l($page.data.lang, "Confirm Login")}
|
||||
|
||||
{#if callingAPI}
|
||||
<Loader class="ml-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,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, es, it, faIR } from "date-fns/locale";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
|
||||
const locales = {
|
||||
@@ -19,6 +19,10 @@ const locales = {
|
||||
"pt-BR": ptBR,
|
||||
tr,
|
||||
"nb-NO": nb,
|
||||
pl,
|
||||
es,
|
||||
it,
|
||||
fa: faIR,
|
||||
};
|
||||
|
||||
const f = function (date, formatStr, locale, tz) {
|
||||
|
||||
+28
-1
@@ -1,16 +1,25 @@
|
||||
{
|
||||
"%status for %duration": "%status für %duration",
|
||||
"120 Days": "120 Tage",
|
||||
"14 Days": "14 Tage",
|
||||
"150 Days": "150 Tage",
|
||||
"180 Days": "180 Tage",
|
||||
"30 Days": "30 Tage",
|
||||
"60 Days": "60 Tage",
|
||||
"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",
|
||||
"All Systems are in Maintenance": "Alle Systeme sind in Wartung",
|
||||
"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 +28,9 @@
|
||||
"Days": "Tage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
|
||||
"Embed": "Einbetten",
|
||||
"Enter the code": "Code eingeben",
|
||||
"Enter your email": "E-Mail-Adresse eingeben",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Geben Sie Ihre E-Mail-Adresse ein, um sich anzumelden. Wenn Sie nicht abonniert haben, werden Sie zum Abonnieren aufgefordert.",
|
||||
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
|
||||
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
|
||||
"IDENTIFIED": "IDENTIFIZIERT",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Einige Systeme sind in Wartung",
|
||||
"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 +72,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"
|
||||
}
|
||||
|
||||
+28
-1
@@ -5,12 +5,21 @@
|
||||
"60 Days": "60 Dage",
|
||||
"7 Days": "7 Dage",
|
||||
"90 Days": "90 Dage",
|
||||
"120 Days": "120 Dage",
|
||||
"150 Days": "150 Dage",
|
||||
"180 Days": "180 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",
|
||||
"All Systems are in Maintenance": "Alle systemer er i vedligeholdelse",
|
||||
"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 +28,9 @@
|
||||
"Days": "Dage",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
|
||||
"Embed": "Integrer",
|
||||
"Enter the code": "Indtast koden",
|
||||
"Enter your email": "Indtast din email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Indtast din email for at logge ind. Hvis du ikke er abonnent, vil du blive bedt om at abonnere.",
|
||||
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
|
||||
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
|
||||
"IDENTIFIED": "IDENTIFICERET",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Nogle Systemer er i Vedligeholdelse",
|
||||
"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 +72,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"
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,16 +1,25 @@
|
||||
{
|
||||
"%status for %duration": "%status for %duration",
|
||||
"120 Days": "120 Days",
|
||||
"14 Days": "14 Days",
|
||||
"150 Days": "150 Days",
|
||||
"180 Days": "180 Days",
|
||||
"30 Days": "30 Days",
|
||||
"60 Days": "60 Days",
|
||||
"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",
|
||||
"All Systems are in Maintenance": "All Systems are in Maintenance",
|
||||
"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 +28,9 @@
|
||||
"Days": "Days",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"Embed": "Embed",
|
||||
"Enter the code": "Enter the code",
|
||||
"Enter your email": "Enter your email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.",
|
||||
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
|
||||
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Some Systems in Maintenance",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
@@ -53,13 +72,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"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"%status for %duration": "%status durante %duration",
|
||||
"120 Days": "120 Días",
|
||||
"14 Days": "14 Días",
|
||||
"150 Days": "150 Días",
|
||||
"180 Days": "180 Días",
|
||||
"30 Days": "30 Días",
|
||||
"60 Days": "60 Días",
|
||||
"7 Days": "7 Días",
|
||||
"90 Days": "90 Días",
|
||||
"Availability per Component": "Disponibilidad por componente",
|
||||
"All Systems are Degraded": "Todos los sistemas están degradados",
|
||||
"All Systems are Down": "Todos los sistemas están caídos",
|
||||
"All Systems are Operational": "Todos los sistemas están operativos",
|
||||
"Back": "Volver",
|
||||
"Badge Copied": "Distintivo copiado",
|
||||
"Badge": "Distintivo",
|
||||
"Browse Events": "Ver eventos",
|
||||
"Change Email": "Cambiar correo electrónico",
|
||||
"Code Copied": "Código copiado",
|
||||
"Confirm Login": "Confirmar inicio de sesión",
|
||||
"Copy Code": "Copiar código",
|
||||
"Copy Link": "Copiar enlace",
|
||||
"DEGRADED": "DEGRADADO",
|
||||
"DOWN": "CAÍDO",
|
||||
"Dark": "Oscuro",
|
||||
"Days": "Días",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incrusta este monitor usando <script> o <iframe> en tu aplicación.",
|
||||
"Embed": "Incrustar",
|
||||
"Enter the code": "Ingresa el código",
|
||||
"Enter your email": "Ingresa tu correo electrónico",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Ingresa tu correo electrónico para iniciar sesión. Si no estás suscrito, se te pedirá que te suscribas.",
|
||||
"Get SVG badge for this monitor": "Obtener distintivo SVG para este monitor",
|
||||
"Get a LIVE Status for this monitor": "Obtener estado EN VIVO para este monitor",
|
||||
"IDENTIFIED": "IDENTIFICADO",
|
||||
"INVESTIGATING": "INVESTIGANDO",
|
||||
"Incident Updates": "Actualizaciones de incidentes",
|
||||
"LIVE Status": "Estado EN VIVO",
|
||||
"Lasted for about %lastedFor": "Duró aproximadamente %lastedFor",
|
||||
"Light": "Claro",
|
||||
"Link Copied": "Enlace copiado",
|
||||
"Login": "Iniciar sesión",
|
||||
"MAINTENANCE": "MANTENIMIENTO",
|
||||
"MONITORING": "MONITOREANDO",
|
||||
"Maintenance Completed": "Mantenimiento completado",
|
||||
"Maintenance in Progress": "Mantenimiento en progreso",
|
||||
"Manage Subscription": "Gestionar suscripción",
|
||||
"Mode": "Modo",
|
||||
"No Data": "Sin datos",
|
||||
"No Incident in %date": "Sin incidentes en %date",
|
||||
"No Incidents": "Sin incidentes",
|
||||
"No Monitor Found": "No se encontró monitor",
|
||||
"No Updates Yet": "Aún no hay actualizaciones",
|
||||
"Ongoing Incidents": "Incidentes en curso",
|
||||
"Pinging": "Haciendo ping",
|
||||
"Please select specific monitors to receive updates from.": "Por favor selecciona monitores específicos para recibir actualizaciones.",
|
||||
"RESOLVED": "RESUELTO",
|
||||
"Recent Incidents": "Incidentes recientes",
|
||||
"Recent Maintenances": "Mantenimientos recientes",
|
||||
"Share this monitor using a link with others": "Comparte este monitor mediante un enlace con otros",
|
||||
"Share": "Compartir",
|
||||
"Some Systems are not working as expected": "Algunos sistemas no funcionan como se espera",
|
||||
"Some Systems Degraded": "Algunos sistemas degradados",
|
||||
"Some Systems Down": "Algunos sistemas caídos",
|
||||
"Standard": "Estándar",
|
||||
"Started %startedAt, lasted for %lastedFor": "Comenzó el %startedAt, duró %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Comenzó el %startedAt, aún en curso",
|
||||
"Started %startedAt, will last for %lastedFor more": "Comenzó el %startedAt, durará %lastedFor más",
|
||||
"Starts %startedAt, will last for %lastedFor": "Empieza el %startedAt, durará %lastedFor",
|
||||
"Starts %startedAt": "Empieza el %startedAt",
|
||||
"Status OK": "Estado OK",
|
||||
"Status": "Estado",
|
||||
"Subscribe": "Suscribirse",
|
||||
"Subscribe to Updates": "Suscribirse a actualizaciones",
|
||||
"Subscribe to all monitors": "Suscribirse a todos los monitores",
|
||||
"Switch to your timezone": "Cambiar a tu zona horaria",
|
||||
"Theme": "Tema",
|
||||
"There are no upcoming maintenances": "No hay mantenimientos próximos",
|
||||
"Timezone set to %tz": "Zona horaria configurada a %tz",
|
||||
"Today": "Hoy",
|
||||
"UP": "ACTIVO",
|
||||
"Update Subscription": "Actualizar suscripción",
|
||||
"Upcoming Maintenance": "Mantenimiento próximo",
|
||||
"Upcoming Maintenances": "Mantenimientos próximos",
|
||||
"Updates": "Actualizaciones",
|
||||
"Uptime": "Tiempo en línea",
|
||||
"View in detail": "Ver en detalle",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Hemos enviado un código a tu correo. Por favor, ingrésalo abajo para confirmar tu inicio de sesión",
|
||||
"You are logged in as %email": "Has iniciado sesión como %email"
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"%status for %duration": "%status به مدت %duration",
|
||||
"120 Days": "۱۲۰ روزه",
|
||||
"14 Days": "۱۴ روزه",
|
||||
"150 Days": "۱۵۰ روزه",
|
||||
"180 Days": "۱۸۰ روزه",
|
||||
"30 Days": "۳۰ روزه",
|
||||
"60 Days": "۶۰ روزه",
|
||||
"7 Days": "۷ روزه",
|
||||
"90 Days": "۹۰ روزه",
|
||||
"Availability per Component": "دسترسپذیری هر کامپوننت",
|
||||
"All Systems are Degraded": "همه سامانهها دچار اختلالاند",
|
||||
"All Systems are Down": "همه سامانهها از کار افتادهاند",
|
||||
"All Systems are Operational": "همه سامانهها در حال کار هستند",
|
||||
"All Systems are in Maintenance": "همه سامانهها در حال تعمیر هستند",
|
||||
"Back": "بازگشت",
|
||||
"Badge Copied": "نشان کپی شد",
|
||||
"Badge": "نشان",
|
||||
"Browse Events": "مشاهده رویدادها",
|
||||
"Change Email": "تغییر ایمیل",
|
||||
"Code Copied": "کد کپی شد",
|
||||
"Confirm Login": "تأیید ورود",
|
||||
"Copy Code": "کپی کد",
|
||||
"Copy Link": "کپی لینک",
|
||||
"DEGRADED": "اختلال",
|
||||
"DOWN": "از کار افتاده",
|
||||
"Dark": "تاریک",
|
||||
"Days": "روزها",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "این مانیتور را با استفاده از <script> یا <iframe> در برنامهتان قرار دهید.",
|
||||
"Embed": "درج",
|
||||
"Enter the code": "کد را وارد کنید",
|
||||
"Enter your email": "ایمیل خود را وارد کنید",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "برای ورود، ایمیل خود را وارد کنید. اگر عضو نیستید، از شما خواسته میشود عضو شوید.",
|
||||
"Get SVG badge for this monitor": "دریافت نشان SVG برای این مانیتور",
|
||||
"Get a LIVE Status for this monitor": "دریافت وضعیت زنده این مانیتور",
|
||||
"IDENTIFIED": "شناسایی شده",
|
||||
"INVESTIGATING": "در حال بررسی",
|
||||
"Incident Updates": "بهروزرسانیهای حادثه",
|
||||
"LIVE Status": "وضعیت زنده",
|
||||
"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 وجود ندارد",
|
||||
"No Incidents": "هیچ حادثهای وجود ندارد",
|
||||
"No Monitor Found": "هیچ مانیتوری یافت نشد",
|
||||
"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": "برخی سامانهها از کار افتادهاند",
|
||||
"Some Systems in Maintenance": "برخی سامانهها در حال تعمیر هستند",
|
||||
"Standard": "معمولی",
|
||||
"Started %startedAt, lasted for %lastedFor": "شروع در %startedAt، به مدت %lastedFor ادامه داشت",
|
||||
"Started %startedAt, still ongoing": "شروع در %startedAt، هنوز ادامه دارد",
|
||||
"Started %startedAt, will last for %lastedFor more": "شروع در %startedAt، تا %lastedFor دیگر ادامه خواهد داشت",
|
||||
"Starts %startedAt, will last for %lastedFor": "شروع در %startedAt، به مدت %lastedFor ادامه دارد",
|
||||
"Starts %startedAt": "شروع در %startedAt",
|
||||
"Status OK": "وضعیت خوب",
|
||||
"Status": "وضعیت",
|
||||
"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": "مشاهده جزئیات",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "کدی به ایمیل شما ارسال کردیم. لطفاً برای تأیید ورود آن را وارد کنید.",
|
||||
"You are logged in as %email": "شما با ایمیل %email وارد شدهاید"
|
||||
}
|
||||
+28
-1
@@ -5,12 +5,21 @@
|
||||
"60 Days": "60 jours",
|
||||
"7 Days": "7 jours",
|
||||
"90 Days": "90 jours",
|
||||
"120 Days": "120 jours",
|
||||
"150 Days": "150 jours",
|
||||
"180 Days": "180 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",
|
||||
"All Systems are in Maintenance": "Tous les systèmes sont en maintenance",
|
||||
"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 +28,9 @@
|
||||
"Days": "Jours",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
|
||||
"Embed": "Intégrer",
|
||||
"Enter the code": "Entrez le code",
|
||||
"Enter your email": "Entrez votre e-mail",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Entrez votre e-mail pour vous connecter. Si vous n'êtes pas abonné, vous serez invité à vous abonner.",
|
||||
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
|
||||
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
|
||||
"IDENTIFIED": "Identifié",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Certains systèmes sont en maintenance",
|
||||
"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 +72,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"
|
||||
}
|
||||
|
||||
+28
-1
@@ -5,12 +5,21 @@
|
||||
"60 Days": "60 दिन",
|
||||
"7 Days": "7 दिन",
|
||||
"90 Days": "90 दिन",
|
||||
"120 Days": "120 दिन",
|
||||
"150 Days": "150 दिन",
|
||||
"180 Days": "180 दिन",
|
||||
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
|
||||
"All Systems are Degraded": "सभी सिस्टम क्षीण हैं",
|
||||
"All Systems are Down": "सभी सिस्टम डाउन हैं",
|
||||
"All Systems are Operational": "सभी सिस्टम कार्यरत हैं",
|
||||
"All Systems are in Maintenance": "सभी सिस्टम रखरखाव में हैं",
|
||||
"Back": "वापस",
|
||||
"Badge Copied": "बैज कॉपी किया गया",
|
||||
"Badge": "बैज",
|
||||
"Browse Events": "ईवेंट ब्राउज़ करें",
|
||||
"Change Email": "ईमेल बदलें",
|
||||
"Code Copied": "कोड कॉपी किया गया",
|
||||
"Confirm Login": "लॉगिन पुष्टि करें",
|
||||
"Copy Code": "कोड कॉपी करें",
|
||||
"Copy Link": "लिंक कॉपी करें",
|
||||
"DEGRADED": "क्षीण",
|
||||
@@ -19,6 +28,9 @@
|
||||
"Days": "दिन",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
|
||||
"Embed": "एम्बेड",
|
||||
"Enter the code": "कोड दर्ज करें",
|
||||
"Enter your email": "अपना ईमेल दर्ज करें",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "लॉगिन करने के लिए अपना ईमेल दर्ज करें। यदि आप सब्सक्राइब नहीं हैं, तो आपको सब्सक्राइब करने के लिए कहा जाएगा।",
|
||||
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
|
||||
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
|
||||
"IDENTIFIED": "पहचाना गया",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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": "कुछ सिस्टम डाउन हैं",
|
||||
"Some Systems in Maintenance": "कुछ सिस्टम रखरखाव में हैं",
|
||||
"Standard": "मानक",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
|
||||
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
|
||||
@@ -53,13 +72,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 के रूप में लॉग इन हैं"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"%status for %duration": "%status per %duration",
|
||||
"120 Days": "120 Giorni",
|
||||
"14 Days": "14 Giorni",
|
||||
"150 Days": "150 Giorni",
|
||||
"180 Days": "180 Giorni",
|
||||
"30 Days": "30 Giorni",
|
||||
"60 Days": "60 Giorni",
|
||||
"7 Days": "7 Giorni",
|
||||
"90 Days": "90 Giorni",
|
||||
"Availability per Component": "Disponibilità per componente",
|
||||
"All Systems are Degraded": "Tutti i sistemi sono degradati",
|
||||
"All Systems are Down": "Tutti i sistemi sono inattivi",
|
||||
"All Systems are Operational": "Tutti i sistemi sono operativi",
|
||||
"All Systems are in Maintenance": "Tutti i sistemi sono in manutenzione",
|
||||
"Back": "Indietro",
|
||||
"Badge Copied": "Badge copiato",
|
||||
"Badge": "Badge",
|
||||
"Browse Events": "Sfoglia eventi",
|
||||
"Change Email": "Cambia email",
|
||||
"Code Copied": "Codice copiato",
|
||||
"Confirm Login": "Conferma accesso",
|
||||
"Copy Code": "Copia codice",
|
||||
"Copy Link": "Copia link",
|
||||
"DEGRADED": "DEGRADATO",
|
||||
"DOWN": "NON DISPONIBILE",
|
||||
"Dark": "Scuro",
|
||||
"Days": "Giorni",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incorpora questo monitor usando <script> o <iframe> nella tua app.",
|
||||
"Embed": "Incorpora",
|
||||
"Enter the code": "Inserisci il codice",
|
||||
"Enter your email": "Inserisci la tua email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Inserisci la tua email per accedere. Se non sei iscritto, ti verrà chiesto di iscriverti.",
|
||||
"Get SVG badge for this monitor": "Ottieni badge SVG per questo monitor",
|
||||
"Get a LIVE Status for this monitor": "Ottieni lo stato LIVE per questo monitor",
|
||||
"IDENTIFIED": "IDENTIFICATO",
|
||||
"INVESTIGATING": "IN ANALISI",
|
||||
"Incident Updates": "Aggiornamenti incidenti",
|
||||
"LIVE Status": "Stato LIVE",
|
||||
"Lasted for about %lastedFor": "Durato circa %lastedFor",
|
||||
"Light": "Chiaro",
|
||||
"Link Copied": "Link copiato",
|
||||
"Login": "Accedi",
|
||||
"MAINTENANCE": "MANUTENZIONE",
|
||||
"MONITORING": "MONITORAGGIO",
|
||||
"Maintenance Completed": "Manutenzione completata",
|
||||
"Maintenance in Progress": "Manutenzione in corso",
|
||||
"Manage Subscription": "Gestisci iscrizione",
|
||||
"Mode": "Modalità",
|
||||
"No Data": "Nessun dato",
|
||||
"No Incident in %date": "Nessun incidente in %date",
|
||||
"No Incidents": "Nessun incidente",
|
||||
"No Monitor Found": "Nessun monitor trovato",
|
||||
"No Updates Yet": "Nessun aggiornamento ancora",
|
||||
"Ongoing Incidents": "Incidenti in corso",
|
||||
"Pinging": "Ping in corso",
|
||||
"Please select specific monitors to receive updates from.": "Seleziona i monitor specifici da cui ricevere aggiornamenti.",
|
||||
"RESOLVED": "RISOLTO",
|
||||
"Recent Incidents": "Incidenti recenti",
|
||||
"Recent Maintenances": "Manutenzioni recenti",
|
||||
"Share this monitor using a link with others": "Condividi questo monitor tramite un link con altri",
|
||||
"Share": "Condividi",
|
||||
"Some Systems are not working as expected": "Alcuni sistemi non funzionano come previsto",
|
||||
"Some Systems Degraded": "Alcuni sistemi degradati",
|
||||
"Some Systems Down": "Alcuni sistemi inattivi",
|
||||
"Some Systems in Maintenance": "Alcuni sistemi in manutenzione",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Iniziato il %startedAt, durato %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Iniziato il %startedAt, ancora in corso",
|
||||
"Started %startedAt, will last for %lastedFor more": "Iniziato il %startedAt, durerà ancora %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor": "Inizia il %startedAt, durerà %lastedFor",
|
||||
"Starts %startedAt": "Inizia il %startedAt",
|
||||
"Status OK": "Stato OK",
|
||||
"Status": "Stato",
|
||||
"Subscribe": "Iscriviti",
|
||||
"Subscribe to Updates": "Iscriviti agli aggiornamenti",
|
||||
"Subscribe to all monitors": "Iscriviti a tutti i monitor",
|
||||
"Switch to your timezone": "Passa al tuo fuso orario",
|
||||
"Theme": "Tema",
|
||||
"There are no upcoming maintenances": "Non ci sono manutenzioni imminenti",
|
||||
"Timezone set to %tz": "Fuso orario impostato su %tz",
|
||||
"Today": "Oggi",
|
||||
"UP": "ATTIVO",
|
||||
"Update Subscription": "Aggiorna iscrizione",
|
||||
"Upcoming Maintenance": "Manutenzione imminente",
|
||||
"Upcoming Maintenances": "Manutenzioni imminenti",
|
||||
"Updates": "Aggiornamenti",
|
||||
"Uptime": "Tempo di attività",
|
||||
"View in detail": "Visualizza nel dettaglio",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Abbiamo inviato un codice alla tua email. Inseriscilo qui sotto per confermare l'accesso",
|
||||
"You are logged in as %email": "Sei connesso come %email"
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,16 +1,25 @@
|
||||
{
|
||||
"%status for %duration": "%duration の間 %status",
|
||||
"120 Days": "120日間",
|
||||
"14 Days": "14日間",
|
||||
"150 Days": "150日間",
|
||||
"180 Days": "180日間",
|
||||
"30 Days": "30日間",
|
||||
"60 Days": "60日間",
|
||||
"7 Days": "7日間",
|
||||
"90 Days": "90日間",
|
||||
"Availability per Component": "コンポーネントごとの可用性",
|
||||
"All Systems are Degraded": "すべてのシステムが劣化しています",
|
||||
"All Systems are Down": "すべてのシステムがダウンしています",
|
||||
"All Systems are Operational": "すべてのシステムが正常に稼働しています",
|
||||
"All Systems are in Maintenance": "すべてのシステムがメンテナンス中です",
|
||||
"Back": "戻る",
|
||||
"Badge Copied": "バッジがコピーされました",
|
||||
"Badge": "バッジ",
|
||||
"Browse Events": "イベントを閲覧",
|
||||
"Change Email": "メールを変更",
|
||||
"Code Copied": "コードがコピーされました",
|
||||
"Confirm Login": "ログインを確認",
|
||||
"Copy Code": "コードをコピー",
|
||||
"Copy Link": "リンクをコピー",
|
||||
"DEGRADED": "劣化",
|
||||
@@ -19,6 +28,9 @@
|
||||
"Days": "日間",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "このモニターをアプリに <script> または <iframe> を使用して埋め込む。",
|
||||
"Embed": "埋め込み",
|
||||
"Enter the code": "コードを入力してください",
|
||||
"Enter your email": "メールアドレスを入力してください",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "ログインするにはメールアドレスを入力してください。登録されていない場合は、購読を促されます。",
|
||||
"Get SVG badge for this monitor": "このモニターのSVGバッジを取得",
|
||||
"Get a LIVE Status for this monitor": "このモニターのライブステータスを取得",
|
||||
"IDENTIFIED": "識別済み",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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": "一部のシステムがダウンしています",
|
||||
"Some Systems in Maintenance": "いくつかのシステムがメンテナンス中です",
|
||||
"Standard": "標準",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
|
||||
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
|
||||
@@ -53,13 +72,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 としてログインしています"
|
||||
}
|
||||
|
||||
+57
-30
@@ -1,17 +1,26 @@
|
||||
{
|
||||
"%status for %duration": "%duration 동안 %status 상태였음",
|
||||
"14 Days": "14 일",
|
||||
"30 Days": "30 ",
|
||||
"60 Days": "60 일",
|
||||
"7 Days": "7 일",
|
||||
"90 Days": "90 일",
|
||||
"Availability per Component": "항목당 가용성",
|
||||
"%status for %duration": "%duration 동안 %status 상태",
|
||||
"120 Days": "120일",
|
||||
"14 Days": "14일",
|
||||
"150 Days": "150일",
|
||||
"180 Days": "180일",
|
||||
"30 Days": "30일",
|
||||
"60 Days": "60일",
|
||||
"7 Days": "7일",
|
||||
"90 Days": "90일",
|
||||
"Availability per Component": "항목 당 가용성",
|
||||
"All Systems are Degraded": "모든 시스템이 성능 저하 상태입니다",
|
||||
"All Systems are Down": "모든 시스템이 중단되었습니다",
|
||||
"All Systems are Operational": "모든 시스템이 정상 작동 중입니다",
|
||||
"All Systems are in Maintenance": "모든 시스템이 유지 보수 중입니다",
|
||||
"Back": "뒤로",
|
||||
"Badge Copied": "배지 복사됨",
|
||||
"Badge Copied": "배지 복사 됨",
|
||||
"Badge": "배지",
|
||||
"Browse Events": "이벤트 찾아보기",
|
||||
"Code Copied": "소스코드 복사됨",
|
||||
"Copy Code": "소스코드 복사",
|
||||
"Change Email": "이메일 변경",
|
||||
"Code Copied": "소스 코드 복사됨",
|
||||
"Confirm Login": "로그인 확인",
|
||||
"Copy Code": "소스 코드 복사",
|
||||
"Copy Link": "링크 복사",
|
||||
"DEGRADED": "성능 저하됨",
|
||||
"DOWN": "중단",
|
||||
@@ -19,47 +28,65 @@
|
||||
"Days": "일",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "<script> 나 <iframe> 태그를 사용하여 내 앱에 이 모니터링을 임베딩 해보세요.",
|
||||
"Embed": "임베드",
|
||||
"Get SVG badge for this monitor": "이 모니터링 항목에 대한 SVG 배지를 구해보세요.",
|
||||
"Get a LIVE Status for this monitor": "이 모니터링 항목에 대한 실시간 상태를 구해보세요.",
|
||||
"IDENTIFIED": "확인됨",
|
||||
"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": "확인 됨",
|
||||
"INVESTIGATING": "조사 중",
|
||||
"Incident Updates": "사건 보고서",
|
||||
"LIVE Status": "실시간 상태",
|
||||
"Lasted for about %lastedFor": "%lastedFor 동안 지속",
|
||||
"Light": "라이트",
|
||||
"Link Copied": "링크 복사됨",
|
||||
"Light": "밝게",
|
||||
"Link Copied": "링크 복사 됨",
|
||||
"Login": "로그인",
|
||||
"MAINTENANCE": "유지보수",
|
||||
"MONITORING": "감시 중",
|
||||
"Maintenance Completed": "유지보수 완료",
|
||||
"Maintenance in Progress": "유지보수 진행 중",
|
||||
"Maintenance Completed": "유지 보수 완료",
|
||||
"Maintenance in Progress": "유지 보수 진행 중",
|
||||
"Manage Subscription": "구독 관리",
|
||||
"Mode": "모드",
|
||||
"No Data": "데이터 없음",
|
||||
"No Incident in %date": "%date 에는 아무 사건도 없었음",
|
||||
"No Incident in %date": "%date에는 아무 사건도 없었음",
|
||||
"No Incidents": "사건 없음",
|
||||
"No Monitor Found": "모니터링 항목을 찾을 수 없음",
|
||||
"No Updates Yet": "아직 아무 업데이트도 없음",
|
||||
"Ongoing Incidents": "진행중 사건",
|
||||
"Ongoing Incidents": "진행 중 사건",
|
||||
"Pinging": "핑 요청",
|
||||
"RESOLVED": "해결됨",
|
||||
"Please select specific monitors to receive updates from.": "업데이트를 받을 특정 모니터를 선택하세요.",
|
||||
"RESOLVED": "해결 됨",
|
||||
"Recent Incidents": "최근 사건",
|
||||
"Recent Maintenances": "최근 유지보수",
|
||||
"Recent Maintenances": "최근 유지 보수",
|
||||
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
|
||||
"Share": "공유",
|
||||
"Some Systems are not working as expected": "일부 시스템이 예상대로 작동하지 않습니다",
|
||||
"Some Systems Degraded": "일부 시스템이 성능 저하 상태입니다",
|
||||
"Some Systems Down": "일부 시스템이 중단되었습니다",
|
||||
"Some Systems in Maintenance": "일부 시스템이 유지 보수 중입니다",
|
||||
"Standard": "기본",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
|
||||
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
|
||||
"Started %startedAt, will last for %lastedFor more": "%startedAt 전에 시작되었으며, %lastedFor 더 지속될 예정입니다",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
|
||||
"Starts %startedAt": "%startedAt 에 시작되었습니다",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt에 시작되었으며, %lastedFor 동안 지속되고 있습니다",
|
||||
"Started %startedAt, still ongoing": "%startedAt에 시작되었으며, 아직 진행 중입니다",
|
||||
"Started %startedAt, will last for %lastedFor more": "%startedAt에 시작되었으며, %lastedFor 간 더 지속될 예정입니다",
|
||||
"Starts %startedAt, will last for %lastedFor": "%startedAt에 시작되며, %lastedFor 간 지속될 예정입니다",
|
||||
"Starts %startedAt": "%startedAt에 시작되었습니다",
|
||||
"Status OK": "양호",
|
||||
"Status": "상태",
|
||||
"Switch to your timezone": "귀하의 시간대로 변경",
|
||||
"Subscribe": "구독",
|
||||
"Subscribe to Updates": "업데이트 구독하기",
|
||||
"Subscribe to all monitors": "모든 모니터 구독",
|
||||
"Switch to your timezone": "내 시간대로 변경",
|
||||
"Theme": "테마",
|
||||
"Timezone set to %tz": "시간대 설정 %tz",
|
||||
"There are no upcoming maintenances": "예정된 유지 보수가 없습니다",
|
||||
"Timezone set to %tz": "%tz(으)로 시간대 설정",
|
||||
"Today": "오늘",
|
||||
"UP": "작동 중",
|
||||
"Upcoming Maintenance": "예정된 유지보수",
|
||||
"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로 로그인했습니다"
|
||||
}
|
||||
|
||||
@@ -54,5 +54,21 @@
|
||||
{
|
||||
"code": "nb-NO",
|
||||
"name": "Norsk Bokmål"
|
||||
},
|
||||
{
|
||||
"code": "pl",
|
||||
"name": "Polski"
|
||||
},
|
||||
{
|
||||
"code": "es",
|
||||
"name": "Español"
|
||||
},
|
||||
{
|
||||
"code": "it",
|
||||
"name": "Italiano"
|
||||
},
|
||||
{
|
||||
"code": "fa",
|
||||
"name": "Persian (فارسی)"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,12 +5,21 @@
|
||||
"60 Days": "60 dager",
|
||||
"7 Days": "7 dager",
|
||||
"90 Days": "90 dager",
|
||||
"120 Days": "120 dager",
|
||||
"150 Days": "150 dager",
|
||||
"180 Days": "180 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",
|
||||
"All Systems are in Maintenance": "Alle systemer er til vedlikehold",
|
||||
"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 +28,9 @@
|
||||
"Days": "Dager",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Bygg inn denne monitoren ved å bruke <script> eller <iframe> i appen din.",
|
||||
"Embed": "Bygg inn",
|
||||
"Enter the code": "Skriv inn koden",
|
||||
"Enter your email": "Skriv inn din e-post",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Skriv inn din e-post for å logge inn. Hvis du ikke er abonnent, vil du bli bedt om å abonnere.",
|
||||
"Get SVG badge for this monitor": "Hent SVG-merke for denne monitoren",
|
||||
"Get a LIVE Status for this monitor": "Få en LIVE-status for denne monitoren",
|
||||
"IDENTIFIED": "IDENTIFISERT",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Noen systemer er til vedlikehold",
|
||||
"Standard": "Standard",
|
||||
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
|
||||
@@ -53,13 +72,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"
|
||||
}
|
||||
|
||||
+28
-1
@@ -1,16 +1,25 @@
|
||||
{
|
||||
"%status for %duration": "%status voor %duration",
|
||||
"120 Days": "120 dagen",
|
||||
"14 Days": "14 dagen",
|
||||
"150 Days": "150 dagen",
|
||||
"180 Days": "180 dagen",
|
||||
"30 Days": "30 dagen",
|
||||
"60 Days": "60 dagen",
|
||||
"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",
|
||||
"All Systems are in Maintenance": "Alle systemen zijn in onderhoud",
|
||||
"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 +28,9 @@
|
||||
"Days": "Dagen",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Sluit deze monitor in met behulp van <script> of <iframe> in je app.",
|
||||
"Embed": "Insluiten",
|
||||
"Enter the code": "Voer de code in",
|
||||
"Enter your email": "Voer je e-mailadres in",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Voer je e-mailadres in om in te loggen. Als je nog niet geabonneerd bent, word je gevraagd om je te abonneren.",
|
||||
"Get SVG badge for this monitor": "SVG-badge ophalen voor deze monitor",
|
||||
"Get a LIVE Status for this monitor": "LIVE-status ophalen voor deze monitor",
|
||||
"IDENTIFIED": "GEIDENTIFICEERD",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Sommige systemen zijn in onderhoud",
|
||||
"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 +72,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"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"%status for %duration": "%status przez %duration",
|
||||
"120 Days": "120 dni",
|
||||
"14 Days": "14 dni",
|
||||
"150 Days": "150 dni",
|
||||
"180 Days": "180 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",
|
||||
"All Systems are in Maintenance": "Wszystkie systemy są w konserwacji",
|
||||
"Back": "Wstecz",
|
||||
"Badge Copied": "Odznaka skopiowana",
|
||||
"Badge": "Odznaka",
|
||||
"Browse Events": "Przeglądaj zdarzenia",
|
||||
"Change Email": "Zmień email",
|
||||
"Code Copied": "Kod skopiowany",
|
||||
"Confirm Login": "Potwierdź logowanie",
|
||||
"Copy Code": "Skopiuj kod",
|
||||
"Copy Link": "Skopiuj link",
|
||||
"DEGRADED": "OGRANICZONA",
|
||||
"DOWN": "NIEDOSTĘPNY",
|
||||
"Dark": "Ciemny",
|
||||
"Days": "Dni",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Osadź ten monitor za pomocą <script> lub <iframe> w swojej aplikacji.",
|
||||
"Embed": "Osadź",
|
||||
"Enter the code": "Wprowadź kod",
|
||||
"Enter your email": "Wprowadź swój email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Wprowadź swój email, aby się zalogować. Jeśli nie masz subskrypcji, zostaniesz poproszony o jej utworzenie.",
|
||||
"Get SVG badge for this monitor": "Pobierz odznakę SVG dla tego monitora",
|
||||
"Get a LIVE Status for this monitor": "Uzyskaj STATUS NA ŻYWO tego monitora",
|
||||
"IDENTIFIED": "ZIDENTYFIKOWANE",
|
||||
"INVESTIGATING": "BADANE",
|
||||
"Incident Updates": "Aktualizacje incydentu",
|
||||
"LIVE Status": "Status NA ŻYWO",
|
||||
"Lasted for about %lastedFor": "Trwało około %lastedFor",
|
||||
"Light": "Jasny",
|
||||
"Link Copied": "Link skopiowany",
|
||||
"Login": "Zaloguj się",
|
||||
"MAINTENANCE": "KONSERWACJA",
|
||||
"MONITORING": "MONITOROWANE",
|
||||
"Maintenance Completed": "Konserwacja zakończona",
|
||||
"Maintenance in Progress": "Trwa konserwacja",
|
||||
"Manage Subscription": "Zarządzaj subskrypcją",
|
||||
"Mode": "Tryb",
|
||||
"No Data": "Brak danych",
|
||||
"No Incident in %date": "Brak incydentów w dniu %date",
|
||||
"No Incidents": "Brak incydentów",
|
||||
"No Monitor Found": "Nie znaleziono monitora",
|
||||
"No Updates Yet": "Brak aktualizacji",
|
||||
"Ongoing Incidents": "Trwające incydenty",
|
||||
"Pinging": "Pingowanie",
|
||||
"Please select specific monitors to receive updates from.": "Wybierz konkretne monitory, z których chcesz otrzymywać aktualizacje.",
|
||||
"RESOLVED": "ROZWIĄZANE",
|
||||
"Recent Incidents": "Ostatnie incydenty",
|
||||
"Recent Maintenances": "Ostatnie konserwacje",
|
||||
"Share this monitor using a link with others": "Udostępnij ten monitor za pomocą linku innym osobom",
|
||||
"Share": "Udostępnij",
|
||||
"Some Systems are not working as expected": "Niektóre systemy nie działają zgodnie z oczekiwaniami",
|
||||
"Some Systems Degraded": "Niektóre systemy działają z ograniczeniami",
|
||||
"Some Systems Down": "Niektóre systemy są niedostępne",
|
||||
"Some Systems in Maintenance": "Niektóre systemy są w konserwacji",
|
||||
"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"
|
||||
}
|
||||
@@ -1,16 +1,25 @@
|
||||
{
|
||||
"%status for %duration": "%status por %duration",
|
||||
"120 Days": "120 Dias",
|
||||
"14 Days": "14 Dias",
|
||||
"150 Days": "150 Dias",
|
||||
"180 Days": "180 Dias",
|
||||
"30 Days": "30 Dias",
|
||||
"60 Days": "60 Dias",
|
||||
"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",
|
||||
"All Systems are in Maintenance": "Todos os Sistemas estão em Manutenção",
|
||||
"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 +28,9 @@
|
||||
"Days": "Dias",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
|
||||
"Embed": "Incorporar",
|
||||
"Enter the code": "Digite o código",
|
||||
"Enter your email": "Digite seu e-mail",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Digite seu e-mail para fazer login. Se você não estiver inscrito, você será solicitado a se inscrever.",
|
||||
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
|
||||
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
|
||||
"IDENTIFIED": "IDENTIFICADO",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Alguns Sistemas estão em Manutenção",
|
||||
"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 +72,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"
|
||||
}
|
||||
|
||||
+60
-33
@@ -1,65 +1,92 @@
|
||||
{
|
||||
"%status for %duration": "%status в течение %duration",
|
||||
"14 Days": "14 Дней",
|
||||
"30 Days": "30 Дней",
|
||||
"60 Days": "60 Дней",
|
||||
"7 Days": "7 Дней",
|
||||
"90 Days": "90 Дней",
|
||||
"120 Days": "120 дней",
|
||||
"14 Days": "14 дней",
|
||||
"150 Days": "150 дней",
|
||||
"180 Days": "180 дней",
|
||||
"30 Days": "30 дней",
|
||||
"60 Days": "60 дней",
|
||||
"7 Days": "7 дней",
|
||||
"90 Days": "90 дней",
|
||||
"Availability per Component": "Доступность по компонентам",
|
||||
"All Systems are Degraded": "Системы работают нестабильно",
|
||||
"All Systems are Down": "Системы не работают",
|
||||
"All Systems are Operational": "Системы работают нормально",
|
||||
"All Systems are in Maintenance": "Системы на обслуживании",
|
||||
"Back": "Назад",
|
||||
"Badge Copied": "Значок скопирован",
|
||||
"Badge": "Значок",
|
||||
"Browse Events": "Просмотр событий",
|
||||
"Change Email": "Изменить электронную почту",
|
||||
"Code Copied": "Код скопирован",
|
||||
"Copy Code": "Копировать код",
|
||||
"Copy Link": "Копировать ссылку",
|
||||
"DEGRADED": "УХУДШЕНО",
|
||||
"DOWN": "НЕДОСТУПНО",
|
||||
"Dark": "Темная",
|
||||
"Days": "Дней",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Встройте этот монитор используя <script> или <iframe> в ваше приложение.",
|
||||
"Embed": "Встроить",
|
||||
"Confirm Login": "Подтвердите вход",
|
||||
"Copy Code": "Скопировать код",
|
||||
"Copy Link": "Скопировать ссылку",
|
||||
"DEGRADED": "НЕСТАБИЛЬНО",
|
||||
"DOWN": "НЕ РАБОТАЕТ",
|
||||
"Dark": "Тёмная",
|
||||
"Days": "дней",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Встройте этот монитор с помощью <script> или <iframe> в ваше приложение.",
|
||||
"Embed": "Вставить",
|
||||
"Enter the code": "Введите код",
|
||||
"Enter your email": "Введите ваш email",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Введите ваш email для входа. Если вы не подписаны, вам будет предложено подписаться.",
|
||||
"Get SVG badge for this monitor": "Получить SVG значок для этого монитора",
|
||||
"Get a LIVE Status for this monitor": "Получить ЖИВОЙ статус для этого монитора",
|
||||
"Get a LIVE Status for this monitor": "Получить текущий статус для этого монитора",
|
||||
"IDENTIFIED": "ВЫЯВЛЕНО",
|
||||
"INVESTIGATING": "ИССЛЕДУЕТСЯ",
|
||||
"Incident Updates": "Обновления инцидента",
|
||||
"LIVE Status": "ЖИВОЙ статус",
|
||||
"Lasted for about %lastedFor": "Длилось около %lastedFor",
|
||||
"INVESTIGATING": "ИЗУЧАЕТСЯ",
|
||||
"Incident Updates": "Обновления инцидентов",
|
||||
"LIVE Status": "Текущий статус",
|
||||
"Lasted for about %lastedFor": "Продолжалось около %lastedFor",
|
||||
"Light": "Светлая",
|
||||
"Link Copied": "Ссылка скопирована",
|
||||
"Login": "Войти",
|
||||
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
|
||||
"MONITORING": "МОНИТОРИНГ",
|
||||
"Maintenance Completed": "Техническое обслуживание завершено",
|
||||
"Maintenance in Progress": "Техническое обслуживание",
|
||||
"Maintenance Completed": "Обслуживание завершено",
|
||||
"Maintenance in Progress": "Обслуживание в процессе",
|
||||
"Manage Subscription": "Управление подпиской",
|
||||
"Mode": "Режим",
|
||||
"No Data": "Нет данных",
|
||||
"No Incident in %date": "Нет инцидентов на %date",
|
||||
"No Incidents": "Нет инцидентов",
|
||||
"No Incident in %date": "Нет инцидентов за %date",
|
||||
"No Incidents": "Инцидентов нет",
|
||||
"No Monitor Found": "Монитор не найден",
|
||||
"No Updates Yet": "Пока нет обновлений",
|
||||
"No Updates Yet": "Обновлений пока нет",
|
||||
"Ongoing Incidents": "Текущие инциденты",
|
||||
"Pinging": "Пингуется",
|
||||
"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": "Некоторые системы недоступны",
|
||||
"Some Systems in Maintenance": "Некоторые системы на обслуживании",
|
||||
"Standard": "Стандартная",
|
||||
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
|
||||
"Started %startedAt, will last for %lastedFor more": "Начнется около %startedAt, продлится еще около %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
|
||||
"Starts %startedAt": "Начнется через %startedAt",
|
||||
"Started %startedAt, lasted for %lastedFor": "Началось %startedAt, продолжалось %lastedFor",
|
||||
"Started %startedAt, still ongoing": "Началось %startedAt, всё ещё продолжается",
|
||||
"Started %startedAt, will last for %lastedFor more": "Началось %startedAt, продлится ещё около %lastedFor",
|
||||
"Starts %startedAt, will last for %lastedFor": "Начнётся %startedAt, продлится %lastedFor",
|
||||
"Starts %startedAt": "Начнётся %startedAt",
|
||||
"Status OK": "Статус ОК",
|
||||
"Status": "Статус",
|
||||
"Subscribe": "Подписаться",
|
||||
"Subscribe to Updates": "Подписаться на обновления",
|
||||
"Subscribe to all monitors": "Подписаться на все мониторы",
|
||||
"Switch to your timezone": "Переключиться на ваш часовой пояс",
|
||||
"Theme": "Тема",
|
||||
"Timezone set to %tz": "Часовой пояс установлен на %tz",
|
||||
"There are no upcoming maintenances": "Предстоящих обслуживаний нет",
|
||||
"Timezone set to %tz": "Часовой пояс установлен: %tz",
|
||||
"Today": "Сегодня",
|
||||
"UP": "РАБОТАЕТ",
|
||||
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
|
||||
"Update Subscription": "Обновить подписку",
|
||||
"Upcoming Maintenance": "Предстоящее обслуживание",
|
||||
"Upcoming Maintenances": "Предстоящие обслуживания",
|
||||
"Updates": "Обновления",
|
||||
"Uptime": "Время работы",
|
||||
"View in detail": "Подробный просмотр"
|
||||
"Uptime": "Время безотказной работы",
|
||||
"View in detail": "Подробнее",
|
||||
"We have sent a code to your email. Please enter it below to confirm your login": "Мы отправили код на вашу почту. Введите его ниже для подтверждения входа",
|
||||
"You are logged in as %email": "Вы вошли как %email"
|
||||
}
|
||||
|
||||
+28
-1
@@ -5,12 +5,21 @@
|
||||
"60 Days": "60 Gün",
|
||||
"7 Days": "7 Gün",
|
||||
"90 Days": "90 Gün",
|
||||
"120 Days": "120 Gün",
|
||||
"150 Days": "150 Gün",
|
||||
"180 Days": "180 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",
|
||||
"All Systems are in Maintenance": "Tüm Sistemler Bakımda",
|
||||
"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 +28,9 @@
|
||||
"Days": "Günler",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
|
||||
"Embed": "Yerleştir",
|
||||
"Enter the code": "Kodu girin",
|
||||
"Enter your email": "E-posta adresinizi girin",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Giriş yapmak için e-posta adresinizi girin. Abone değilseniz, abone olmanız istenecektir.",
|
||||
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
|
||||
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
|
||||
"IDENTIFIED": "TANIMLANDI",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Bazı Sistemler Bakımda",
|
||||
"Standard": "Standart",
|
||||
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
|
||||
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
|
||||
@@ -53,13 +72,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"
|
||||
}
|
||||
|
||||
+28
-1
@@ -5,12 +5,21 @@
|
||||
"60 Days": "60 ngày",
|
||||
"7 Days": "7 ngày",
|
||||
"90 Days": "90 ngày",
|
||||
"120 Days": "120 ngày",
|
||||
"150 Days": "150 ngày",
|
||||
"180 Days": "180 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",
|
||||
"All Systems are in Maintenance": "Tất cả hệ thống đang bảo trì",
|
||||
"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 +28,9 @@
|
||||
"Days": "Ngày",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Nhúng trình theo dõi này vào ứng dụng của bạn bằng <script> hoặc <iframe>.",
|
||||
"Embed": "Nhúng",
|
||||
"Enter the code": "Nhập mã",
|
||||
"Enter your email": "Nhập email của bạn",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Nhập email của bạn để đăng nhập. Nếu bạn chưa đăng ký, bạn sẽ được nhắc đăng ký.",
|
||||
"Get SVG badge for this monitor": "Lấy huy hiệu SVG cho trình theo dõi này",
|
||||
"Get a LIVE Status for this monitor": "Lấy trạng thái TRỰC TIẾP cho trình theo dõi này",
|
||||
"IDENTIFIED": "Đã xác định",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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",
|
||||
"Some Systems in Maintenance": "Một số hệ thống đang bảo trì",
|
||||
"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 +72,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"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
{
|
||||
"%status for %duration": "%duration 内的 %status",
|
||||
"120 Days": "120天",
|
||||
"14 Days": "14天",
|
||||
"150 Days": "150天",
|
||||
"180 Days": "180天",
|
||||
"30 Days": "30天",
|
||||
"60 Days": "60天",
|
||||
"7 Days": "7天",
|
||||
"90 Days": "90天",
|
||||
"Availability per Component": "每个组件的可用性",
|
||||
"All Systems are Degraded": "所有系统性能下降",
|
||||
"All Systems are Down": "所有系统不可用",
|
||||
"All Systems are Operational": "所有系统运行正常",
|
||||
"All Systems are in Maintenance": "所有系统正在维护",
|
||||
"Back": "返回",
|
||||
"Badge Copied": "徽章已复制",
|
||||
"Badge": "徽章",
|
||||
"Browse Events": "浏览事件",
|
||||
"Change Email": "更改邮箱",
|
||||
"Code Copied": "代码已复制",
|
||||
"Confirm Login": "确认登录",
|
||||
"Copy Code": "复制代码",
|
||||
"Copy Link": "复制链接",
|
||||
"DEGRADED": "性能下降",
|
||||
@@ -19,6 +28,9 @@
|
||||
"Days": "天",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "使用 <script> 或 <iframe> 将此监控嵌入您的应用。",
|
||||
"Embed": "嵌入",
|
||||
"Enter the code": "输入验证码",
|
||||
"Enter your email": "输入您的邮箱",
|
||||
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "输入您的邮箱进行登录。如果您尚未订阅,将会提示您进行订阅。",
|
||||
"Get SVG badge for this monitor": "获取此监控的SVG徽章",
|
||||
"Get a LIVE Status for this monitor": "获取此监控的实时状态",
|
||||
"IDENTIFIED": "已识别",
|
||||
@@ -28,10 +40,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 +54,16 @@
|
||||
"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": "部分系统不可用",
|
||||
"Some Systems in Maintenance": "部分系统正在维护",
|
||||
"Standard": "标准",
|
||||
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
|
||||
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
|
||||
@@ -53,13 +72,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 登录"
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ async function closeIncident(alert, comment) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const SITE = "./config/site.yaml";
|
||||
const UP = "UP";
|
||||
const DOWN = "DOWN";
|
||||
const DEGRADED = "DEGRADED";
|
||||
const MAINTENANCE = "MAINTENANCE";
|
||||
const NO_DATA = "NO_DATA";
|
||||
const API_TIMEOUT = 10 * 1000; // 10 seconds
|
||||
const AnalyticsProviders = {
|
||||
|
||||
@@ -1,227 +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, ValidateEmail } 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: "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: "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",
|
||||
},
|
||||
];
|
||||
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);
|
||||
@@ -278,9 +166,123 @@ 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;
|
||||
let maintenanceCount = 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++;
|
||||
} else if (status.status === "MAINTENANCE") {
|
||||
maintenanceCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = upsCount + degradedCount + downCount + maintenanceCount;
|
||||
|
||||
if (total === 0) {
|
||||
return {
|
||||
text: "No Systems",
|
||||
upsPercentage: 0,
|
||||
degradedPercentage: 0,
|
||||
downsPercentage: 0,
|
||||
maintenancePercentage: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let upsPercentage = Math.round((upsCount / total) * 100);
|
||||
let degradedPercentage = Math.round((degradedCount / total) * 100);
|
||||
let downsPercentage = Math.round((downCount / total) * 100);
|
||||
let maintenancePercentage = Math.round((maintenanceCount / total) * 100);
|
||||
|
||||
let message = "";
|
||||
|
||||
// Determine message based on the combination of system states
|
||||
if (upsCount > 0 && degradedCount === 0 && downCount === 0 && maintenanceCount == 0) {
|
||||
// UP=1|DOWN=0|DEGRADED=0|MAINTENANCE=0
|
||||
message = "All Systems are Operational";
|
||||
} else if (upsCount === 0 && degradedCount > 0 && downCount === 0 && maintenanceCount == 0) {
|
||||
// UP=0|DOWN=0|DEGRADED=1|MAINTENANCE=0
|
||||
message = "All Systems are Degraded";
|
||||
} else if (upsCount === 0 && degradedCount === 0 && downCount > 0 && maintenanceCount == 0) {
|
||||
// UP=0|DOWN=1|DEGRADED=0|MAINTENANCE=0
|
||||
message = "All Systems are Down";
|
||||
} else if (upsCount === 0 && degradedCount === 0 && downCount === 0 && maintenanceCount > 0) {
|
||||
message = "All Systems are in Maintenance";
|
||||
} else if (
|
||||
(upsCount > 0 && degradedCount > 0 && downCount > 0 && maintenanceCount > 0) ||
|
||||
(upsCount === 0 && degradedCount > 0 && downCount > 0 && maintenanceCount > 0)
|
||||
) {
|
||||
// UP=1|DOWN=1|DEGRADED=1|MAINTENANCE=1 or UP=0|DOWN=1|DEGRADED=1|MAINTENANCE=1
|
||||
message = "Some Systems are not working as expected";
|
||||
} else if (upsCount > 0 && degradedCount === 0 && downCount > 0 && maintenanceCount == 0) {
|
||||
// UP=1|DOWN=1|DEGRADED=0|MAINTENANCE=0
|
||||
message = "Some Systems Down";
|
||||
} else if (upsCount > 0 && degradedCount > 0 && downCount === 0 && maintenanceCount == 0) {
|
||||
// UP=1|DOWN=0|DEGRADED=1|MAINTENANCE=0
|
||||
message = "Some Systems Degraded";
|
||||
} else if (upsCount > 0 && degradedCount === 0 && downCount === 0 && maintenanceCount > 0) {
|
||||
// UP=1|DOWN=0|DEGRADED=0|MAINTENANCE=1
|
||||
message = "Some Systems in Maintenance";
|
||||
}
|
||||
|
||||
//if percentage is not 100 sum, then add remaining to up
|
||||
if (upsPercentage + degradedPercentage + downsPercentage + maintenancePercentage < 100) {
|
||||
upsPercentage = 100 - (degradedPercentage + downsPercentage + maintenancePercentage);
|
||||
}
|
||||
return {
|
||||
text: message,
|
||||
upsPercentage,
|
||||
degradedPercentage,
|
||||
downsPercentage,
|
||||
maintenancePercentage
|
||||
};
|
||||
};
|
||||
|
||||
export const GetMonitorsParsed = async (query) => {
|
||||
// Retrieve monitors from the database based on the provided query
|
||||
const rawMonitors = await db.getMonitors(query);
|
||||
@@ -402,6 +404,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) {
|
||||
@@ -415,7 +511,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,
|
||||
@@ -441,6 +537,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, {
|
||||
@@ -468,11 +577,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)
|
||||
@@ -622,6 +733,7 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
|
||||
UP: 0,
|
||||
DOWN: 0,
|
||||
DEGRADED: 0,
|
||||
MAINTENANCE: 0,
|
||||
NO_DATA: 0,
|
||||
};
|
||||
}
|
||||
@@ -640,6 +752,7 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
|
||||
UP: group.UP,
|
||||
DOWN: group.DOWN,
|
||||
DEGRADED: group.DEGRADED,
|
||||
MAINTENANCE: group.MAINTENANCE,
|
||||
NO_DATA: group.NO_DATA,
|
||||
}));
|
||||
};
|
||||
@@ -671,6 +784,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,
|
||||
};
|
||||
@@ -696,6 +813,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);
|
||||
};
|
||||
|
||||
@@ -739,9 +876,9 @@ export const RemoveIncidentMonitor = async (incident_id, monitor_tag) => {
|
||||
};
|
||||
|
||||
export const AddIncidentMonitor = async (incident_id, monitor_tag, monitor_impact) => {
|
||||
//monitor_impact must be DOWN or DEGRADED or NONE
|
||||
if (!["DOWN", "DEGRADED"].includes(monitor_impact)) {
|
||||
throw new Error("Monitor impact must be either DOWN, DEGRADED ");
|
||||
//monitor_impact must be DOWN or DEGRADED or MAINTENANCE or NONE
|
||||
if (!["DOWN", "DEGRADED", "MAINTENANCE"].includes(monitor_impact)) {
|
||||
throw new Error("Monitor impact must be either DOWN, DEGRADED, MAINTENANCE");
|
||||
}
|
||||
|
||||
//check if monitor exists
|
||||
@@ -756,6 +893,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);
|
||||
};
|
||||
|
||||
@@ -783,6 +925,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 = {
|
||||
@@ -808,7 +955,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
|
||||
@@ -862,6 +1013,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);
|
||||
|
||||
@@ -1003,7 +1193,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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1258,6 +1448,74 @@ export const DeleteMonitorCompletelyUsingTag = async (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");
|
||||
@@ -1347,3 +1605,92 @@ 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: data.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,
|
||||
};
|
||||
};
|
||||
|
||||
//updateSubscriptionTriggerStatus
|
||||
export const UpdateSubscriptionTriggerStatus = async (id, status) => {
|
||||
return await db.updateSubscriptionTriggerStatus(id, status);
|
||||
};
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
// @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: "homeDataMaxDays",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
];
|
||||
@@ -109,7 +109,7 @@ export function IsValidColors(colors) {
|
||||
return false;
|
||||
}
|
||||
if (typeof colors !== "object") return false;
|
||||
const validColorKeys = ["UP", "DOWN", "DEGRADED"];
|
||||
const validColorKeys = ["UP", "DOWN", "DEGRADED", "MAINTENANCE"];
|
||||
for (const key of validColorKeys) {
|
||||
if (typeof colors[key] !== "string" || !/^#[0-9A-Fa-f]{6}$/.test(colors[key])) return false;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -41,6 +42,10 @@ async function manualIncident(monitor) {
|
||||
for (let i = 0; i < impactArr.length; i++) {
|
||||
const element = impactArr[i];
|
||||
|
||||
if (element.monitor_impact === "MAINTENANCE") {
|
||||
impact = "MAINTENANCE";
|
||||
break;
|
||||
}
|
||||
if (element.monitor_impact === "DOWN") {
|
||||
impact = "DOWN";
|
||||
break;
|
||||
@@ -83,7 +88,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,13 +106,16 @@ 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") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "HEARTBEAT") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
} else if (monitor.monitor_type === "GAMEDIG") {
|
||||
realTimeData[startOfMinute] = await serviceClient.execute();
|
||||
}
|
||||
|
||||
manualData = await manualIncident(monitor);
|
||||
@@ -139,7 +147,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,
|
||||
|
||||
@@ -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({
|
||||
@@ -370,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,
|
||||
@@ -390,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);
|
||||
}
|
||||
@@ -603,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)
|
||||
@@ -940,6 +1046,232 @@ class DbImpl {
|
||||
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;
|
||||
|
||||
@@ -92,15 +92,27 @@ const seedSiteData = {
|
||||
UP: "#67ab95",
|
||||
DOWN: "#ca3038",
|
||||
DEGRADED: "#e6ca61",
|
||||
MAINTENANCE: "#6679cc",
|
||||
},
|
||||
font: {
|
||||
cssSrc: "",
|
||||
family: "",
|
||||
},
|
||||
categories: [{ name: "Home", description: "Monitors for Home Page" }],
|
||||
categories: [{ name: "Home", description: "Monitors for Home Page", isHidden: false }],
|
||||
homeIncidentCount: 5,
|
||||
homeIncidentStartTimeWithin: 30,
|
||||
homeDataMaxDays: {
|
||||
desktop: {
|
||||
maxDays: 90,
|
||||
selectableDays: [1, 7, 14, 30, 60, 90],
|
||||
},
|
||||
mobile: {
|
||||
maxDays: 90,
|
||||
selectableDays: [1, 7, 14, 30, 60, 90],
|
||||
},
|
||||
},
|
||||
kenerTheme: "default",
|
||||
showSiteStatus: "NO",
|
||||
};
|
||||
|
||||
export default seedSiteData;
|
||||
|
||||
@@ -64,7 +64,7 @@ class Discord {
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ class Email {
|
||||
return await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
console.error("Error sending email via SMTP", error);
|
||||
return error;
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class Slack {
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +93,12 @@ class Webhook {
|
||||
body: this.transformData(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error from webhook`);
|
||||
throw new Error(`Error from webhook: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error sending webhook", error);
|
||||
return error;
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-3
@@ -78,13 +78,16 @@ function getCountOfSimilarStatuesEnd(arr, statusType) {
|
||||
return count;
|
||||
}
|
||||
|
||||
const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
const FetchData = async function (site, monitor, localTz, selectedLang, lang, isMobile) {
|
||||
const secondsInDay = 24 * 60 * 60;
|
||||
//get offset from utc in minutes
|
||||
const nowUTC = GetMinuteStartNowTimestampUTC();
|
||||
const midnightUTC = GetDayStartTimestampUTC(nowUTC);
|
||||
|
||||
let deviceType = isMobile ? "mobile" : "desktop";
|
||||
let homeDataMaxDays = site.homeDataMaxDays[deviceType];
|
||||
|
||||
const midnightTz = BeginningOfDay({ timeZone: localTz });
|
||||
const midnight90DaysAgoTz = midnightTz - 90 * 24 * 60 * 60;
|
||||
const midnight90DaysAgoTz = midnightTz - (homeDataMaxDays.maxDays - 1) * 24 * 60 * 60;
|
||||
const NO_DATA = "No Data";
|
||||
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(nowUTC) - midnightTz) / 60);
|
||||
const maxDateTodayTimestampTz = BeginningOfMinute({ timeZone: localTz });
|
||||
@@ -122,6 +125,11 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
summaryDuration = getSummaryDuration(dayData.DOWN, selectedLang);
|
||||
summaryStatus = "DOWN";
|
||||
}
|
||||
if (dayData.MAINTENANCE > 0) {
|
||||
cssClass = returnStatusClass(dayData.MAINTENANCE, dayData.total, StatusObj.MAINTENANCE, site.barStyle);
|
||||
summaryDuration = getSummaryDuration(dayData.MAINTENANCE, selectedLang);
|
||||
summaryStatus = "MAINTENANCE";
|
||||
}
|
||||
if (dayData.NO_DATA === dayData.total) {
|
||||
cssClass = StatusObj.NO_DATA;
|
||||
summaryStatus = NO_DATA;
|
||||
@@ -175,6 +183,11 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
summaryStatus = "DOWN";
|
||||
summaryColorClass = "api-down";
|
||||
}
|
||||
if (!!lastRow && lastRow.status == "MAINTENANCE") {
|
||||
summaryDuration = getSummaryDuration(getCountOfSimilarStatuesEnd(todayDataDb, "MAINTENANCE"), selectedLang);
|
||||
summaryStatus = "MAINTENANCE";
|
||||
summaryColorClass = "api-maintenance";
|
||||
}
|
||||
if (lastRow.status === "NO_DATA") {
|
||||
summaryStatus = NO_DATA;
|
||||
summaryColorClass = "api-nodata";
|
||||
@@ -198,6 +211,7 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
midnight90DaysAgo: midnight90DaysAgoTz,
|
||||
maxDateTodayTimestamp: maxDateTodayTimestampTz,
|
||||
startOfTheDay: midnightTz,
|
||||
homeDataMaxDays,
|
||||
};
|
||||
};
|
||||
export { FetchData };
|
||||
|
||||
@@ -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;
|
||||
@@ -4,27 +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) {
|
||||
data.type = REALTIME;
|
||||
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 + 500, maxTime);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class GroupCall {
|
||||
@@ -39,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
|
||||
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
|
||||
import { GetLastHeartbeat } from "../controllers/controller.js";
|
||||
import { Cron } from "croner";
|
||||
|
||||
class HeartbeatCall {
|
||||
monitor;
|
||||
@@ -11,12 +12,37 @@ class HeartbeatCall {
|
||||
}
|
||||
|
||||
async execute() {
|
||||
let nowMinute = GetMinuteStartNowTimestampUTC();
|
||||
let nowMinute = GetMinuteStartNowTimestampUTC(); // current time in seconds (minute start)
|
||||
let latestData = await GetLastHeartbeat(this.monitor.tag);
|
||||
if (!latestData) {
|
||||
return {};
|
||||
}
|
||||
let latency = nowMinute - latestData.timestamp;
|
||||
|
||||
// Use croner to calculate expected heartbeat time
|
||||
let expectedTime;
|
||||
try {
|
||||
const cronJob = Cron(this.monitor.cron);
|
||||
const prevDate = cronJob.previousRun();
|
||||
expectedTime = Math.floor(prevDate.getTime() / 1000); // seconds
|
||||
} catch (err) {
|
||||
return {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
type: ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
// If heartbeat was received after or at expected time, it's UP
|
||||
if (latestData.timestamp >= expectedTime) {
|
||||
return {
|
||||
status: UP,
|
||||
latency: nowMinute - latestData.timestamp,
|
||||
type: REALTIME,
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate how late the expected heartbeat is
|
||||
let latency = nowMinute - expectedTime;
|
||||
let downRemainingMinutes = Number(this.monitor.type_data.downRemainingMinutes);
|
||||
if (latency > downRemainingMinutes * 60) {
|
||||
return {
|
||||
@@ -25,6 +51,7 @@ class HeartbeatCall {
|
||||
type: REALTIME,
|
||||
};
|
||||
}
|
||||
|
||||
let degradedRemainingMinutes = Number(this.monitor.type_data.degradedRemainingMinutes);
|
||||
if (latency > degradedRemainingMinutes * 60) {
|
||||
return {
|
||||
@@ -33,6 +60,7 @@ class HeartbeatCall {
|
||||
type: REALTIME,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: UP,
|
||||
latency: latency,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<link rel="preload" as="image" href="{{logo_url}}" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
</head>
|
||||
<body
|
||||
style="
|
||||
background-color: rgb(243, 244, 246);
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
"
|
||||
>
|
||||
<!--$-->
|
||||
<div
|
||||
style="
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
line-height: 1px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
"
|
||||
>
|
||||
Your verification code: {{email_code}}
|
||||
</div>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 8px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
padding: 24px;
|
||||
max-width: 600px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width: 100%">
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="margin-top: 8px; margin-bottom: 32px; text-align: center"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
alt="{{brand_name}}"
|
||||
height="40"
|
||||
src="{{logo_url}}"
|
||||
style="
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
"
|
||||
width="120"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h1
|
||||
style="
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: rgb(31, 41, 55);
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
Verification Code
|
||||
</h1>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
To complete your {{action}} for {{brand_name}} updates, please use the
|
||||
verification code below:
|
||||
</p>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background-color: rgb(249, 250, 251);
|
||||
border-width: 1px;
|
||||
border-color: rgb(229, 231, 235);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<p
|
||||
style="
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
color: rgb(31, 41, 55);
|
||||
margin: 0px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
"
|
||||
>
|
||||
{{email_code}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(75, 85, 99);
|
||||
margin-bottom: 24px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
This code will expire in 5 minutes. If you didn't request this code, you
|
||||
can safely ignore this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--7--><!--/$-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,248 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<link rel="preload" as="image" href="{{logo_url}}" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
</head>
|
||||
<body
|
||||
style="
|
||||
background-color: rgb(243, 244, 246);
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
"
|
||||
>
|
||||
<!--$-->
|
||||
<div
|
||||
style="
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
line-height: 1px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
"
|
||||
>
|
||||
{{title}}
|
||||
</div>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width: 100%">
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background-color: #e4e5ec;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
>
|
||||
<tbody style="width: 100%">
|
||||
<tr style="width: 100%">
|
||||
<td data-id="__react-email-column" style="text-align: center">
|
||||
<img
|
||||
alt="{{brand_name}}"
|
||||
src="{{logo_url}}"
|
||||
style="
|
||||
height: auto;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
"
|
||||
width="80"
|
||||
/>
|
||||
<p
|
||||
style="
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #191919;
|
||||
margin: 8px 0 0 0;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
{{brand_name}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
padding-top: 32px;
|
||||
padding-bottom: 32px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h1
|
||||
style="
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: rgb(31, 41, 55);
|
||||
margin-bottom: 16px;
|
||||
"
|
||||
>
|
||||
{{title}}
|
||||
</h1>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(55, 65, 81);
|
||||
margin-bottom: 16px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
Dear Valued Customer,
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(55, 65, 81);
|
||||
margin-bottom: 16px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
We would like to provide you with an update regarding the current system
|
||||
status.
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
font-size: 16px;
|
||||
color: rgb(55, 65, 81);
|
||||
margin-bottom: 16px;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
"
|
||||
>
|
||||
<strong>Update:</strong>
|
||||
{{update_message}}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="{{incident_url}}"
|
||||
style="
|
||||
background-color: rgb(22, 163, 74);
|
||||
color: rgb(255, 255, 255);
|
||||
font-weight: 700;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
text-decoration-line: none;
|
||||
text-align: center;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
line-height: 100%;
|
||||
text-decoration: none;
|
||||
max-width: 100%;
|
||||
mso-padding-alt: 0px;
|
||||
padding: 12px 24px 12px 24px;
|
||||
"
|
||||
target="_blank"
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%; mso-text-raise: 18" hidden
|
||||
>   </i
|
||||
><!
|
||||
[endif]--></span
|
||||
><span
|
||||
style="
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
line-height: 120%;
|
||||
mso-padding-alt: 0px;
|
||||
mso-text-raise: 9px;
|
||||
"
|
||||
>View Incident Details</span
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i style="mso-font-width: 400%" hidden
|
||||
>   ​</i
|
||||
><!
|
||||
[endif]--></span
|
||||
></a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr
|
||||
style="
|
||||
border-top-width: 1px;
|
||||
border-color: rgb(209, 213, 219);
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-top: 1px solid #eaeaea;
|
||||
"
|
||||
/>
|
||||
<footer>
|
||||
<p style="text-align: center; color: #6b7280; font-size: 16px; margin: 24px 0 0 0">
|
||||
Thank you,<br />The {{brand_name}} Team
|
||||
</p>
|
||||
</footer>
|
||||
<div style="height: 32px"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--7--><!--/$-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -149,6 +149,7 @@ const StatusObj = {
|
||||
UP: "api-up",
|
||||
DEGRADED: "api-degraded",
|
||||
DOWN: "api-down",
|
||||
MAINTENANCE: "api-maintenance",
|
||||
NO_DATA: "api-nodata",
|
||||
};
|
||||
// @ts-ignore
|
||||
@@ -303,6 +304,13 @@ function ValidateEmail(email) {
|
||||
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,
|
||||
@@ -333,4 +341,5 @@ export {
|
||||
GetDbType,
|
||||
HashString,
|
||||
ValidateEmail,
|
||||
GenerateRandomNumber,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -2,61 +2,63 @@
|
||||
import i18n from "$lib/i18n/server";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { base } from "$app/paths";
|
||||
import {
|
||||
GetAllSiteData,
|
||||
IsSetupComplete,
|
||||
IsLoggedInSession
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
import MobileDetect from "mobile-detect";
|
||||
import { GetAllSiteData, IsSetupComplete, IsLoggedInSession } 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");
|
||||
}
|
||||
const userAgent = request.headers.get("user-agent");
|
||||
const md = new MobileDetect(userAgent);
|
||||
const isMobile = !!md.mobile();
|
||||
let isSetupComplete = await IsSetupComplete();
|
||||
if (!isSetupComplete) {
|
||||
throw redirect(302, base + "/manage/setup");
|
||||
}
|
||||
|
||||
if (process.env.KENER_SECRET_KEY === undefined) {
|
||||
throw redirect(302, base + "/manage/setup");
|
||||
}
|
||||
if (process.env.KENER_SECRET_KEY === undefined) {
|
||||
throw redirect(302, base + "/manage/setup");
|
||||
}
|
||||
const query = url.searchParams;
|
||||
let isLoggedIn = await IsLoggedInSession(cookies);
|
||||
|
||||
let isLoggedIn = await IsLoggedInSession(cookies);
|
||||
let site = await GetAllSiteData();
|
||||
const headers = request.headers;
|
||||
let localTz = "UTC";
|
||||
|
||||
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 showNav = true;
|
||||
if (url.pathname.startsWith("/embed")) {
|
||||
showNav = false;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
const query = url.searchParams;
|
||||
//load all files from lib locales folder
|
||||
let selectedLang = query.get("locale") ? query.get("locale") : "en";
|
||||
const localTzQuery = query.get("tz");
|
||||
if (!!localTzQuery) {
|
||||
localTz = localTzQuery;
|
||||
}
|
||||
|
||||
let embed = false;
|
||||
if (route.id.endsWith("monitor-[tag]")) {
|
||||
embed = true;
|
||||
}
|
||||
let showNav = true;
|
||||
if (url.pathname.startsWith("/embed")) {
|
||||
showNav = false;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
const bgc = query.get("bgc") ? "#" + query.get("bgc") : "";
|
||||
return {
|
||||
site: site,
|
||||
localTz: localTz,
|
||||
showNav,
|
||||
isBot,
|
||||
lang: i18n(String(selectedLang)),
|
||||
selectedLang: selectedLang,
|
||||
embed,
|
||||
bgc,
|
||||
isLoggedIn: !!isLoggedIn.user
|
||||
};
|
||||
//load all files from lib locales folder
|
||||
let selectedLang = query.get("locale") ? query.get("locale") : "en";
|
||||
|
||||
let embed = false;
|
||||
if (route.id.endsWith("monitor-[tag]")) {
|
||||
embed = true;
|
||||
}
|
||||
|
||||
const bgc = query.get("bgc") ? "#" + query.get("bgc") : "";
|
||||
return {
|
||||
site: site,
|
||||
localTz: localTz,
|
||||
showNav,
|
||||
isBot,
|
||||
lang: i18n(String(selectedLang)),
|
||||
selectedLang: selectedLang,
|
||||
embed,
|
||||
bgc,
|
||||
isLoggedIn: !!isLoggedIn.user,
|
||||
isMobile: isMobile,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,24 +29,8 @@
|
||||
} else {
|
||||
defaultLocaleValue = allLocales.find((locale) => locale.code === defaultLocaleKey).name;
|
||||
}
|
||||
/**
|
||||
* @param {string} locale
|
||||
*/
|
||||
function setLanguage(locale) {
|
||||
document.cookie = `localLang=${locale};max-age=${60 * 60 * 24 * 365 * 30}`;
|
||||
if (locale === defaultLocaleKey) return;
|
||||
defaultLocaleValue = allLocales[locale];
|
||||
}
|
||||
|
||||
let Analytics;
|
||||
onMount(async () => {
|
||||
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (localTz != data.localTz) {
|
||||
if (data.isBot === false) {
|
||||
document.cookie = "localTz=" + localTz + ";max-age=" + 60 * 60 * 24 * 365 * 30;
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
if (defaultTheme != "none") {
|
||||
setMode(defaultTheme);
|
||||
}
|
||||
|
||||
@@ -3,33 +3,34 @@ import { FetchData } from "$lib/server/page";
|
||||
import { GetMonitors } from "$lib/server/controllers/controller.js";
|
||||
|
||||
export async function load({ params, route, url, parent }) {
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
const monitorsActive = [];
|
||||
const query = url.searchParams;
|
||||
const theme = query.get("theme");
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (monitors[i].tag !== params.tag) {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].default_status;
|
||||
monitors[i].embed = true;
|
||||
let data = await FetchData(
|
||||
siteData,
|
||||
monitors[i],
|
||||
parentData.localTz,
|
||||
parentData.selectedLang,
|
||||
parentData.lang
|
||||
);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
theme,
|
||||
openIncidents: []
|
||||
};
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
const monitorsActive = [];
|
||||
const query = url.searchParams;
|
||||
const theme = query.get("theme");
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (monitors[i].tag !== params.tag) {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].default_status;
|
||||
monitors[i].embed = true;
|
||||
let data = await FetchData(
|
||||
siteData,
|
||||
monitors[i],
|
||||
parentData.localTz,
|
||||
parentData.selectedLang,
|
||||
parentData.lang,
|
||||
parentData.isMobile,
|
||||
);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
theme,
|
||||
openIncidents: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
// @ts-nocheck
|
||||
// @ts-ignore
|
||||
export async function GET({ url, params }) {
|
||||
const { tag } = params;
|
||||
const query = url.searchParams;
|
||||
const uriEmbedded = query.get("monitor");
|
||||
const theme = query.get("theme") || "light";
|
||||
const bgc = query.get("bgc") || "transparent";
|
||||
const locale = query.get("locale") || "en";
|
||||
const uriOriginal = uriEmbedded;
|
||||
const { tag } = params;
|
||||
const query = url.searchParams;
|
||||
const uriEmbedded = query.get("monitor");
|
||||
const theme = query.get("theme") || "light";
|
||||
const bgc = query.get("bgc") || "transparent";
|
||||
const locale = query.get("locale") || "en";
|
||||
const tz = query.get("tz") || "NONE";
|
||||
const uriOriginal = uriEmbedded;
|
||||
|
||||
const currentSlug = tag;
|
||||
const js = `
|
||||
const currentSlug = tag;
|
||||
const js = `
|
||||
(function () {
|
||||
//find user timezone
|
||||
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
let tz = "${tz}";
|
||||
if(tz === "NONE") {
|
||||
if(!!localTz){
|
||||
tz = localTz;
|
||||
} else {
|
||||
tz = "UTC";
|
||||
}
|
||||
}
|
||||
|
||||
var createEmbedFrame = function () {
|
||||
var uid = "KENER_" + ~~(new Date().getTime() / 86400000);
|
||||
var uriOriginal = "${uriOriginal}";
|
||||
var uriOriginalNoProtocol = uriOriginal.split("//").pop();
|
||||
var uriEmbedded = "${uriEmbedded}?theme=${theme}&bgc=${bgc}&locale=${locale}";
|
||||
var uriEmbedded = "${uriEmbedded}?theme=${theme}&bgc=${bgc}&locale=${locale}&tz=" + tz;
|
||||
var currentSlug = "${currentSlug}";
|
||||
var target = document.querySelector("script[src*='" + uriOriginalNoProtocol + "']");
|
||||
var iframe = document.createElement("iframe");
|
||||
@@ -66,9 +78,9 @@ export async function GET({ url, params }) {
|
||||
}).call(this);
|
||||
|
||||
`;
|
||||
return new Response(js, {
|
||||
headers: {
|
||||
"Content-Type": "application/javascript"
|
||||
}
|
||||
});
|
||||
return new Response(js, {
|
||||
headers: {
|
||||
"Content-Type": "application/javascript",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,52 +1,64 @@
|
||||
// @ts-nocheck
|
||||
import i18n from "$lib/i18n/server";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import MobileDetect from "mobile-detect";
|
||||
|
||||
import { base } from "$app/paths";
|
||||
import {
|
||||
GetAllSiteData,
|
||||
IsSetupComplete,
|
||||
IsLoggedInSession,
|
||||
GetLocaleFromCookie
|
||||
GetAllSiteData,
|
||||
IsSetupComplete,
|
||||
IsLoggedInSession,
|
||||
GetLocaleFromCookie,
|
||||
IsEmailSetup,
|
||||
GetSubscriptionTriggerByEmail,
|
||||
} 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");
|
||||
}
|
||||
const userAgent = request.headers.get("user-agent");
|
||||
const md = new MobileDetect(userAgent);
|
||||
const isMobile = !!md.mobile();
|
||||
|
||||
if (process.env.KENER_SECRET_KEY === undefined || process.env.ORIGIN === undefined) {
|
||||
throw redirect(302, base + "/manage/setup");
|
||||
}
|
||||
let isSetupComplete = await IsSetupComplete();
|
||||
if (!isSetupComplete) {
|
||||
throw redirect(302, base + "/manage/setup");
|
||||
}
|
||||
|
||||
let isLoggedIn = await IsLoggedInSession(cookies);
|
||||
if (process.env.KENER_SECRET_KEY === undefined || process.env.ORIGIN === undefined) {
|
||||
throw redirect(302, base + "/manage/setup");
|
||||
}
|
||||
|
||||
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 isLoggedIn = await IsLoggedInSession(cookies);
|
||||
|
||||
// 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 site = await GetAllSiteData();
|
||||
const headers = request.headers;
|
||||
let localTz = "UTC";
|
||||
const localTzCookie = cookies.get("localTz");
|
||||
if (!!localTzCookie) {
|
||||
localTz = localTzCookie;
|
||||
}
|
||||
|
||||
let selectedLang = GetLocaleFromCookie(site, cookies);
|
||||
// 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;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
let selectedLang = GetLocaleFromCookie(site, cookies);
|
||||
|
||||
const query = url.searchParams;
|
||||
const bgc = query.get("bgc") ? "#" + query.get("bgc") : "";
|
||||
const emailSubscriptionTrigger = await GetSubscriptionTriggerByEmail();
|
||||
|
||||
return {
|
||||
site: site,
|
||||
localTz: localTz,
|
||||
isBot,
|
||||
lang: i18n(String(selectedLang)),
|
||||
selectedLang: selectedLang,
|
||||
isLoggedIn: !!isLoggedIn.user,
|
||||
canSendEmail: IsEmailSetup(),
|
||||
canUsersSubscribe: !!emailSubscriptionTrigger && emailSubscriptionTrigger.subscription_trigger_status == "ACTIVE",
|
||||
isMobile: isMobile,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,7 +109,8 @@
|
||||
--bg-custom: {data.bgc};
|
||||
--up-color: {data.site.colors.UP};
|
||||
--down-color: {data.site.colors.DOWN};
|
||||
--degraded-color: {data.site.colors.DEGRADED}
|
||||
--degraded-color: {data.site.colors.DEGRADED};
|
||||
--maintenance-color: {data.site.colors.MAINTENANCE};
|
||||
"
|
||||
class="kener-theme-{kenerTheme}"
|
||||
>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// @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";
|
||||
import { redirect, error } from "@sveltejs/kit";
|
||||
|
||||
function removeTags(str) {
|
||||
if (str === null || str === "") return false;
|
||||
@@ -30,7 +31,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 +42,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") {
|
||||
@@ -104,7 +107,14 @@ export async function load({ parent, url }) {
|
||||
monitors = SortMonitor(siteData.monitorSort, monitors);
|
||||
const monitorsActive = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
let data = await FetchData(siteData, monitors[i], parentData.localTz, parentData.selectedLang, parentData.lang);
|
||||
let data = await FetchData(
|
||||
siteData,
|
||||
monitors[i],
|
||||
parentData.localTz,
|
||||
parentData.selectedLang,
|
||||
parentData.lang,
|
||||
parentData.isMobile,
|
||||
);
|
||||
monitors[i].pageData = data;
|
||||
|
||||
monitors[i].activeIncidents = [];
|
||||
@@ -148,6 +158,9 @@ export async function load({ parent, url }) {
|
||||
let allCategories = siteData.categories;
|
||||
let selectedCategory = allCategories.find((category) => category.name === query.get("category"));
|
||||
if (selectedCategory) {
|
||||
if (!!selectedCategory.isHidden && !parentData.isLoggedIn) {
|
||||
throw error(404, "Category not found");
|
||||
}
|
||||
pageTitle = selectedCategory.name + " - " + pageTitle;
|
||||
pageDescription = selectedCategory.description;
|
||||
canonical = canonical + "?category=" + query.get("category");
|
||||
@@ -157,6 +170,8 @@ export async function load({ parent, url }) {
|
||||
subtitle: selectedCategory.description,
|
||||
image: selectedCategory.image,
|
||||
};
|
||||
} else {
|
||||
throw error(404, "Category not found");
|
||||
}
|
||||
}
|
||||
if (isCategoryPage || isMonitorPage || isGroupPage) {
|
||||
@@ -174,7 +189,6 @@ export async function load({ parent, url }) {
|
||||
return isPresent;
|
||||
});
|
||||
}
|
||||
|
||||
allOpenIncidents = allOpenIncidents.map((incident) => {
|
||||
let incidentMonitors = incident.monitors;
|
||||
let monitorTags = incidentMonitors.map((monitor) => monitor.monitor_tag);
|
||||
@@ -194,8 +208,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 +226,6 @@ export async function load({ parent, url }) {
|
||||
hero,
|
||||
categoryName: query.get("category"),
|
||||
canonical,
|
||||
systemDataMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
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";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
export let data;
|
||||
let shareMenusToggle = false;
|
||||
@@ -27,7 +29,7 @@
|
||||
let pageLoaded = false;
|
||||
$: {
|
||||
if (pageLoaded) {
|
||||
if (shareMenusToggle) {
|
||||
if (shareMenusToggle || subscribeMenuToggle) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "auto";
|
||||
@@ -56,6 +58,14 @@
|
||||
tag: activeMonitor.tag
|
||||
});
|
||||
}
|
||||
|
||||
let subscribeMenuToggle = false;
|
||||
function openSubscribeMenu() {
|
||||
subscribeMenuToggle = true;
|
||||
}
|
||||
function closeSubscribeMenu() {
|
||||
subscribeMenuToggle = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -94,6 +104,46 @@
|
||||
</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 class="bg-api-maintenance h-4" style="width: {data.systemDataMessage.maintenancePercentage}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="section-actions relative 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 bg-background shadow-sm">
|
||||
{#if data.canSendEmail && data.canUsersSubscribe}
|
||||
<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
|
||||
@@ -169,13 +219,13 @@
|
||||
class="section-legend mx-auto mb-2 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
|
||||
id=""
|
||||
>
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-left">
|
||||
<div class="flex w-full flex-col justify-between gap-4 md:flex-row">
|
||||
<div class="md:content-left flex justify-center">
|
||||
<Badge class="border-0 md:pl-0" variant="outline">
|
||||
{l(data.lang, "Availability per Component")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-right">
|
||||
<div class="flex justify-center md:justify-end">
|
||||
<Badge variant="outline" class="border-0 md:pr-0">
|
||||
<span class="bg-api-up mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"></span>
|
||||
<span class="mr-3">
|
||||
@@ -188,18 +238,23 @@
|
||||
</span>
|
||||
|
||||
<span class="bg-api-down mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"></span>
|
||||
<span class="">
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "DOWN")}
|
||||
</span>
|
||||
|
||||
<span class="bg-api-maintenance mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"></span>
|
||||
<span class="">
|
||||
{l(data.lang, "MAINTENANCE")}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="section-monitors z-20 mx-auto mb-8 flex w-full flex-1 flex-col items-start justify-center backdrop-blur-[2px] md:w-[655px]"
|
||||
class="section-monitors z-20 mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-center justify-center"
|
||||
>
|
||||
<Card.Root class="monitor-root">
|
||||
<Card.Content class="monitors-card p-0">
|
||||
<Card.Root class="monitor-root w-full">
|
||||
<Card.Content class="monitors-card p-0 ">
|
||||
{#each data.monitors as monitor}
|
||||
<Monitor
|
||||
on:show_shareMenu={showShareMenu}
|
||||
@@ -215,9 +270,9 @@
|
||||
{/if}
|
||||
{#if data.site.categories && data.pageType == "home"}
|
||||
<section
|
||||
class="section-categories relative z-10 mx-auto mb-8 w-full max-w-[890px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
|
||||
class="section-categories relative z-10 mx-auto mb-8 w-full max-w-[655px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
|
||||
>
|
||||
{#each data.site.categories.filter((e) => e.name != "Home") as category}
|
||||
{#each data.site.categories.filter((category) => category.name != "Home" && (!!!category.isHidden || $page.data.isLoggedIn)) as category}
|
||||
<a href={`?category=${category.name}`} rel="external">
|
||||
<Card.Root class="mb-4 hover:bg-secondary">
|
||||
<Card.Header class="bounce-right relative w-full cursor-pointer px-4 ">
|
||||
@@ -292,3 +347,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,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 });
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -18,10 +18,6 @@
|
||||
|
||||
let incidentSmartDates = {};
|
||||
|
||||
function tsToDate(mnt) {
|
||||
return mnt.unix();
|
||||
}
|
||||
|
||||
incidents.forEach((incident) => {
|
||||
let startTime = incident.start_date_time;
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { onMount } from "svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import ExternalLink from "lucide-svelte/icons/external-link";
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<div class="min-h-[70vh]">
|
||||
|
||||
@@ -9,11 +9,15 @@ import { format } from "date-fns";
|
||||
|
||||
import {
|
||||
CreateUpdateMonitor,
|
||||
UpdateMonitoringData,
|
||||
InsertKeyValue,
|
||||
GetMonitors,
|
||||
CreateUpdateTrigger,
|
||||
UpdateSubscriptionTriggerStatus,
|
||||
GetAllTriggers,
|
||||
UpdateTriggerData,
|
||||
GetSubscriptionTriggerByEmail,
|
||||
CreateSubscriptionTrigger,
|
||||
GetAllAlertsPaginated,
|
||||
GetIncidentByIDDashboard,
|
||||
GetMonitorsParsed,
|
||||
@@ -44,9 +48,11 @@ import {
|
||||
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) {
|
||||
@@ -147,6 +153,9 @@ 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") {
|
||||
@@ -225,6 +234,10 @@ export async function POST({ request, cookies }) {
|
||||
],
|
||||
};
|
||||
resp = await notificationClient.send(testObj);
|
||||
//check if resp is error
|
||||
if (resp.error) {
|
||||
throw new Error(resp.error);
|
||||
}
|
||||
} else if (action == "testMonitor") {
|
||||
let monitorID = data.monitor_id;
|
||||
let monitors = await GetMonitorsParsed({ id: monitorID });
|
||||
@@ -234,8 +247,19 @@ 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);
|
||||
} else if (action == "updateSubscriptionTriggerStatus") {
|
||||
resp = await UpdateSubscriptionTriggerStatus(data.id, data.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
resp = { error: error.message };
|
||||
return json(resp, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -845,38 +845,38 @@
|
||||
<div
|
||||
class="bg-hover state-{newComment.state} mt-2 grid grid-cols-4 overflow-hidden rounded-md border text-xs font-medium"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
|
||||
on:click={() => {
|
||||
setCommentState("INVESTIGATING");
|
||||
}}
|
||||
>
|
||||
INVESTIGATING
|
||||
</div>
|
||||
<div
|
||||
</button>
|
||||
<button
|
||||
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
|
||||
on:click={() => {
|
||||
setCommentState("IDENTIFIED");
|
||||
}}
|
||||
>
|
||||
IDENTIFIED
|
||||
</div>
|
||||
<div
|
||||
</button>
|
||||
<button
|
||||
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
|
||||
on:click={() => {
|
||||
setCommentState("MONITORING");
|
||||
}}
|
||||
>
|
||||
MONITORING
|
||||
</div>
|
||||
<div
|
||||
</button>
|
||||
<button
|
||||
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
|
||||
on:click={() => {
|
||||
setCommentState("RESOLVED");
|
||||
}}
|
||||
>
|
||||
RESOLVED
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4 flex w-full gap-4">
|
||||
@@ -1092,6 +1092,11 @@
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Label>Impact</Select.Label>
|
||||
{#if currentIncident.incident_type == "MAINTENANCE"}
|
||||
<Select.Item value="MAINTENANCE" label="MAINTENANCE" class="text-api-maintenance text-sm font-medium">
|
||||
MAINTENANCE
|
||||
</Select.Item>
|
||||
{/if}
|
||||
<Select.Item value="DOWN" label="DOWN" class="text-api-down text-sm font-medium">
|
||||
DOWN
|
||||
</Select.Item>
|
||||
|
||||
@@ -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,436 @@
|
||||
<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 = {
|
||||
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
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function updateSubscriptionTriggerStatus(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: "updateSubscriptionTriggerStatus",
|
||||
data: {
|
||||
id: id,
|
||||
status: status
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function handleActiveInactive(e) {
|
||||
if (subscriptionTrigger.id) {
|
||||
subscriptionTrigger.subscription_trigger_status = e.target.checked ? "ACTIVE" : "INACTIVE";
|
||||
updateSubscriptionTriggerStatus(subscriptionTrigger.id, subscriptionTrigger.subscription_trigger_status);
|
||||
} else {
|
||||
await saveSubscriptionTrigger();
|
||||
pageLoading = true;
|
||||
await loadSubscriptionTrigger();
|
||||
await loadSubscribers();
|
||||
pageLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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" />
|
||||
{:else if canSendEmail}
|
||||
<label class="absolute right-0 top-0 flex cursor-pointer items-center justify-between gap-2 text-sm">
|
||||
<Label class="text-xs font-medium ">
|
||||
{subscriptionTrigger.subscription_trigger_status == "ACTIVE" ? "Active" : "Inactive"}
|
||||
</Label>
|
||||
<input
|
||||
type="checkbox"
|
||||
value=""
|
||||
disabled={!canSendEmail}
|
||||
class="peer sr-only"
|
||||
checked={subscriptionTrigger.subscription_trigger_status == "ACTIVE"}
|
||||
on:change={handleActiveInactive}
|
||||
/>
|
||||
<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>
|
||||
{/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 || subscriptionTrigger.subscription_trigger_status != "ACTIVE"}
|
||||
/>
|
||||
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 || subscriptionTrigger.subscription_trigger_status != "ACTIVE"}
|
||||
/>
|
||||
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 || subscriptionTrigger.subscription_trigger_status != "ACTIVE"}
|
||||
/>
|
||||
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 || subscriptionTrigger.subscription_trigger_status != "ACTIVE"}
|
||||
/>
|
||||
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 || subscriptionTrigger.subscription_trigger_status != "ACTIVE"}
|
||||
/>
|
||||
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 || subscriptionTrigger.subscription_trigger_status != "ACTIVE"}
|
||||
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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -10,6 +10,7 @@ const PORT = Number(process.env.PORT) || 3000;
|
||||
const base = process.env.KENER_BASE_PATH || "";
|
||||
const VITE_BUILD_ENV = process.env.VITE_BUILD_ENV || "development"; // Default to "development"
|
||||
const isProduction = VITE_BUILD_ENV === "production";
|
||||
const ORIGIN = process.env.ORIGIN || `http://localhost:${PORT}`;
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
optimizeDeps: {
|
||||
@@ -49,6 +50,7 @@ export default defineConfig(({ mode }) => ({
|
||||
version(),
|
||||
],
|
||||
server: {
|
||||
allowedHosts: [new URL(ORIGIN).hostname],
|
||||
port: PORT,
|
||||
watch: {
|
||||
ignored: ["**/src/lib/server/data/**"], // Adjust the path to the file you want to ignore
|
||||
|
||||
Reference in New Issue
Block a user