Compare commits

..

76 Commits

Author SHA1 Message Date
Raj Nandan Sharma fbaebdcbcf Merge pull request #344 from rajnandan1/fix-ping-non-root
refactor: clean up Dockerfile and improve security practices
2025-03-12 22:49:47 +05:30
Raj Nandan Sharma 3f9716b0a4 fix: update donation links from GitHub Sponsors to Buy Me a Coffee 2025-03-12 22:48:53 +05:30
Raj Nandan Sharma 57d32197cf chore: update version to 3.2.3 and reflect changes in documentation and headers 2025-03-12 22:05:25 +05:30
Raj Nandan Sharma 9e6a3f26c2 refactor: clean up Dockerfile and improve security practices 2025-03-12 21:30:06 +05:30
github-actions 23314beb15 Auto-generate README.md with release versions 2025-03-08 18:58:59 +00:00
Raj Nandan Sharma 64b94da585 Merge pull request #340 from rajnandan1/release/3.2.2
Release/3.2.2
2025-03-08 22:57:28 +05:30
Raj Nandan Sharma 1609b4fc50 refactor: remove unnecessary future and ongoing incident checks from IncidentNew component 2025-03-08 22:56:57 +05:30
Raj Nandan Sharma dfffffb101 fix: remove database directory writable check from entrypoint script 2025-03-08 22:26:14 +05:30
Raj Nandan Sharma 81250a117a chore: update version to 3.2.2 in package.json and documentation 2025-03-08 22:18:53 +05:30
Raj Nandan Sharma 72bd0241e9 fix: streamline incident creation logic for database compatibility , fixes #325 2025-03-08 22:17:48 +05:30
Raj Nandan Sharma 63c24f147f fix: remove redundant startup command in entrypoint script #338 2025-03-08 21:19:46 +05:30
Raj Nandan Sharma 88fb7df3f5 Enhances incident display with time status
Improves the incident display by adding time status information
such as "Starts in", "Started", and "Will last for".
Also fixes database directory write permissions on startup.

Also fixes #337
2025-03-08 21:17:34 +05:30
Raj Nandan Sharma 320b1a0cc5 Improves webhook and notification handling
- Makes the monitor tags wrap on smaller screens.
- Validates webhook body.
- Adds user agent to webhook.
- Fixes Discord logo URL construction.

Issue #336
2025-03-07 20:09:35 +05:30
Raj Nandan Sharma 08b168ee6e Merge pull request #331 from rajnandan1/feature/hb1
Adds heartbeat monitor and improvements
2025-03-01 21:56:21 +05:30
Raj Nandan Sharma d6a87ac81a docs: update heartbeat monitors description for clarity
refactor: remove unused RandomString import from monitorSheet component

chore: clean up commented-out code in FetchData function
2025-03-01 21:55:37 +05:30
Raj Nandan Sharma 615dba42b8 fix: change default monitor type from HEARTBEAT to NONE 2025-03-01 21:02:12 +05:30
Raj Nandan Sharma 3ffec4f1fe Adds heartbeat monitor and improvements
Implements push-based monitoring via heartbeats.

Fixes data interpolation issues.

Enhances UI and documentation.
2025-03-01 21:00:05 +05:30
Raj Nandan Sharma 60b4b6f207 Merge pull request #327 from rajnandan1/feature/hb
feat: enhance documentation and add donation banner for Kener
2025-02-28 10:00:37 +05:30
Raj Nandan Sharma b9f5eb56c5 feat: enhance documentation and add donation banner for Kener 2025-02-28 09:59:39 +05:30
github-actions 7b9ae73044 Auto-generate README.md with release versions 2025-02-27 05:47:27 +00:00
Raj Nandan Sharma 87f2c33eba Merge pull request #323 from rajnandan1/feature/optimize-1
feat: upgrade to version 3.2.0 with improved monitor evaluation funct…
2025-02-27 10:54:40 +05:30
Raj Nandan Sharma 0a73a8b10a fix: update eval function to use responseRaw instead of responseData 2025-02-27 10:53:23 +05:30
Raj Nandan Sharma af65404fd3 feat: upgrade to version 3.2.0 with improved monitor evaluation functions and enhanced API support 2025-02-27 10:46:59 +05:30
github-actions 398f891dac Auto-generate README.md with release versions 2025-02-27 01:19:17 +00:00
Raj Nandan Sharma 6a2375a774 Merge pull request #322 from rajnandan1/fix/optimize
fix: eval not working for api
2025-02-27 06:30:30 +05:30
Raj Nandan Sharma 43bbcf4015 fix: eval not working for api 2025-02-27 06:29:39 +05:30
github-actions fadb563337 Auto-generate README.md with release versions 2025-02-26 07:02:13 +00:00
Raj Nandan Sharma 1ae54b3906 Improve formatting and add video tutorial section in quick start documentation 2025-02-26 12:20:17 +05:30
Raj Nandan Sharma 1c069e2ee2 Bump version to 3.1.9 and update documentation layout 2025-02-26 12:12:17 +05:30
Raj Nandan Sharma ed1099ac11 Merge pull request #319 from rajnandan1/feature/sql-monitor
Adds SQL monitor functionality as asked in #244
2025-02-26 12:09:39 +05:30
Raj Nandan Sharma 99d3a7e046 Adds SQL monitor functionality as asked in #244
Implements the SQL monitor feature, allowing users to monitor database connections and queries.

Adds UI elements for configuring SQL monitor parameters, including connection string, query, and timeout.

Validates user inputs for SQL monitor configuration.
2025-02-26 12:06:05 +05:30
Raj Nandan Sharma 2144acf34d Merge pull request #318 from rajnandan1/feature/ssl
Add SSL monitor functionality and related documentation #317
2025-02-26 09:15:46 +05:30
Raj Nandan Sharma 7a8ad8e833 Enhance port validation logic in SSL configuration for improved error handling 2025-02-26 09:14:04 +05:30
Raj Nandan Sharma 3b45f33692 Remove unnecessary assignment of type_data in monitorSheet component 2025-02-26 09:11:26 +05:30
Raj Nandan Sharma b4a2340ec7 Add SSL monitor functionality and related documentation #317 2025-02-26 09:09:20 +05:30
Raj Nandan Sharma 5910e3b930 Merge pull request #315 from rajnandan1/feature/css-cls
Refactor section classes for improved styling and organization in the…
2025-02-24 22:05:54 +05:30
Raj Nandan Sharma fd58beaa69 Refactor section classes for improved clarity and organization in incident page layout 2025-02-24 22:05:16 +05:30
Raj Nandan Sharma 5449e422a5 Refactor section classes for improved styling and organization in the hero and event sections 2025-02-24 22:02:53 +05:30
Raj Nandan Sharma dfb1784b6f Merge pull request #312 from rajnandan1/feature/cheerio
Add cheerio dependency and enhance API call examples with HTML parsing
2025-02-24 07:01:24 +05:30
Raj Nandan Sharma d956c60b48 Add cheerio dependency and enhance API call examples with HTML parsing 2025-02-24 06:56:09 +05:30
github-actions 69e1f2af6e Auto-generate README.md with release versions 2025-02-23 12:41:00 +00:00
Raj Nandan Sharma 7cc2f5ed4b Refactor i18n documentation for improved clarity and consistency in localization instructions 2025-02-23 17:46:54 +05:30
Raj Nandan Sharma ce2b17a5c9 Merge pull request #311 from rajnandan1/fix/double-trigger
Refactors incident handling and cron scheduling
2025-02-23 17:45:35 +05:30
Raj Nandan Sharma 0fcc60bf65 Bump version to 3.1.8 and update changelog with new features, improvements, and fixes 2025-02-23 17:39:26 +05:30
Raj Nandan Sharma 45ac25055b Enhances internationalization support by adding localized timezone messages and updating UI text for language consistency 2025-02-23 16:55:44 +05:30
Raj Nandan Sharma d3e201f2e4 Adds timezone support and UI toggle
Improves date formatting by adding timezone support using `date-fns-tz`.

Allows users to switch between different timezones via a new UI toggle in the settings.
Updates dependencies and integrates timezone functionality into date formatting functions.
2025-02-23 16:35:41 +05:30
Raj Nandan Sharma 693735dc2e Improves monitor component and incident handling
Refactors the monitor component for better data display and user interaction, including improved uptime calculations and a dropdown for selecting time ranges.

Enhances incident creation and handling by adding incident sources and refining incident filtering.

Addresses UI responsiveness on smaller screens.
2025-02-22 23:00:33 +05:30
Raj Nandan Sharma a4fa85dd79 Refactors incident handling and cron scheduling
Improves incident management by filtering out existing auto incidents when creating manual incidents.

Enhances cron job scheduling by removing and adding jobs dynamically based on active monitors and prevents duplicated incidents.
Also, ensures jobs get triggered in the correct order.
2025-02-22 11:48:39 +05:30
github-actions 29f70860ef Auto-generate README.md with release versions 2025-02-20 03:17:31 +00:00
Kyle ae882f6afc Merge pull request #305 from kaffolder7/fix/docker-image-tagging
fix: docker gha build
2025-02-19 19:19:57 -05:00
Kyle 57941f67ce Merge branch 'rajnandan1:main' into fix/docker-image-tagging 2025-02-19 19:19:10 -05:00
Kyle Affolder ab356d516b fix(docker): gha build
Pulled version number from release rather than via tag (since this workflow is triggered by release).
2025-02-19 19:17:56 -05:00
Kyle Affolder d11b3f1a99 fix(docker): gha build 2025-02-19 19:15:14 -05:00
Kyle 83ba08dd6b Merge pull request #304 from kaffolder7/fix/docker-image-tagging
fix(docker): clean up Docker tagging
2025-02-19 18:41:08 -05:00
Kyle Affolder 34cc00a1f6 fix(docker): clean up Docker tagging
Simplified Docker tagging - in turn fixes broken `alpine` tag to correctly point to latest stable Alpine release. Changes include:

- No more type=ref,event=branch – because this workflow is only for releases & manual triggers on `main` branch.
- Ensures `alpine` tag is always created for Alpine variant builds.
- Ensures `latest` tag is always created for Debian builds.
- Ensures all semver-based tags work correctly for both variants.
2025-02-19 18:39:34 -05:00
github-actions 7b86429056 Auto-generate README.md with release versions 2025-02-19 22:50:32 +00:00
Kyle e67c1b1539 Merge pull request #303 from kaffolder7/fix/docker-image-tagging
debug(docker): tagging issue
2025-02-19 17:36:20 -05:00
Kyle Affolder e501f8e05d debug(docker): tagging issue
Debugging `alpine` tag as it does not seem to be pointing to the most recent stable release.
2025-02-19 17:35:46 -05:00
github-actions 9e40d89317 Auto-generate README.md with release versions 2025-02-19 04:58:24 +00:00
Raj Nandan Sharma 5d475ab23f Merge pull request #302 from rajnandan1/fix/incidents-1
Fix/incidents 1
2025-02-19 10:12:08 +05:30
Raj Nandan Sharma 01cb39e18e Bump version to 3.1.7 and update favicon type label; improve incident description formatting 2025-02-19 10:10:31 +05:30
Raj Nandan Sharma 0d0ef25970 Improve documentation for SMTP configuration and add external link button in monitors management 2025-02-19 09:52:09 +05:30
Raj Nandan Sharma 6c766e2001 Enhances incident management and SMTP configuration
Improves incident display and management by introducing configurable incident group views and enhancing comment rendering to support HTML content.

Solves the bug raised in #295 where server crashes when an incident is created from an alert

Refines SMTP email settings by adding TLS configuration and allowing username/password to be optional. #300 and #298

Also, fixes a bug where only home page was being filtered. Now all pages are filtered. #297
2025-02-19 07:05:24 +05:30
Raj Nandan Sharma 1c73166120 Merge pull request #292 from kaffolder7/feature/embed-webfont
feat: embed project webfont
2025-02-18 07:46:56 +05:30
github-actions fc5dbc85dc Auto-generate README.md with release versions 2025-02-18 02:06:31 +00:00
Raj Nandan Sharma 622327cdf6 Merge pull request #294 from rajnandan1/fix/293-and-287
Bump version to 3.1.6 and update affected status handling in database…
2025-02-18 07:22:13 +05:30
Raj Nandan Sharma b0a4cd5c42 Bump version to 3.1.6 and update affected status handling in database queries for pg db reported in #293 and #287 2025-02-18 07:20:08 +05:30
Kyle Affolder 4e5fb26be1 add: CSS rule in EditorConfig 2025-02-17 12:53:29 -05:00
Kyle Affolder 6fc80bc0fc change: ensure full Lato font is kept from build
Ensures that the full variant of the Lato font-family is kept/removed from Docker image build. (Keeps the size small!)
2025-02-17 12:27:32 -05:00
Kyle Affolder 292667ac29 add: Lato webfont
Noticed multiple individuals commenting about insecure/privacy-unfriendly Lato webfont library being served via Google Fonts. I had formerly suggested replacing this with BunnyFonts and was happy to see that added as a placeholder, however, I also understand someone’s comment about this being loaded from an external resource.

This brings that webfont local. Size of webfont files should minimally grow Docker image sizes and I think we should prioritize UI and privacy by including it locally. The font’s licensing is OFL, so we are allowed to package it for distribution with this project.

I’m including both the full font family (for archival purposes) and Latin subset of this font. The Latin variant is used in the Docker image build (since this will apply to the majority of users and keep the Docker image smaller). If users need to extend this with their own subsets, they can always load those as a custom font. :)
2025-02-17 12:26:15 -05:00
Kyle Affolder 8c03058f8d update: combine to avoid multiple processes
Combined to reduce spawning multiple `rm` processes.
2025-02-17 12:21:57 -05:00
github-actions 25e73d097c Auto-generate README.md with release versions 2025-02-17 05:36:58 +00:00
Raj Nandan Sharma 1970cd8891 Merge pull request #291 from rajnandan1/fix/288-interpolation
Fix/288 interpolation
2025-02-17 10:51:34 +05:30
Raj Nandan Sharma 8ae9b2dec1 Bump version to 3.1.5 and update documentation layout 2025-02-17 10:48:22 +05:30
Raj Nandan Sharma 1662608984 Improves monitor status and data handling
Adds a NO_DATA status to handle cases where monitor data is unavailable.
Refactors data interpolation and aggregation logic for better accuracy and clarity.
Updates documentation links.

fixes #288
2025-02-17 10:47:47 +05:30
github-actions 42693983f1 Auto-generate README.md with release versions 2025-02-17 01:51:55 +00:00
148 changed files with 5103 additions and 3267 deletions
+5
View File
@@ -33,6 +33,11 @@ trim_trailing_whitespace = false
indent_style = space
indent_size = 2
# CSS & PostCSS files
[*.{css,postcss}]
indent_style = space
indent_size = 2
# Markdown files
[*.md]
indent_style = space
+12 -13
View File
@@ -96,19 +96,17 @@ jobs:
${{ env.DOCKERHUB_IMAGE_NAME }}
${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }}
tags: |
# Tag non-main branches (...for Alpine variant, add suffix)
type=ref,event=branch,suffix=-${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' && github.ref != 'refs/heads/main' }}
type=ref,event=branch,enable=${{ matrix.variant == 'debian' && github.ref != 'refs/heads/main' }}
# Tag matrix.variant (Alpine) releases as version (1.0.0), major.minor (1.0), major (1), and matrix.variant (alpine)
type=semver,pattern={{version}},suffix=-${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' }}
type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' }}
type=semver,pattern={{major}},suffix=-${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' }}
type=raw,value=${{ matrix.variant }},enable=${{ matrix.variant == 'alpine' && github.ref == 'refs/heads/main' }}
# Tag default (Debian) releases as version (1.0.0), major.minor (1.0), major (1), and latest
# Debian Variant Tags
type=semver,pattern={{version}},enable=${{ matrix.variant == 'debian' }}
type=semver,pattern={{major}}.{{minor}},enable=${{ matrix.variant == 'debian' }}
type=semver,pattern={{major}},enable=${{ matrix.variant == 'debian' }}
type=raw,value=latest,enable=${{ matrix.variant == 'debian' && github.ref == 'refs/heads/main' }}
type=raw,value=latest,enable=${{ matrix.variant == 'debian' }}
# Alpine Variant Tags
type=semver,pattern={{version}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
type=semver,pattern={{major}}.{{minor}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
type=semver,pattern={{major}},suffix=-alpine,enable=${{ matrix.variant == 'alpine' }}
type=raw,value=alpine,enable=${{ matrix.variant == 'alpine' }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
@@ -143,13 +141,14 @@ jobs:
- name: Save Build Version to Repository Variable
if: matrix.variant == 'debian' && github.run_attempt == 1
run: |
VERSION="${{ steps.meta.outputs.version }}"
# VERSION="${{ steps.meta.outputs.version }}"
VERSION=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "")
# Check if VERSION is empty and set a fallback value
if [ -z "$VERSION" ]; then
# Fetch the latest release using Git
VERSION=$(git tag -l --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || echo "3.1.0")
VERSION=$(git tag -l --sort=-version:refname | grep -E '^(v)?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || echo "3.1.0")
fi
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Setting BUILD_VERSION to $VERSION"
gh variable set BUILD_VERSION --body "$VERSION"
+26 -16
View File
@@ -57,10 +57,11 @@ COPY . .
# TODO: Reevaluate permissions (possibly reduce?)...
# Remove docs directory and ensure required directories exist
RUN rm -rf src/routes/\(docs\) && \
rm -rf static/documentation && \
mkdir -p uploads database && \
# TODO: Consider changing below to `chmod -R u-rwX,g=rX,o= uploads database`
RUN rm -rf src/routes/\(docs\) \
static/documentation \
static/fonts/lato/full && \
mkdir -p uploads database && \
# TODO: Consider changing below to `chmod -R u-rwX,g=rX,o= uploads database`
chmod -R 750 uploads database
# Build the application and remove `devDependencies`
@@ -77,15 +78,15 @@ RUN apt-get update && apt-get install -y \
iputils-ping=3:20221126-1+deb12u1 \
sqlite3=3.40.1-2+deb12u1 \
tzdata=2024b-0+deb12u1 \
# TODO: Is it ok to change to `curl` here so that we don't have to maintain `wget` version mismatch between Debian architectures? (`curl` is only used for the container healthcheck and because there is an Alpine variant (best!) we probably don't care if the Debian image ends up building bigger due to `curl`.)
curl=7.88.1-10+deb12u8 && \
# TODO: Is it ok to change to `curl` here so that we don't have to maintain `wget` version mismatch between Debian architectures? (`curl` is only used for the container healthcheck and because there is an Alpine variant (best!) we probably don't care if the Debian image ends up building bigger due to `curl`.)
curl=7.88.1-10+deb12u8 && \
rm -rf /var/lib/apt/lists/*
FROM node:${ALPINE_VERSION} AS final-alpine
RUN apk add --no-cache --update \
iputils=20240905-r0 \
sqlite=3.48.0-r0 \
tzdata
iputils=20240905-r0 \
sqlite=3.48.0-r0 \
tzdata
FROM final-${VARIANT} AS final
@@ -119,25 +120,34 @@ COPY --chown=node:node --from=builder /app/main.js ./main.js
COPY --chown=node:node --from=builder /app/openapi.json ./openapi.json
COPY --chown=node:node --from=builder /app/openapi.yaml ./openapi.yaml
# Set capabilities for ping (before changing to non-root user)
RUN if [ -f "/etc/alpine-release" ]; then \
# Alpine path
setcap cap_net_raw+ep /bin/ping; \
else \
# Debian path
setcap cap_net_raw+ep /usr/bin/ping; \
fi
# 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
# TODO: To improve security, consider dropping unnecessary capabilities instead of granting image all network capabilities of host. (Maybe `setcap cap_net_raw+p /usr/bin/ping`, etc.) Could also drop all and then grant only the capabilities that are explicitly needed. Some examples are commented out below...
# setcap cap_net_bind_service=+ep /usr/local/bin/node
# setcap cap_net_bind_service=+ep /usr/bin/ping
# setcap cap_net_bind_service=+ep /usr/bin/ping6
# setcap cap_net_bind_service=+ep /usr/bin/tracepath
# setcap cap_net_bind_service=+ep /usr/bin/clockdiff
# TODO: To improve security, consider dropping unnecessary capabilities instead of granting image all network capabilities of host. (Maybe `setcap cap_net_raw+p /usr/bin/ping`, etc.) Could also drop all and then grant only the capabilities that are explicitly needed. Some examples are commented out below...
# setcap cap_net_bind_service=+ep /usr/local/bin/node
# setcap cap_net_bind_service=+ep /usr/bin/ping
# setcap cap_net_bind_service=+ep /usr/bin/ping6
# setcap cap_net_bind_service=+ep /usr/bin/tracepath
# setcap cap_net_bind_service=+ep /usr/bin/clockdiff
# Expose the application port
EXPOSE $PORT
# Add a healthcheck to the container; `wget` vs. `curl` depending on base image. Using this approach because `wget` does not actually maintain versioning across architectures, so we cannot pin a `wget` version (in above `final-debian` base, `apt-get install`) between differing architectures (e.g. arm64, amd64)
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD sh -c 'if [ -f "/etc/alpine-release" ]; then wget --quiet --spider http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; else curl --silent --head --fail http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; fi'
CMD sh -c 'if [ -f "/etc/alpine-release" ]; then wget --quiet --spider http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; else curl --silent --head --fail http://localhost:$HEALTHCHECK_PORT$HEALTHCHECK_PATH || exit 1; fi'
# TODO: Revisit letting user define $PUID & $PGID overrides (e.g. `addgroup -g $PGID newgroup && adduser -D -G newgroup -u $PUID node`) as well as potentially ensure no root user exists. (Make sure no processes are running as root, first!)
# Use a non-root user (recommended for security)
+11 -11
View File
@@ -79,7 +79,7 @@ npm run dev
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.3"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.1.3-blue" alt="Kener latest stable version: 3.1.3" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.2-blue" alt="Kener latest stable version: 3.2.2" /></a>
#### Available Tags
@@ -93,38 +93,38 @@ Official Docker images for **Kener** are available on [Docker Hub](https://hub.d
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
<td>Latest stable release (aka 3.1.3)</td>
<td>Latest stable release (aka 3.2.2)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.3" target="_blank"><code>3.1.3</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2" target="_blank"><code>3.2.2</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1" target="_blank"><code>3.1</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.1.3) release within that minor version (3.1.x)</td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2" target="_blank"><code>3.2</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.2) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
<td>Major version tag pointing to the latest stable (3.1.3) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.2) release within that major version (3.x.x)</td>
</tr>
<tr>
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 &nbsp;<strong><em>(smallest image size)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
<td>Latest stable release (aka 3.1.3)</td>
<td>Latest stable release (aka 3.2.2)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1.3-alpine" target="_blank"><code>3.1.3-alpine</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.2-alpine" target="_blank"><code>3.2.2-alpine</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.1-alpine" target="_blank"><code>3.1-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.1.3) release within that minor version (3.1.x)</td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2-alpine" target="_blank"><code>3.2-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.2) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
<td>Major version tag pointing to the latest stable (3.1.3) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.2) release within that major version (3.x.x)</td>
</tr>
</table>
+207 -48
View File
@@ -7,6 +7,165 @@ description: Changelogs for Kener
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
## v3.2.1
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **New Heartbeat Monitors**
- Added support for push-based monitoring via heartbeats
- Monitors systems that send periodic signals to confirm they're running
- Configurable thresholds for degraded and down states
- Secured with secret token authentication
- Accessible via simple HTTP API endpoints
- **Data Interpolation Improvements**
- Fixed data interpolation issues for more accurate uptime calculations
- Improved handling of timestamp boundaries
### Fixes & Improvements
- **UI Enhancements**
- Better mobile responsiveness for theme toggles and controls
- Fixed layout issues with bottom navigation buttons
- Improved timezone selector display
- **Data Processing**
- Fixed edge case issues with uptime calculations
- Improved handling of "No Data" status in summaries
- More accurate time range display across timezones
- **Documentation**
- Added comprehensive documentation for Heartbeat Monitors
- Updated API documentation with new endpoints
## v3.2.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **Improved Monitor Evaluation Functions**
- Replaced unsafe `eval()` with secure `Function()` constructors across all monitor types
- Added support for using modules (like cheerio) directly in evaluation functions
- **Enhanced API Monitors**
- Raw response data is now passed directly to eval functions instead of base64 encoded
- Added `modules` parameter with access to cheerio for HTML parsing
- Updated default evaluation function to use the new parameter structure
- **Improved TCP & Ping Monitors**
- Simplified evaluation functions with direct access to ping/TCP data
- Removed unnecessary base64 encoding/decoding steps
- Better error handling for invalid evaluation functions
- **Documentation Updates**
- Updated all examples and documentation for the new evaluation function signatures
- Added more detailed explanations of input parameters
- Improved examples showing usage with the new parameter structure
### Breaking Changes
- **Monitor Evaluation Functions**
- Custom evaluation functions will need to be updated to the new parameter structure
- API monitors: `(statusCode, responseTime, responseRaw, modules)` instead of `(statusCode, responseTime, responseDataBase64)`
- TCP/Ping monitors: `(arrayOfPings)` instead of `(responseDataBase64)`
### Fixes
- Fixed "cheerio is undefined" errors in API monitor evaluations
- Improved error handling and logging for monitor evaluation failures
- Security enhancements by removing `eval()` usage
### Migration
If you're using custom evaluation functions in your monitors, you'll need to update them to the new format:
#### API Monitors
```javascript
// Old format
;(async function (statusCode, responseTime, responseDataBase64) {
const resp = atob(responseDataBase64)
// Your logic here
})
```
```javascript
// New format
;(async function (statusCode, responseTime, responseRaw, modules) {
// responseRaw is the direct response - no need to decode
// Access cheerio with modules.cheerio
// Your logic here
})
```
#### TCP/Ping Monitors
```javascript
// Old format
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
// Your logic here
})
```
```javascript
// New format
;(async function (arrayOfPings) {
// arrayOfPings is directly available - no need to decode
// Your logic here
})
```
## v3.1.8
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **Timezone Support & UI Toggle**
- Added timezone support using `date-fns-tz` for improved date formatting.
- Introduced a UI toggle in settings to switch between different timezones.
- **Enhanced Incident Management**
- Improved incident filtering to prevent duplicate auto incidents when creating manual incidents.
- Added support for incident sources to refine incident handling.
- **Dynamic Cron Job Scheduling**
- Cron jobs are now dynamically added and removed based on active monitors.
- Ensures jobs are triggered in the correct order and prevents duplicate incidents.
- **Monitor Component Improvements**
- Refactored monitor component for better data display and interaction.
- Improved uptime calculations.
- Added a dropdown to select time ranges for better visibility.
### Fixes & Improvements
- **Refactored Incident Handling & Scheduling** for better reliability and performance.
- **UI Responsiveness Fixes** to improve the experience on smaller screens.
- **Dependency Updates** to support new timezone functionality and ensure stability.
## v3.0.10
<picture>
@@ -16,15 +175,15 @@ Here are the changelogs for Kener. Changelogs are only published when there are
### Features
- Added TCP monitors
- Added TCP monitors
### Breaking Changes
- Ping monitors will break.
- Ping monitors will break.
### Fixes
- Bug fixes in the UI
- Bug fixes in the UI
## v3.0.9
@@ -35,16 +194,16 @@ Here are the changelogs for Kener. Changelogs are only published when there are
### Features
- Support of SMTP for email notifications. Read more [here](/docs/triggers/#email-smtp)
- Introduction of event type `MAINTENANCE` for incidents.
- You can write eval function for ping now in monitors. Read more [here](/docs/monitors-ping/#eval)
- Added category filter for monitor management.
- Support of SMTP for email notifications. Read more [here](/docs/triggers/#email-smtp)
- Introduction of event type `MAINTENANCE` for incidents.
- You can write eval function for ping now in monitors. Read more [here](/docs/monitors-ping/#eval)
- Added category filter for monitor management.
### Fixes
- Support longer TLD in `siteURL` example `https://example.network`
- Remove googleapis preconnect and preload
- Fixed wrong action url in webhook.
- Support longer TLD in `siteURL` example `https://example.network`
- Remove googleapis preconnect and preload
- Fixed wrong action url in webhook.
## v3.0.1
@@ -57,8 +216,8 @@ Here are the changes in this release
### Features
- Support for i18n in dates.
- Support of i18n in monitor embeds. Read more [here](/docs/embed#javascript-parameters)
- Support for i18n in dates.
- Support of i18n in monitor embeds. Read more [here](/docs/embed#javascript-parameters)
## v3.0.0
@@ -71,18 +230,18 @@ Here are the changes in this release
### Features
- New APIs for creating incidents and pushing updates. Read more [here](/docs/kener-apis)
- Incident management is now part of the admin UI and removed from the config file.
- The UI colors have been updated to be more muted.
- Email Notifications for incidents using [resend](https://resend.com).
- New Kener management portal. No monitors.yaml or site.yaml needed anymore
- Login Page and Setup Page
- Remove Github dependency
- Options to disable square or dot pattern
- Support for new languages
- Multiple DB support (mysql, postgres, sqlite3)
- New API reference
- New documentation site
- New APIs for creating incidents and pushing updates. Read more [here](/docs/kener-apis)
- Incident management is now part of the admin UI and removed from the config file.
- The UI colors have been updated to be more muted.
- Email Notifications for incidents using [resend](https://resend.com).
- New Kener management portal. No monitors.yaml or site.yaml needed anymore
- Login Page and Setup Page
- Remove Github dependency
- Options to disable square or dot pattern
- Support for new languages
- Multiple DB support (mysql, postgres, sqlite3)
- New API reference
- New documentation site
## v2.0.0
@@ -95,14 +254,14 @@ Here are the changes in this release
### Features
- Added support for sqlite3 and removed dependency on file system
- Added support for postgres database. Read more [here](/docs/database)
- Added support for alerting. Read more [here](/docs/alerting)
- Added color customization. Read more [here](/docs/customize-site#color)
- Added three new customizations for home page. Read more [here](/docs/customize-site#barstyle)
- `barStyle`
- `barRoundness`
- `summaryStyle`
- Added support for sqlite3 and removed dependency on file system
- Added support for postgres database. Read more [here](/docs/database)
- Added support for alerting. Read more [here](/docs/alerting)
- Added color customization. Read more [here](/docs/customize-site#color)
- Added three new customizations for home page. Read more [here](/docs/customize-site#barstyle)
- `barStyle`
- `barRoundness`
- `summaryStyle`
### Migration
@@ -119,27 +278,27 @@ Here are the changes in this release
### Features
- Added support for `hideURLForGet` in monitors. Read more [here](/docs/monitors)
- New SVG badges for LIVE status. Read more [here](/docs/status-badges#live)
- `[Breaking Change]` Removed dependency on Environment variable `PUBLIC_KENER_FOLDER`. Read more [here](#v0-0-16-migration)
- Simplified build and deploy process
- Added support for fonts. Read more [here](/docs/customize-site#font)
- Added support for home page pattern. Read more [here](/docs/customize-site#pattern)
- Added support for adding your analytics provider. Read more [here](/docs/site-analytics)
- New Documentation Site
- Addes support for `sqaures` pattern in home page. Read more [here](/docs/customize-site#pattern)
- Redesigned the UI for better consistency
- Embed now supports background color using a parameter `bgc`. Read more [here](/docs/embed#javascript-parameters)
- Now title in `site.yaml` is `<title>` and `siteName` is actually the name of the site. Read more [here](/docs/customize-site#siteName)
- Added support for `hideURLForGet` in monitors. Read more [here](/docs/monitors)
- New SVG badges for LIVE status. Read more [here](/docs/status-badges#live)
- `[Breaking Change]` Removed dependency on Environment variable `PUBLIC_KENER_FOLDER`. Read more [here](#v0-0-16-migration)
- Simplified build and deploy process
- Added support for fonts. Read more [here](/docs/customize-site#font)
- Added support for home page pattern. Read more [here](/docs/customize-site#pattern)
- Added support for adding your analytics provider. Read more [here](/docs/site-analytics)
- New Documentation Site
- Addes support for `sqaures` pattern in home page. Read more [here](/docs/customize-site#pattern)
- Redesigned the UI for better consistency
- Embed now supports background color using a parameter `bgc`. Read more [here](/docs/embed#javascript-parameters)
- Now title in `site.yaml` is `<title>` and `siteName` is actually the name of the site. Read more [here](/docs/customize-site#siteName)
### Migration
#### Source
- Move data from `PUBLIC_KENER_FOLDER` to `/database` file.
- Move `site.yaml` to `/config` folder
- Move `monitors.yaml` to `/config` folder
- Move data from `PUBLIC_KENER_FOLDER` to `/database` file.
- Move `site.yaml` to `/config` folder
- Move `monitors.yaml` to `/config` folder
#### Docker
- Use `-v $(pwd)/database:/app/database` and `-v $(pwd)/config:/app/config` in your docker run command
- Use `-v $(pwd)/database:/app/database` and `-v $(pwd)/config:/app/config` in your docker run command
+14
View File
@@ -93,10 +93,24 @@ export SMTP_SECURE=0
export SMTP_FROM_EMAIL=Some Name <user@example.com>
```
<div class=" note info ">
Generally, port 465 expects implicit SSL (SMTP_SECURE: 1), while port 587 and port 25 usually use STARTTLS (SMTP_SECURE: 0).
So, if you are using SMTP on port 465, set SMTP_SECURE: 1; otherwise, for ports 25 or 587, set SMTP_SECURE: 0.
</div>
<div class=" note danger ">
If you set SMTP variables, Kener will use SMTP to send emails. If you set RESEND_API_KEY, Kener will use resend to send emails. If you do both Kener will use SMTP.
</div>
<div class=" note info ">
If your SMTP provider does require username and password, you can set `SMTP_USER` and `SMTP_PASS` to `-`.
</div>
## Using .env
You can also use a `.env` file to set these variables. Create a `.env` file in the root of the project and add the variables like below
+30 -75
View File
@@ -1,5 +1,5 @@
---
title: Kener Documentation
title: Kener Documentation | Open Source Status Page
description: Kener is a feature-rich and modern status page system built with SvelteKit and NodeJS. It is open-source and free to use.
---
@@ -36,86 +36,41 @@ description: Kener is a feature-rich and modern status page system built with Sv
</picture>
</div>
<div class="flex gap-2 kener-home-links">
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://kener.ing">Live Demo</a>
</div>
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://kener.ing/docs/quick-start">Quick Start</a>
</div>
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://github.com/rajnandan1/kener">Clone</a>
</div>
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://kener.ing/docs/deployment">Deploy</a>
</div>
<div class="flex-1 border rounded-md py-4 px-2 text-center">
<a href="https://kener.ing/docs/kener-apis">APIs</a>
</div>
<a href="https://kener.ing" class="flex-1 border rounded-md py-4 px-2 text-center">
Live Demo
</a>
<a href="https://kener.ing/docs/quick-start" class="flex-1 border rounded-md py-4 px-2 text-center">
Quick Start
</a>
<a href="https://github.com/rajnandan1/kener" class="flex-1 border rounded-md py-4 px-2 text-center">
Clone
</a>
<a href="https://kener.ing/docs/deployment" class="flex-1 border rounded-md py-4 px-2 text-center">
Deploy
</a>
<a href="https://kener.ing/docs/kener-apis" class="flex-1 border rounded-md py-4 px-2 text-center">
APIs
</a>
</div>
## What is Kener?
## Forget Boring Status Pages. Hello Kener!
Kener is status page system built with Sveltekit and NodeJS. It does not try to replace the Datadogs and Atlassian of the world. It tries to help some who wants to come up with a status page that looks nice and minimum overhead, in a modern way.
**Kener** isn't just another status page it's a beautifully crafted, lightning-fast monitoring system that makes your services look good even when they're down. Built with modern tech (**SvelteKit** + **NodeJS**), it delivers enterprise-grade reliability monitoring without the enterprise price tag (it's completely free!).
It comes with all the basic asks for a status page. It is open-source and free to use.
### Why Teams Are Switching to Kener
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing because it was a
cheaply available domain.
- **📈 Up and running in minutes**, not days from Docker to deployment in less time than it takes to make coffee
- **🎭 Dark mode that actually looks good** because your 3 AM incident responses shouldn't burn your retinas
- **🧩 Ridiculously customizable** make it match your brand or go wild with a unique design
- **⚡ Blazing fast performance** server-rendered for speed that both users and search engines love
- **🌐 Works everywhere** auto-adjusts to visitors' time zones and looks great on any device
- **🔄 Real-time data** with gorgeous visualizations that make your uptime metrics pop
- **🛠️ Modern tooling** without the bloat everything you need, nothing you don't
## Quick Deployment
### Used by Companies That Value Simplicity and Power
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
From indie developers to growing startups, teams are choosing Kener when they need reliability monitoring that doesn't require a dedicated DevOps team to maintain.
## Features
"_My team set up Kener in less than an hour, and our customers love the transparency it provides. The embedded widgets on our pricing page increased conversions by showing our reliability upfront._" A happy user
### Monitoring and Tracking
- Advanced application performance monitoring tools
- Types of Monitors: HTTP, TCP, DNS, ICMP etc
- Real-time network monitor software capabilities
- Polls HTTP endpoint or Push data to monitor using Rest APIs
- Handles Timezones for visitors
- Categorize Monitors into different Sections
- Cron-based scheduling for monitors. Minimum per minute
- Construct complex API Polls - Chain, Secrets etc
- Supports a Default Status for Monitors
- Supports base path for hosting in k8s
- Pre-built docker image for easy deployment
### Customization and Branding
- Customizable status page
- Badge generation for status and uptime of Monitors
- Support for custom domains
- Embed Monitor as an iframe or widget
- Light + Dark Theme
- Internationalization support
- Beautifully Crafted Status Page
### Incident Management
- Incident Management
- Incident Communication
- Comprehensive APIs for Incident Management
### User Experience and Design
- 100% Accessibility Score
- Easy installation and setup
- User-friendly interface
- Responsive design for various devices
- Auto SEO and Social Media ready
- Server Side Rendering
## Technologies used
- [SvelteKit](https://kit.svelte.dev/)
- [shadcn-svelte](https://www.shadcn-svelte.com/)
## Support Me
If you are using Kener and want to support me, you can do so by sponsoring me on GitHub or buying me a coffee.
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
## Ready to Transform Your Status Page?
+93 -90
View File
@@ -28,65 +28,68 @@ Copy and update the translation file in the `locales` folder. The translation fi
```json
{
"%status for %duration": "%status for %duration",
"14 Days": "14 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
"7 Days": "7 Days",
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"Back": "Back",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Code Copied": "Code Copied",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"DEGRADED": "DEGRADED",
"DOWN": "DOWN",
"Dark": "Dark",
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
"INVESTIGATING": "INVESTIGATING",
"Incident Updates": "Incident Updates",
"LIVE Status": "LIVE Status",
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
"No Incidents": "No Incidents",
"No Monitor Found": "No Monitor Found",
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"RESOLVED": "RESOLVED",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Theme",
"Today": "Today",
"UP": "UP",
"Upcoming Maintenance": "Upcoming Maintenance",
"Updates": "Updates",
"Uptime": "Uptime"
"%status for %duration": "%status for %duration",
"14 Days": "14 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
"7 Days": "7 Days",
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"Back": "Back",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Code Copied": "Code Copied",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"DEGRADED": "DEGRADED",
"DOWN": "DOWN",
"Dark": "Dark",
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
"INVESTIGATING": "INVESTIGATING",
"Incident Updates": "Incident Updates",
"LIVE Status": "LIVE Status",
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
"No Incidents": "No Incidents",
"No Monitor Found": "No Monitor Found",
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"RESOLVED": "RESOLVED",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Started %startedAt, will last for %lastedFor more": "Started %startedAt, will last for %lastedFor more",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Switch to your timezone": "Switch to your timezone",
"Theme": "Theme",
"Timezone set to %tz": "Timezone set to %tz",
"Today": "Today",
"UP": "UP",
"Upcoming Maintenance": "Upcoming Maintenance",
"Updates": "Updates",
"Uptime": "Uptime"
}
```
@@ -100,30 +103,30 @@ Open `src/lib/locales/locales.json` and add the locale code and the name of the
```json
[
{
"code": "en",
"name": "English"
},
{
"code": "hi",
"name": "हिन्दी"
},
{
"code": "zh-CN",
"name": "中文"
},
{
"code": "ja",
"name": "日本語"
},
{
"code": "vi",
"name": "Tiếng Việt"
},
{
"code": "fr",
"name": "Français"
}
{
"code": "en",
"name": "English"
},
{
"code": "hi",
"name": "हिन्दी"
},
{
"code": "zh-CN",
"name": "中文"
},
{
"code": "ja",
"name": "日本語"
},
{
"code": "vi",
"name": "Tiếng Việt"
},
{
"code": "fr",
"name": "Français"
}
]
```
@@ -132,7 +135,7 @@ Open `src/lib/locales/locales.json` and add the locale code and the name of the
Open `src/lib/i18n/client.js` and add the locale code to the `locales` array.
```js
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale"
```
This should be valid for the date-fns library. You can find the list of locales [here](https://github.com/date-fns/date-fns/blob/9bb51691f201c3ec05ab832acbc5d478f2e5c47a/docs/i18nLocales.md).
@@ -140,7 +143,7 @@ This should be valid for the date-fns library. You can find the list of locales
Next add a mapping in the `locales` object.
```js
const locales = { ru, en: enUS, hi, dk: de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko };
const locales = { ru, en: enUS, hi, dk: de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko }
```
### Step 4
@@ -155,11 +158,11 @@ DO NOT forget to raise a PR with the new translation file so that others can use
There are few variables that you you should not change,
- %status : This will be replaced by the status
- %lastedFor : This will be replaced by the duration the incident lasted
- %startedAt : This will be replaced by the time the incident started
- %date : This will be replaced by the date
- %duration : This will be replaced by the duration
- %status : This will be replaced by the status
- %lastedFor : This will be replaced by the duration the incident lasted
- %startedAt : This will be replaced by the time the incident started
- %date : This will be replaced by the date
- %duration : This will be replaced by the duration
## Dates
+38 -16
View File
@@ -51,7 +51,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
> `{status:"DEGRADED", latency: 200}`.
```javascript
(async function (statusCode, responseTime, responseDataBase64) {
(async function (statusCode, responseTime, responseRaw, modules) {
let statusCodeShort = Math.floor(statusCode/100);
let status = 'DOWN'
if(statusCodeShort >=2 && statusCodeShort <= 3) {
@@ -64,23 +64,19 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
})
```
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
- `responseRaw` **REQUIRED** is the raw response of the API
- `modules` is an object that has [`cheerio`](https://www.npmjs.com/package/cheerio) that you can use to parse HTML. `const $ = modules.cheerio.load(responseRaw)`
```js
let decodedResp = atob(responseDataBase64)
//if the response is a json object
//let jsonResp = JSON.parse(decodedResp)
```
#### Example
### Example 1
The following example shows how to use the eval function to evaluate the response. The function checks if the status code is 2XX then the status is UP, if the status code is 5XX then the status is DOWN. If the response contains the word `Unknown Error` then the status is DOWN. If the response time is greater than 2000 then the status is DEGRADED.
```javascript
;(async function (statusCode, responseTime, responseDataBase64) {
const resp = atob(responseDataBase64) //convert base64 to string
(async function (statusCode, responseTime, responseRaw, modules) {
let status = "DOWN"
@@ -95,7 +91,7 @@ The following example shows how to use the eval function to evaluate the respons
//if the status code is 5XX then the status is DOWN
if (/^[5]\d{2}$/.test(statusCode)) status = "DOWN"
if (resp.includes("Unknown Error")) {
if (responseRaw.includes("Unknown Error")) {
status = "DOWN"
}
@@ -106,11 +102,13 @@ The following example shows how to use the eval function to evaluate the respons
})
```
### Example 2
This next example shows how to call another API withing eval. It is scrapping the second last script tag from the response and checking if the heading is "No recent issues" then the status is UP else it is DOWN.
```javascript
;(async function raj(statusCode, responseTime, responseDataBase64) {
let htmlString = atob(responseDataBase64)
```js
(async function (statusCode, responseTime, responseRaw, modules) {
let htmlString = responseRaw;
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g)
if (scriptTags && scriptTags.length >= 2) {
// Extract the second last script tag's src attribute
@@ -136,6 +134,30 @@ This next example shows how to call another API withing eval. It is scrapping th
})
```
### Example 3
The next example shows how to use cheerio to parse bitbucket status page and check if all the components are operational. If all the components are operational then the status is UP else it is DOWN.
```js
(async function (statusCode, responseTime, responseDataBase64, modules) {
let html = atob(responseDataBase64)
const $ = modules.cheerio.load(html)
const components = $(".components-section .components-container .component-container")
let status = true
components.each((index, element) => {
const name = $(element).find(".component-name").text().trim()
const statusText = $(element).find(".component-status").text().trim()
if (statusText !== "Operational") {
status = false
}
})
return {
status: status ? "UP" : "DOWN",
latency: responseTime
}
})
```
## Examples
### Website Monitor
+112
View File
@@ -0,0 +1,112 @@
---
title: Heartbeat Monitors | Kener
description: Heartbeat monitors allow you to monitor systems using a "push" based approach
---
# Heartbeat Monitors
Heartbeat monitors allow you to monitor systems using a "push" based approach, where your services send signals (or "heartbeats") to Kener on a regular basis. If Kener doesn't receive a heartbeat within the configured time period, it will mark the system as degraded or down.
This is useful for monitoring services that:
- Run on a schedule
- Cannot be directly accessed for pull-based monitoring
- Need to report their own health status
## How It Works
1. You configure a heartbeat monitor in Kener
2. Kener provides a unique URL endpoint for your service to ping
3. Your service sends HTTP requests to this endpoint at regular intervals
4. If Kener doesn't receive a ping within the configured time window, it will mark the service as degraded or down
## Setting Up a Heartbeat Monitor
To create a heartbeat monitor:
1. Navigate to the Monitors page in the dashboard
2. Click "Add Monitor"
3. Select "Heartbeat" as the monitor type
4. Configure the following settings:
<div class="border rounded-md">
![Heartbeat Monitor Configuration](/documentation/m_hb.png)
</div>
### Configuration Options
| Option | Description |
| ------------------------ | -------------------------------------------------------------------------------------------- |
| Secret String | A unique secret that authenticates your heartbeat requests |
| Degraded After (Minutes) | Number of minutes after which the service is considered degraded if no heartbeat is received |
| Down After (Minutes) | Number of minutes after which the service is considered down if no heartbeat is received |
## Sending Heartbeats
Your service needs to send HTTP requests to the Kener heartbeat URL. Both GET and POST requests are supported.
### Endpoint Format
```
https://<your-kener-instance>/api/heartbeat/<tag>:<secretString>
```
Where:
- `<tag>` is the tag you configured for your monitor
- `<secretString>` is the secret you configured during setup
### Example
If your monitor has:
- Tag: `nightly-backup`
- Secret String: `super-secret-key`
Your service should send requests to:
```
https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
```
You can use any HTTP client to send these requests, such as curl:
```bash
curl https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
```
Or in a scheduled script:
```bash
#!/bin/bash
# Run your task here
./perform-backup.sh
# Send heartbeat to Kener
curl https://<your-kener-instance>/api/heartbeat/nightly-backup:super-secret-key
```
## Monitoring State
The monitor status works as follows:
- **UP**: Heartbeats are being received within the configured time windows
- **DEGRADED**: No heartbeat has been received for longer than the "Degraded After" time period
- **DOWN**: No heartbeat has been received for longer than the "Down After" time period
## Security Considerations
- Keep your secret string confidential, as anyone with knowledge of it can send heartbeats
- Consider using HTTPS for your Kener instance to encrypt heartbeat communications
- Rotate the secret string periodically by updating the monitor configuration
## Best Practices
- Set the "Degraded After" time slightly longer than your expected interval between heartbeats
- Set the "Down After" time based on your SLA requirements
- For critical systems, consider sending heartbeats more frequently than required
- Add logging in your service to track successful and failed heartbeat attempts
Heartbeat monitors provide a simple but effective way to ensure your scheduled tasks and services are running properly, without requiring external polling.
+5 -10
View File
@@ -32,8 +32,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
> `{status:"DEGRADED", latency: 200}`.
```javascript
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
@@ -49,12 +48,9 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
})
```
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
- `arrayOfPings` **REQUIRED** is an array of Ping Response Objects as shown below.
```js
let decodedResp = atob(responseDataBase64)
let jsonResp = JSON.parse(decodedResp)
console.log(jsonResp)
```json
/*
[
{
@@ -93,7 +89,7 @@ console.log(jsonResp)
### Understanding the Input
The input to the eval function is a base64 encoded string. You will have to decode it and then parse it to get the array of objects. Each object in the array represents the ping response of a host.
Each object in the array represents the ping response of a host.
- `alive` is a boolean. It is true if the host is alive and false if the host is down.
- `min` is a string. It is the minimum latency of the pings.
@@ -109,8 +105,7 @@ The input to the eval function is a base64 encoded string. You will have to deco
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
```javascript
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
+63
View File
@@ -0,0 +1,63 @@
---
title: SQL Monitors | Kener
description: Monitor a SQL database
---
# SQL Monitors
SQL monitors are used to monitor a SQL database. You can monitor a SQL database by adding the database connection details to the monitor. The SQL monitor will check the database connection and notify you if the database connection is down.
<div class="border rounded-md">
![Monitors SQL](/documentation/m_sql.png)
</div>
## Database Type
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The database type is the type of the database you want to monitor. The database type should be one of the following values:
- mysql
- postgres
Instead of giving the connection string as plain text you can add environment variables to the connection string. The environment variables should be in the format of `$`. The environment variables should be defined in the `.env` file.
Example: `mysql://$MYSQL_USER:$MYSQL_PASSWORD@$MYSQL_HOST:$MYSQL_PORT/$MYSQL_DATABASE`
Make sure to define the environment variables in the `.env` file as shown below
```env
MYSQL_USER=root
MYSQL_PASSWORD=password
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=database
```
## Timeout
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The timeout is the time in milliseconds to wait for the database connection to respond.
## Connection String
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The connection string is the connection string of the database you want to monitor. The connection string should be a valid connection string for the database type you have selected.
## SQL Query
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The SQL query is the query you want to run on the database. The SQL query should be a valid SQL query for the database type you have selected.
+52
View File
@@ -0,0 +1,52 @@
---
title: SSL Monitors | Kener
description: Monitor the SSL certificate of a domain
---
# SSL Monitors
SSL monitors are used to monitor the SSL certificate of a domain. You can monitor the SSL certificate of a domain by adding the domain name to the monitor. The SSL monitor will check the SSL certificate of the domain and notify you if the SSL certificate is about to expire.
<div class="border rounded-md">
![Monitors SSL](/documentation/m_ssl.png)
</div>
## Host
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The host is the domain name of the website you want to monitor. The host should be a valid domain name.
## Port
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The port is the ssl port of the website you want to monitor. The default ssl port is 443.
## Degraded If
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
Set this to the number of hours that still remains for the certificate to expire. If the certificate is about to expire in the given number of hours, the monitor status will be set to degraded. Example if you send the value to be 24, the monitor status will be set to degraded if the certificate is about to expire in 24 hours.
## Down If
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
Set this to the number of hours that still remains for the certificate to expire. If the certificate is about to expire in the given number of hours, the monitor status will be set to down. Example if you send the value to be 12, the monitor status will be set to down if the certificate is about to expire in 12 hours.
<div class="note">
`Degraded If` should be more than `Down If`.
</div>
+18 -23
View File
@@ -5,7 +5,7 @@ description: Learn how to set up and work with TCP monitors in kener.
# TCP Monitors
TCP monitors are used to monitor the livenees of your servers. You can use TCP monitors to monitor the uptime of your servers and get notified when they are down.
TCP monitors are used to monitor the liveness of your servers. You can use TCP monitors to monitor the uptime of your servers and get notified when they are down.
<div class="border rounded-md">
@@ -17,10 +17,10 @@ TCP monitors are used to monitor the livenees of your servers. You can use TCP m
You can add as many hosts as you want to monitor. The host can be an IP(IP4 and IP6) address or a domain name.
- Type: Choose the type of host you want to monitor. It can be either `IP4` or `IP6` or `DOMAIN`.
- Host: Enter the IP address or domain name of the host you want to monitor.
- Port: Enter the port number of the host you want to monitor.
- Timeout: Enter the timeout in milliseconds for each ping request of each host
- Type: Choose the type of host you want to monitor. It can be either `IP4` or `IP6` or `DOMAIN`.
- Host: Enter the IP address or domain name of the host you want to monitor.
- Port: Enter the port number of the host you want to monitor.
- Timeout: Enter the timeout in milliseconds for each ping request of each host
## Eval
@@ -32,8 +32,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
> `{status:"DEGRADED", latency: 200}`.
```javascript
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
@@ -53,12 +52,9 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
})
```
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
- `arrayOfPings` **REQUIRED** is an array of TCP Response Objects as shown below.
```js
let decodedResp = atob(responseDataBase64)
let jsonResp = JSON.parse(decodedResp)
console.log(jsonResp)
```json
/*
[
{
@@ -88,24 +84,23 @@ console.log(jsonResp)
### Understanding the Input
The input to the eval function is a base64 encoded string. You will have to decode it and then parse it to get the array of objects. Each object in the array represents the ping response of a host.
Each object in the array represents the tcp response of a host.
- `host`: The host that was pinged.
- `port`: The port that was pinged. Defaults to 80 if not provided.
- `type`: The type of IP address. Can be `IP4` or `IP6`.
- `status`: The status of the ping. Can be `open` , `error` or `timeout`.
- `open`: The host is reachable.
- `error`: There was an error while pinging the host.
- `timeout`: The host did not respond in time.
- `latency`: The time taken to ping the host. This is in milliseconds.
- `host`: The host that was pinged.
- `port`: The port that was pinged. Defaults to 80 if not provided.
- `type`: The type of IP address. Can be `IP4` or `IP6`.
- `status`: The status of the ping. Can be `open` , `error` or `timeout`.
- `open`: The host is reachable.
- `error`: There was an error while pinging the host.
- `timeout`: The host did not respond in time.
- `latency`: The time taken to ping the host. This is in milliseconds.
### Example
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
```javascript
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
+21 -10
View File
@@ -109,15 +109,15 @@ To monitor the ping, you need to provide the IP address or the domain name. If t
To add a trigger to a monitor make sure you have created a trigger. You can read more about triggers [here](/docs/triggers).
- Click on the 🔔 icon on the top right corner of the monitor.
- Add details for either DOWN or DEGRADED.
- Failure Threshold(Required): The number of consecutive failures before the trigger is activated.
- Success Threshold(Required): The number of consecutive successes before the trigger is deactivated.
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in Github. So make sure you have set up the Github token in the environment variables. The incident will be closed when the monitor is back to UP.
- Severity (Required): The severity of the incident. It can be `Critcal` or `Warning`.
- Custom Message (Required): Add your owner alert message.
- Choose Triggers: Choose the triggers you want to activate the trigger. You can choose multiple triggers.
- It will take upto 1 minute for the trigger to be activated.
- Click on the 🔔 icon on the top right corner of the monitor.
- Add details for either DOWN or DEGRADED.
- Failure Threshold(Required): The number of consecutive failures before the trigger is activated.
- Success Threshold(Required): The number of consecutive successes before the trigger is deactivated.
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in Github. So make sure you have set up the Github token in the environment variables. The incident will be closed when the monitor is back to UP.
- Severity (Required): The severity of the incident. It can be `Critcal` or `Warning`.
- Custom Message (Required): Add your owner alert message.
- Choose Triggers: Choose the triggers you want to activate the trigger. You can choose multiple triggers.
- It will take upto 1 minute for the trigger to be activated.
---
@@ -135,6 +135,17 @@ You can activate the monitor by switching the toggle to on. Activation a monitor
<div class="note info">
Any changes in the a live monitor will take one minute to reflect.
Any changes in the a live monitor will take 2 minutes to reflect.
</div>
## Supported Monitors
- [API Monitors](/docs/monitors-api)
- [DNS Monitors](/docs/monitors-dns)
- [PING Monitors](/docs/monitors-ping)
- [TCP Monitors](/docs/monitors-tcp)
- [SQL Monitors](/docs/monitors-sql)
- [SSL Monitors](/docs/monitors-ssl)
- [HEARTBEAT Monitors](/docs/monitors-heartbeat)
- [GROUP Monitors](/docs/monitors-group)
+18 -12
View File
@@ -9,9 +9,9 @@ Here is a demonstration of how to get started with Kener in seconds
## Requirements
- Node.js Minimum version required is `v18`.
- Git
- sqlite3
- Node.js Minimum version required is `v18`.
- Git
- sqlite3
## Clone the repository
@@ -54,9 +54,9 @@ Status Page Manage Portal will be at [http://localhost:3000/manage/app/site](htt
If this is the first time your are launching kener then you would be redirected to the [set up page](/manage/setup). Fill in the details and click on `Let's Go` button.
- **Name**: Your Name
- **Email**: Your Email
- **Password**: Your Password
- **Name**: Your Name
- **Email**: Your Email
- **Password**: Your Password
Please note that the email should be a valid email address and password should be atleast 8 characters long with uppercase lowercase and numbers.
@@ -64,13 +64,19 @@ Please note that the email should be a valid email address and password should b
Once you have created the user, you can login with the credentials you have provided by going to the [login page](/manage/signin)
## Video Tutorial
You can watch the video tutorial on how to get started with Kener
[![Kener Video Tutorial](https://img.youtube.com/vi/4L9PnWPqMPw/0.jpg)](https://www.youtube.com/watch?v=4L9PnWPqMPw)
## Next Steps
Learn how to configure kener by going through one of the topics
- [Monitors](/docs/monitors): Learn how to set up and work with monitors in kener.
- [Triggers](/docs/triggers): Learn how to set up and work with triggers in kener.
- [Environment Variables](/docs/environment-vars): Learn how to set up and work with environment variables in kener.
- [API](/docs/kener-apis): Learn how to use the API in kener.
- [Databases](/docs/database): Learn how to set up and work with databases in kener.
- [Theme](/docs/theme): Learn how to set up and work with theme in kener.
- [Monitors](/docs/monitors): Learn how to set up and work with monitors in kener.
- [Triggers](/docs/triggers): Learn how to set up and work with triggers in kener.
- [Environment Variables](/docs/environment-vars): Learn how to set up and work with environment variables in kener.
- [API](/docs/kener-apis): Learn how to use the API in kener.
- [Databases](/docs/database): Learn how to set up and work with databases in kener.
- [Theme](/docs/theme): Learn how to set up and work with theme in kener.
+46 -25
View File
@@ -49,31 +49,7 @@
"link": "/docs/monitors",
"file": "/monitors.md"
},
{
"title": "API/Website Monitor",
"link": "/docs/monitors-api",
"file": "/monitors-api.md"
},
{
"title": "Ping Monitor",
"link": "/docs/monitors-ping",
"file": "/monitors-ping.md"
},
{
"title": "TCP Monitor",
"link": "/docs/monitors-tcp",
"file": "/monitors-tcp.md"
},
{
"title": "DNS Monitor",
"link": "/docs/monitors-dns",
"file": "/monitors-dns.md"
},
{
"title": "Group Monitor",
"link": "/docs/monitors-group",
"file": "/monitors-group.md"
},
{
"title": "Setup Triggers",
"link": "/docs/triggers",
@@ -132,6 +108,51 @@
}
]
},
{
"sectionTitle": "Monitors",
"children": [
{
"title": "API/Website Monitor",
"link": "/docs/monitors-api",
"file": "/monitors-api.md"
},
{
"title": "Ping Monitor",
"link": "/docs/monitors-ping",
"file": "/monitors-ping.md"
},
{
"title": "TCP Monitor",
"link": "/docs/monitors-tcp",
"file": "/monitors-tcp.md"
},
{
"title": "DNS Monitor",
"link": "/docs/monitors-dns",
"file": "/monitors-dns.md"
},
{
"title": "Group Monitor",
"link": "/docs/monitors-group",
"file": "/monitors-group.md"
},
{
"title": "SSL Monitor",
"link": "/docs/monitors-ssl",
"file": "/monitors-ssl.md"
},
{
"title": "SQL Monitor",
"link": "/docs/monitors-sql",
"file": "/monitors-sql.md"
},
{
"title": "Heartbeat Monitor",
"link": "/docs/monitors-heartbeat",
"file": "/monitors-heartbeat.md"
}
]
},
{
"sectionTitle": "API Reference",
"children": [
+6
View File
@@ -216,6 +216,12 @@ Since the password will be stored as plain text we encourage to use environment
</div>
<div class=" note info ">
If your SMTP provider does require username and password, you can set `SMTP_USER` and `SMTP_PASS` to `-`.
</div>
### To
<span class="text-red-500 text-xs font-semibold">
+18 -17
View File
@@ -9,28 +9,29 @@ const databaseType = databaseURLParts[0];
const databasePath = databaseURLParts[1];
const knexOb = {
migrations: {
directory: "./migrations"
},
seeds: {
directory: "./seeds"
}
migrations: {
directory: "./migrations",
},
seeds: {
directory: "./seeds",
},
databaseType,
};
if (databaseType === "sqlite") {
knexOb.client = "better-sqlite3";
knexOb.connection = {
filename: databasePath
};
knexOb.useNullAsDefault = true;
knexOb.client = "better-sqlite3";
knexOb.connection = {
filename: databasePath,
};
knexOb.useNullAsDefault = true;
} else if (databaseType === "postgresql") {
knexOb.client = "pg";
knexOb.connection = databaseURL;
knexOb.client = "pg";
knexOb.connection = databaseURL;
} else if (databaseType === "mysql") {
knexOb.client = "mysql2";
knexOb.connection = databaseURL;
knexOb.client = "mysql2";
knexOb.connection = databaseURL;
} else {
console.error("Invalid database type");
process.exit(1);
console.error("Invalid database type");
process.exit(1);
}
export default knexOb;
@@ -0,0 +1,11 @@
export function up(knex) {
return knex.schema.alterTable("incidents", function (table) {
table.text("incident_source").defaultTo("DASHBOARD");
});
}
export function down(knex) {
return knex.schema.alterTable("incidents", function (table) {
table.dropColumn("incident_source");
});
}
+224 -2
View File
@@ -1,12 +1,12 @@
{
"name": "kener",
"version": "3.0.12",
"version": "3.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kener",
"version": "3.0.12",
"version": "3.1.8",
"license": "MIT",
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
@@ -18,9 +18,11 @@
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.5.0",
"bits-ui": "^0.9.9",
"cheerio": "^1.0.0",
"clsx": "^2.0.0",
"croner": "^7.0.5",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dns2": "^2.1.0",
"dotenv": "^16.4.5",
"express": "^4.18.2",
@@ -1700,6 +1702,12 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -1929,6 +1937,76 @@
"binary-search": "^1.3.5"
}
},
"node_modules/cheerio": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"encoding-sniffer": "^0.2.0",
"htmlparser2": "^9.1.0",
"parse5": "^7.1.2",
"parse5-htmlparser2-tree-adapter": "^7.0.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^6.19.5",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=18.17"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
"css-what": "^6.1.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cheerio/node_modules/htmlparser2": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.5.0"
}
},
"node_modules/cheerio/node_modules/undici": {
"version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -2247,6 +2325,22 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
"integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling=="
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
@@ -2266,6 +2360,18 @@
"integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==",
"license": "MIT"
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2339,6 +2445,15 @@
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-tz": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"license": "MIT",
"peerDependencies": {
"date-fns": "^3.0.0 || ^4.0.0"
}
},
"node_modules/date-picker-svelte": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/date-picker-svelte/-/date-picker-svelte-2.15.1.tgz",
@@ -2686,6 +2801,31 @@
"node": ">= 0.8"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/encoding-sniffer/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -5376,6 +5516,18 @@
"set-blocking": "^2.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/number-flow": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/number-flow/-/number-flow-0.4.2.tgz",
@@ -5501,6 +5653,43 @@
"node": ">=4"
}
},
"node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parseley": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
@@ -8303,6 +8492,39 @@
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "kener",
"version": "3.1.4",
"version": "3.2.3",
"private": false,
"license": "MIT",
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
@@ -77,9 +77,11 @@
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.5.0",
"bits-ui": "^0.9.9",
"cheerio": "^1.0.0",
"clsx": "^2.0.0",
"croner": "^7.0.5",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dns2": "^2.1.0",
"dotenv": "^16.4.5",
"express": "^4.18.2",
+62 -60
View File
@@ -1,88 +1,90 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@layer base {
:root {
--background: 0 0% 100%;
--background-kener: hsl(0, 0%, 100%);
--background-kener-rgba: rgba(255, 255, 255, 0.5);
--foreground: 240 10% 4%;
:root {
--background: 0 0% 100%;
--background-kener: hsl(0, 0%, 100%);
--background-kener-rgba: rgba(255, 255, 255, 0.5);
--muted: 0 0% 92.9%;
--muted-foreground: 215.4 16.3% 46.9%;
--foreground: 240 10% 4%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 4%;
--muted: 0 0% 92.9%;
--muted-foreground: 215.4 16.3% 46.9%;
--card: 0 0% 99%;
--card-foreground: 240 10% 4%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 4%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--card: 0 0% 99%;
--card-foreground: 240 10% 4%;
--date-picker-background: hsl(0 0% 99%);
--date-picker-foreground: hsl(240 10% 4%);
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--date-picker-background: hsl(0 0% 99%);
--date-picker-foreground: hsl(240 10% 4%);
--secondary: 0 0% 92.9%;
--secondary-foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--secondary: 0 0% 92.9%;
--secondary-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--ring: 240 10% 4%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--radius: 0.5rem;
}
--ring: 240 10% 4%;
.dark {
--background: 240 7% 11%;
--background-kener: hsl(240 7% 11%);
--background-kener-rgba: rgba(30, 30, 26, 0.35);
--foreground: 60 10% 86%;
--radius: 0.5rem;
}
--muted: 240 4% 16%;
--muted-foreground: 0 0% 40%;
.dark {
--background: 240 7% 11%;
--background-kener: hsl(240 7% 11%);
--background-kener-rgba: rgba(30, 30, 26, 0.35);
--foreground: 60 10% 86%;
--popover: 230 9% 14%;
--popover-foreground: 210 40% 98%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 40%;
--card: 230 9% 14%;
--card-foreground: 60 10% 86%;
--popover: 230 9% 14%;
--popover-foreground: 210 40% 98%;
--border: 60 1% 17%;
--input: 60 6% 17%;
--card: 230 9% 14%;
--card-foreground: 60 10% 86%;
--date-picker-background: hsl(240 4% 16%);
--date-picker-foreground: hsl(60 10% 86%);
--border: 60 1% 17%;
--input: 60 6% 17%;
--primary: 60 10% 86%;
--primary-foreground: 222.2 47.4% 11.2%;
--date-picker-background: hsl(240 4% 16%);
--date-picker-foreground: hsl(60 10% 86%);
--secondary: 240 1% 18%;
--secondary-foreground: 60 10% 86%;
--primary: 60 10% 86%;
--primary-foreground: 222.2 47.4% 11.2%;
--accent: 240 4% 16%;
--accent-foreground: 210 40% 98%;
--secondary: 240 1% 18%;
--secondary-foreground: 60 10% 86%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--accent: 240 4% 16%;
--accent-foreground: 210 40% 98%;
--ring: hsl(212.7, 26.8%, 83.9);
}
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: hsl(212.7, 26.8%, 83.9);
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+3 -2
View File
@@ -18,9 +18,10 @@ main {
text-decoration: none;
background-color: var(--bg-background);
}
.kener-home-links > div:hover {
.kener-home-links > a:hover {
transition: all 0.3s;
box-shadow: 0 0 8px 1.5px #3e9a4b;
border-color: #ed702d;
background-color: #ed702d;
}
.accm input:checked ~ div {
+90
View File
@@ -0,0 +1,90 @@
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-hairline.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-hairline.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-hairline.ttf") format("truetype");
font-weight: 100;
font-style: normal;
}
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-hairline-italic.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-hairline-italic.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-hairline-italic.ttf") format("truetype");
font-weight: 100;
font-style: italic;
}
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-light.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-light.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-light.ttf") format("truetype");
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-light-italic.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-light-italic.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-light-italic.ttf") format("truetype");
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-regular.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-regular.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-regular.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-regular-italic.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-regular-italic.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-regular-italic.ttf") format("truetype");
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-bold.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-bold.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-bold-italic.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-bold-italic.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-bold-italic.ttf") format("truetype");
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-black.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-black.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-black.ttf") format("truetype");
font-weight: 900;
font-style: normal;
}
@font-face {
font-family: "Lato";
src:
url("./fonts/lato/latin_subset/lato-black-italic.woff2") format("woff2"),
url("./fonts/lato/latin_subset/lato-black-italic.woff") format("woff"),
url("./fonts/lato/latin_subset/lato-black-italic.ttf") format("truetype");
font-weight: 900;
font-style: italic;
}
+44 -5
View File
@@ -41,7 +41,8 @@
.squares-pattern::after {
content: "";
position: absolute;
background-image: linear-gradient(#444cf7 1px, transparent 1px),
background-image:
linear-gradient(#444cf7 1px, transparent 1px),
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
-webkit-mask-image: linear-gradient(
180deg,
@@ -87,7 +88,8 @@
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-image: linear-gradient(#616fbf 1px, transparent 1px),
background-image:
linear-gradient(#616fbf 1px, transparent 1px),
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
}
@@ -192,6 +194,12 @@ section {
border-bottom: 1px solid;
border-color: hsl(var(--border) / var(--tw-border-opacity));
}
@media (max-width: 767px) {
.monitors-card .monitor {
padding: 1.2em 1em 0.875em 1em;
}
}
.monitors-card .monitor:last-child {
border-bottom: none;
}
@@ -470,7 +478,8 @@ textarea::placeholder {
}
.mesh {
background-image: linear-gradient(
background-image:
linear-gradient(
323deg,
rgba(255, 255, 255, 0.01) 0%,
rgba(255, 255, 255, 0.01) 16.667%,
@@ -601,7 +610,8 @@ textarea::placeholder {
}
.dark .mesh {
background-image: linear-gradient(
background-image:
linear-gradient(
158deg,
rgba(84, 84, 84, 0.03) 0%,
rgba(84, 84, 84, 0.03) 20%,
@@ -815,7 +825,8 @@ textarea::placeholder {
}
.button-77:active:not(:disabled):after {
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
background-image:
linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
linear-gradient(92.83deg, #ff7426 0, #f93a13 100%);
bottom: 4px;
left: 4px;
@@ -866,3 +877,31 @@ textarea::placeholder {
transform: rotate(15deg);
}
}
.dots-animation::after {
content: ".";
animation: dots 1.5s steps(4) infinite;
}
@keyframes dots {
0% {
content: ".";
}
16% {
content: "..";
}
32% {
content: "...";
}
48% {
content: "....";
}
64% {
content: "...";
}
80% {
content: "..";
}
100% {
content: ".";
}
}
+248 -245
View File
@@ -1,288 +1,291 @@
// @ts-nocheck
import { base } from "$app/paths";
function siteDataExtractFromDb(data, obj) {
let requestedObject = { ...obj };
for (const key in requestedObject) {
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
const element = data[key];
if (data[key]) {
requestedObject[key] = data[key];
}
}
}
//remove any keys that are still null or empty
for (const key in requestedObject) {
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
const element = requestedObject[key];
if (element === null || element === "") {
delete requestedObject[key];
}
}
}
return requestedObject;
let requestedObject = { ...obj };
for (const key in requestedObject) {
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
const element = data[key];
if (data[key]) {
requestedObject[key] = data[key];
}
}
}
//remove any keys that are still null or empty
for (const key in requestedObject) {
if (Object.prototype.hasOwnProperty.call(requestedObject, key)) {
const element = requestedObject[key];
if (element === null || element === "") {
delete requestedObject[key];
}
}
}
return requestedObject;
}
//a function to make an api call to /manage/api/ to store site data
function storeSiteData(data) {
return fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "storeSiteData", data })
});
return fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ action: "storeSiteData", data }),
});
}
const allRecordTypes = {
A: 1,
NS: 2,
MD: 3,
MF: 4,
CNAME: 5,
SOA: 6,
MB: 7,
MG: 8,
MR: 9,
NULL: 10,
WKS: 11,
PTR: 12,
HINFO: 13,
MINFO: 14,
MX: 15,
TXT: 16,
RP: 17,
AFSDB: 18,
X25: 19,
ISDN: 20,
RT: 21,
NSAP: 22,
NSAP_PTR: 23,
SIG: 24,
KEY: 25,
PX: 26,
GPOS: 27,
AAAA: 28,
LOC: 29,
NXT: 30,
EID: 31,
NIMLOC: 32,
SRV: 33,
ATMA: 34,
NAPTR: 35,
KX: 36,
CERT: 37,
A6: 38,
DNAME: 39,
SINK: 40,
OPT: 41,
APL: 42,
DS: 43,
SSHFP: 44,
IPSECKEY: 45,
RRSIG: 46,
NSEC: 47,
DNSKEY: 48,
DHCID: 49,
NSEC3: 50,
NSEC3PARAM: 51,
TLSA: 52,
SMIMEA: 53,
HIP: 55,
NINFO: 56,
RKEY: 57,
TALINK: 58,
CDS: 59,
CDNSKEY: 60,
OPENPGPKEY: 61,
CSYNC: 62,
SPF: 99,
UINFO: 100,
UID: 101,
GID: 102,
UNSPEC: 103,
NID: 104,
L32: 105,
L64: 106,
LP: 107,
EUI48: 108,
EUI64: 109,
TKEY: 249,
TSIG: 250,
IXFR: 251,
AXFR: 252,
MAILB: 253,
MAILA: 254,
ANY: 255
A: 1,
NS: 2,
MD: 3,
MF: 4,
CNAME: 5,
SOA: 6,
MB: 7,
MG: 8,
MR: 9,
NULL: 10,
WKS: 11,
PTR: 12,
HINFO: 13,
MINFO: 14,
MX: 15,
TXT: 16,
RP: 17,
AFSDB: 18,
X25: 19,
ISDN: 20,
RT: 21,
NSAP: 22,
NSAP_PTR: 23,
SIG: 24,
KEY: 25,
PX: 26,
GPOS: 27,
AAAA: 28,
LOC: 29,
NXT: 30,
EID: 31,
NIMLOC: 32,
SRV: 33,
ATMA: 34,
NAPTR: 35,
KX: 36,
CERT: 37,
A6: 38,
DNAME: 39,
SINK: 40,
OPT: 41,
APL: 42,
DS: 43,
SSHFP: 44,
IPSECKEY: 45,
RRSIG: 46,
NSEC: 47,
DNSKEY: 48,
DHCID: 49,
NSEC3: 50,
NSEC3PARAM: 51,
TLSA: 52,
SMIMEA: 53,
HIP: 55,
NINFO: 56,
RKEY: 57,
TALINK: 58,
CDS: 59,
CDNSKEY: 60,
OPENPGPKEY: 61,
CSYNC: 62,
SPF: 99,
UINFO: 100,
UID: 101,
GID: 102,
UNSPEC: 103,
NID: 104,
L32: 105,
L64: 106,
LP: 107,
EUI48: 108,
EUI64: 109,
TKEY: 249,
TSIG: 250,
IXFR: 251,
AXFR: 252,
MAILB: 253,
MAILA: 254,
ANY: 255,
};
const ValidateIpAddress = function (input) {
// Check if input is a valid IPv4 address with an optional port
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IP4";
}
// Check if input is a valid IPv4 address with an optional port
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IP4";
}
// Improved IPv6 regex that better handles compressed notation
const ipv6Regex =
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){0,7}:|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){6}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|(?:[0-9a-fA-F]{1,4}:){1,7}(?::|:[0-9a-fA-F]{1,4})|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$/;
if (ipv6Regex.test(input)) {
return "IP6";
}
// Improved IPv6 regex that better handles compressed notation
const ipv6Regex =
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){0,7}:|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){6}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|(?:[0-9a-fA-F]{1,4}:){1,7}(?::|:[0-9a-fA-F]{1,4})|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$/;
if (ipv6Regex.test(input)) {
return "IP6";
}
// Check if input is a valid domain name with an optional port
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "DOMAIN";
}
// Check if input is a valid domain name with an optional port
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "DOMAIN";
}
// If none of the above conditions match, the input is invalid
return "Invalid";
// If none of the above conditions match, the input is invalid
return "Invalid";
};
function IsValidHost(domain) {
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
}
function IsValidNameServer(nameServer) {
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
}
const IsValidURL = function (url) {
return /^(http|https):\/\/[^ "]+$/.test(url);
return /^(http|https):\/\/[^ "]+$/.test(url);
};
function ValidateCronExpression(cronExp) {
// Check if expression is provided and is a string
if (!cronExp || typeof cronExp !== "string") {
return { isValid: false, message: "Cron expression must be a non-empty string" };
}
// Check if expression is provided and is a string
if (!cronExp || typeof cronExp !== "string") {
return { isValid: false, message: "Cron expression must be a non-empty string" };
}
// Split the expression into its components
const fields = cronExp.trim().split(/\s+/);
// Split the expression into its components
const fields = cronExp.trim().split(/\s+/);
// Standard cron should have 5 or 6 fields
// minute hour day-of-month month day-of-week [year]
if (fields.length < 5 || fields.length > 6) {
return {
isValid: false,
message: "Cron expression must have 5 or 6 fields"
};
}
// Standard cron should have 5 or 6 fields
// minute hour day-of-month month day-of-week [year]
if (fields.length < 5 || fields.length > 6) {
return {
isValid: false,
message: "Cron expression must have 5 or 6 fields",
};
}
// Define field constraints
const fieldConstraints = [
{ name: "minute", min: 0, max: 59 },
{ name: "hour", min: 0, max: 23 },
{ name: "day", min: 1, max: 31 },
{ name: "month", min: 1, max: 12 },
{ name: "weekday", min: 0, max: 6 },
{ name: "year", min: 1970, max: 2099 } // Optional field
];
// Define field constraints
const fieldConstraints = [
{ name: "minute", min: 0, max: 59 },
{ name: "hour", min: 0, max: 23 },
{ name: "day", min: 1, max: 31 },
{ name: "month", min: 1, max: 12 },
{ name: "weekday", min: 0, max: 6 },
{ name: "year", min: 1970, max: 2099 }, // Optional field
];
// Valid characters in cron expressions
const validChars = /^[\d/*,\-]+$/;
// Valid characters in cron expressions
const validChars = /^[\d/*,\-]+$/;
// Validate each field
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
const constraint = fieldConstraints[i];
// Validate each field
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
const constraint = fieldConstraints[i];
// Check for valid characters
if (!validChars.test(field)) {
return {
isValid: false,
message: `Invalid characters in ${constraint.name} field`
};
}
// Check for valid characters
if (!validChars.test(field)) {
return {
isValid: false,
message: `Invalid characters in ${constraint.name} field`,
};
}
// Handle special characters
if (field === "*") {
continue; // Asterisk is valid for all fields
}
// Handle special characters
if (field === "*") {
continue; // Asterisk is valid for all fields
}
// Handle lists (comma-separated values)
if (field.includes(",")) {
const values = field.split(",");
for (const value of values) {
if (!isValidRange(value, constraint.min, constraint.max)) {
return {
isValid: false,
message: `Invalid value in ${constraint.name} field: ${value}`
};
}
}
continue;
}
// Handle lists (comma-separated values)
if (field.includes(",")) {
const values = field.split(",");
for (const value of values) {
if (!isValidRange(value, constraint.min, constraint.max)) {
return {
isValid: false,
message: `Invalid value in ${constraint.name} field: ${value}`,
};
}
}
continue;
}
// Handle ranges (with hyphens)
if (field.includes("-")) {
const [start, end] = field.split("-").map(Number);
if (
start == null ||
end == null ||
start < constraint.min ||
end > constraint.max ||
start > end
) {
return {
isValid: false,
message: `Invalid range in ${constraint.name} field: ${field}`
};
}
continue;
}
// Handle ranges (with hyphens)
if (field.includes("-")) {
const [start, end] = field.split("-").map(Number);
if (start == null || end == null || start < constraint.min || end > constraint.max || start > end) {
return {
isValid: false,
message: `Invalid range in ${constraint.name} field: ${field}`,
};
}
continue;
}
// Handle steps (with forward slash)
if (field.includes("/")) {
const [range, step] = field.split("/");
if (range !== "*" && !isValidRange(range, constraint.min, constraint.max)) {
return {
isValid: false,
message: `Invalid range in ${constraint.name} field: ${range}`
};
}
if (!isValidRange(step, 1, constraint.max)) {
return {
isValid: false,
message: `Invalid step value in ${constraint.name} field: ${step}`
};
}
continue;
}
// Handle steps (with forward slash)
if (field.includes("/")) {
const [range, step] = field.split("/");
if (range !== "*" && !isValidRange(range, constraint.min, constraint.max)) {
return {
isValid: false,
message: `Invalid range in ${constraint.name} field: ${range}`,
};
}
if (!isValidRange(step, 1, constraint.max)) {
return {
isValid: false,
message: `Invalid step value in ${constraint.name} field: ${step}`,
};
}
continue;
}
// Handle plain numbers
if (!isValidRange(field, constraint.min, constraint.max)) {
return {
isValid: false,
message: `Invalid value in ${constraint.name} field: ${field}`
};
}
}
// Handle plain numbers
if (!isValidRange(field, constraint.min, constraint.max)) {
return {
isValid: false,
message: `Invalid value in ${constraint.name} field: ${field}`,
};
}
}
return { isValid: true, message: "Valid cron expression" };
return { isValid: true, message: "Valid cron expression" };
}
function isValidRange(value, min, max) {
const num = Number(value);
return !isNaN(num) && num >= min && num <= max;
const num = Number(value);
return !isNaN(num) && num >= min && num <= max;
}
function SortMonitor(monitorSort, resp) {
let monitors = [];
if (!!monitorSort && monitorSort.length > 0) {
monitors = monitorSort.map((id) => resp.find((m) => m.id == id)).filter((m) => !!m);
//append any new monitors
monitors = [...monitors, ...resp.filter((m) => !monitorSort.includes(m.id))];
} else {
monitors = resp;
}
return monitors;
let monitors = [];
if (!!monitorSort && monitorSort.length > 0) {
monitors = monitorSort.map((id) => resp.find((m) => m.id == id)).filter((m) => !!m);
//append any new monitors
monitors = [...monitors, ...resp.filter((m) => !monitorSort.includes(m.id))];
} else {
monitors = resp;
}
return monitors;
}
//js function to generate 32 character random string
function RandomString(length) {
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = "";
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
export {
siteDataExtractFromDb,
storeSiteData,
allRecordTypes,
ValidateIpAddress,
IsValidHost,
IsValidNameServer,
IsValidURL,
ValidateCronExpression,
SortMonitor
siteDataExtractFromDb,
storeSiteData,
allRecordTypes,
ValidateIpAddress,
IsValidHost,
IsValidNameServer,
IsValidURL,
ValidateCronExpression,
SortMonitor,
RandomString,
};
+95 -40
View File
@@ -1,16 +1,16 @@
<script>
import { formatDistanceToNow, formatDistance } from "date-fns";
import { Settings } from "lucide-svelte";
import { Settings, ArrowRight } from "lucide-svelte";
import * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import { Tooltip } from "bits-ui";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
export let incident;
export let index;
export let lang;
export let selectedLang = "en";
let startTime = new Date(incident.start_date_time * 1000);
let endTime = new Date();
let nowTime = new Date();
@@ -18,59 +18,76 @@
endTime = new Date(incident.end_date_time * 1000);
}
let incidentType = incident.incident_type;
const lastedFor = fd(startTime, endTime, selectedLang);
const startedAt = fdn(startTime, selectedLang);
const lastedFor = fd(startTime, endTime, $page.data.selectedLang);
const remainingTime = fd(nowTime, endTime, $page.data.selectedLang);
const startedAt = fdn(startTime, $page.data.selectedLang);
let isFuture = false;
//is future incident
let incidentTimeStatus = "";
if (nowTime < startTime) {
isFuture = true;
incidentTimeStatus = "YET_TO_START";
} else if (nowTime >= startTime && nowTime <= endTime) {
incidentTimeStatus = "ONGOING";
} else if (nowTime > endTime) {
incidentTimeStatus = "COMPLETED";
}
let accordionValue = "incident-0";
if ($page.data.site.incidentGroupView == "COLLAPSED") {
accordionValue = "incident-collapse";
} else if ($page.data.site.incidentGroupView == "EXPANDED") {
accordionValue = index;
}
let incidentDateSummary = "";
let maintenanceBadge = "";
let maintenanceBadgeColor = "";
if (!isFuture && incident.state != "RESOLVED") {
incidentDateSummary = l(lang, "Started %startedAt, still ongoing", {
if (incidentTimeStatus == "YET_TO_START") {
incidentDateSummary = l($page.data.lang, "Starts %startedAt", { startedAt });
if (incidentType === "MAINTENANCE") {
incidentDateSummary = l($page.data.lang, "Starts %startedAt, will last for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
}
} else if (incidentTimeStatus == "ONGOING") {
incidentDateSummary = l($page.data.lang, "Started %startedAt, still ongoing", {
startedAt
});
maintenanceBadge = "Maintenance in Progress";
maintenanceBadgeColor = "text-maintenance-in-progress";
} else if (!isFuture && incident.state == "RESOLVED") {
incidentDateSummary = l(lang, "Started %startedAt, lasted for %lastedFor", {
if (incidentType === "MAINTENANCE") {
incidentDateSummary = l($page.data.lang, "Started %startedAt, will last for %lastedFor more", {
startedAt,
lastedFor: remainingTime
});
maintenanceBadge = "Maintenance in Progress";
maintenanceBadgeColor = "text-maintenance-in-progress";
}
} else if (incidentTimeStatus == "COMPLETED") {
incidentDateSummary = l($page.data.lang, "Started %startedAt, lasted for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Maintenance Completed";
maintenanceBadgeColor = "text-maintenance-completed";
} else if (isFuture && incident.state != "RESOLVED") {
incidentDateSummary = l(lang, "Starts %startedAt", { startedAt });
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
} else if (isFuture && incident.state == "RESOLVED") {
incidentDateSummary = l(lang, "Starts %startedAt, will last for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
}
</script>
<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.Item value="incident-0">
<Accordion.Item value={accordionValue}>
<Accordion.Trigger class="px-4 hover:bg-muted hover:no-underline">
<div class="w-full text-left hover:no-underline">
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
<span class="badge-{incident.state}">
{l(lang, incident.state)}
{l($page.data.lang, incident.state)}
</span>
{:else if incidentType == "MAINTENANCE"}
<span class="{maintenanceBadgeColor} ">
{l(lang, maintenanceBadge)}
{l($page.data.lang, maintenanceBadge)}
</span>
{/if}
{#if $page.data.isLoggedIn}
@@ -84,20 +101,48 @@
{/if}
</p>
<p class="scroll-m-20 text-lg font-medium tracking-tight">
<p class="font-medium">
{incident.title}
</p>
{#if !!incidentDateSummary}
<p class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground">
{incidentDateSummary}
</p>
<Tooltip.Root openDelay={100} side="bottom" align="end">
<Tooltip.Trigger
class=" text-ellipsis whitespace-nowrap text-xs font-medium tracking-normal text-muted-foreground"
>
{incidentDateSummary}
</Tooltip.Trigger>
<Tooltip.Content class=" z-20 mt-11" side="bottom" align="end">
<div
class="items-center justify-center rounded border bg-primary px-1.5 py-1 text-xs font-medium text-primary-foreground shadow-popover"
>
{f(
new Date(incident.start_date_time * 1000),
"MMMM do yyyy, h:mm:ss a",
$page.data.selectedLang,
$page.data.localTz
)}
{#if incident.end_date_time}
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
{f(
new Date(incident.end_date_time * 1000),
"MMMM do yyyy, h:mm:ss a",
$page.data.selectedLang,
$page.data.localTz
)}
{:else}
<ArrowRight class="mx-1 -mt-0.5 inline h-3 w-3" />
<span class="dots-animation inline-block w-6 text-left"></span>
{/if}
</div>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</div>
</Accordion.Trigger>
<Accordion.Content>
<div class="px-4 pt-2">
{#if incident.monitors.length > 0}
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
{#each incident.monitors as monitor}
<div class="tag-affected-text flex gap-x-2 rounded-md bg-secondary px-1 py-1 pr-2">
<div
@@ -116,7 +161,7 @@
</div>
{/if}
<p class="my-3 text-xs font-semibold uppercase text-muted-foreground">
{l(lang, "Updates")}
{l($page.data.lang, "Updates")}
</p>
{#if incident.comments.length > 0}
{#if incidentType == "INCIDENT"}
@@ -126,16 +171,21 @@
<div
class="absolute top-0 w-28 -translate-x-32 rounded border bg-secondary px-1.5 py-1 text-center text-xs font-semibold"
>
{l(lang, comment.state)}
{l($page.data.lang, comment.state)}
</div>
<time class=" mb-1 text-sm font-medium leading-none text-muted-foreground">
{f(new Date(comment.commented_at * 1000), "MMMM do yyyy, h:mm:ss a", selectedLang)}
{f(
new Date(comment.commented_at * 1000),
"MMMM do yyyy, h:mm:ss a",
$page.data.selectedLang,
$page.data.localTz
)}
</time>
<p class="mb-4 text-sm font-normal">
{comment.comment}
</p>
<div class="mb-4 text-sm font-normal">
{@html comment.comment}
</div>
</li>
{/each}
</ol>
@@ -144,7 +194,12 @@
{#each incident.comments as comment}
<li class="relative pb-2 last:border-0">
<time class=" mb-1 text-sm font-medium leading-none text-muted-foreground">
{f(new Date(comment.commented_at * 1000), "MMMM do yyyy, h:mm:ss a", selectedLang)}
{f(
new Date(comment.commented_at * 1000),
"MMMM do yyyy, h:mm:ss a",
$page.data.selectedLang,
$page.data.localTz
)}
</time>
<p class="mb-2 text-sm font-normal">
@@ -156,7 +211,7 @@
{/if}
{:else}
<p class="text-sm font-medium">
{l(lang, "No Updates Yet")}
{l($page.data.lang, "No Updates Yet")}
</p>
{/if}
</div>
+64 -18
View File
@@ -26,8 +26,11 @@
subtitle: ""
};
let footerHTML = "";
let tzToggle = "NO";
let homeIncidentCount = 10;
let homeIncidentStartTimeWithin = 30;
let incidentGroupView = "EXPAND_FIRST";
let nav = [];
let categories = [];
let i18n = {
@@ -61,9 +64,15 @@
if (data.siteData.homeIncidentStartTimeWithin) {
homeIncidentStartTimeWithin = data.siteData.homeIncidentStartTimeWithin;
}
if (data.siteData.incidentGroupView) {
incidentGroupView = data.siteData.incidentGroupView;
}
if (data.siteData.footerHTML) {
footerHTML = data.siteData.footerHTML;
}
if (data.siteData.tzToggle) {
tzToggle = data.siteData.tzToggle;
}
if (data.siteData.i18n) {
i18n = data.siteData.i18n;
//add locales that are not present in the data
@@ -138,7 +147,8 @@
formStateIncident = "loading";
let resp = await storeSiteData({
homeIncidentCount: homeIncidentCount,
homeIncidentStartTimeWithin: homeIncidentStartTimeWithin
homeIncidentStartTimeWithin: homeIncidentStartTimeWithin,
incidentGroupView: incidentGroupView
});
//print data
let data = await resp.json();
@@ -213,7 +223,8 @@
async function formSubmiti18n() {
formStatei18n = "loading";
let resp = await storeSiteData({
i18n: JSON.stringify(i18n)
i18n: JSON.stringify(i18n),
tzToggle: tzToggle
});
//print data
let data = await resp.json();
@@ -228,9 +239,7 @@
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Hero Section</Card.Title>
<Card.Description>
Configure the hero section of your site. This is the first section that appears on your site.
</Card.Description>
<Card.Description>Configure the hero section of your site.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitHero}>
@@ -298,7 +307,7 @@
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitIncident}>
<div class="flex max-w-md flex-row justify-start gap-2">
<div class="flex flex-row justify-start gap-2">
<div class="">
<Label for="hero_title">Maximum Number to Show</Label>
<Input bind:value={homeIncidentCount} class="mt-2" type="text" id="homeIncidentCount" placeholder="10" />
@@ -313,6 +322,33 @@
placeholder="2 days"
/>
</div>
<div>
<Label for="incidentGroupView" class="text-sm font-medium">Incident Group View</Label>
<Select.Root
portal={null}
onSelectedChange={(e) => {
incidentGroupView = e.value;
}}
selected={{
value: incidentGroupView,
label: incidentGroupView.replace("_", " ")
}}
>
<Select.Trigger class=" mt-2 w-[200px]" id="incidentGroupView">
<Select.Value bind:value={incidentGroupView} placeholder="Favicon Type" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Favicon Type</Select.Label>
<Select.Item value="EXPAND_FIRST" label="EXPAND FIRST" class="text-sm font-medium"
>EXPAND FIRST</Select.Item
>
<Select.Item value="COLLAPSED" label="COLLAPSED" class="text-sm font-medium">COLLAPSED</Select.Item>
<Select.Item value="EXPANDED" label="EXPANDED" class="text-sm font-medium">EXPANDED</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="pt-8">
<Button type="submit" disabled={formStateIncident === "loading"}>
Save
@@ -333,9 +369,7 @@
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Navigation</Card.Title>
<Card.Description>
Configure the navigation of your site. This is the first section that appears on your site.
</Card.Description>
<Card.Description>Configure the navigation of your site.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitNav}>
@@ -452,9 +486,7 @@
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Internationalization</Card.Title>
<Card.Description>
Configure the languages of your site. This is the first section that appears on your site.
</Card.Description>
<Card.Description>Configure the languages of your site.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmiti18n}>
@@ -497,6 +529,24 @@
</Select.Group>
</Select.Content>
</Select.Root>
<p class="pt-4 text-lg font-medium">Timezone Switching</p>
<p class="text-sm font-medium text-muted-foreground">
Kener will automatically detect the user's timezone and show the status page in their timezone. You can let
users switch between different timezones if you want.
</p>
<p class="text-sm font-medium">
<label>
<input
on:change={(e) => {
tzToggle = e.target.checked === true ? "YES" : "NO";
}}
type="checkbox"
checked={tzToggle === "YES"}
/>
Allow users to switch timezones
</label>
</p>
<div class="flex w-full justify-end">
<Button type="submit" disabled={formStatei18n === "loading"}>
Save
@@ -512,9 +562,7 @@
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Site Categories</Card.Title>
<Card.Description>
Configure the categories of your site. This is the first section that appears on your site.
</Card.Description>
<Card.Description>Configure the categories of your site.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitCategories}>
@@ -574,9 +622,7 @@
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Site Footer</Card.Title>
<Card.Description>
Configure the footer of your site. This is the first section that appears on your site.
</Card.Description>
<Card.Description>Configure the footer of your site.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitFooter}>
+279 -4
View File
@@ -1,6 +1,6 @@
<script>
import { Button } from "$lib/components/ui/button";
import { Plus, X, Loader } from "lucide-svelte";
import { Plus, X, Loader, Clipboard, Check } from "lucide-svelte";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { clickOutsideAction, slide } from "svelte-legos";
@@ -9,6 +9,7 @@
import * as Select from "$lib/components/ui/select";
import { createEventDispatcher } from "svelte";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import {
allRecordTypes,
ValidateIpAddress,
@@ -84,6 +85,10 @@
let invalidFormMessage = "";
function copyToClipboard(t) {
navigator.clipboard.writeText(t);
}
async function isValidEval(ev) {
if (ev.endsWith(";")) {
invalidFormMessage = "Eval should not end with semicolon";
@@ -277,8 +282,106 @@
timeout: parseInt(newMonitor.groupConfig.timeout),
hideMonitors: newMonitor.groupConfig.hideMonitors
});
}
} else if (newMonitor.monitor_type === "SSL") {
//validating host
if (!newMonitor.sslConfig.host) {
invalidFormMessage = "Host is required";
return;
}
if (!IsValidHost(newMonitor.sslConfig.host)) {
invalidFormMessage = "Invalid Host";
return;
}
//validating port
if (
!!!newMonitor.sslConfig.port ||
isNaN(newMonitor.sslConfig.port) ||
newMonitor.sslConfig.port < 1 ||
newMonitor.sslConfig.port > 65535
) {
invalidFormMessage = "Port should be valid";
return;
}
//check if degradedRemainingHours > 0
if (
!!!newMonitor.sslConfig.degradedRemainingHours ||
isNaN(newMonitor.sslConfig.degradedRemainingHours) ||
newMonitor.sslConfig.degradedRemainingHours < 0
) {
invalidFormMessage = "Degraded Remaining Hours should be greater than 0";
return;
}
//check if downRemainingHours > 0
if (
!!!newMonitor.sslConfig.downRemainingHours ||
isNaN(newMonitor.sslConfig.downRemainingHours) ||
newMonitor.sslConfig.downRemainingHours < 0
) {
invalidFormMessage = "Down Remaining Hours should be greater than 0";
return;
}
newMonitor.sslConfig.degradedRemainingHours = Number(newMonitor.sslConfig.degradedRemainingHours);
newMonitor.sslConfig.downRemainingHours = Number(newMonitor.sslConfig.downRemainingHours);
//check if degradedRemainingHours > downRemainingHours
if (newMonitor.sslConfig.degradedRemainingHours <= newMonitor.sslConfig.downRemainingHours) {
invalidFormMessage = "Degraded Remaining Hours should be greater than Down Remaining Hours";
return;
}
newMonitor.type_data = JSON.stringify(newMonitor.sslConfig);
} else if (newMonitor.monitor_type === "SQL") {
//connectionString cannot be empty
if (!!!newMonitor.sqlConfig.connectionString) {
invalidFormMessage = "Connection String is required";
return;
}
//connection string has to start with postgresql or mysql
if (
!newMonitor.sqlConfig.connectionString.startsWith("postgresql://") &&
!newMonitor.sqlConfig.connectionString.startsWith("mysql://")
) {
invalidFormMessage = "Connection string should start with postgresql:// or mysql2://";
return;
}
//timeout should be positive number
if (newMonitor.sqlConfig.timeout < 1) {
invalidFormMessage = "Timeout should be greater than 0";
return;
}
//query cannot be empty
if (!!!newMonitor.sqlConfig.query) {
invalidFormMessage = "SQL Query is required";
return;
}
newMonitor.type_data = JSON.stringify(newMonitor.sqlConfig);
} else if (newMonitor.monitor_type === "HEARTBEAT") {
//newMonitor.heartbeatConfig.degradedRemainingMinutes should be a number and greater than equal to 1
if (
!!!newMonitor.heartbeatConfig.degradedRemainingMinutes ||
isNaN(newMonitor.heartbeatConfig.degradedRemainingMinutes) ||
newMonitor.heartbeatConfig.degradedRemainingMinutes < 1
) {
invalidFormMessage = "Degraded or grace Period should at least be 1 minute";
return;
}
//newMonitor.heartbeatConfig.downRemainingMinutes should be a number and greater than degradedRemainingMinutes
if (
!!!newMonitor.heartbeatConfig.downRemainingMinutes ||
isNaN(newMonitor.heartbeatConfig.downRemainingMinutes) ||
newMonitor.heartbeatConfig.downRemainingMinutes <= newMonitor.heartbeatConfig.degradedRemainingMinutes
) {
invalidFormMessage = "Down or impacted Period should be greater than Degraded or grace Period";
return;
}
newMonitor.type_data = JSON.stringify(newMonitor.heartbeatConfig);
}
formState = "loading";
try {
@@ -302,6 +405,11 @@
}
}
let typeOfLogoUpload = newMonitor.image.startsWith("http") ? "URL" : "FILE";
let databaseTypes = {
pg: "PostgreSQL",
mysql2: "MySQL"
};
</script>
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
@@ -341,7 +449,7 @@
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</div>
@@ -523,6 +631,9 @@
<Select.Item value="DNS" label="DNS" class="text-sm font-medium">DNS</Select.Item>
<Select.Item value="TCP" label="TCP" class="text-sm font-medium">TCP</Select.Item>
<Select.Item value="GROUP" label="GROUP" class="text-sm font-medium">GROUP</Select.Item>
<Select.Item value="SSL" label="SSL" class="text-sm font-medium">SSL</Select.Item>
<Select.Item value="SQL" label="SQL" class="text-sm font-medium">SQL</Select.Item>
<Select.Item value="HEARTBEAT" label="HEARTBEAT" class="text-sm font-medium">HEARTBEAT</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
@@ -896,7 +1007,10 @@
</Label>
<Input bind:value={newMonitor.groupConfig.timeout} class="w-40" id="timeout" />
<p class="my-1 text-xs text-muted-foreground">
Maximum Time in milliseconds it will wait for all the monitors to resolved in that particular timestamp
Maximum Time in milliseconds it will wait for all the monitors to resolved in that particular timestamp.
Read the <a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-group">
docs
</a> to learn more
</p>
</div>
<div class="col-span-6">
@@ -935,6 +1049,167 @@
</ul>
</div>
</div>
{:else if newMonitor.monitor_type == "SSL"}
<div class="mt-4 grid grid-cols-7 gap-2">
<div class="col-span-2">
<Label for="sslHost">Host</Label>
<Input placeholder="example.com" bind:value={newMonitor.sslConfig.host} id="sslHost" />
</div>
<div class="col-span-1">
<Label for="sslHost">Port</Label>
<Input placeholder="443" bind:value={newMonitor.sslConfig.port} id="sslPort" />
</div>
<div class="relative col-span-2">
<Label for="degradedRemainingHours">Degraded If (in hours)</Label>
<Input
class="pl-20"
type="number"
placeholder="x hours"
bind:value={newMonitor.sslConfig.degradedRemainingHours}
id="degradedRemainingHours"
/>
<span class="absolute left-2 top-9 -mt-0.5 text-sm font-medium text-muted-foreground">Expires In</span>
</div>
<div class="relative col-span-2">
<Label for="downRemainingHours">Down If (in hours)</Label>
<Input
class="pl-20"
placeholder="x hours"
type="number"
bind:value={newMonitor.sslConfig.downRemainingHours}
id="downRemainingHours"
/>
<span class="absolute left-2 top-9 -mt-0.5 text-sm font-medium text-muted-foreground">Expires In</span>
</div>
<div class="col-span-7">
<span class="text-xs text-muted-foreground"
>Degraded Remaining Hours should be greater than Down Remaining Hours. Refer to the
<a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-ssl"
>documentation</a
>
</span>
</div>
</div>
{:else if newMonitor.monitor_type == "SQL"}
<div>
<div class="mt-4 flex gap-2">
<div class="w-36">
<Label for="sqlConfigdbType">Database Type</Label>
<Select.Root
portal={null}
onSelectedChange={(e) => {
newMonitor.sqlConfig.dbType = e.value;
}}
selected={{
value: newMonitor.sqlConfig.dbType,
label: databaseTypes[newMonitor.sqlConfig.dbType]
}}
>
<Select.Trigger id="sqlConfigdbType">
<Select.Value bind:value={newMonitor.sqlConfig.dbType} placeholder="DB" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Select DB</Select.Label>
<Select.Item value="pg" label={databaseTypes.pg} class="text-sm font-medium">
{databaseTypes.pg}
</Select.Item>
<Select.Item value="mysql2" label={databaseTypes.mysql2} class="text-sm font-medium">
{databaseTypes.mysql2}
</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="w-36">
<Label for="dbtimeout">Timeout(ms)</Label>
<Input
placeholder="in milliseconds"
type="number"
bind:value={newMonitor.sqlConfig.timeout}
id="dbtimeout"
/>
</div>
</div>
<div class="mt-2 grid grid-cols-1">
<div class="col-span-1">
<Label for="connectionString">Connection String</Label>
<Input
bind:value={newMonitor.sqlConfig.connectionString}
id="connectionString"
placeholder="Enter the connection string"
/>
</div>
</div>
<div class="mt-2 grid grid-cols-1">
<div class="col-span-1">
<Label for="sqlQuery">SQL Query</Label>
<Input bind:value={newMonitor.sqlConfig.query} id="sqlQuery" placeholder="SELECT 1" />
</div>
</div>
<div class="col-span-1">
<span class="text-xs text-muted-foreground"
>Refer to the
<a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-sql">
documentation
</a> for more details
</span>
</div>
</div>
{:else if newMonitor.monitor_type == "HEARTBEAT"}
<div>
<div class="relative mt-4 justify-start gap-x-4">
<p class="truncate rounded-md border bg-popover p-2 pr-8 text-sm font-medium">
Heart beat url: <span class="text-muted-foreground">
{$page.data.siteData.siteURL +
`${base}/api/heartbeat/${newMonitor.tag}:${newMonitor.heartbeatConfig.secretString}`}</span
>
</p>
<Button
class="copybtn absolute right-2 top-2 flex h-6 w-6 justify-center p-1"
variant="ghost"
size="icon"
on:click={() =>
copyToClipboard(
$page.data.siteData.siteURL +
`${base}/api/heartbeat/${newMonitor.tag}:${newMonitor.heartbeatConfig.secretString}`
)}
>
<Check class="check-btn absolute h-4 w-4 text-green-500" />
<Clipboard class="copy-btn absolute h-4 w-4" />
</Button>
</div>
<div class="mt-2 flex flex-row justify-start gap-x-4">
<div>
<Label for="degradedRemainingMinutes">Degraded or grace Period (in minutes)</Label>
<Input
placeholder="in minutes"
class="w-full"
type="number"
bind:value={newMonitor.heartbeatConfig.degradedRemainingMinutes}
id="degradedRemainingMinutes"
/>
</div>
<div>
<Label for="downRemainingMinutes">Down or impacted Period (in minutes)</Label>
<Input
placeholder="in minutes"
type="number"
bind:value={newMonitor.heartbeatConfig.downRemainingMinutes}
id="downRemainingMinutes"
/>
</div>
</div>
<div class="col-span-1">
<span class="text-xs text-muted-foreground"
>Refer to the
<a target="_blank" class="font-medium text-primary" href="https://kener.ing/docs/monitors-heartbeat">
documentation
</a> for more details
</span>
</div>
</div>
{/if}
</div>
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
+30 -4
View File
@@ -1,6 +1,6 @@
<script>
import { Button } from "$lib/components/ui/button";
import { Plus, X, Settings, Bell, Loader, ArrowDownUp, Grip } from "lucide-svelte";
import { Plus, X, Settings, Bell, Loader, ArrowDownUp, Grip, ExternalLink } from "lucide-svelte";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { base } from "$app/paths";
@@ -8,7 +8,7 @@
import { onMount } from "svelte";
import * as Card from "$lib/components/ui/card";
import * as Select from "$lib/components/ui/select";
import { storeSiteData, SortMonitor } from "$lib/clientTools.js";
import { storeSiteData, SortMonitor, RandomString } from "$lib/clientTools.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { dndzone } from "svelte-dnd-action";
import { flip } from "svelte/animate";
@@ -92,7 +92,24 @@
monitors: [],
timeout: 10000,
hideMonitors: false
})
}),
sslConfig: {
host: "",
port: 443,
degradedRemainingHours: 1,
downRemainingHours: 0
},
sqlConfig: {
connectionString: "",
query: "SELECT 1",
timeout: 5000,
dbType: "pg" //mysql2
},
heartbeatConfig: {
degradedRemainingMinutes: 1,
downRemainingMinutes: 2,
secretString: RandomString(32)
}
};
}
@@ -124,6 +141,12 @@
newMonitor.tcpConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "GROUP") {
newMonitor.groupConfig = createGroupConfig(JSON.parse(newMonitor.type_data));
} else if (newMonitor.monitor_type == "SSL") {
newMonitor.sslConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "SQL") {
newMonitor.sqlConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "HEARTBEAT") {
newMonitor.heartbeatConfig = JSON.parse(newMonitor.type_data);
}
showAddMonitor = true;
}
@@ -469,6 +492,9 @@
>
<Bell class="inline h-4 w-4" />
</Button>
<Button variant="secondary" class="h-8 w-8 p-2" rel="external" href="{base}/?monitor={monitor.tag}">
<ExternalLink class="inline h-4 w-4" />
</Button>
<Button variant="secondary" class="h-8 w-8 p-2" href="#{monitor.tag}">
<Settings class="inline h-4 w-4" />
</Button>
@@ -545,7 +571,7 @@
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</div>
+9 -1
View File
@@ -149,6 +149,14 @@
}
}
}
if (newTrigger.trigger_meta.has_webhook_body) {
if (newTrigger.trigger_meta.webhook_body == "") {
invalidFormMessage = "Webhook Body is required";
formState = "idle";
return;
}
}
}
//newTrigger.name present not empty
if (newTrigger.name == "") {
@@ -423,7 +431,7 @@
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</div>
+67 -54
View File
@@ -3,6 +3,7 @@
import { onMount } from "svelte";
import { Badge } from "$lib/components/ui/badge";
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { base } from "$app/paths";
import { sub, startOfDay, getUnixTime } from "date-fns";
import GMI from "$lib/components/gmi.svelte";
@@ -38,7 +39,7 @@
.post(`${base}/api/today/incidents`, {
tag: monitor.tag,
startTs: monitor.pageData.midnight90DaysAgo,
endTs: monitor.pageData.midnightTomorrow,
endTs: monitor.pageData.maxDateTodayTimestamp,
localTz: localTz
})
.then((response) => {
@@ -52,10 +53,13 @@
}
function getToday(startTs, incidentIDs) {
let endTs = Math.min(startTs + 86400, monitor.pageData.maxDateTodayTimestamp);
axios
.post(`${base}/api/today`, {
monitor: monitor,
localTz: localTz,
endTs: endTs,
startTs: startTs,
incidentIDs: incidentIDs
})
@@ -85,27 +89,33 @@
{
text: `${l(lang, "90 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 90 }))),
endTs: monitor.pageData.maxDateTodayTimestamp,
value: uptime90Day
},
{
text: `${l(lang, "60 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 59 })))
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 59 }))),
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "30 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 29 })))
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 29 }))),
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "14 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 13 })))
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 13 }))),
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "7 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 })))
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 }))),
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: l(lang, "Today"),
startTs: getUnixTime(startOfDay(new Date()))
startTs: getUnixTime(startOfDay(new Date())),
endTs: monitor.pageData.maxDateTodayTimestamp
}
];
@@ -113,7 +123,7 @@
let rolledAt = 0;
let rollerLoading = false;
async function rollSummary(r) {
let newRolledAt = (rolledAt + r) % uptimesRollers.length;
let newRolledAt = r;
if (uptimesRollers[newRolledAt].value === undefined) {
rollerLoading = true;
@@ -121,7 +131,8 @@
let resp = await axios.post(`${base}/api/today/aggregated`, {
monitor: monitor,
startTs: uptimesRollers[newRolledAt].startTs
startTs: uptimesRollers[newRolledAt].startTs,
endTs: uptimesRollers[newRolledAt].endTs
});
uptimesRollers[newRolledAt].value = resp.data.uptime;
rollerLoading = false;
@@ -157,19 +168,20 @@
let dateFetchedFor = "";
let dayUptime = "NA";
let loadingDayData = false;
function dailyDataGetter(e, bar, incidentObj) {
if (embed) {
return;
}
let incidentIDs = incidentObj?.ids || [];
dayUptime = "NA";
dateFetchedFor = f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang);
dateFetchedFor = f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang, $page.data.localTz);
showDailyDataModal = true;
loadingDayData = true;
dayIncidentsFull = [];
setTimeout(() => {
getToday(bar.timestamp, incidentIDs);
}, 750);
}, 50);
}
</script>
@@ -179,7 +191,12 @@
<div class="pt-0">
<div class="scroll-m-20 pr-5 text-xl font-medium tracking-tight">
{#if monitor.image}
<GMI src={monitor.image} classList="absolute left-6 top-6 inline h-5 w-5" alt={monitor.name} srcset="" />
<GMI
src={monitor.image}
classList="absolute left-6 top-6 inline h-5 w-5 hidden md:block"
alt={monitor.name}
srcset=""
/>
{/if}
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
{monitor.name}
@@ -229,55 +246,51 @@
{/if}
<div class="col-span-12 min-h-[94px] pt-2 md:w-[546px]">
<div class="col-span-12">
<div class="flex justify-between">
<div class=" ">
<div class="flex gap-x-1">
{#if rolledAt > 0}
<Button variant="ghost" class="h-5 w-5 p-0" on:click={() => rollSummary(-1)}>
<ChevronLeft class="h-4 w-4" />
</Button>
{/if}
<div class="flex text-xs font-semibold">
<span>
<div class="flex flex-wrap justify-between gap-x-1">
<div class="">
<DropdownMenu.Root class="">
<DropdownMenu.Trigger class="mr-2 flex ">
<Button variant="secondary" class="h-6 px-2 py-2 text-xs">
{uptimesRollers[rolledAt].text}
</span>
<span class="">
{#if rollerLoading}
<Loader class="mx-1 -mt-0.5 inline h-4 w-4 animate-spin" />
{:else}
<TrendingUp class="mx-1 -mt-0.5 inline h-3 w-3" />
{/if}
{#if isNaN(uptimesRollers[rolledAt].value)}
<span class="text-muted-foreground">-</span>
{:else}
<NumberFlow
value={uptimesRollers[rolledAt].value}
format={{
notation: "standard",
minimumFractionDigits: 4,
maximumFractionDigits: 4
}}
suffix="%"
/>
{/if}
</span>
</div>
{#if rolledAt < uptimesRollers.length - 1}
<Button variant="ghost" class="h-5 w-5 p-0" on:click={() => rollSummary(1)}>
<ChevronRight class="h-4 w-4" />
</Button>
{/if}
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Content class={!!embed ? "h-[60px] overflow-y-auto" : ""}>
{#each uptimesRollers as roller, i}
<DropdownMenu.Group>
<DropdownMenu.Item
class="text-xs {rolledAt == i ? 'bg-secondary' : ''} font-semibold"
on:click={() => rollSummary(i)}
>
{roller.text}
</DropdownMenu.Item>
</DropdownMenu.Group>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div class="pt-0.5 text-right">
<div class="text-api-up truncate text-xs font-semibold text-{monitor.pageData.summaryColorClass}">
<div class="flex gap-x-2 pt-2 text-right">
{#if rollerLoading}
<Loader class=" mt-0.5 inline h-3.5 w-3.5 animate-spin text-muted-foreground" />
{/if}
{#if !isNaN(uptimesRollers[rolledAt].value)}
<NumberFlow
class="border-r pr-2 text-xs font-semibold"
value={uptimesRollers[rolledAt].value}
format={{
notation: "standard",
minimumFractionDigits: 4,
maximumFractionDigits: 4
}}
suffix="%"
/>
{/if}
<div class="truncate text-xs font-semibold text-{monitor.pageData.summaryColorClass}">
{monitor.pageData.summaryStatus}
</div>
</div>
</div>
<div
class="relative col-span-12 mt-1"
class="relative col-span-12 mt-0.5"
use:clickOutsideAction
on:clickoutside={(e) => {
showDailyDataModal = false;
@@ -316,7 +329,7 @@
{#if bar.showDetails}
<div class="show-hover absolute text-sm">
<div class="text-{bar.textClass} text-xs font-semibold">
{f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang)}
{f(new Date(bar.timestamp * 1000), "EEEE, MMMM do, yyyy", selectedLang, $page.data.localTz)}
-
{bar.summaryStatus}
</div>
@@ -366,7 +379,7 @@
<p>
<span class="text-{bar.cssClass}"></span>
{f(new Date(bar.timestamp * 1000), "hh:mm a", selectedLang)}
{f(new Date(bar.timestamp * 1000), "hh:mm a", selectedLang, $page.data.localTz)}
</p>
{#if bar.status != "NO_DATA"}
<p class="pl-2">
+18 -16
View File
@@ -40,22 +40,24 @@
</a>
{/each}
</nav>
<DropdownMenu.Root class="">
<DropdownMenu.Trigger class="mr-2 flex md:hidden">
<Button variant="outline" size="sm">
<Menu size={14} />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{#each data.site.nav as navItem}
<DropdownMenu.Group>
<DropdownMenu.Item>
<a rel="external" href={navItem.url}> {navItem.name} </a>
</DropdownMenu.Item>
</DropdownMenu.Group>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
<div class="flex md:hidden">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline" size="sm">
<Menu size={14} />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{#each data.site.nav as navItem}
<DropdownMenu.Group>
<DropdownMenu.Item>
<a rel="external" href={navItem.url}> {navItem.name} </a>
</DropdownMenu.Item>
</DropdownMenu.Group>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/if}
</div>
</div>
+47 -46
View File
@@ -2,70 +2,71 @@
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr, nb } from "date-fns/locale";
import { formatInTimeZone } from "date-fns-tz";
const locales = {
ru,
en: enUS,
hi,
de,
"zh-CN": zhCN,
vi,
ja,
nl,
dk: da,
fr,
ko,
"pt-BR": ptBR,
tr,
"nb-NO": nb
ru,
en: enUS,
hi,
de,
"zh-CN": zhCN,
vi,
ja,
nl,
dk: da,
fr,
ko,
"pt-BR": ptBR,
tr,
"nb-NO": nb,
};
const f = function (date, formatStr, locale) {
return format(date, formatStr, {
locale: locales[locale] || enUS
});
const f = function (date, formatStr, locale, tz) {
return formatInTimeZone(date, tz, formatStr, {
locale: locales[locale] || enUS,
});
};
const fd = function (start, end, locale) {
return formatDistance(start, end, { addSuffix: false, locale: locales[locale] });
return formatDistance(start, end, { addSuffix: false, locale: locales[locale] });
};
const fdn = function (start, locale) {
return formatDistanceToNow(start, { addSuffix: true, locale: locales[locale] });
return formatDistanceToNow(start, { addSuffix: true, locale: locales[locale] });
};
const fdm = function (duration, locale) {
return formatDuration(duration, {
format: ["days", "hours", "minutes"],
zero: false,
delimiter: " ",
locale: locales[locale]
});
return formatDuration(duration, {
format: ["days", "hours", "minutes"],
zero: false,
delimiter: " ",
locale: locales[locale],
});
};
const l = function (sessionLangMap, key, args = {}) {
try {
let obj = sessionLangMap[key];
try {
let obj = sessionLangMap[key];
// Replace placeholders in the string using the args object
if (obj && typeof obj === "string") {
obj = obj.replace(/%\w+/g, (placeholder) => {
const argKey = placeholder.slice(1); // Remove the `%` to get the key
return args[argKey] !== undefined ? args[argKey] : placeholder;
});
}
return obj || key;
} catch (e) {
return key;
}
// Replace placeholders in the string using the args object
if (obj && typeof obj === "string") {
obj = obj.replace(/%\w+/g, (placeholder) => {
const argKey = placeholder.slice(1); // Remove the `%` to get the key
return args[argKey] !== undefined ? args[argKey] : placeholder;
});
}
return obj || key;
} catch (e) {
return key;
}
};
const summaryTime = function (summaryStatus) {
if (summaryStatus == "No Data") {
return "No Data";
}
if (summaryStatus == "UP") {
return "Status OK";
}
if (summaryStatus == "No Data") {
return "No Data";
}
if (summaryStatus == "UP") {
return "Status OK";
}
return "%status for %duration";
return "%status for %duration";
};
export { l, summaryTime, f, formatDistance, fd, fdn, fdm };
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%status für %duration",
"14 Days": "14 Tage",
"30 Days": "30 Tage",
"60 Days": "60 Tage",
"7 Days": "7 Tage",
"90 Days": "90 Tage",
"Availability per Component": "Verfügbarkeit pro Komponente",
"Back": "Zurück",
"Badge Copied": "Abzeichen kopiert",
"Badge": "Abzeichen",
"Browse Events": "Ereignisse durchsuchen",
"Code Copied": "Code kopiert",
"Copy Code": "Code kopieren",
"Copy Link": "Link kopieren",
"DEGRADED": "EINGESCHRÄNKT",
"DOWN": "AUSGEFALLEN",
"Dark": "Dunkel",
"Days": "Tage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
"Embed": "Einbetten",
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
"IDENTIFIED": "IDENTIFIZIERT",
"INVESTIGATING": "UNTERSUCHUNG",
"Incident Updates": "Vorfall-Updates",
"LIVE Status": "LIVE-Status",
"Lasted for about %lastedFor": "Dauerte etwa %lastedFor",
"Light": "Hell",
"Link Copied": "Link kopiert",
"MAINTENANCE": "WARTUNG",
"MONITORING": "ÜBERWACHUNG",
"Maintenance Completed": "Wartung abgeschlossen",
"Maintenance in Progress": "Wartung läuft",
"Mode": "Modus",
"No Data": "Keine Daten",
"No Incident in %date": "Kein Vorfall am %date",
"No Incidents": "Keine Vorfälle",
"No Monitor Found": "Kein Monitor gefunden",
"No Updates Yet": "Noch keine Updates",
"Ongoing Incidents": "Laufende Vorfälle",
"Pinging": "Pingen",
"RESOLVED": "GELÖST",
"Recent Incidents": "Kürzliche Vorfälle",
"Recent Maintenances": "Kürzliche Wartungen",
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
"Share": "Teilen",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
"Starts %startedAt": "Beginnt in %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Thema",
"Today": "Heute",
"UP": "VERFÜGBAR",
"Upcoming Maintenance": "Bevorstehende Wartung",
"Updates": "Updates",
"Uptime": "Verfügbarkeit"
"%status for %duration": "%status für %duration",
"14 Days": "14 Tage",
"30 Days": "30 Tage",
"60 Days": "60 Tage",
"7 Days": "7 Tage",
"90 Days": "90 Tage",
"Availability per Component": "Verfügbarkeit pro Komponente",
"Back": "Zurück",
"Badge Copied": "Abzeichen kopiert",
"Badge": "Abzeichen",
"Browse Events": "Ereignisse durchsuchen",
"Code Copied": "Code kopiert",
"Copy Code": "Code kopieren",
"Copy Link": "Link kopieren",
"DEGRADED": "EINGESCHRÄNKT",
"DOWN": "AUSGEFALLEN",
"Dark": "Dunkel",
"Days": "Tage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
"Embed": "Einbetten",
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
"IDENTIFIED": "IDENTIFIZIERT",
"INVESTIGATING": "UNTERSUCHUNG",
"Incident Updates": "Vorfall-Updates",
"LIVE Status": "LIVE-Status",
"Lasted for about %lastedFor": "Dauerte etwa %lastedFor",
"Light": "Hell",
"Link Copied": "Link kopiert",
"MAINTENANCE": "WARTUNG",
"MONITORING": "ÜBERWACHUNG",
"Maintenance Completed": "Wartung abgeschlossen",
"Maintenance in Progress": "Wartung läuft",
"Mode": "Modus",
"No Data": "Keine Daten",
"No Incident in %date": "Kein Vorfall am %date",
"No Incidents": "Keine Vorfälle",
"No Monitor Found": "Kein Monitor gefunden",
"No Updates Yet": "Noch keine Updates",
"Ongoing Incidents": "Laufende Vorfälle",
"Pinging": "Pingen",
"RESOLVED": "GELÖST",
"Recent Incidents": "Kürzliche Vorfälle",
"Recent Maintenances": "Kürzliche Wartungen",
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
"Share": "Teilen",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
"Started %startedAt, will last for %lastedFor more": "Begann vor etwa %startedAt und wird noch etwa %lastedFor dauern",
"Starts %startedAt, will last for %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
"Starts %startedAt": "Beginnt in %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Switch to your timezone": "Wechseln Sie zu Ihrer Zeitzone",
"Theme": "Thema",
"Timezone set to %tz": "Zeitzone auf %tz gesetzt",
"Today": "Heute",
"UP": "VERFÜGBAR",
"Upcoming Maintenance": "Bevorstehende Wartung",
"Updates": "Updates",
"Uptime": "Verfügbarkeit"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%status i %duration",
"14 Days": "14 Dage",
"30 Days": "30 Dage",
"60 Days": "60 Dage",
"7 Days": "7 Dage",
"90 Days": "90 Dage",
"Availability per Component": "Tilgængelighed pr. Komponent",
"Back": "Tilbage",
"Badge Copied": "Mærke Kopieret",
"Badge": "Mærke",
"Browse Events": "Gennemse begivenheder",
"Code Copied": "Kode Kopieret",
"Copy Code": "Kopier Kode",
"Copy Link": "Kopier Link",
"DEGRADED": "FORRINGET",
"DOWN": "NEDE",
"Dark": "Mørk",
"Days": "Dage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
"Embed": "Integrer",
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
"IDENTIFIED": "IDENTIFICERET",
"INVESTIGATING": "UNDERSØGER",
"Incident Updates": "Hændelsesopdateringer",
"LIVE Status": "LIVE-status",
"Lasted for about %lastedFor": "Varede i cirka %lastedFor",
"Light": "Lys",
"Link Copied": "Link Kopieret",
"MAINTENANCE": "VEDLIGEHOLDELSE",
"MONITORING": "OVERVÅGNING",
"Maintenance Completed": "Vedligeholdelse afsluttet",
"Maintenance in Progress": "Vedligeholdelse i gang",
"Mode": "Tilstand",
"No Data": "Ingen Data",
"No Incident in %date": "Ingen Hændelse Den %date",
"No Incidents": "Ingen Hændelser",
"No Monitor Found": "Ingen Monitor Fundet",
"No Updates Yet": "Ingen Opdateringer Endnu",
"Ongoing Incidents": "Igangværende Hændelser",
"Pinging": "Pinger",
"RESOLVED": "LØST",
"Recent Incidents": "Seneste Hændelser",
"Recent Maintenances": "Seneste Vedligeholdelser",
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
"Share": "Del",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
"Starts %startedAt": "Starter om %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Tema",
"Today": "I dag",
"UP": "OPPE",
"Upcoming Maintenance": "Kommende vedligeholdelse",
"Updates": "Opdateringer",
"Uptime": "Oppetid"
"%status for %duration": "%status i %duration",
"14 Days": "14 Dage",
"30 Days": "30 Dage",
"60 Days": "60 Dage",
"7 Days": "7 Dage",
"90 Days": "90 Dage",
"Availability per Component": "Tilgængelighed pr. Komponent",
"Back": "Tilbage",
"Badge Copied": "Mærke Kopieret",
"Badge": "Mærke",
"Browse Events": "Gennemse begivenheder",
"Code Copied": "Kode Kopieret",
"Copy Code": "Kopier Kode",
"Copy Link": "Kopier Link",
"DEGRADED": "FORRINGET",
"DOWN": "NEDE",
"Dark": "Mørk",
"Days": "Dage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
"Embed": "Integrer",
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
"IDENTIFIED": "IDENTIFICERET",
"INVESTIGATING": "UNDERSØGER",
"Incident Updates": "Hændelsesopdateringer",
"LIVE Status": "LIVE-status",
"Lasted for about %lastedFor": "Varede i cirka %lastedFor",
"Light": "Lys",
"Link Copied": "Link Kopieret",
"MAINTENANCE": "VEDLIGEHOLDELSE",
"MONITORING": "OVERVÅGNING",
"Maintenance Completed": "Vedligeholdelse afsluttet",
"Maintenance in Progress": "Vedligeholdelse i gang",
"Mode": "Tilstand",
"No Data": "Ingen Data",
"No Incident in %date": "Ingen Hændelse Den %date",
"No Incidents": "Ingen Hændelser",
"No Monitor Found": "Ingen Monitor Fundet",
"No Updates Yet": "Ingen Opdateringer Endnu",
"Ongoing Incidents": "Igangværende Hændelser",
"Pinging": "Pinger",
"RESOLVED": "LØST",
"Recent Incidents": "Seneste Hændelser",
"Recent Maintenances": "Seneste Vedligeholdelser",
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
"Share": "Del",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
"Started %startedAt, will last for %lastedFor more": "Startede for cirka %startedAt siden, vil vare i cirka %lastedFor mere",
"Starts %startedAt, will last for %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
"Starts %startedAt": "Starter om %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Switch to your timezone": "Skift til din tidszone",
"Theme": "Tema",
"Timezone set to %tz": "Tidszone sat til %tz",
"Today": "I dag",
"UP": "OPPE",
"Upcoming Maintenance": "Kommende vedligeholdelse",
"Updates": "Opdateringer",
"Uptime": "Oppetid"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%status for %duration",
"14 Days": "14 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
"7 Days": "7 Days",
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"Back": "Back",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Code Copied": "Code Copied",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"DEGRADED": "DEGRADED",
"DOWN": "DOWN",
"Dark": "Dark",
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
"INVESTIGATING": "INVESTIGATING",
"Incident Updates": "Incident Updates",
"LIVE Status": "LIVE Status",
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
"No Incidents": "No Incidents",
"No Monitor Found": "No Monitor Found",
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"RESOLVED": "RESOLVED",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Theme",
"Today": "Today",
"UP": "UP",
"Upcoming Maintenance": "Upcoming Maintenance",
"Updates": "Updates",
"Uptime": "Uptime"
"%status for %duration": "%status for %duration",
"14 Days": "14 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
"7 Days": "7 Days",
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"Back": "Back",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Code Copied": "Code Copied",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"DEGRADED": "DEGRADED",
"DOWN": "DOWN",
"Dark": "Dark",
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
"INVESTIGATING": "INVESTIGATING",
"Incident Updates": "Incident Updates",
"LIVE Status": "LIVE Status",
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
"No Incidents": "No Incidents",
"No Monitor Found": "No Monitor Found",
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"RESOLVED": "RESOLVED",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
"Started %startedAt, will last for %lastedFor more": "Started %startedAt, will last for %lastedFor more",
"Starts %startedAt, will last for %lastedFor": "Starts %startedAt, will last for %lastedFor",
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Switch to your timezone": "Switch to your timezone",
"Theme": "Theme",
"Timezone set to %tz": "Timezone set to %tz",
"Today": "Today",
"UP": "UP",
"Upcoming Maintenance": "Upcoming Maintenance",
"Updates": "Updates",
"Uptime": "Uptime"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%status pendant %duration",
"14 Days": "14 jours",
"30 Days": "30 jours",
"60 Days": "60 jours",
"7 Days": "7 jours",
"90 Days": "90 jours",
"Availability per Component": "Disponibilité par composant",
"Back": "Retour",
"Badge Copied": "Badge copié",
"Badge": "Badge",
"Browse Events": "Parcourir les événements",
"Code Copied": "Code copié",
"Copy Code": "Copier le code",
"Copy Link": "Copier le lien",
"DEGRADED": "Dégradé",
"DOWN": "Hors ligne",
"Dark": "Sombre",
"Days": "Jours",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
"Embed": "Intégrer",
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
"IDENTIFIED": "Identifié",
"INVESTIGATING": "En cours d'investigation",
"Incident Updates": "Mises à jour des incidents",
"LIVE Status": "Statut LIVE",
"Lasted for about %lastedFor": "A duré environ %lastedFor",
"Light": "Clair",
"Link Copied": "Lien copié",
"MAINTENANCE": "Maintenance",
"MONITORING": "En surveillance",
"Maintenance Completed": "Maintenance terminée",
"Maintenance in Progress": "Maintenance en cours",
"Mode": "Mode",
"No Data": "Pas de données",
"No Incident in %date": "Pas d'incident le %date",
"No Incidents": "Pas d'incidents",
"No Monitor Found": "Aucun moniteur trouvé",
"No Updates Yet": "Pas encore de mises à jour",
"Ongoing Incidents": "Incidents en cours",
"Pinging": "Ping en cours",
"RESOLVED": "Résolu",
"Recent Incidents": "Incidents récents",
"Recent Maintenances": "Maintenances récentes",
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
"Share": "Partager",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
"Starts %startedAt": "Commence dans %startedAt",
"Status OK": "Statut OK",
"Status": "Statut",
"Theme": "Thème",
"Today": "Aujourd'hui",
"UP": "En ligne",
"Upcoming Maintenance": "Maintenance à venir",
"Updates": "Mises à jour",
"Uptime": "Temps de fonctionnement"
"%status for %duration": "%status pendant %duration",
"14 Days": "14 jours",
"30 Days": "30 jours",
"60 Days": "60 jours",
"7 Days": "7 jours",
"90 Days": "90 jours",
"Availability per Component": "Disponibilité par composant",
"Back": "Retour",
"Badge Copied": "Badge copié",
"Badge": "Badge",
"Browse Events": "Parcourir les événements",
"Code Copied": "Code copié",
"Copy Code": "Copier le code",
"Copy Link": "Copier le lien",
"DEGRADED": "Dégradé",
"DOWN": "Hors ligne",
"Dark": "Sombre",
"Days": "Jours",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
"Embed": "Intégrer",
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
"IDENTIFIED": "Identifié",
"INVESTIGATING": "En cours d'investigation",
"Incident Updates": "Mises à jour des incidents",
"LIVE Status": "Statut LIVE",
"Lasted for about %lastedFor": "A duré environ %lastedFor",
"Light": "Clair",
"Link Copied": "Lien copié",
"MAINTENANCE": "Maintenance",
"MONITORING": "En surveillance",
"Maintenance Completed": "Maintenance terminée",
"Maintenance in Progress": "Maintenance en cours",
"Mode": "Mode",
"No Data": "Pas de données",
"No Incident in %date": "Pas d'incident le %date",
"No Incidents": "Pas d'incidents",
"No Monitor Found": "Aucun moniteur trouvé",
"No Updates Yet": "Pas encore de mises à jour",
"Ongoing Incidents": "Incidents en cours",
"Pinging": "Ping en cours",
"RESOLVED": "Résolu",
"Recent Incidents": "Incidents récents",
"Recent Maintenances": "Maintenances récentes",
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
"Share": "Partager",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
"Started %startedAt, will last for %lastedFor more": "Commencé il y a environ %startedAt, durera environ %lastedFor de plus",
"Starts %startedAt, will last for %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
"Starts %startedAt": "Commence dans %startedAt",
"Status OK": "Statut OK",
"Status": "Statut",
"Switch to your timezone": "Passer à votre fuseau horaire",
"Theme": "Thème",
"Timezone set to %tz": "Fuseau horaire défini sur %tz",
"Today": "Aujourd'hui",
"UP": "En ligne",
"Upcoming Maintenance": "Maintenance à venir",
"Updates": "Mises à jour",
"Uptime": "Temps de fonctionnement"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%duration के लिए %status",
"14 Days": "14 दिन",
"30 Days": "30 दिन",
"60 Days": "60 दिन",
"7 Days": "7 दिन",
"90 Days": "90 दिन",
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
"Back": "वापस",
"Badge Copied": "बैज कॉपी किया गया",
"Badge": "बैज",
"Browse Events": "ईवेंट ब्राउज़ करें",
"Code Copied": "कोड कॉपी किया गया",
"Copy Code": "कोड कॉपी करें",
"Copy Link": "लिंक कॉपी करें",
"DEGRADED": "क्षीण",
"DOWN": "डाउन",
"Dark": "डार्क",
"Days": "दिन",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
"Embed": "एम्बेड",
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
"IDENTIFIED": "पहचाना गया",
"INVESTIGATING": "जांच चल रही है",
"Incident Updates": "घटना अपडेट",
"LIVE Status": "लाइव स्टेटस",
"Lasted for about %lastedFor": "%lastedFor तक चला",
"Light": "लाइट",
"Link Copied": "लिंक कॉपी किया गया",
"MAINTENANCE": "मेंटेनेंस",
"MONITORING": "निगरानी",
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
"Maintenance in Progress": "मेंटेनेंस जारी है",
"Mode": "मोड",
"No Data": "कोई डेटा नहीं",
"No Incident in %date": "%date को कोई घटना नहीं",
"No Incidents": "कोई घटना नहीं",
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
"Ongoing Incidents": "चल रही घटनाएँ",
"Pinging": "पिंगिंग",
"RESOLVED": "सुलझाया गया",
"Recent Incidents": "हाल की घटनाएँ",
"Recent Maintenances": "हाल की मेंटेनेंस",
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
"Share": "साझा करें",
"Standard": "मानक",
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
"Starts %startedAt": "%startedAt में शुरू होगा",
"Status OK": "स्थिति ठीक है",
"Status": "स्थिति",
"Theme": "थीम",
"Today": "आज",
"UP": "अप",
"Upcoming Maintenance": "आगामी मेंटेनेंस",
"Updates": "अपडेट्स",
"Uptime": "अपटाइम"
"%status for %duration": "%duration के लिए %status",
"14 Days": "14 दिन",
"30 Days": "30 दिन",
"60 Days": "60 दिन",
"7 Days": "7 दिन",
"90 Days": "90 दिन",
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
"Back": "वापस",
"Badge Copied": "बैज कॉपी किया गया",
"Badge": "बैज",
"Browse Events": "ईवेंट ब्राउज़ करें",
"Code Copied": "कोड कॉपी किया गया",
"Copy Code": "कोड कॉपी करें",
"Copy Link": "लिंक कॉपी करें",
"DEGRADED": "क्षीण",
"DOWN": "डाउन",
"Dark": "डार्क",
"Days": "दिन",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
"Embed": "एम्बेड",
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
"IDENTIFIED": "पहचाना गया",
"INVESTIGATING": "जांच चल रही है",
"Incident Updates": "घटना अपडेट",
"LIVE Status": "लाइव स्टेटस",
"Lasted for about %lastedFor": "%lastedFor तक चला",
"Light": "लाइट",
"Link Copied": "लिंक कॉपी किया गया",
"MAINTENANCE": "मेंटेनेंस",
"MONITORING": "निगरानी",
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
"Maintenance in Progress": "मेंटेनेंस जारी है",
"Mode": "मोड",
"No Data": "कोई डेटा नहीं",
"No Incident in %date": "%date को कोई घटना नहीं",
"No Incidents": "कोई घटना नहीं",
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
"Ongoing Incidents": "चल रही घटनाएँ",
"Pinging": "पिंगिंग",
"RESOLVED": "सुलझाया गया",
"Recent Incidents": "हाल की घटनाएँ",
"Recent Maintenances": "हाल की मेंटेनेंस",
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
"Share": "साझा करें",
"Standard": "मानक",
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
"Started %startedAt, will last for %lastedFor more": "%startedAt शुरू होगा, %lastedFor और चलेगा",
"Starts %startedAt, will last for %lastedFor": "%startedAt में शुरू होगा, %lastedFor तक चलेगा",
"Starts %startedAt": "%startedAt में शुरू होगा",
"Status OK": "स्थिति ठीक है",
"Status": "स्थिति",
"Switch to your timezone": "अपने समय क्षेत्र में स्विच करें",
"Theme": "थीम",
"Timezone set to %tz": "समय क्षेत्र सेट किया गया %tz",
"Today": "आज",
"UP": "अप",
"Upcoming Maintenance": "आगामी मेंटेनेंस",
"Updates": "अपडेट्स",
"Uptime": "अपटाइम"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%duration の間 %status",
"14 Days": "14日間",
"30 Days": "30日間",
"60 Days": "60日間",
"7 Days": "7日間",
"90 Days": "90日間",
"Availability per Component": "コンポーネントごとの可用性",
"Back": "戻る",
"Badge Copied": "バッジがコピーされました",
"Badge": "バッジ",
"Browse Events": "イベントを閲覧",
"Code Copied": "コードがコピーされました",
"Copy Code": "コードをコピー",
"Copy Link": "リンクをコピー",
"DEGRADED": "劣化",
"DOWN": "ダウン",
"Dark": "ダーク",
"Days": "日間",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "このモニターをアプリに <script> または <iframe> を使用して埋め込む。",
"Embed": "埋め込み",
"Get SVG badge for this monitor": "このモニターのSVGバッジを取得",
"Get a LIVE Status for this monitor": "このモニターのライブステータスを取得",
"IDENTIFIED": "識別済み",
"INVESTIGATING": "調査中",
"Incident Updates": "インシデント更新",
"LIVE Status": "ライブステータス",
"Lasted for about %lastedFor": "約 %lastedFor 続きました",
"Light": "ライト",
"Link Copied": "リンクがコピーされました",
"MAINTENANCE": "メンテナンス",
"MONITORING": "モニタリング中",
"Maintenance Completed": "メンテナンス完了",
"Maintenance in Progress": "メンテナンス中",
"Mode": "モード",
"No Data": "データなし",
"No Incident in %date": "%date にインシデントなし",
"No Incidents": "インシデントなし",
"No Monitor Found": "モニターが見つかりません",
"No Updates Yet": "まだ更新はありません",
"Ongoing Incidents": "進行中のインシデント",
"Pinging": "ピング中",
"RESOLVED": "解決済み",
"Recent Incidents": "最近のインシデント",
"Recent Maintenances": "最近のメンテナンス",
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
"Share": "共有",
"Standard": "標準",
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
"Starts %startedAt, will last for %lastedFor": "%startedAt に開始し、 %lastedFor 続きます",
"Starts %startedAt": "%startedAt 後に開始",
"Status OK": "ステータス正常",
"Status": "ステータス",
"Theme": "テー",
"Today": "今日",
"UP": "稼働中",
"Upcoming Maintenance": "今後のメンテナンス",
"Updates": "更新",
"Uptime": "稼働時間"
"%status for %duration": "%duration の間 %status",
"14 Days": "14日間",
"30 Days": "30日間",
"60 Days": "60日間",
"7 Days": "7日間",
"90 Days": "90日間",
"Availability per Component": "コンポーネントごとの可用性",
"Back": "戻る",
"Badge Copied": "バッジがコピーされました",
"Badge": "バッジ",
"Browse Events": "イベントを閲覧",
"Code Copied": "コードがコピーされました",
"Copy Code": "コードをコピー",
"Copy Link": "リンクをコピー",
"DEGRADED": "劣化",
"DOWN": "ダウン",
"Dark": "ダーク",
"Days": "日間",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "このモニターをアプリに <script> または <iframe> を使用して埋め込む。",
"Embed": "埋め込み",
"Get SVG badge for this monitor": "このモニターのSVGバッジを取得",
"Get a LIVE Status for this monitor": "このモニターのライブステータスを取得",
"IDENTIFIED": "識別済み",
"INVESTIGATING": "調査中",
"Incident Updates": "インシデント更新",
"LIVE Status": "ライブステータス",
"Lasted for about %lastedFor": "約 %lastedFor 続きました",
"Light": "ライト",
"Link Copied": "リンクがコピーされました",
"MAINTENANCE": "メンテナンス",
"MONITORING": "モニタリング中",
"Maintenance Completed": "メンテナンス完了",
"Maintenance in Progress": "メンテナンス中",
"Mode": "モード",
"No Data": "データなし",
"No Incident in %date": "%date にインシデントなし",
"No Incidents": "インシデントなし",
"No Monitor Found": "モニターが見つかりません",
"No Updates Yet": "まだ更新はありません",
"Ongoing Incidents": "進行中のインシデント",
"Pinging": "ピング中",
"RESOLVED": "解決済み",
"Recent Incidents": "最近のインシデント",
"Recent Maintenances": "最近のメンテナンス",
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
"Share": "共有",
"Standard": "標準",
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
"Started %startedAt, will last for %lastedFor more": "%startedAt に開始し、さらに %lastedFor 続きます",
"Starts %startedAt, will last for %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
"Starts %startedAt": "%startedAt 後に開始",
"Status OK": "ステータス正常",
"Status": "テータス",
"Switch to your timezone": "あなたのタイムゾーンに切り替え",
"Theme": "テーマ",
"Timezone set to %tz": "タイムゾーンを %tz に設定",
"Today": "今日",
"UP": "稼働中",
"Upcoming Maintenance": "今後のメンテナンス",
"Updates": "更新",
"Uptime": "稼働時間"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%duration 동안 %status 상태였음",
"14 Days": "14 일",
"30 Days": "30 ",
"60 Days": "60 일",
"7 Days": "7 일",
"90 Days": "90 일",
"Availability per Component": "항목당 가용성",
"Back": "뒤로",
"Badge Copied": "배지 복사됨",
"Badge": "배지",
"Browse Events": "이벤트 찾아보기",
"Code Copied": "소스코드 복사됨",
"Copy Code": "소스코드 복사",
"Copy Link": "링크 복사",
"DEGRADED": "성능 저하됨",
"DOWN": "중단",
"Dark": "다크",
"Days": "일",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "<script> 나 <iframe> 태그를 사용하여 내 앱에 이 모니터링을 임베딩 해보세요.",
"Embed": "임베드",
"Get SVG badge for this monitor": "이 모니터링 항목에 대한 SVG 배지를 구해보세요.",
"Get a LIVE Status for this monitor": "이 모니터링 항목에 대한 실시간 상태를 구해보세요.",
"IDENTIFIED": "확인됨",
"INVESTIGATING": "조사 중",
"Incident Updates": "사건 보고서",
"LIVE Status": "실시간 상태",
"Lasted for about %lastedFor": "%lastedFor 동안 지속",
"Light": "라이트",
"Link Copied": "링크 복사됨",
"MAINTENANCE": "유지보수",
"MONITORING": "감시 중",
"Maintenance Completed": "유지보수 완료",
"Maintenance in Progress": "유지보수 진행 중",
"Mode": "모드",
"No Data": "데이터 없음",
"No Incident in %date": "%date 에는 아무 사건도 없었음",
"No Incidents": "사건 없음",
"No Monitor Found": "모니터링 항목을 찾을 수 없음",
"No Updates Yet": "아직 아무 업데이트도 없음",
"Ongoing Incidents": "진행중 사건",
"Pinging": "핑 요청",
"RESOLVED": "해결됨",
"Recent Incidents": "최근 사건",
"Recent Maintenances": "최근 유지보수",
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
"Share": "공유",
"Standard": "기본",
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
"Starts %startedAt, will last for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
"Starts %startedAt": "%startedAt 에 시작되었니다",
"Status OK": "양호",
"Status": "상태",
"Theme": "테마",
"Today": "오늘",
"UP": "작동 중",
"Upcoming Maintenance": "예정된 유지보수",
"Updates": "업데이트",
"Uptime": "업타임"
"%status for %duration": "%duration 동안 %status 상태였음",
"14 Days": "14 일",
"30 Days": "30 ",
"60 Days": "60 일",
"7 Days": "7 일",
"90 Days": "90 일",
"Availability per Component": "항목당 가용성",
"Back": "뒤로",
"Badge Copied": "배지 복사됨",
"Badge": "배지",
"Browse Events": "이벤트 찾아보기",
"Code Copied": "소스코드 복사됨",
"Copy Code": "소스코드 복사",
"Copy Link": "링크 복사",
"DEGRADED": "성능 저하됨",
"DOWN": "중단",
"Dark": "다크",
"Days": "일",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "<script> 나 <iframe> 태그를 사용하여 내 앱에 이 모니터링을 임베딩 해보세요.",
"Embed": "임베드",
"Get SVG badge for this monitor": "이 모니터링 항목에 대한 SVG 배지를 구해보세요.",
"Get a LIVE Status for this monitor": "이 모니터링 항목에 대한 실시간 상태를 구해보세요.",
"IDENTIFIED": "확인됨",
"INVESTIGATING": "조사 중",
"Incident Updates": "사건 보고서",
"LIVE Status": "실시간 상태",
"Lasted for about %lastedFor": "%lastedFor 동안 지속",
"Light": "라이트",
"Link Copied": "링크 복사됨",
"MAINTENANCE": "유지보수",
"MONITORING": "감시 중",
"Maintenance Completed": "유지보수 완료",
"Maintenance in Progress": "유지보수 진행 중",
"Mode": "모드",
"No Data": "데이터 없음",
"No Incident in %date": "%date 에는 아무 사건도 없었음",
"No Incidents": "사건 없음",
"No Monitor Found": "모니터링 항목을 찾을 수 없음",
"No Updates Yet": "아직 아무 업데이트도 없음",
"Ongoing Incidents": "진행중 사건",
"Pinging": "핑 요청",
"RESOLVED": "해결됨",
"Recent Incidents": "최근 사건",
"Recent Maintenances": "최근 유지보수",
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
"Share": "공유",
"Standard": "기본",
"Started %startedAt, lasted for %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
"Started %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
"Started %startedAt, will last for %lastedFor more": "%startedAt 전에 시작되었으며, %lastedFor 지속될 예정입니다",
"Starts %startedAt, will last for %lastedFor": "%startedAt 에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
"Starts %startedAt": "%startedAt 에 시작되었습니다",
"Status OK": "양호",
"Status": "상태",
"Switch to your timezone": "귀하의 시간대로 변경",
"Theme": "테마",
"Timezone set to %tz": "시간대 설정 %tz",
"Today": "오늘",
"UP": "작동 중",
"Upcoming Maintenance": "예정된 유지보수",
"Updates": "업데이트",
"Uptime": "업타임"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%status i %duration",
"14 Days": "14 dager",
"30 Days": "30 dager",
"60 Days": "60 dager",
"7 Days": "7 dager",
"90 Days": "90 dager",
"Availability per Component": "Tilgjengelighet per komponent",
"Back": "Tilbake",
"Badge Copied": "Merke kopiert",
"Badge": "Merke",
"Browse Events": "Bla gjennom hendelser",
"Code Copied": "Kode kopiert",
"Copy Code": "Kopier kode",
"Copy Link": "Kopier lenke",
"DEGRADED": "NEDSATT",
"DOWN": "NEDE",
"Dark": "Mørk",
"Days": "Dager",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Bygg inn denne monitoren ved å bruke <script> eller <iframe> i appen din.",
"Embed": "Bygg inn",
"Get SVG badge for this monitor": "Hent SVG-merke for denne monitoren",
"Get a LIVE Status for this monitor": "Få en LIVE-status for denne monitoren",
"IDENTIFIED": "IDENTIFISERT",
"INVESTIGATING": "UNDERSØKER",
"Incident Updates": "Hendelsesoppdateringer",
"LIVE Status": "LIVE-status",
"Lasted for about %lastedFor": "Varte i omtrent %lastedFor",
"Light": "Lys",
"Link Copied": "Lenke kopiert",
"MAINTENANCE": "VEDLIKEHOLD",
"MONITORING": "OVERVÅKER",
"Maintenance Completed": "Vedlikehold fullført",
"Maintenance in Progress": "Vedlikehold pågår",
"Mode": "Modus",
"No Data": "Ingen data",
"No Incident in %date": "Ingen hendelser på %date",
"No Incidents": "Ingen hendelser",
"No Monitor Found": "Ingen monitor funnet",
"No Updates Yet": "Ingen oppdateringer ennå",
"Ongoing Incidents": "Pågående hendelser",
"Pinging": "Pinger",
"RESOLVED": "LØST",
"Recent Incidents": "Nylige hendelser",
"Recent Maintenances": "Nylige vedlikehold",
"Share this monitor using a link with others": "Del denne monitoren med andre via lenke",
"Share": "Del",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
"Starts %startedAt, will last for %lastedFor": "Starter %startedAt, vil vare i %lastedFor",
"Starts %startedAt": "Starter %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Tema",
"Today": "I dag",
"UP": "OPPE",
"Upcoming Maintenance": "Kommende vedlikehold",
"Updates": "Oppdateringer",
"Uptime": "Oppetid"
"%status for %duration": "%status i %duration",
"14 Days": "14 dager",
"30 Days": "30 dager",
"60 Days": "60 dager",
"7 Days": "7 dager",
"90 Days": "90 dager",
"Availability per Component": "Tilgjengelighet per komponent",
"Back": "Tilbake",
"Badge Copied": "Merke kopiert",
"Badge": "Merke",
"Browse Events": "Bla gjennom hendelser",
"Code Copied": "Kode kopiert",
"Copy Code": "Kopier kode",
"Copy Link": "Kopier lenke",
"DEGRADED": "NEDSATT",
"DOWN": "NEDE",
"Dark": "Mørk",
"Days": "Dager",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Bygg inn denne monitoren ved å bruke <script> eller <iframe> i appen din.",
"Embed": "Bygg inn",
"Get SVG badge for this monitor": "Hent SVG-merke for denne monitoren",
"Get a LIVE Status for this monitor": "Få en LIVE-status for denne monitoren",
"IDENTIFIED": "IDENTIFISERT",
"INVESTIGATING": "UNDERSØKER",
"Incident Updates": "Hendelsesoppdateringer",
"LIVE Status": "LIVE-status",
"Lasted for about %lastedFor": "Varte i omtrent %lastedFor",
"Light": "Lys",
"Link Copied": "Lenke kopiert",
"MAINTENANCE": "VEDLIKEHOLD",
"MONITORING": "OVERVÅKER",
"Maintenance Completed": "Vedlikehold fullført",
"Maintenance in Progress": "Vedlikehold pågår",
"Mode": "Modus",
"No Data": "Ingen data",
"No Incident in %date": "Ingen hendelser på %date",
"No Incidents": "Ingen hendelser",
"No Monitor Found": "Ingen monitor funnet",
"No Updates Yet": "Ingen oppdateringer ennå",
"Ongoing Incidents": "Pågående hendelser",
"Pinging": "Pinger",
"RESOLVED": "LØST",
"Recent Incidents": "Nylige hendelser",
"Recent Maintenances": "Nylige vedlikehold",
"Share this monitor using a link with others": "Del denne monitoren med andre via lenke",
"Share": "Del",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
"Started %startedAt, will last for %lastedFor more": "Startet %startedAt, vil vare i %lastedFor til",
"Starts %startedAt, will last for %lastedFor": "Starter %startedAt, vil vare i %lastedFor",
"Starts %startedAt": "Starter %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Switch to your timezone": "Bytt til din tidssone",
"Theme": "Tema",
"Timezone set to %tz": "Tidssone satt til %tz",
"Today": "I dag",
"UP": "OPPE",
"Upcoming Maintenance": "Kommende vedlikehold",
"Updates": "Oppdateringer",
"Uptime": "Oppetid"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%status voor %duration",
"14 Days": "14 dagen",
"30 Days": "30 dagen",
"60 Days": "60 dagen",
"7 Days": "7 dagen",
"90 Days": "90 dagen",
"Availability per Component": "Beschikbaarheid per component",
"Back": "Terug",
"Badge Copied": "Badge gekopieerd",
"Badge": "Badge",
"Browse Events": "Evenementen bekijken",
"Code Copied": "Code gekopieerd",
"Copy Code": "Kopieer code",
"Copy Link": "Kopieer link",
"DEGRADED": "BEPERKT",
"DOWN": "NIET BEREIKBAAR",
"Dark": "Donker",
"Days": "Dagen",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Sluit deze monitor in met behulp van <script> of <iframe> in je app.",
"Embed": "Insluiten",
"Get SVG badge for this monitor": "SVG-badge ophalen voor deze monitor",
"Get a LIVE Status for this monitor": "LIVE-status ophalen voor deze monitor",
"IDENTIFIED": "GEIDENTIFICEERD",
"INVESTIGATING": "ONDERZOEK BEZIG",
"Incident Updates": "Incident updates",
"LIVE Status": "LIVE status",
"Lasted for about %lastedFor": "Duurde ongeveer %lastedFor",
"Light": "Licht",
"Link Copied": "Link gekopieerd",
"MAINTENANCE": "ONDERHOUD",
"MONITORING": "MONITOREN",
"Maintenance Completed": "Onderhoud voltooid",
"Maintenance in Progress": "Onderhoud bezig",
"Mode": "Modus",
"No Data": "Geen gegevens",
"No Incident in %date": "Geen incident op %date",
"No Incidents": "Geen incidenten",
"No Monitor Found": "Geen monitor gevonden",
"No Updates Yet": "Nog geen updates",
"Ongoing Incidents": "Lopende incidenten",
"Pinging": "Pingen",
"RESOLVED": "OPGELOST",
"Recent Incidents": "Recente incidenten",
"Recent Maintenances": "Recente onderhoudsbeurten",
"Share this monitor using a link with others": "Deel deze monitor met anderen via een link",
"Share": "Delen",
"Standard": "Standaard",
"Started %startedAt, lasted for %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
"Started %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
"Starts %startedAt, will last for %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
"Starts %startedAt": "Begint over %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Thema",
"Today": "Vandaag",
"UP": "BEREIKBAAR",
"Upcoming Maintenance": "Aankomend onderhoud",
"Updates": "Updates",
"Uptime": "Uptime"
"%status for %duration": "%status voor %duration",
"14 Days": "14 dagen",
"30 Days": "30 dagen",
"60 Days": "60 dagen",
"7 Days": "7 dagen",
"90 Days": "90 dagen",
"Availability per Component": "Beschikbaarheid per component",
"Back": "Terug",
"Badge Copied": "Badge gekopieerd",
"Badge": "Badge",
"Browse Events": "Evenementen bekijken",
"Code Copied": "Code gekopieerd",
"Copy Code": "Kopieer code",
"Copy Link": "Kopieer link",
"DEGRADED": "BEPERKT",
"DOWN": "NIET BEREIKBAAR",
"Dark": "Donker",
"Days": "Dagen",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Sluit deze monitor in met behulp van <script> of <iframe> in je app.",
"Embed": "Insluiten",
"Get SVG badge for this monitor": "SVG-badge ophalen voor deze monitor",
"Get a LIVE Status for this monitor": "LIVE-status ophalen voor deze monitor",
"IDENTIFIED": "GEIDENTIFICEERD",
"INVESTIGATING": "ONDERZOEK BEZIG",
"Incident Updates": "Incident updates",
"LIVE Status": "LIVE status",
"Lasted for about %lastedFor": "Duurde ongeveer %lastedFor",
"Light": "Licht",
"Link Copied": "Link gekopieerd",
"MAINTENANCE": "ONDERHOUD",
"MONITORING": "MONITOREN",
"Maintenance Completed": "Onderhoud voltooid",
"Maintenance in Progress": "Onderhoud bezig",
"Mode": "Modus",
"No Data": "Geen gegevens",
"No Incident in %date": "Geen incident op %date",
"No Incidents": "Geen incidenten",
"No Monitor Found": "Geen monitor gevonden",
"No Updates Yet": "Nog geen updates",
"Ongoing Incidents": "Lopende incidenten",
"Pinging": "Pingen",
"RESOLVED": "OPGELOST",
"Recent Incidents": "Recente incidenten",
"Recent Maintenances": "Recente onderhoudsbeurten",
"Share this monitor using a link with others": "Deel deze monitor met anderen via een link",
"Share": "Delen",
"Standard": "Standaard",
"Started %startedAt, lasted for %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
"Started %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
"Started %startedAt, will last for %lastedFor more": "Begint %startedAt, zal nog ongeveer %lastedFor duren",
"Starts %startedAt, will last for %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
"Starts %startedAt": "Begint over %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Switch to your timezone": "Schakel over naar je tijdzone",
"Theme": "Thema",
"Timezone set to %tz": "Tijdzone ingesteld op %tz",
"Today": "Vandaag",
"UP": "BEREIKBAAR",
"Upcoming Maintenance": "Aankomend onderhoud",
"Updates": "Updates",
"Uptime": "Uptime"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%status por %duration",
"14 Days": "14 Dias",
"30 Days": "30 Dias",
"60 Days": "60 Dias",
"7 Days": "7 Dias",
"90 Days": "90 Dias",
"Availability per Component": "Disponibilidade por Componente",
"Back": "Voltar",
"Badge Copied": "Emblema Copiado",
"Badge": "Emblema",
"Browse Events": "Navegar Eventos",
"Code Copied": "Código Copiado",
"Copy Code": "Copiar Código",
"Copy Link": "Copiar Link",
"DEGRADED": "FALHA",
"DOWN": "INDISPONÍVEL",
"Dark": "Escuro",
"Days": "Dias",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
"Embed": "Incorporar",
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
"IDENTIFIED": "IDENTIFICADO",
"INVESTIGATING": "INVESTIGANDO",
"Incident Updates": "Atualizações do Incidente",
"LIVE Status": "Status AO VIVO",
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
"Light": "Claro",
"Link Copied": "Link Copiado",
"MAINTENANCE": "MANUTENÇÃO",
"MONITORING": "MONITORANDO",
"Maintenance Completed": "Manutenção Concluída",
"Maintenance in Progress": "Manutenção em Progresso",
"Mode": "Modo",
"No Data": "Sem dados",
"No Incident in %date": "Nenhum incidente em %date",
"No Incidents": "Sem Incidentes",
"No Monitor Found": "Nenhum monitor encontrado",
"No Updates Yet": "Nenhuma atualização ainda",
"Ongoing Incidents": "Incidentes em Andamento",
"Pinging": "Animado",
"RESOLVED": "RESOLVIDO",
"Recent Incidents": "Incidentes Recentes",
"Recent Maintenances": "Manutenções Recentes",
"Share this monitor using a link with others": "Compartilhar este monitor através do link",
"Share": "Compartilhar",
"Standard": "Padrão",
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
"Starts %startedAt, will last for %lastedFor": "Terá início em %startedAt, com duração de %lastedFor",
"Starts %startedAt": "Inicia em %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Theme": "Tema",
"Today": "Hoje",
"UP": "OPERACIONAL",
"Upcoming Maintenance": "Manutenção Agendada",
"Updates": "Atualizações",
"Uptime": "Tempo de Atividade"
"%status for %duration": "%status por %duration",
"14 Days": "14 Dias",
"30 Days": "30 Dias",
"60 Days": "60 Dias",
"7 Days": "7 Dias",
"90 Days": "90 Dias",
"Availability per Component": "Disponibilidade por Componente",
"Back": "Voltar",
"Badge Copied": "Emblema Copiado",
"Badge": "Emblema",
"Browse Events": "Navegar Eventos",
"Code Copied": "Código Copiado",
"Copy Code": "Copiar Código",
"Copy Link": "Copiar Link",
"DEGRADED": "FALHA",
"DOWN": "INDISPONÍVEL",
"Dark": "Escuro",
"Days": "Dias",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
"Embed": "Incorporar",
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
"IDENTIFIED": "IDENTIFICADO",
"INVESTIGATING": "INVESTIGANDO",
"Incident Updates": "Atualizações do Incidente",
"LIVE Status": "Status AO VIVO",
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
"Light": "Claro",
"Link Copied": "Link Copiado",
"MAINTENANCE": "MANUTENÇÃO",
"MONITORING": "MONITORANDO",
"Maintenance Completed": "Manutenção Concluída",
"Maintenance in Progress": "Manutenção em Progresso",
"Mode": "Modo",
"No Data": "Sem dados",
"No Incident in %date": "Nenhum incidente em %date",
"No Incidents": "Sem Incidentes",
"No Monitor Found": "Nenhum monitor encontrado",
"No Updates Yet": "Nenhuma atualização ainda",
"Ongoing Incidents": "Incidentes em Andamento",
"Pinging": "Animado",
"RESOLVED": "RESOLVIDO",
"Recent Incidents": "Incidentes Recentes",
"Recent Maintenances": "Manutenções Recentes",
"Share this monitor using a link with others": "Compartilhar este monitor através do link",
"Share": "Compartilhar",
"Standard": "Padrão",
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
"Started %startedAt, will last for %lastedFor more": "Iniciará em %startedAt, durará mais %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Terá início em %startedAt, com duração de %lastedFor",
"Starts %startedAt": "Inicia em %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Switch to your timezone": "Mudar para o seu fuso horário",
"Theme": "Tema",
"Timezone set to %tz": "Fuso horário definido para %tz",
"Today": "Hoje",
"UP": "OPERACIONAL",
"Upcoming Maintenance": "Manutenção Agendada",
"Updates": "Atualizações",
"Uptime": "Tempo de Atividade"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%status в течение %duration",
"14 Days": "14 Дней",
"30 Days": "30 Дней",
"60 Days": "60 Дней",
"7 Days": "7 Дней",
"90 Days": "90 Дней",
"Availability per Component": "Доступность по компонентам",
"Back": "Назад",
"Badge Copied": "Значок скопирован",
"Badge": "Значок",
"Browse Events": "Просмотр событий",
"Code Copied": "Код скопирован",
"Copy Code": "Копировать код",
"Copy Link": "Копировать ссылку",
"DEGRADED": "УХУДШЕНО",
"DOWN": "НЕДОСТУПНО",
"Dark": "Темная",
"Days": "Дней",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Встройте этот монитор используя <script> или <iframe> в ваше приложение.",
"Embed": "Встроить",
"Get SVG badge for this monitor": "Получить SVG значок для этого монитора",
"Get a LIVE Status for this monitor": "Получить ЖИВОЙ статус для этого монитора",
"IDENTIFIED": "ВЫЯВЛЕНО",
"INVESTIGATING": "ИССЛЕДУЕТСЯ",
"Incident Updates": "Обновления инцидента",
"LIVE Status": "ЖИВОЙ статус",
"Lasted for about %lastedFor": "Длилось около %lastedFor",
"Light": "Светлая",
"Link Copied": "Ссылка скопирована",
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
"MONITORING": "МОНИТОРИНГ",
"Maintenance Completed": "Техническое обслуживание завершено",
"Maintenance in Progress": "Техническое обслуживание",
"Mode": "Режим",
"No Data": "Нет данных",
"No Incident in %date": "Нет инцидентов на %date",
"No Incidents": "Нет инцидентов",
"No Monitor Found": "Монитор не найден",
"No Updates Yet": "Пока нет обновлений",
"Ongoing Incidents": "Текущие инциденты",
"Pinging": "Пингуется",
"RESOLVED": "РЕШЕНО",
"Recent Incidents": "Недавние инциденты",
"Recent Maintenances": "Последние обслуживания",
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
"Share": "Поделиться",
"Standard": "Стандартная",
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
"Starts %startedAt": "Начнется через %startedAt",
"Status OK": "Статус ОК",
"Status": "Статус",
"Theme": "Тема",
"Today": "Сегодня",
"UP": "РАБОТАЕТ",
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
"Updates": "Обновления",
"Uptime": "Время работы"
"%status for %duration": "%status в течение %duration",
"14 Days": "14 Дней",
"30 Days": "30 Дней",
"60 Days": "60 Дней",
"7 Days": "7 Дней",
"90 Days": "90 Дней",
"Availability per Component": "Доступность по компонентам",
"Back": "Назад",
"Badge Copied": "Значок скопирован",
"Badge": "Значок",
"Browse Events": "Просмотр событий",
"Code Copied": "Код скопирован",
"Copy Code": "Копировать код",
"Copy Link": "Копировать ссылку",
"DEGRADED": "УХУДШЕНО",
"DOWN": "НЕДОСТУПНО",
"Dark": "Темная",
"Days": "Дней",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Встройте этот монитор используя <script> или <iframe> в ваше приложение.",
"Embed": "Встроить",
"Get SVG badge for this monitor": "Получить SVG значок для этого монитора",
"Get a LIVE Status for this monitor": "Получить ЖИВОЙ статус для этого монитора",
"IDENTIFIED": "ВЫЯВЛЕНО",
"INVESTIGATING": "ИССЛЕДУЕТСЯ",
"Incident Updates": "Обновления инцидента",
"LIVE Status": "ЖИВОЙ статус",
"Lasted for about %lastedFor": "Длилось около %lastedFor",
"Light": "Светлая",
"Link Copied": "Ссылка скопирована",
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
"MONITORING": "МОНИТОРИНГ",
"Maintenance Completed": "Техническое обслуживание завершено",
"Maintenance in Progress": "Техническое обслуживание",
"Mode": "Режим",
"No Data": "Нет данных",
"No Incident in %date": "Нет инцидентов на %date",
"No Incidents": "Нет инцидентов",
"No Monitor Found": "Монитор не найден",
"No Updates Yet": "Пока нет обновлений",
"Ongoing Incidents": "Текущие инциденты",
"Pinging": "Пингуется",
"RESOLVED": "РЕШЕНО",
"Recent Incidents": "Недавние инциденты",
"Recent Maintenances": "Последние обслуживания",
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
"Share": "Поделиться",
"Standard": "Стандартная",
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
"Started %startedAt, will last for %lastedFor more": "Начнется около %startedAt, продлится еще около %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
"Starts %startedAt": "Начнется через %startedAt",
"Status OK": "Статус ОК",
"Status": "Статус",
"Switch to your timezone": "Переключиться на ваш часовой пояс",
"Theme": "Тема",
"Timezone set to %tz": "Часовой пояс установлен на %tz",
"Today": "Сегодня",
"UP": "РАБОТАЕТ",
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
"Updates": "Обновления",
"Uptime": "Время работы"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%duration boyunca %status",
"14 Days": "14 Gün",
"30 Days": "30 Gün",
"60 Days": "60 Gün",
"7 Days": "7 Gün",
"90 Days": "90 Gün",
"Availability per Component": "Bileşen Kullanılabilirliği",
"Back": "Geri",
"Badge Copied": "Rozet Kopyalandı",
"Badge": "Rozet",
"Browse Events": "Etkinliklere Göz At",
"Code Copied": "Kod Kopyalandı",
"Copy Code": "Kodu Kopyala",
"Copy Link": "Bağlantıyı Kopyala",
"DEGRADED": "BOZUK",
"DOWN": "ÇALIŞMIYOR",
"Dark": "Koyu",
"Days": "Günler",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
"Embed": "Yerleştir",
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
"IDENTIFIED": "TANIMLANDI",
"INVESTIGATING": "İNCELENİYOR",
"Incident Updates": "Olay Güncellemeleri",
"LIVE Status": "CANLI Durum",
"Lasted for about %lastedFor": "%lastedFor sürdü",
"Light": "Açık",
"Link Copied": "Bağlantı Kopyalandı",
"MAINTENANCE": "BAKIM",
"MONITORING": "İZLEME",
"Maintenance Completed": "Bakım Tamamlandı",
"Maintenance in Progress": "Bakım Devam Ediyor",
"Mode": "Mod",
"No Data": "Veri Yok",
"No Incident in %date": "%date Tarihinde Olay Yok",
"No Incidents": "Olay Yok",
"No Monitor Found": "Servis Bulunamadı",
"No Updates Yet": "Henüz Güncelleme Yok",
"Ongoing Incidents": "Devam Eden Olaylar",
"Pinging": "Pinging",
"RESOLVED": "ÇÖZÜLDÜ",
"Recent Incidents": "Son Olaylar",
"Recent Maintenances": "Son Bakımlar",
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
"Share": "Paylaş",
"Standard": "Standart",
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
"Starts %startedAt, will last for %lastedFor": "%startedAt başlıyor, %lastedFor sürecek",
"Starts %startedAt": "%startedAt başlıyor",
"Status OK": "Durum UYGUN",
"Status": "Durum",
"Theme": "Tema",
"Today": "Bugün",
"UP": "ÇALIŞIYOR",
"Upcoming Maintenance": "Yaklaşan Bakım",
"Updates": "Güncellemeler",
"Uptime": alışma Süresi"
"%status for %duration": "%duration boyunca %status",
"14 Days": "14 Gün",
"30 Days": "30 Gün",
"60 Days": "60 Gün",
"7 Days": "7 Gün",
"90 Days": "90 Gün",
"Availability per Component": "Bileşen Kullanılabilirliği",
"Back": "Geri",
"Badge Copied": "Rozet Kopyalandı",
"Badge": "Rozet",
"Browse Events": "Etkinliklere Göz At",
"Code Copied": "Kod Kopyalandı",
"Copy Code": "Kodu Kopyala",
"Copy Link": "Bağlantıyı Kopyala",
"DEGRADED": "BOZUK",
"DOWN": "ÇALIŞMIYOR",
"Dark": "Koyu",
"Days": "Günler",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
"Embed": "Yerleştir",
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
"IDENTIFIED": "TANIMLANDI",
"INVESTIGATING": "İNCELENİYOR",
"Incident Updates": "Olay Güncellemeleri",
"LIVE Status": "CANLI Durum",
"Lasted for about %lastedFor": "%lastedFor sürdü",
"Light": "Açık",
"Link Copied": "Bağlantı Kopyalandı",
"MAINTENANCE": "BAKIM",
"MONITORING": "İZLEME",
"Maintenance Completed": "Bakım Tamamlandı",
"Maintenance in Progress": "Bakım Devam Ediyor",
"Mode": "Mod",
"No Data": "Veri Yok",
"No Incident in %date": "%date Tarihinde Olay Yok",
"No Incidents": "Olay Yok",
"No Monitor Found": "Servis Bulunamadı",
"No Updates Yet": "Henüz Güncelleme Yok",
"Ongoing Incidents": "Devam Eden Olaylar",
"Pinging": "Pinging",
"RESOLVED": "ÇÖZÜLDÜ",
"Recent Incidents": "Son Olaylar",
"Recent Maintenances": "Son Bakımlar",
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
"Share": "Paylaş",
"Standard": "Standart",
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
"Started %startedAt, will last for %lastedFor more": "%startedAt başladı, %lastedFor daha sürecek",
"Starts %startedAt, will last for %lastedFor": "%startedAt başlıyor, %lastedFor sürecek",
"Starts %startedAt": "%startedAt başlıyor",
"Status OK": "Durum UYGUN",
"Status": "Durum",
"Switch to your timezone": "Saat diliminize geçin",
"Theme": "Tema",
"Timezone set to %tz": "Saat dilimi %tz olarak ayarlandı",
"Today": "Bugün",
"UP": ALIŞIYOR",
"Upcoming Maintenance": "Yaklaşan Bakım",
"Updates": "Güncellemeler",
"Uptime": "Çalışma Süresi"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%status trong %duration",
"14 Days": "14 ngày",
"30 Days": "30 ngày",
"60 Days": "60 ngày",
"7 Days": "7 ngày",
"90 Days": "90 ngày",
"Availability per Component": "Tính khả dụng theo thành phần",
"Back": "Quay lại",
"Badge Copied": "Huy hiệu đã được sao chép",
"Badge": "Huy hiệu",
"Browse Events": "Duyệt sự kiện",
"Code Copied": "Mã đã được sao chép",
"Copy Code": "Sao chép mã",
"Copy Link": "Sao chép liên kết",
"DEGRADED": "Suy giảm",
"DOWN": "Ngừng hoạt động",
"Dark": "Tối",
"Days": "Ngày",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Nhúng trình theo dõi này vào ứng dụng của bạn bằng <script> hoặc <iframe>.",
"Embed": "Nhúng",
"Get SVG badge for this monitor": "Lấy huy hiệu SVG cho trình theo dõi này",
"Get a LIVE Status for this monitor": "Lấy trạng thái TRỰC TIẾP cho trình theo dõi này",
"IDENTIFIED": "Đã xác định",
"INVESTIGATING": "Đang điều tra",
"Incident Updates": "Cập nhật sự cố",
"LIVE Status": "Trạng thái TRỰC TIẾP",
"Lasted for about %lastedFor": "Kéo dài khoảng %lastedFor",
"Light": "Sáng",
"Link Copied": "Liên kết đã được sao chép",
"MAINTENANCE": "Bảo trì",
"MONITORING": "Đang giám sát",
"Maintenance Completed": "Bảo trì đã hoàn thành",
"Maintenance in Progress": "Bảo trì đang diễn ra",
"Mode": "Chế độ",
"No Data": "Không có dữ liệu",
"No Incident in %date": "Không có sự cố vào %date",
"No Incidents": "Không có sự cố",
"No Monitor Found": "Không tìm thấy trình theo dõi",
"No Updates Yet": "Chưa có cập nhật",
"Ongoing Incidents": "Sự cố đang diễn ra",
"Pinging": "Đang kiểm tra",
"RESOLVED": "Đã giải quyết",
"Recent Incidents": "Sự cố gần đây",
"Recent Maintenances": "Bảo trì gần đây",
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
"Share": "Chia sẻ",
"Standard": "Tiêu chuẩn",
"Started %startedAt, lasted for %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
"Started %startedAt, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
"Starts %startedAt, will last for %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
"Starts %startedAt": "Bắt đầu trong %startedAt",
"Status OK": "Trạng thái OK",
"Status": "Trạng thái",
"Theme": "Giao diện",
"Today": "Hôm nay",
"UP": "Hoạt động",
"Upcoming Maintenance": "Bảo trì sắp tới",
"Updates": "Cập nhật",
"Uptime": "Thời gian hoạt động"
"%status for %duration": "%status trong %duration",
"14 Days": "14 ngày",
"30 Days": "30 ngày",
"60 Days": "60 ngày",
"7 Days": "7 ngày",
"90 Days": "90 ngày",
"Availability per Component": "Tính khả dụng theo thành phần",
"Back": "Quay lại",
"Badge Copied": "Huy hiệu đã được sao chép",
"Badge": "Huy hiệu",
"Browse Events": "Duyệt sự kiện",
"Code Copied": "Mã đã được sao chép",
"Copy Code": "Sao chép mã",
"Copy Link": "Sao chép liên kết",
"DEGRADED": "Suy giảm",
"DOWN": "Ngừng hoạt động",
"Dark": "Tối",
"Days": "Ngày",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Nhúng trình theo dõi này vào ứng dụng của bạn bằng <script> hoặc <iframe>.",
"Embed": "Nhúng",
"Get SVG badge for this monitor": "Lấy huy hiệu SVG cho trình theo dõi này",
"Get a LIVE Status for this monitor": "Lấy trạng thái TRỰC TIẾP cho trình theo dõi này",
"IDENTIFIED": "Đã xác định",
"INVESTIGATING": "Đang điều tra",
"Incident Updates": "Cập nhật sự cố",
"LIVE Status": "Trạng thái TRỰC TIẾP",
"Lasted for about %lastedFor": "Kéo dài khoảng %lastedFor",
"Light": "Sáng",
"Link Copied": "Liên kết đã được sao chép",
"MAINTENANCE": "Bảo trì",
"MONITORING": "Đang giám sát",
"Maintenance Completed": "Bảo trì đã hoàn thành",
"Maintenance in Progress": "Bảo trì đang diễn ra",
"Mode": "Chế độ",
"No Data": "Không có dữ liệu",
"No Incident in %date": "Không có sự cố vào %date",
"No Incidents": "Không có sự cố",
"No Monitor Found": "Không tìm thấy trình theo dõi",
"No Updates Yet": "Chưa có cập nhật",
"Ongoing Incidents": "Sự cố đang diễn ra",
"Pinging": "Đang kiểm tra",
"RESOLVED": "Đã giải quyết",
"Recent Incidents": "Sự cố gần đây",
"Recent Maintenances": "Bảo trì gần đây",
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
"Share": "Chia sẻ",
"Standard": "Tiêu chuẩn",
"Started %startedAt, lasted for %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
"Started %startedAt, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
"Started %startedAt, will last for %lastedFor more": "Bắt đầu khoảng %startedAt trước, sẽ kéo dài thêm khoảng %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
"Starts %startedAt": "Bắt đầu trong %startedAt",
"Status OK": "Trạng thái OK",
"Status": "Trạng thái",
"Switch to your timezone": "Chuyển sang múi giờ của bạn",
"Theme": "Giao diện",
"Timezone set to %tz": "Đã đặt múi giờ %tz",
"Today": "Hôm nay",
"UP": "Hoạt động",
"Upcoming Maintenance": "Bảo trì sắp tới",
"Updates": "Cập nhật",
"Uptime": "Thời gian hoạt động"
}
+62 -59
View File
@@ -1,61 +1,64 @@
{
"%status for %duration": "%duration 内的 %status",
"14 Days": "14天",
"30 Days": "30天",
"60 Days": "60天",
"7 Days": "7天",
"90 Days": "90天",
"Availability per Component": "每个组件的可用性",
"Back": "返回",
"Badge Copied": "徽章已复制",
"Badge": "徽章",
"Browse Events": "浏览事件",
"Code Copied": "代码已复制",
"Copy Code": "复制代码",
"Copy Link": "复制链接",
"DEGRADED": "性能下降",
"DOWN": "不可用",
"Dark": "深色",
"Days": "天",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "使用 <script> 或 <iframe> 将此监控嵌入您的应用。",
"Embed": "嵌入",
"Get SVG badge for this monitor": "获取此监控的SVG徽章",
"Get a LIVE Status for this monitor": "获取此监控的实时状态",
"IDENTIFIED": "已识别",
"INVESTIGATING": "正在调查",
"Incident Updates": "事件更新",
"LIVE Status": "实时状态",
"Lasted for about %lastedFor": "持续了约 %lastedFor",
"Light": "浅色",
"Link Copied": "链接已复制",
"MAINTENANCE": "维护",
"MONITORING": "正在监控",
"Maintenance Completed": "维护已完成",
"Maintenance in Progress": "维护进行中",
"Mode": "模式",
"No Data": "无数据",
"No Incident in %date": "%date 无事件",
"No Incidents": "无事件",
"No Monitor Found": "未找到监控",
"No Updates Yet": "尚无更新",
"Ongoing Incidents": "正在处理的事件",
"Pinging": "正在测试",
"RESOLVED": "已解决",
"Recent Incidents": "最近的事件",
"Recent Maintenances": "最近的维护",
"Share this monitor using a link with others": "通过链接与他人分享此监控",
"Share": "分享",
"Standard": "标准",
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
"Starts %startedAt, will last for %lastedFor": "%startedAt 开始,将持续约 %lastedFor",
"Starts %startedAt": "%startedAt 后开始",
"Status OK": "状态正常",
"Status": "状态",
"Theme": "主题",
"Today": "今天",
"UP": "可用",
"Upcoming Maintenance": "即将进行的维护",
"Updates": "更新",
"Uptime": "正常运行时间"
"%status for %duration": "%duration 内的 %status",
"14 Days": "14天",
"30 Days": "30天",
"60 Days": "60天",
"7 Days": "7天",
"90 Days": "90天",
"Availability per Component": "每个组件的可用性",
"Back": "返回",
"Badge Copied": "徽章已复制",
"Badge": "徽章",
"Browse Events": "浏览事件",
"Code Copied": "代码已复制",
"Copy Code": "复制代码",
"Copy Link": "复制链接",
"DEGRADED": "性能下降",
"DOWN": "不可用",
"Dark": "深色",
"Days": "天",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "使用 <script> 或 <iframe> 将此监控嵌入您的应用。",
"Embed": "嵌入",
"Get SVG badge for this monitor": "获取此监控的SVG徽章",
"Get a LIVE Status for this monitor": "获取此监控的实时状态",
"IDENTIFIED": "已识别",
"INVESTIGATING": "正在调查",
"Incident Updates": "事件更新",
"LIVE Status": "实时状态",
"Lasted for about %lastedFor": "持续了约 %lastedFor",
"Light": "浅色",
"Link Copied": "链接已复制",
"MAINTENANCE": "维护",
"MONITORING": "正在监控",
"Maintenance Completed": "维护已完成",
"Maintenance in Progress": "维护进行中",
"Mode": "模式",
"No Data": "无数据",
"No Incident in %date": "%date 无事件",
"No Incidents": "无事件",
"No Monitor Found": "未找到监控",
"No Updates Yet": "尚无更新",
"Ongoing Incidents": "正在处理的事件",
"Pinging": "正在测试",
"RESOLVED": "已解决",
"Recent Incidents": "最近的事件",
"Recent Maintenances": "最近的维护",
"Share this monitor using a link with others": "通过链接与他人分享此监控",
"Share": "分享",
"Standard": "标准",
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
"Started %startedAt, will last for %lastedFor more": "大约 %startedAt 开始,将持续约 %lastedFor",
"Starts %startedAt, will last for %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
"Starts %startedAt": "%startedAt 后开始",
"Status OK": "状态正常",
"Status": "状态",
"Switch to your timezone": "切换到您的时区",
"Theme": "主题",
"Timezone set to %tz": "时区设置为 %tz",
"Today": "今天",
"UP": "可用",
"Upcoming Maintenance": "即将进行的维护",
"Updates": "更新",
"Uptime": "正常运行时间"
}
+162 -174
View File
@@ -3,12 +3,12 @@ import notification from "./notification/notif.js";
import moment from "moment";
import {
GetAllSiteData,
GetAllTriggers,
CreateIncident,
AddIncidentComment,
AddIncidentMonitor,
InsertNewAlert
GetAllSiteData,
GetAllTriggers,
CreateIncident,
AddIncidentComment,
AddIncidentMonitor,
InsertNewAlert,
} from "./controllers/controller.js";
import db from "./db/db.js";
@@ -17,197 +17,185 @@ const TRIGGERED = "TRIGGERED";
const RESOLVED = "RESOLVED";
async function createJSONCommonAlert(monitor, config, alert, severity) {
let siteData = await GetAllSiteData();
let siteURL = siteData.siteURL;
let id = monitor.tag + "-" + alert.id;
let alert_name = monitor.name + " " + alert.monitor_status;
let source = "Kener";
let timestamp = new Date().toISOString();
let description = config.description || "Monitor has failed";
let status = alert.alert_status;
let details = {
metric: monitor.name,
current_value: alert.health_checks,
threshold: config.failureThreshold
};
let actions = [
{
text: "View Monitor",
url: siteURL + "?monitor=" + monitor.tag
}
];
return {
id,
alert_name,
severity,
status,
source,
timestamp,
description,
details,
actions
};
let siteData = await GetAllSiteData();
let siteURL = siteData.siteURL;
let id = monitor.tag + "-" + alert.id;
let alert_name = monitor.name + " " + alert.monitor_status;
let source = "Kener";
let timestamp = new Date().toISOString();
let description = config.description || "Monitor has failed";
let status = alert.alert_status;
let details = {
metric: monitor.name,
current_value: alert.health_checks,
threshold: config.failureThreshold,
};
let actions = [
{
text: "View Monitor",
url: siteURL + "?monitor=" + monitor.tag,
},
];
return {
id,
alert_name,
severity,
status,
source,
timestamp,
description,
details,
actions,
};
}
async function createNewIncident(monitor, alert, commonData) {
let startDateTime = moment(alert.created_at).unix();
let payload = {
start_date_time: startDateTime,
title: commonData.alert_name
};
let startDateTime = moment(alert.created_at).unix();
let payload = {
start_date_time: startDateTime,
title: commonData.alert_name,
incident_source: "ALERT",
};
let update = commonData.description;
update =
update +
`. Monitor Name: ${monitor.name}, Incident Status: ${commonData.status}, Severity: ${commonData.severity}, Monitor Status: ${alert.monitor_status}, Monitor Health Checks: ${alert.health_checks}, Monitor Failure Threshold: ${commonData.details.threshold}, Visit - ${commonData.actions[0].url}`;
let update = commonData.description;
update =
update +
`<div style="line-height: 1.5;">
<p><strong>Monitor Name:</strong> ${monitor.name}</p>
<p><strong>Incident Status:</strong> ${commonData.status}</p>
<p><strong>Severity:</strong> ${commonData.severity}</p>
<p><strong>Monitor Status:</strong> ${alert.monitor_status}</p>
<p><strong>Monitor Health Checks:</strong> ${alert.health_checks}</p>
<p><strong>Monitor Failure Threshold:</strong> ${commonData.details.threshold}</p>
<p><strong>Visit:</strong> <a href="${commonData.actions[0].url}">${commonData.actions[0].url}</a></p>
</div>`;
let newIncident = await CreateIncident(payload);
let newIncident = await CreateIncident(payload);
//add update to incident
await AddIncidentComment(newIncident.incident_id, update, "INVESTIGATING", startDateTime);
//add update to incident
await AddIncidentComment(newIncident.incident_id, update, "INVESTIGATING", startDateTime);
//add monitor to incident
await AddIncidentMonitor(newIncident.incident_id, monitor.tag, alert.monitor_status);
//add monitor to incident
await AddIncidentMonitor(newIncident.incident_id, monitor.tag, alert.monitor_status);
return newIncident;
return newIncident;
}
async function closeIncident(alert, comment) {
let incident_id = alert.incident_number;
return await AddIncidentComment(
incident_id,
comment,
"RESOLVED",
moment(alert.updated_at).unix()
);
let incident_id = alert.incident_number;
return await AddIncidentComment(incident_id, comment, "RESOLVED", moment(alert.updated_at).unix());
}
function createClosureComment(alert, commonJSON) {
let comment = "The incident has been auto resolved";
let downtimeDuration = moment(alert.updated_at).diff(moment(alert.created_at), "minutes");
comment = comment + `, Total downtime: ` + downtimeDuration + ` minutes`;
return comment;
let comment = "The incident has been auto resolved";
let downtimeDuration = moment(alert.updated_at).diff(moment(alert.created_at), "minutes");
comment = comment + `, Total downtime: ` + downtimeDuration + ` minutes`;
return comment;
}
async function alerting(m) {
let monitor = await db.getMonitorByTag(m.tag);
let siteData = await GetAllSiteData();
const triggers = await GetAllTriggers({
status: "ACTIVE"
});
const triggerObj = {};
if (!!monitor.down_trigger) {
triggerObj.down_trigger = JSON.parse(monitor.down_trigger);
}
if (!!monitor.degraded_trigger) {
triggerObj.degraded_trigger = JSON.parse(monitor.degraded_trigger);
}
let monitor = await db.getMonitorByTag(m.tag);
let siteData = await GetAllSiteData();
const triggers = await GetAllTriggers({
status: "ACTIVE",
});
const triggerObj = {};
if (!!monitor.down_trigger) {
triggerObj.down_trigger = JSON.parse(monitor.down_trigger);
}
if (!!monitor.degraded_trigger) {
triggerObj.degraded_trigger = JSON.parse(monitor.degraded_trigger);
}
for (const key in triggerObj) {
if (Object.prototype.hasOwnProperty.call(triggerObj, key)) {
const alertConfig = triggerObj[key];
const monitor_status = alertConfig.trigger_type;
for (const key in triggerObj) {
if (Object.prototype.hasOwnProperty.call(triggerObj, key)) {
const alertConfig = triggerObj[key];
const monitor_status = alertConfig.trigger_type;
const failureThreshold = alertConfig.failureThreshold;
const successThreshold = alertConfig.successThreshold;
const monitor_tag = monitor.tag;
const alertingChannels = alertConfig.triggers; //array of numbers of trigger ids
const createIncident = alertConfig.createIncident === "YES";
const allMonitorClients = [];
const sendTrigger = alertConfig.active;
const severity = alertConfig.severity;
if (!sendTrigger) {
continue;
}
if (alertingChannels.length > 0) {
for (let i = 0; i < alertingChannels.length; i++) {
const triggerID = alertingChannels[i];
const trigger = triggers.find((c) => c.id === triggerID);
if (!trigger) {
console.error(
`Triggers ${triggerID} not found in server triggers for monitor ${monitor_tag}`
);
continue;
}
if (trigger.trigger_status !== "ACTIVE") {
console.error(`Triggers ${triggerID} is not active`);
continue;
}
const notificationClient = new notification(trigger, siteData, monitor);
allMonitorClients.push(notificationClient);
}
}
const failureThreshold = alertConfig.failureThreshold;
const successThreshold = alertConfig.successThreshold;
const monitor_tag = monitor.tag;
const alertingChannels = alertConfig.triggers; //array of numbers of trigger ids
const createIncident = alertConfig.createIncident === "YES";
const allMonitorClients = [];
const sendTrigger = alertConfig.active;
const severity = alertConfig.severity;
if (!sendTrigger) {
continue;
}
if (alertingChannels.length > 0) {
for (let i = 0; i < alertingChannels.length; i++) {
const triggerID = alertingChannels[i];
const trigger = triggers.find((c) => c.id === triggerID);
if (!trigger) {
console.error(`Triggers ${triggerID} not found in server triggers for monitor ${monitor_tag}`);
continue;
}
if (trigger.trigger_status !== "ACTIVE") {
console.error(`Triggers ${triggerID} is not active`);
continue;
}
const notificationClient = new notification(trigger, siteData, monitor);
allMonitorClients.push(notificationClient);
}
}
let isAffected = await db.consecutivelyStatusFor(
monitor_tag,
monitor_status,
failureThreshold
);
let alertExists = await db.alertExists(monitor_tag, monitor_status, TRIGGERED);
let activeAlert = null;
if (alertExists) {
activeAlert = await db.getActiveAlert(monitor_tag, monitor_status, TRIGGERED);
}
if (isAffected && !alertExists) {
activeAlert = await InsertNewAlert({
monitor_tag: monitor_tag,
monitor_status: monitor_status,
alert_status: TRIGGERED,
health_checks: failureThreshold
});
let commonJSON = await createJSONCommonAlert(
monitor,
alertConfig,
activeAlert,
severity
);
let isAffected = await db.consecutivelyStatusFor(monitor_tag, monitor_status, failureThreshold);
let alertExists = await db.alertExists(monitor_tag, monitor_status, TRIGGERED);
let activeAlert = null;
if (alertExists) {
activeAlert = await db.getActiveAlert(monitor_tag, monitor_status, TRIGGERED);
}
if (isAffected && !alertExists) {
activeAlert = await InsertNewAlert({
monitor_tag: monitor_tag,
monitor_status: monitor_status,
alert_status: TRIGGERED,
health_checks: failureThreshold,
});
let commonJSON = await createJSONCommonAlert(monitor, alertConfig, activeAlert, severity);
if (allMonitorClients.length > 0) {
for (let i = 0; i < allMonitorClients.length; i++) {
const client = allMonitorClients[i];
client.send(commonJSON);
}
}
if (createIncident) {
let incident = await createNewIncident(monitor, activeAlert, commonJSON);
if (allMonitorClients.length > 0) {
for (let i = 0; i < allMonitorClients.length; i++) {
const client = allMonitorClients[i];
client.send(commonJSON);
}
}
if (createIncident) {
let incident = await createNewIncident(monitor, activeAlert, commonJSON);
if (!!incident) {
//send incident to incident channel
await db.addIncidentNumberToAlert(activeAlert.id, incident.incident_id);
}
}
} else if (isAffected && alertExists) {
await db.incrementAlertHealthChecks(activeAlert.id);
} else if (!isAffected && alertExists) {
let isUp = await db.consecutivelyStatusFor(monitor_tag, "UP", successThreshold);
if (isUp) {
await db.updateAlertStatus(activeAlert.id, RESOLVED);
activeAlert.alert_status = RESOLVED;
let commonJSON = await createJSONCommonAlert(
monitor,
alertConfig,
activeAlert,
severity
);
if (allMonitorClients.length > 0) {
for (let i = 0; i < allMonitorClients.length; i++) {
const client = allMonitorClients[i];
client.send(commonJSON);
}
}
if (!!activeAlert.incident_number) {
let comment = createClosureComment(activeAlert, commonJSON);
if (!!incident) {
//send incident to incident channel
await db.addIncidentNumberToAlert(activeAlert.id, incident.incident_id);
}
}
} else if (isAffected && alertExists) {
await db.incrementAlertHealthChecks(activeAlert.id);
} else if (!isAffected && alertExists) {
let isUp = await db.consecutivelyStatusFor(monitor_tag, "UP", successThreshold);
if (isUp) {
await db.updateAlertStatus(activeAlert.id, RESOLVED);
activeAlert.alert_status = RESOLVED;
let commonJSON = await createJSONCommonAlert(monitor, alertConfig, activeAlert, severity);
if (allMonitorClients.length > 0) {
for (let i = 0; i < allMonitorClients.length; i++) {
const client = allMonitorClients[i];
client.send(commonJSON);
}
}
if (!!activeAlert.incident_number) {
let comment = createClosureComment(activeAlert, commonJSON);
try {
await closeIncident(activeAlert, comment);
} catch (error) {
console.log(error);
}
}
}
}
}
}
try {
await closeIncident(activeAlert, comment);
} catch (error) {
console.log(error);
}
}
}
}
}
}
}
export default alerting;
+10 -4
View File
@@ -8,13 +8,12 @@ const SITE = "./config/site.yaml";
const UP = "UP";
const DOWN = "DOWN";
const DEGRADED = "DEGRADED";
const NO_DATA = "NO_DATA";
const API_TIMEOUT = 10 * 1000; // 10 seconds
const AnalyticsProviders = {
GA: "https://unpkg.com/@analytics/google-analytics@1.0.7/dist/@analytics/google-analytics.min.js",
AMPLITUDE:
"https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js",
MIXPANEL:
"https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js",
AMPLITUDE: "https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js",
MIXPANEL: "https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js",
};
const AllRecordTypes = {
A: 1,
@@ -102,6 +101,9 @@ const REALTIME = "realtime";
const TIMEOUT = "timeout";
const ERROR = "error";
const MANUAL = "manual";
const WEBHOOK = "webhook";
const DEFAULT_STATUS = "default_status";
const SIGNAL = "signal";
export {
MONITOR,
@@ -117,4 +119,8 @@ export {
TIMEOUT,
ERROR,
MANUAL,
NO_DATA,
WEBHOOK,
DEFAULT_STATUS,
SIGNAL,
};
+105 -37
View File
@@ -17,6 +17,8 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import crypto from "crypto";
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
import { UP, DOWN, DEGRADED, NO_DATA, REALTIME, SIGNAL } from "../constants.js";
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC } from "../tool.js";
const saltRounds = 10;
const DUMMY_SECRET = "DUMMY_SECRET";
@@ -105,6 +107,12 @@ const siteDataKeys = [
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "tzToggle",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "barStyle",
//PARTIAL or FULL
@@ -153,6 +161,11 @@ const siteDataKeys = [
isValid: (value) => parseInt(value) >= 1,
data_type: "string",
},
{
key: "incidentGroupView",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
];
export function InsertKeyValue(key, value) {
@@ -199,17 +212,28 @@ export const CreateUpdateMonitor = async (monitor) => {
export const GetMonitors = async (data) => {
return await db.getMonitors(data);
};
export const GetMonitorsParsed = async (data) => {
let monitors = await db.getMonitors(data);
let parsedMonitors = monitors.map((m) => {
let parsedMonitor = { ...m };
if (!!parsedMonitor.type_data) {
parsedMonitor.type_data = JSON.parse(parsedMonitor.type_data);
export const GetMonitorsParsed = async (query) => {
// Retrieve monitors from the database based on the provided query
const rawMonitors = await db.getMonitors(query);
// Parse type_data if available for each monitor
const parsedMonitors = rawMonitors.map((monitor) => {
const monitorData = { ...monitor };
if (monitorData.type_data) {
try {
monitorData.type_data = JSON.parse(monitorData.type_data);
} catch (error) {
// Fallback to an empty object if JSON parsing fails
monitorData.type_data = {};
}
} else {
parsedMonitor.type_data = {};
monitorData.type_data = {};
}
return parsedMonitor;
return monitorData;
});
return parsedMonitors;
};
@@ -275,6 +299,40 @@ export const VerifyPassword = async (plainTextPassword, hashedPassword) => {
}
};
export const GetLatestMonitoringData = async (monitor_tag) => {
return await db.getLatestMonitoringData(monitor_tag);
};
export const GetLastHeartbeat = async (monitor_tag) => {
return await db.getLastHeartbeat(monitor_tag);
};
export const RegisterHeartbeat = async (tag, secret) => {
let monitor = await db.getMonitorByTag(tag);
if (!monitor) {
return null;
}
let typeData = monitor.type_data;
if (!typeData) {
return null;
}
try {
let heartbeatConfig = JSON.parse(typeData);
let heartbeatSecret = heartbeatConfig.secretString;
if (heartbeatSecret === secret) {
return await db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: GetMinuteStartNowTimestampUTC(GetNowTimestampUTC()),
status: UP,
latency: 0,
type: SIGNAL,
});
}
} catch (e) {
console.error("Error registering heartbeat:", e);
}
return null;
};
export const GenerateToken = async (data) => {
try {
const token = jwt.sign(data, process.env.KENER_SECRET_KEY || DUMMY_SECRET, {
@@ -386,31 +444,32 @@ export const HashString = (str) => {
return hash.digest("hex");
};
export const InterpolateData = (data, start, anchorStatus, e) => {
let finalData = [];
let status = anchorStatus || "UP";
let end = start;
if (!!data && data.length > 0) {
end = data[data.length - 1].timestamp;
export const InterpolateData = (rawData, startTimestamp, initialStatus, overrideEndTimestamp) => {
const interpolatedData = [];
let currentStatus = initialStatus || "UP";
let endTimestamp = startTimestamp;
if (rawData && rawData.length > 0) {
endTimestamp = rawData[rawData.length - 1].timestamp;
}
if (overrideEndTimestamp) {
endTimestamp = overrideEndTimestamp;
}
if (e) {
end = e;
}
let dataMap = data.reduce((acc, d) => {
acc[d.timestamp] = d;
return acc;
const dataByTimestamp = rawData.reduce((accumulator, entry) => {
accumulator[entry.timestamp] = entry;
return accumulator;
}, {});
for (let i = start; i <= end; i += 60) {
let nowData = dataMap[i];
if (!!nowData) {
status = nowData.status;
for (let timestamp = startTimestamp; timestamp <= endTimestamp; timestamp += 60) {
const currentEntry = dataByTimestamp[timestamp];
if (currentEntry) {
currentStatus = currentEntry.status;
}
finalData.push({ timestamp: i, status: status });
interpolatedData.push({ timestamp, status: currentStatus });
}
return finalData;
return interpolatedData;
};
export const GetLastStatusBefore = async (monitor_tag, timestamp) => {
@@ -418,7 +477,19 @@ export const GetLastStatusBefore = async (monitor_tag, timestamp) => {
if (data) {
return data.status;
}
return "UP";
return NO_DATA;
};
export const AggregateData = (rawData) => {
//data like [{ timestamp: 1732435920, status: 'NO_DATA' }]
let rawDataWithStatus = rawData.filter((data) => data.status !== NO_DATA);
const total = rawDataWithStatus.length;
const UPs = rawDataWithStatus.filter((data) => data.status === UP).length;
const DOWNs = rawDataWithStatus.filter((data) => data.status === DOWN).length;
const DEGRADEDs = rawDataWithStatus.filter((data) => data.status === DEGRADED).length;
const NO_DATAs = total - (UPs + DOWNs + DEGRADEDs);
return { total, UPs, DOWNs, DEGRADEDs, NO_DATAs };
};
export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, timezoneOffsetMinutes = 0) => {
@@ -431,7 +502,7 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
let rawData = await db.getDataGroupByDayAlternative(monitor_tag, start, end);
let anchorStatus = await GetLastStatusBefore(monitor_tag, start);
rawData = InterpolateData(rawData, start, anchorStatus, rawData.length == 0 ? end - 60 : null);
rawData = InterpolateData(rawData, start, anchorStatus, end);
const groupedData = rawData.reduce((acc, row) => {
// Calculate day group considering timezone offset
@@ -443,16 +514,13 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
UP: 0,
DOWN: 0,
DEGRADED: 0,
latencySum: 0,
latencies: [],
NO_DATA: 0,
};
}
const group = acc[dayGroup];
group.total++;
group[row.status]++;
group.latencySum += row.latency;
group.latencies.push(row.latency);
return acc;
}, {});
@@ -464,9 +532,7 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
UP: group.UP,
DOWN: group.DOWN,
DEGRADED: group.DEGRADED,
avgLatency: group.total > 0 ? Number((group.latencySum / group.total).toFixed(3)) : null,
maxLatency: group.latencies.length > 0 ? Number(Math.max(...group.latencies).toFixed(3)) : null,
minLatency: group.latencies.length > 0 ? Number(Math.min(...group.latencies).toFixed(3)) : null,
NO_DATA: group.NO_DATA,
}));
};
@@ -483,6 +549,7 @@ export const CreateIncident = async (data) => {
end_date_time: !!data.end_date_time ? data.end_date_time : null,
state: !!data.state ? data.state : "INVESTIGATING",
incident_type: !!data.incident_type ? data.incident_type : "INCIDENT",
incident_source: !!data.incident_source ? data.incident_source : "DASHBOARD",
};
//incident_type == INCIDENT delete endDateTime
@@ -497,7 +564,7 @@ export const CreateIncident = async (data) => {
let newIncident = await db.createIncident(incident);
return {
incident_id: newIncident[0],
incident_id: newIncident.id,
};
};
@@ -673,6 +740,7 @@ export const GetIncidentsDashboard = async (data) => {
for (let i = 0; i < incidents.length; i++) {
incidents[i].monitors = await GetIncidentMonitors(incidents[i].id);
incidents[i].isAutoCreated = await db.alertExistsIncident(incidents[i].id);
}
return {
+16 -15
View File
@@ -1,7 +1,7 @@
// @ts-nocheck
import axios from "axios";
import { Ping, ExtractIPv6HostAndPort, TCP } from "./ping.js";
import { UP, DOWN, DEGRADED } from "./constants.js";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL, DEFAULT_STATUS } from "./constants.js";
import Service from "./services/service.js";
import { GetMinuteStartNowTimestampUTC, ReplaceAllOccurrences, GetRequiredSecrets, Wait } from "./tool.js";
@@ -15,11 +15,6 @@ import DNSResolver from "./dns.js";
dotenv.config();
const REALTIME = "realtime";
const TIMEOUT = "timeout";
const ERROR = "error";
const MANUAL = "manual";
const alertingQueue = 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)
@@ -46,12 +41,6 @@ async function manualIncident(monitor) {
for (let i = 0; i < impactArr.length; i++) {
const element = impactArr[i];
let autoIncidents = await db.getActiveAlertIncident(monitor.tag, element.monitor_impact, element.id);
if (!!autoIncidents) {
continue;
}
if (element.monitor_impact === "DOWN") {
impact = "DOWN";
break;
@@ -72,6 +61,7 @@ async function manualIncident(monitor) {
type: MANUAL,
},
};
return manualData;
}
@@ -111,9 +101,16 @@ const Minuter = async (monitor) => {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "GROUP") {
realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
} else if (monitor.monitor_type === "SSL") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "SQL") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "HEARTBEAT") {
realTimeData[startOfMinute] = await serviceClient.execute();
}
manualData = await manualIncident(monitor);
//merge noData, apiData, webhookData, dayData
let mergedData = {};
@@ -122,17 +119,21 @@ const Minuter = async (monitor) => {
mergedData[startOfMinute] = {
status: monitor.default_status,
latency: 0,
type: "default_status",
type: DEFAULT_STATUS,
};
}
}
for (const timestamp in realTimeData) {
mergedData[timestamp] = realTimeData[timestamp];
if (!!realTimeData[timestamp] && !!realTimeData[timestamp].status) {
mergedData[timestamp] = realTimeData[timestamp];
}
}
for (const timestamp in manualData) {
mergedData[timestamp] = manualData[timestamp];
if (!!manualData[timestamp] && !!manualData[timestamp].status) {
mergedData[timestamp] = manualData[timestamp];
}
}
for (const timestamp in mergedData) {
+65 -7
View File
@@ -1,5 +1,6 @@
// @ts-nocheck
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
import { GetMinuteStartNowTimestampUTC, GetDbType } from "../tool.js";
import { MANUAL, SIGNAL } from "../constants.js";
import Knex from "knex";
class DbImpl {
@@ -39,6 +40,15 @@ class DbImpl {
.first();
}
async getLastHeartbeat(monitor_tag) {
return await this.knex("monitoring_data")
.where("monitor_tag", monitor_tag)
.where("type", SIGNAL)
.orderBy("timestamp", "desc")
.limit(1)
.first();
}
//given monitor_tag, start and end timestamp in utc seconds return total degraded, up, down, avg(latency), max(latency), min(latency)
async getAggregatedMonitoringData(monitor_tag, start, end) {
return await this.knex("monitoring_data")
@@ -116,19 +126,20 @@ class DbImpl {
qb.select("*")
.from("monitoring_data")
.where("monitor_tag", monitor_tag)
.andWhere("type", "!=", MANUAL)
.orderBy("timestamp", "desc")
.limit(lastX);
})
.select(
this.knex.raw(
"CASE WHEN COUNT(*) <= SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) THEN 1 ELSE 0 END as isAffected",
"CASE WHEN COUNT(*) <= SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) THEN 1 ELSE 0 END as is_affected",
[status],
),
)
.from("last_records")
.first();
return result.isAffected === 1;
return result.is_affected === 1;
}
//insert alert
@@ -141,6 +152,17 @@ class DbImpl {
});
}
//given incident_number check if there is an alert
async alertExistsIncident(incident_number) {
const result = await this.knex("monitor_alerts")
.count("* as count")
.where({
incident_number,
})
.first();
return result.count > 0;
}
//check if alert exists given monitor_tag, monitor_status, trigger_status
async alertExists(monitor_tag, monitor_status, alert_status) {
const result = await this.knex("monitor_alerts")
@@ -165,6 +187,17 @@ class DbImpl {
.first();
}
//get active alert given incident id, monitor tag, monitor status
async getAllActiveAlertIncidents(monitor_tag) {
return await this.knex("monitor_alerts")
.where({
monitor_tag,
alert_status: "TRIGGERED",
})
.andWhere("incident_number", ">", 0)
.orderBy("id", "desc");
}
//return active alert for a monitor_tag, monitor_status, trigger_status = ACTIVE
async getActiveAlert(monitor_tag, monitor_status, alert_status) {
return await this.knex("monitor_alerts")
@@ -425,7 +458,10 @@ class DbImpl {
}
async createIncident(data) {
return await this.knex("incidents").insert({
const dbType = GetDbType(); // sqlite, postgresql, mysql
// Common insert data
const insertData = {
title: data.title,
start_date_time: data.start_date_time,
end_date_time: data.end_date_time,
@@ -434,7 +470,27 @@ class DbImpl {
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
incident_type: data.incident_type,
});
incident_source: data.incident_source,
};
// PostgreSQL supports returning clause
if (dbType === "postgresql") {
const [incident] = await this.knex("incidents").insert(insertData).returning("*");
return incident;
}
// MySQL and SQLite need different approaches
else {
// Insert and get the ID
const result = await this.knex("incidents").insert(insertData);
// Different handling for MySQL vs SQLite
let id = result[0];
// Fetch the newly created record
const incident = await this.knex("incidents").where("id", id).first();
return incident;
}
}
//get incidents paginated
@@ -593,7 +649,8 @@ class DbImpl {
.andWhere("i.start_date_time", "<=", timestamp)
.andWhere("i.status", "OPEN")
.andWhere("i.incident_type", "INCIDENT")
.andWhere("i.state", "!=", "RESOLVED");
.andWhere("i.state", "!=", "RESOLVED")
.andWhere("i.incident_source", "!=", "ALERT");
}
//get maintenance incidents by monitor tag
@@ -611,7 +668,8 @@ class DbImpl {
.andWhere("i.end_date_time", ">=", timestamp)
.andWhere("i.status", "OPEN")
.andWhere("i.incident_type", "MAINTENANCE")
.andWhere("i.state", "=", "RESOLVED");
.andWhere("i.state", "=", "RESOLVED")
.andWhere("i.incident_source", "!=", "ALERT");
}
//given array of ids get incidents
+1
View File
@@ -72,6 +72,7 @@ const seedSiteData = {
],
theme: "none",
themeToggle: "YES",
tzToggle: "YES",
barStyle: "PARTIAL",
barRoundness: "SHARP",
summaryStyle: "CURRENT",
+89 -83
View File
@@ -1,93 +1,99 @@
// @ts-nocheck
class Discord {
url;
headers;
method;
siteData;
monitorData;
url;
headers;
method;
siteData;
monitorData;
constructor(url, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener"
};
constructor(url, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener",
};
this.url = url;
this.headers = Object.assign(kenerHeader, {});
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
}
this.url = url;
this.headers = Object.assign(kenerHeader, {});
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
}
transformData(data) {
let siteURL = this.siteData.siteURL;
let logo =
this.siteData.siteURL +
(!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "") +
this.siteData.logo;
transformData(data) {
let siteURL = this.siteData.siteURL;
let color = 13250616; //down;
if (data.severity === "warning") {
color = 15125089;
}
if (data.status === "RESOLVED") {
color = 5156244;
}
return {
username: this.siteData.siteName,
avatar_url: logo,
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
embeds: [
{
title: data.alert_name,
description: data.description,
url: data.actions[0].url,
color: color,
fields: [
{
name: "Monitor",
value: data.details.metric,
inline: false
},
{
name: "Alert ID",
value: data.id,
inline: false
},
{
name: "Current Value",
value: data.details.current_value,
inline: true
},
{
name: "Threshold",
value: data.details.threshold,
inline: true
}
],
footer: {
text: "Kener",
icon_url: logo
},
timestamp: data.timestamp
}
]
};
}
//remove / if there at the end of the url
if (siteURL.endsWith("/")) {
siteURL = siteURL.slice(0, -1);
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.transformData(data))
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
if (!this.siteData.logo.startsWith("http"))
this.siteData.logo = `${siteURL}${!!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : ""}${this.siteData.logo}`;
let logo = this.siteData.logo;
let color = 13250616; //down;
if (data.severity === "warning") {
color = 15125089;
}
if (data.status === "RESOLVED") {
color = 5156244;
}
return {
username: this.siteData.siteName,
avatar_url: logo,
content: `## ${data.alert_name}\n${data.status === "TRIGGERED" ? "🔴 Triggered" : "🟢 Resolved"}\n${data.description}\nClick [here](${data.actions[0].url}) for more.`,
embeds: [
{
title: data.alert_name,
description: data.description,
url: data.actions[0].url,
color: color,
fields: [
{
name: "Monitor",
value: data.details.metric,
inline: false,
},
{
name: "Alert ID",
value: data.id,
inline: false,
},
{
name: "Current Value",
value: data.details.current_value,
inline: true,
},
{
name: "Threshold",
value: data.details.threshold,
inline: true,
},
],
footer: {
text: "Kener",
icon_url: logo,
},
timestamp: data.timestamp,
},
],
};
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.transformData(data)),
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
}
export default Discord;
+25 -16
View File
@@ -3,24 +3,33 @@ import nodemailer from "nodemailer";
import { HashString } from "../controllers/controller.js";
const transports = {};
export default function getSMTPTransport(meta) {
//convert meta to string and generate has id
//convert meta to string and generate has id
let transportId = "smtp_" + HashString(JSON.stringify(meta));
let transportId = "smtp_" + HashString(JSON.stringify(meta));
if (!!transports[transportId]) {
return transports[transportId];
}
if (!!transports[transportId]) {
return transports[transportId];
}
const transporter = nodemailer.createTransport({
host: meta.smtp_host,
port: Number(meta.smtp_port) || 587,
secure: meta.smtp_secure, // true for 465, false for other ports
auth: {
user: meta.smtp_user,
pass: meta.smtp_pass
}
});
transports[transportId] = transporter;
let options = {
host: meta.smtp_host,
port: Number(meta.smtp_port) || 587,
secure: meta.smtp_secure, // true for 465, false for other ports
auth: {
user: meta.smtp_user,
pass: meta.smtp_pass,
},
tls: {
rejectUnauthorized: !!meta.smtp_secure, // false if smtp_secure is 0
},
};
return transporter;
if (meta.smtp_user === "-" || meta.smtp_pass === "-") {
delete options.auth;
}
const transporter = nodemailer.createTransport(options);
transports[transportId] = transporter;
return transporter;
}
+77 -79
View File
@@ -3,92 +3,90 @@
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
class Webhook {
url;
headers;
method;
siteData;
monitorData;
trigger_meta;
url;
headers;
method;
siteData;
monitorData;
trigger_meta;
constructor(trigger_meta, method, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener/3.0.0"
};
let headers = trigger_meta.headers;
this.trigger_meta = trigger_meta;
this.url = trigger_meta.url;
this.headers = kenerHeader;
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
this.headers[header.key] = header.value;
}
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
constructor(trigger_meta, method, siteData, monitorData) {
const kenerHeader = {
"Content-Type": "application/json",
"User-Agent": "Kener/3.2.3",
};
let headers = trigger_meta.headers;
this.trigger_meta = trigger_meta;
this.url = trigger_meta.url;
this.headers = kenerHeader;
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
this.headers[header.key] = header.value;
}
this.method = "POST";
this.siteData = siteData;
this.monitorData = monitorData;
let envSecrets = GetRequiredSecrets(
`${this.url} ${JSON.stringify(this.headers)} ${JSON.stringify(this.trigger_meta.webhook_body)}`
);
//replace secrets in url and headers
let envSecrets = GetRequiredSecrets(
`${this.url} ${JSON.stringify(this.headers)} ${JSON.stringify(this.trigger_meta.webhook_body)}`,
);
//replace secrets in url and headers
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.url = ReplaceAllOccurrences(this.url, secret.find, secret.replace);
this.headers = JSON.parse(
ReplaceAllOccurrences(JSON.stringify(this.headers), secret.find, secret.replace)
);
}
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.url = ReplaceAllOccurrences(this.url, secret.find, secret.replace);
this.headers = JSON.parse(ReplaceAllOccurrences(JSON.stringify(this.headers), secret.find, secret.replace));
}
if (!!this.trigger_meta.has_webhook_body && !!this.trigger_meta.webhook_body) {
envSecrets = GetRequiredSecrets(this.trigger_meta.webhook_body);
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.trigger_meta.webhook_body = ReplaceAllOccurrences(
this.trigger_meta.webhook_body,
secret.find,
secret.replace
);
}
}
}
if (!!this.trigger_meta.has_webhook_body && !!this.trigger_meta.webhook_body) {
envSecrets = GetRequiredSecrets(this.trigger_meta.webhook_body);
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
this.trigger_meta.webhook_body = ReplaceAllOccurrences(
this.trigger_meta.webhook_body,
secret.find,
secret.replace,
);
}
}
}
transformData(data) {
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
let body = this.trigger_meta.webhook_body;
body = ReplaceAllOccurrences(body, "${id}", data.id);
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
body = ReplaceAllOccurrences(body, "${severity}", data.severity);
body = ReplaceAllOccurrences(body, "${status}", data.status);
body = ReplaceAllOccurrences(body, "${source}", data.source);
body = ReplaceAllOccurrences(body, "${timestamp}", data.timestamp);
body = ReplaceAllOccurrences(body, "${description}", data.description);
body = ReplaceAllOccurrences(body, "${metric}", data.details.metric);
body = ReplaceAllOccurrences(body, "${current_value}", data.details.current_value);
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
return body;
}
transformData(data) {
if (!!!this.trigger_meta.has_webhook_body) return JSON.stringify(data);
if (!!!this.trigger_meta.webhook_body) return JSON.stringify(data);
let body = this.trigger_meta.webhook_body;
body = ReplaceAllOccurrences(body, "${id}", data.id);
body = ReplaceAllOccurrences(body, "${alert_name}", data.alert_name);
body = ReplaceAllOccurrences(body, "${severity}", data.severity);
body = ReplaceAllOccurrences(body, "${status}", data.status);
body = ReplaceAllOccurrences(body, "${source}", data.source);
body = ReplaceAllOccurrences(body, "${timestamp}", data.timestamp);
body = ReplaceAllOccurrences(body, "${description}", data.description);
body = ReplaceAllOccurrences(body, "${metric}", data.details.metric);
body = ReplaceAllOccurrences(body, "${current_value}", data.details.current_value);
body = ReplaceAllOccurrences(body, "${threshold}", data.details.threshold);
body = ReplaceAllOccurrences(body, "${action_text}", data.actions[0].text);
body = ReplaceAllOccurrences(body, "${action_url}", data.actions[0].url);
return body;
}
type() {
return "webhook";
}
type() {
return "webhook";
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.transformData(data)
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
async send(data) {
try {
const response = await fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.transformData(data),
});
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
}
}
}
export default Webhook;
+161 -171
View File
@@ -2,211 +2,201 @@
// @ts-ignore
import db from "$lib/server/db/db.js";
import {
GetMinuteStartNowTimestampUTC,
BeginningOfDay,
StatusObj,
ParseUptime,
GetDayStartTimestampUTC
GetMinuteStartNowTimestampUTC,
BeginningOfDay,
StatusObj,
ParseUptime,
GetDayStartTimestampUTC,
BeginningOfMinute,
} from "$lib/server/tool.js";
import { formatDuration, intervalToDuration } from "date-fns";
import { fdm, l, summaryTime } from "$lib/i18n/client";
import {
GetDataGroupByDayAlternative,
InterpolateData,
GetLastStatusBefore
GetDataGroupByDayAlternative,
InterpolateData,
GetLastStatusBefore,
} from "$lib/server/controllers/controller.js";
import { UP, DOWN, DEGRADED, NO_DATA } from "$lib/server/constants.js";
function getSummaryDuration(numOfMinute, selectedLang) {
// Convert minutes to milliseconds and create duration object
const duration = intervalToDuration({
start: new Date(0),
end: new Date(numOfMinute * 60 * 1000)
});
// Convert minutes to milliseconds and create duration object
const duration = intervalToDuration({
start: new Date(0),
end: new Date(numOfMinute * 60 * 1000),
});
return fdm(duration, selectedLang);
return fdm(duration, selectedLang);
}
function getTimezoneOffset(timeZone) {
const formatter = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "short" });
const parts = formatter.formatToParts(new Date());
const timeZoneOffset = parts.find((part) => part.type === "timeZoneName")?.value;
const formatter = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "short" });
const parts = formatter.formatToParts(new Date());
const timeZoneOffset = parts.find((part) => part.type === "timeZoneName")?.value;
const match = timeZoneOffset.match(/([+-]\d{2}):?(\d{2})/);
if (!match) return 0;
const match = timeZoneOffset.match(/([+-]\d{2}):?(\d{2})/);
if (!match) return 0;
const [, hours, minutes] = match;
return parseInt(hours) * 60 + parseInt(minutes);
const [, hours, minutes] = match;
return parseInt(hours) * 60 + parseInt(minutes);
}
function returnStatusClass(val, total, c, barStyle) {
// return "api-degraded-10";
if (barStyle === undefined || barStyle == "FULL") {
return c;
} else if (barStyle == "PARTIAL") {
let totalHeight = total;
let cl = `api-up`;
if (val > 0 && val <= 0.1 * totalHeight) {
cl = c + "-10";
} else if (val > 0.1 * totalHeight && val <= 0.2 * totalHeight) {
cl = c + "-20";
} else if (val > 0.2 * totalHeight && val <= 0.4 * totalHeight) {
cl = c + "-40";
} else if (val > 0.4 * totalHeight && val <= 0.6 * totalHeight) {
cl = c + "-60";
} else if (val > 0.6 * totalHeight && val <= 0.8 * totalHeight) {
cl = c + "-80";
} else if (val > 0.8 * totalHeight && val < totalHeight) {
cl = c + "-90";
} else if (val == totalHeight) {
cl = c;
}
return cl;
}
return c;
// return "api-degraded-10";
if (barStyle === undefined || barStyle == "FULL") {
return c;
} else if (barStyle == "PARTIAL") {
let totalHeight = total;
let cl = `api-up`;
if (val > 0 && val <= 0.1 * totalHeight) {
cl = c + "-10";
} else if (val > 0.1 * totalHeight && val <= 0.2 * totalHeight) {
cl = c + "-20";
} else if (val > 0.2 * totalHeight && val <= 0.4 * totalHeight) {
cl = c + "-40";
} else if (val > 0.4 * totalHeight && val <= 0.6 * totalHeight) {
cl = c + "-60";
} else if (val > 0.6 * totalHeight && val <= 0.8 * totalHeight) {
cl = c + "-80";
} else if (val > 0.8 * totalHeight && val < totalHeight) {
cl = c + "-90";
} else if (val == totalHeight) {
cl = c;
}
return cl;
}
return c;
}
function getCountOfSimilarStatuesEnd(arr, statusType) {
let count = 0;
for (let i = arr.length - 1; i >= 0; i--) {
if (arr[i].status === statusType) {
count++;
} else {
break;
}
}
return count;
let count = 0;
for (let i = arr.length - 1; i >= 0; i--) {
if (arr[i].status === statusType) {
count++;
} else {
break;
}
}
return count;
}
const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
const secondsInDay = 24 * 60 * 60;
//get offset from utc in minutes
const now = GetMinuteStartNowTimestampUTC() + 60;
const midnight = BeginningOfDay({ timeZone: localTz });
const midnight90DaysAgo = midnight - 90 * 24 * 60 * 60;
const NO_DATA = "No Data";
const midnightTomorrow = midnight + secondsInDay;
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(now) - midnight) / 60);
const _90Day = {};
let latestTimestamp = 0;
const secondsInDay = 24 * 60 * 60;
//get offset from utc in minutes
const nowUTC = GetMinuteStartNowTimestampUTC();
const midnightUTC = GetDayStartTimestampUTC(nowUTC);
const midnightTz = BeginningOfDay({ timeZone: localTz });
const midnight90DaysAgoTz = midnightTz - 90 * 24 * 60 * 60;
const NO_DATA = "No Data";
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(nowUTC) - midnightTz) / 60);
const maxDateTodayTimestampTz = BeginningOfMinute({ timeZone: localTz });
const _90Day = {};
let latestTimestamp = 0;
let dbData = await GetDataGroupByDayAlternative(
monitor.tag,
midnight90DaysAgo,
midnightTomorrow,
offsetInMinutes
);
let totalDegradedCount = 0;
let totalDownCount = 0;
let totalUpCount = 0;
let dbData = await GetDataGroupByDayAlternative(
monitor.tag,
midnight90DaysAgoTz,
maxDateTodayTimestampTz,
offsetInMinutes,
);
let totalDegradedCount = 0;
let totalDownCount = 0;
let totalUpCount = 0;
for (let i = 0; i < dbData.length; i++) {
let dayData = dbData[i];
let ts = dayData.timestamp;
let cssClass = StatusObj.UP;
let summaryDuration = 0;
let summaryStatus = "UP";
latestTimestamp = ts;
totalDegradedCount += dayData.DEGRADED;
totalDownCount += dayData.DOWN;
totalUpCount += dayData.UP;
for (let i = 0; i < dbData.length; i++) {
let dayData = dbData[i];
let ts = dayData.timestamp;
let cssClass = StatusObj.UP;
let summaryDuration = 0;
let summaryStatus = UP;
latestTimestamp = ts;
totalDegradedCount += dayData.DEGRADED;
totalDownCount += dayData.DOWN;
totalUpCount += dayData.UP;
if (dayData.DEGRADED >= monitor.day_degraded_minimum_count) {
cssClass = returnStatusClass(dayData.DEGRADED, dayData.total, StatusObj.DEGRADED, site.barStyle);
summaryDuration = getSummaryDuration(dayData.DEGRADED, selectedLang);
summaryStatus = "DEGRADED";
}
if (dayData.DOWN >= monitor.day_down_minimum_count) {
cssClass = returnStatusClass(dayData.DOWN, dayData.total, StatusObj.DOWN, site.barStyle);
if (dayData.DEGRADED >= monitor.day_degraded_minimum_count) {
cssClass = returnStatusClass(
dayData.DEGRADED,
dayData.total,
StatusObj.DEGRADED,
site.barStyle
);
summaryDuration = getSummaryDuration(dayData.DEGRADED, selectedLang);
summaryStatus = "DEGRADED";
}
if (dayData.DOWN >= monitor.day_down_minimum_count) {
cssClass = returnStatusClass(
dayData.DOWN,
dayData.total,
StatusObj.DOWN,
site.barStyle
);
summaryDuration = getSummaryDuration(dayData.DOWN, selectedLang);
summaryStatus = "DOWN";
}
if (dayData.NO_DATA === dayData.total) {
cssClass = StatusObj.NO_DATA;
summaryStatus = NO_DATA;
}
summaryDuration = getSummaryDuration(dayData.DOWN, selectedLang);
summaryStatus = "DOWN";
}
_90Day[ts] = {
border: true,
};
_90Day[ts] = {
border: true
};
_90Day[ts].timestamp = ts;
_90Day[ts].cssClass = cssClass;
_90Day[ts].summaryStatus = l(lang, summaryTime(summaryStatus), {
status: l(lang, summaryStatus),
duration: summaryDuration,
});
_90Day[ts].timestamp = ts;
_90Day[ts].cssClass = cssClass;
_90Day[ts].summaryStatus = l(lang, summaryTime(summaryStatus), {
status: l(lang, summaryStatus),
duration: summaryDuration
});
_90Day[ts].textClass = cssClass.replace(/-\d+$/, "");
}
_90Day[ts].textClass = cssClass.replace(/-\d+$/, "");
}
let uptime90DayNumerator = totalUpCount + totalDegradedCount;
let uptime90DayDenominator = totalUpCount + totalDownCount + totalDegradedCount;
let uptime90DayNumerator = totalUpCount + totalDegradedCount;
let uptime90DayDenominator = totalUpCount + totalDownCount + totalDegradedCount;
//remove degraded from uptime
if (monitor.include_degraded_in_downtime === "YES") {
uptime90DayNumerator = totalUpCount;
}
// return _90Day;
let uptime90Day = ParseUptime(uptime90DayNumerator, uptime90DayDenominator);
//remove degraded from uptime
if (monitor.include_degraded_in_downtime === "YES") {
uptime90DayNumerator = totalUpCount;
}
// return _90Day;
let uptime90Day = ParseUptime(uptime90DayNumerator, uptime90DayDenominator);
let summaryDuration = 0;
let summaryStatus = "UP";
let summaryDuration = 0;
let summaryStatus = "UP";
let summaryColorClass = "api-nodata";
let summaryColorClass = "api-nodata";
let todayDataDb = await db.getMonitoringData(monitor.tag, midnight, midnight + secondsInDay);
let anchorStatus = await GetLastStatusBefore(monitor.tag, midnight);
todayDataDb = InterpolateData(
todayDataDb,
midnight,
anchorStatus,
todayDataDb.length == 0 ? midnight + secondsInDay : null
);
let todayDataDb = await db.getMonitoringData(monitor.tag, midnightTz, maxDateTodayTimestampTz);
let anchorStatus = await GetLastStatusBefore(monitor.tag, midnightTz);
todayDataDb = InterpolateData(todayDataDb, midnightTz, anchorStatus, maxDateTodayTimestampTz);
if (site.summaryStyle === "CURRENT") {
summaryColorClass = "api-up";
if (site.summaryStyle === "CURRENT") {
summaryColorClass = "api-up";
let lastRow = todayDataDb[todayDataDb.length - 1];
let lastRow = todayDataDb[todayDataDb.length - 1];
if (!!lastRow && lastRow.status == "DEGRADED") {
summaryDuration = getSummaryDuration(
getCountOfSimilarStatuesEnd(todayDataDb, "DEGRADED"),
selectedLang
);
summaryStatus = "DEGRADED";
summaryColorClass = "api-degraded";
}
if (!!lastRow && lastRow.status == "DOWN") {
summaryDuration = getSummaryDuration(
getCountOfSimilarStatuesEnd(todayDataDb, "DOWN"),
selectedLang
);
summaryStatus = "DOWN";
summaryColorClass = "api-down";
}
summaryStatus = l(lang, summaryTime(summaryStatus), {
status: l(lang, summaryStatus),
duration: summaryDuration
});
} else {
let lastData = _90Day[latestTimestamp];
summaryColorClass = lastData.cssClass.replace(/-\d+$/, "");
summaryDuration = lastData.summaryDuration;
summaryStatus = lastData.summaryStatus;
}
return {
_90Day: _90Day,
uptime90Day: uptime90Day,
summaryStatus: summaryStatus,
summaryColorClass: summaryColorClass,
barRoundness: site.barRoundness,
midnight90DaysAgo: midnight90DaysAgo,
midnightTomorrow: midnightTomorrow
};
if (!!lastRow && lastRow.status == "DEGRADED") {
summaryDuration = getSummaryDuration(getCountOfSimilarStatuesEnd(todayDataDb, "DEGRADED"), selectedLang);
summaryStatus = "DEGRADED";
summaryColorClass = "api-degraded";
}
if (!!lastRow && lastRow.status == "DOWN") {
summaryDuration = getSummaryDuration(getCountOfSimilarStatuesEnd(todayDataDb, "DOWN"), selectedLang);
summaryStatus = "DOWN";
summaryColorClass = "api-down";
}
if (lastRow.status === "NO_DATA") {
summaryStatus = NO_DATA;
summaryColorClass = "api-nodata";
}
summaryStatus = l(lang, summaryTime(summaryStatus), {
status: l(lang, summaryStatus),
duration: summaryDuration,
});
} else {
let lastData = _90Day[latestTimestamp];
summaryColorClass = lastData.cssClass.replace(/-\d+$/, "");
summaryDuration = lastData.summaryDuration;
summaryStatus = lastData.summaryStatus;
}
return {
_90Day: _90Day,
uptime90Day: uptime90Day,
summaryStatus: summaryStatus,
summaryColorClass: summaryColorClass,
barRoundness: site.barRoundness,
midnight90DaysAgo: midnight90DaysAgoTz,
maxDateTodayTimestamp: maxDateTodayTimestampTz,
};
};
export { FetchData };
+13 -5
View File
@@ -2,8 +2,9 @@
import axios from "axios";
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import * as cheerio from "cheerio";
const defaultEval = `(async function (statusCode, responseTime, responseData) {
const defaultEval = `(async function (statusCode, responseTime, responseRaw, modules) {
let statusCodeShort = Math.floor(statusCode/100);
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
return {
@@ -45,7 +46,8 @@ class ApiCall {
let method = this.monitor.type_data.method;
let timeout = this.monitor.type_data.timeout || 5000;
let tag = this.monitor.tag;
let monitorEval = !!this.monitor.type_data.monitorEval ? this.monitor.type_data.monitorEval : defaultEval;
let monitorEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : defaultEval;
for (let i = 0; i < this.envSecrets.length; i++) {
const secret = this.envSecrets[i];
@@ -113,12 +115,18 @@ class ApiCall {
}
}
resp = Buffer.from(resp).toString("base64");
let evalResp = undefined;
let modules = { cheerio };
try {
evalResp = await eval(monitorEval + `(${statusCode}, ${latency}, "${resp}")`);
const evalFunction = new Function(
"statusCode",
"responseTime",
"responseRaw",
"modules",
`return (${monitorEval})(statusCode, responseTime, responseRaw, modules);`,
);
evalResp = await evalFunction(statusCode, latency, resp, modules);
} catch (error) {
console.log(`Error in monitorEval for ${tag}`, error.message);
}
+44
View File
@@ -0,0 +1,44 @@
// @ts-nocheck
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
import { GetLastHeartbeat } from "../controllers/controller.js";
class HeartbeatCall {
monitor;
constructor(monitor) {
this.monitor = monitor;
}
async execute() {
let nowMinute = GetMinuteStartNowTimestampUTC();
let latestData = await GetLastHeartbeat(this.monitor.tag);
if (!latestData) {
return {};
}
let latency = nowMinute - latestData.timestamp;
let downRemainingMinutes = Number(this.monitor.type_data.downRemainingMinutes);
if (latency > downRemainingMinutes * 60) {
return {
status: DOWN,
latency: latency,
type: REALTIME,
};
}
let degradedRemainingMinutes = Number(this.monitor.type_data.degradedRemainingMinutes);
if (latency > degradedRemainingMinutes * 60) {
return {
status: DEGRADED,
latency: latency,
type: REALTIME,
};
}
return {
status: UP,
latency: latency,
type: REALTIME,
};
}
}
export default HeartbeatCall;
+11 -21
View File
@@ -1,18 +1,9 @@
// @ts-nocheck
import axios from "axios";
import { Ping } from "../ping.js";
import {
UP,
DOWN,
DEGRADED,
REALTIME,
TIMEOUT,
ERROR,
MANUAL,
} from "../constants.js";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
const defaultPingEval = `(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
const defaultPingEval = `(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
@@ -36,9 +27,7 @@ class PingCall {
async execute() {
let hosts = this.monitor.type_data.hosts;
let pingEval = !!this.monitor.type_data.pingEval
? this.monitor.type_data.pingEval
: defaultPingEval;
let pingEval = !!this.monitor.type_data.pingEval ? this.monitor.type_data.pingEval : defaultPingEval;
let tag = this.monitor.tag;
if (hosts === undefined) {
console.log(
@@ -54,20 +43,21 @@ class PingCall {
let arrayOfPings = [];
for (let i = 0; i < hosts.length; i++) {
const host = hosts[i];
arrayOfPings.push(
await Ping(host.type, host.host, host.timeout, host.count),
);
arrayOfPings.push(await Ping(host.type, host.host, host.timeout, host.count));
}
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString(
"base64",
);
let evalResp = undefined;
try {
evalResp = await eval(pingEval + `("${respBase64}")`);
const evalFunction = new Function("arrayOfPings", `return (${pingEval})(arrayOfPings);`);
evalResp = await evalFunction(arrayOfPings);
} catch (error) {
console.log(`Error in pingEval for ${tag}`, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
//reduce to get the status
return {
+9
View File
@@ -4,6 +4,9 @@ import PingCall from "./pingCall.js";
import TcpCall from "./tcpCall.js";
import DnsCall from "./dnsCall.js";
import GroupCall from "./groupCall.js";
import SSLCall from "./sslCall.js";
import SqlCall from "./sqlCall.js";
import HeartbeatCall from "./heartbeatCall.js";
class Service {
service;
@@ -21,6 +24,12 @@ class Service {
this.service = null;
} else if (monitor.monitor_type === "GROUP") {
this.service = new GroupCall(monitor);
} else if (monitor.monitor_type === "SSL") {
this.service = new SSLCall(monitor);
} else if (monitor.monitor_type === "SQL") {
this.service = new SqlCall(monitor);
} else if (monitor.monitor_type === "HEARTBEAT") {
this.service = new HeartbeatCall(monitor);
} else {
console.log("Invalid monitor.monitor_type ", monitor.monitor_type);
process.exit(1);
+89
View File
@@ -0,0 +1,89 @@
// @ts-nocheck
import Knex from "knex";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import { GetRequiredSecrets, ReplaceAllOccurrences } from "../tool.js";
class SqlCall {
monitor;
envSecrets;
constructor(monitor) {
this.monitor = monitor;
this.envSecrets = GetRequiredSecrets(`${monitor.type_data.connectionString}`);
}
async execute() {
let client = this.monitor.type_data.dbType;
let connection = this.monitor.type_data.connectionString;
for (let i = 0; i < this.envSecrets.length; i++) {
const secret = this.envSecrets[i];
connection = ReplaceAllOccurrences(connection, secret.find, secret.replace);
}
let query = this.monitor.type_data.query;
let timeout = this.monitor.type_data.timeout || 5000;
const startTime = Date.now();
let knexInstance = null;
try {
// Create database configuration
const config = {
client: client,
connection: connection,
pool: { min: 0, max: 1 },
acquireConnectionTimeout: timeout,
};
// Initialize knex
knexInstance = Knex(config);
// Set up a timeout for the query execution
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Query timeout")), timeout);
});
// Execute query
const queryPromise = knexInstance.raw(query);
// Race the promises to handle timeouts
await Promise.race([queryPromise, timeoutPromise]);
// Calculate latency
const latency = Date.now() - startTime;
return {
status: UP,
latency: latency,
type: REALTIME,
};
} catch (error) {
const latency = Date.now() - startTime;
// Handle timeout specifically
if (error.message === "Query timeout") {
return {
status: DOWN,
latency: latency,
type: TIMEOUT,
};
}
// Handle other connection or query errors
return {
status: DOWN,
latency: latency,
type: ERROR,
};
} finally {
// Destroy knex instance to clean up connections
if (knexInstance) {
try {
await knexInstance.destroy();
} catch (destroyError) {
console.error("Error closing database connection:", destroyError);
}
}
}
}
}
export default SqlCall;
+74
View File
@@ -0,0 +1,74 @@
// @ts-nocheck
import tls from "tls";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
const getSSLExpiry = (domain, port = 443) => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const socket = tls.connect(port, domain, { servername: domain }, () => {
const { valid_to } = socket.getPeerCertificate();
if (!valid_to) {
reject(new Error("No certificate found."));
return;
}
const latency = Date.now() - startTime;
const timeTillExpiryMs = new Date(valid_to) - new Date();
resolve({
domain,
expiryDate: new Date(valid_to),
timeTillExpiryMs,
latency,
});
socket.end();
});
socket.on("error", (err) => reject(err));
});
};
class SSLCall {
monitor;
constructor(monitor) {
this.monitor = monitor;
}
async execute() {
let domain = this.monitor.type_data.host;
let port = parseInt(this.monitor.type_data.port || "443");
let degradedRemainingHours = Number(this.monitor.type_data.degradedRemainingHours);
let downRemainingHours = Number(this.monitor.type_data.downRemainingHours);
try {
const { timeTillExpiryMs, latency } = await getSSLExpiry(domain, port);
const timeTillExpiryHours = timeTillExpiryMs / 1000 / 60 / 60;
if (timeTillExpiryHours > degradedRemainingHours) {
return {
status: UP,
latency: latency,
type: REALTIME,
};
} else if (timeTillExpiryHours > downRemainingHours) {
return {
status: DEGRADED,
latency: latency,
type: REALTIME,
};
} else {
return {
status: DOWN,
latency: latency,
type: REALTIME,
};
}
} catch (error) {
console.log(`Error checking SSL for ${domain}:`, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
}
}
export default SSLCall;
+8 -4
View File
@@ -3,8 +3,7 @@ import axios from "axios";
import { TCP } from "../ping.js";
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
const defaultTcpEval = `(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
const defaultTcpEval = `(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
@@ -51,14 +50,19 @@ class TcpCall {
const host = hosts[i];
arrayOfPings.push(await TCP(host.type, host.host, host.port, host.timeout));
}
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString("base64");
let evalResp = undefined;
try {
evalResp = await eval(tcpEval + `("${respBase64}")`);
const evalFunction = new Function("arrayOfPings", `return (${tcpEval})(arrayOfPings);`);
evalResp = await evalFunction(arrayOfPings);
} catch (error) {
console.log(`Error in tcpEval for ${tag}`, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
//reduce to get the status
return {
+47 -28
View File
@@ -1,46 +1,58 @@
// @ts-nocheck
import figlet from "figlet";
import { Cron } from "croner";
import { Minuter } from "./cron-minute.js";
import db from "./db/db.js";
import { GetAllSiteData, GetMonitorsParsed, HashString } from "./controllers/controller.js";
import { fileURLToPath } from "url";
import { dirname } from "path";
import { dirname, resolve } from "path";
import fs from "fs";
const jobs = [];
process.env.TZ = "UTC";
let isStartUP = true;
const jobSchedule = async () => {
const activeMonitors = (await GetMonitorsParsed({ status: "ACTIVE" })).map((m) => {
return {
...m,
hash: "Tag: " + m.tag + " Hash:" + HashString(JSON.stringify(m)),
};
});
// Get the version from package.json
const getVersion = () => {
try {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packagePath = resolve(__dirname, "../../../package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
return packageJson.version;
} catch (error) {
console.error("Error reading version:", error);
return "unknown";
}
};
//stop and remove jobs not present in activeMonitors
const scheduleCronJobs = async () => {
// Fetch and map all active monitors, creating a unique hash for each
const activeMonitors = (await GetMonitorsParsed({ status: "ACTIVE" })).map((monitor) => ({
...monitor,
hash: "Tag: " + monitor.tag + " Hash:" + HashString(JSON.stringify(monitor)),
}));
// Remove any cron jobs that are no longer in activeMonitors
for (let i = 0; i < jobs.length; i++) {
const job = jobs[i];
const monitor = activeMonitors.find((m) => m.hash === job.name);
if (!monitor) {
job.stop();
const existingJob = jobs[i];
const matchingMonitor = activeMonitors.find((m) => m.hash === existingJob.name);
if (!matchingMonitor) {
existingJob.stop();
jobs.splice(i, 1);
console.log("REMOVING JOB WITH NAME " + existingJob.name);
i--;
console.log("REMOVING JOB WITH NAME " + job.name);
}
}
//add new jobs from activeMonitors if not present already in jobs
for (let i = 0; i < activeMonitors.length; i++) {
const monitor = activeMonitors[i];
const job = jobs.find((j) => j.name === monitor.hash);
if (!job) {
const j = Cron(
// Create new cron jobs for monitors that don't already have one
for (const monitor of activeMonitors) {
const existingJob = jobs.find((j) => j.name === monitor.hash);
if (!existingJob) {
const newJob = Cron(
monitor.cron,
async () => {
async (job) => {
await Minuter(monitor);
},
{
@@ -48,18 +60,22 @@ const jobSchedule = async () => {
name: monitor.hash,
},
);
console.log("ADDING NEW JOB WITH NAME " + j.name);
j.trigger();
jobs.push(j);
console.log("ADDING NEW JOB WITH NAME " + newJob.name);
if (isStartUP) {
newJob.trigger();
}
jobs.push(newJob);
}
}
isStartUP = false;
};
async function Startup() {
const mainJob = Cron(
"* * * * *",
async () => {
await jobSchedule();
await scheduleCronJobs();
},
{
protect: true,
@@ -69,12 +85,15 @@ async function Startup() {
mainJob.trigger();
figlet("Kener is UP!", function (err, data) {
const version = getVersion();
figlet("Kener v" + version, function (err, data) {
if (err) {
console.log("Something went wrong...");
return;
}
console.log(data);
console.log(`Kener version ${version} is running!`);
});
}
+236 -243
View File
@@ -1,325 +1,318 @@
// @ts-nocheck
import { AllRecordTypes } from "./constants.js";
import knexOb from "../../../knexfile.js";
import dotenv from "dotenv";
dotenv.config();
const IsValidURL = function (url) {
return /^(http|https):\/\/[^ "]+$/.test(url);
return /^(http|https):\/\/[^ "]+$/.test(url);
};
const IsStringURLSafe = function (str) {
const regex = /^[A-Za-z0-9\-_.~]+$/;
return regex.test(str);
const regex = /^[A-Za-z0-9\-_.~]+$/;
return regex.test(str);
};
const IsValidHTTPMethod = function (method) {
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
};
//return given timestamp in UTC
const GetNowTimestampUTC = function () {
//use js date instead of moment
const now = new Date();
const timestamp = now.getTime();
return Math.floor(timestamp / 1000);
//use js date instead of moment
const now = new Date();
const timestamp = now.getTime();
return Math.floor(timestamp / 1000);
};
//return given timestamp minute start timestamp in UTC
const GetMinuteStartTimestampUTC = function (timestamp) {
//use js date instead of moment
const now = new Date(timestamp * 1000);
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
//use js date instead of moment
const now = new Date(timestamp * 1000);
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0,
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
};
//return current timestamp minute start timestamp in UTC
const GetMinuteStartNowTimestampUTC = function () {
//use js date instead of moment
const now = new Date();
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
//use js date instead of moment
const now = new Date();
const minuteStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
0,
0,
);
const minuteStartTimestamp = minuteStart.getTime();
return Math.floor(minuteStartTimestamp / 1000);
};
//return given timestamp day start timestamp in UTC
const GetDayStartTimestampUTC = function (timestamp) {
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayStart = new Date(
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0)
);
const dayStartTimestamp = dayStart.getTime();
return Math.floor(dayStartTimestamp / 1000);
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayStart = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0));
const dayStartTimestamp = dayStart.getTime();
return Math.floor(dayStartTimestamp / 1000);
};
const GetDayEndTimestampUTC = function (timestamp) {
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayEnd = new Date(
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999)
);
const dayEndTimestamp = dayEnd.getTime();
return Math.floor(dayEndTimestamp / 1000) + 60;
//use js date instead of moment
const now = new Date(timestamp * 1000);
const dayEnd = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999));
const dayEndTimestamp = dayEnd.getTime();
return Math.floor(dayEndTimestamp / 1000) + 60;
};
const DurationInMinutes = function (start, end) {
return Math.floor((end - start) / 60);
return Math.floor((end - start) / 60);
};
const GetDayStartWithOffset = function (timeStampInSeconds, offsetInMinutes) {
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
//have to figure out when to add a day
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
//have to figure out when to add a day
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
//if xtm - 330 > 1 day , add a day to xtm - 330
//if xtm - 330 > 1 day , add a day to xtm - 330
if (offsetInMinutes < 0) {
//add one day to dayStartThen
dayStartThen = dayStartThen + 24 * 60 * 60;
}
return dayStartThen + offsetInMinutes * 60;
if (offsetInMinutes < 0) {
//add one day to dayStartThen
dayStartThen = dayStartThen + 24 * 60 * 60;
}
return dayStartThen + offsetInMinutes * 60;
};
const BeginningOfDay = (options = {}) => {
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
hour: "numeric",
minute: "numeric",
second: "numeric"
}).formatToParts(date);
const hour = parseInt(parts.find((i) => i.type === "hour").value);
const minute = parseInt(parts.find((i) => i.type === "minute").value);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(
1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000)
);
return dt.getTime() / 1000;
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
hour: "numeric",
minute: "numeric",
second: "numeric",
}).formatToParts(date);
const hour = parseInt(parts.find((i) => i.type === "hour").value);
const minute = parseInt(parts.find((i) => i.type === "minute").value);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000));
return dt.getTime() / 1000;
};
const BeginningOfMinute = (options = {}) => {
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
second: "numeric"
}).formatToParts(date);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(1000 * Math.floor((date - second * 1000) / 1000));
return dt.getTime() / 1000;
const { date = new Date(), timeZone } = options;
const parts = Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
second: "numeric",
}).formatToParts(date);
const second = parseInt(parts.find((i) => i.type === "second").value);
const dt = new Date(1000 * Math.floor((date - second * 1000) / 1000));
return dt.getTime() / 1000;
};
const ValidateIpAddress = function (input) {
// Check if input is a valid IPv4 address
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IPv4";
}
// Check if input is a valid IPv4 address
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(input)) {
return "IPv4";
}
// Check if input is a valid IPv6 address
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
if (ipv6Regex.test(input)) {
return "IPv6";
}
// Check if input is a valid IPv6 address
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
if (ipv6Regex.test(input)) {
return "IPv6";
}
// Check if input is a valid domain name
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "Domain Name";
}
// Check if input is a valid domain name
const domainRegex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
if (domainRegex.test(input)) {
return "Domain Name";
}
// If none of the above conditions match, the input is invalid
return "Invalid";
// If none of the above conditions match, the input is invalid
return "Invalid";
};
function checkIfDuplicateExists(arr) {
return new Set(arr).size !== arr.length;
return new Set(arr).size !== arr.length;
}
function GetWordsStartingWithDollar(text) {
const regex = /\$\w+/g;
const wordsArray = text.match(regex);
return wordsArray || [];
const regex = /\$\w+/g;
const wordsArray = text.match(regex);
return wordsArray || [];
}
const StatusObj = {
UP: "api-up",
DEGRADED: "api-degraded",
DOWN: "api-down",
NO_DATA: "api-nodata"
UP: "api-up",
DEGRADED: "api-degraded",
DOWN: "api-down",
NO_DATA: "api-nodata",
};
// @ts-ignore
const ParseUptime = function (up, all) {
if (all === 0) return String("-");
if (up == 0) return String("0");
if (up == all) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
//return 50% as 50% and not 50.0000%
if (((up / all) * 100) % 10 == 0) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
return String(((up / all) * parseFloat(100)).toFixed(4));
if (all === 0) return String("-");
if (up == 0) return String("0");
if (up == all) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
//return 50% as 50% and not 50.0000%
if (((up / all) * 100) % 10 == 0) {
return String(((up / all) * parseFloat(100)).toFixed(0));
}
return String(((up / all) * parseFloat(100)).toFixed(4));
};
const ParsePercentage = function (n) {
if (isNaN(n)) return "-";
if (n == 0) {
return "0";
}
if (n == 100) {
return "100";
}
return n.toFixed(4);
if (isNaN(n)) return "-";
if (n == 0) {
return "0";
}
if (n == 100) {
return "100";
}
return n.toFixed(4);
};
//valid domain name
function IsValidHost(domain) {
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
const regex = /^[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/;
return regex.test(domain);
}
//valid nameserver
function IsValidNameServer(nameServer) {
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
//8.8.8.8 example
const regex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
return regex.test(nameServer);
}
//valid dns record type
function IsValidRecordType(recordType) {
return AllRecordTypes.hasOwnProperty(recordType);
return AllRecordTypes.hasOwnProperty(recordType);
}
function ReplaceAllOccurrences(originalString, searchString, replacement) {
const regex = new RegExp(`\\${searchString}`, "g");
const replacedString = originalString.replace(regex, replacement);
return replacedString;
const regex = new RegExp(`\\${searchString}`, "g");
const replacedString = originalString.replace(regex, replacement);
return replacedString;
}
function GetRequiredSecrets(str) {
let envSecrets = [];
const requiredSecrets = GetWordsStartingWithDollar(str).map((x) => x.substr(1));
for (const [key, value] of Object.entries(process.env)) {
if (requiredSecrets.indexOf(key) !== -1) {
envSecrets.push({
find: `$${key}`,
replace: value
});
}
}
return envSecrets;
let envSecrets = [];
const requiredSecrets = GetWordsStartingWithDollar(str).map((x) => x.substr(1));
for (const [key, value] of Object.entries(process.env)) {
if (requiredSecrets.indexOf(key) !== -1) {
envSecrets.push({
find: `$${key}`,
replace: value,
});
}
}
return envSecrets;
}
function ValidateMonitorAlerts(alerts) {
if (!alerts) {
console.log("Alerts object is not provided.");
return false;
}
// if down degraded not present return false
if (!alerts.hasOwnProperty("DOWN") && !alerts.hasOwnProperty("DEGRADED")) {
console.log("Alerts object does not have DOWN or DEGRADED properties.");
return false;
}
let statues = Object.keys(alerts);
//can be either DOWN or DEGRADED
let validKeys = ["DOWN", "DEGRADED"];
for (let i = 0; i < statues.length; i++) {
const keyName = statues[i];
if (!validKeys.includes(keyName)) {
console.log(`Invalid key found in alerts: ${keyName}`);
return false;
}
}
for (const key in alerts) {
if (Object.prototype.hasOwnProperty.call(alerts, key)) {
const element = alerts[key];
const triggers = element.triggers;
//if triggers not present return false
if (!element.hasOwnProperty("triggers")) {
console.log(`Triggers not present for key: ${key}`);
return false;
}
//if length of triggers is 0 return false
if (triggers.length === 0) {
console.log(`Triggers length is 0 for key: ${key}`);
return false;
}
if (
!element.hasOwnProperty("failureThreshold") ||
!element.hasOwnProperty("successThreshold")
) {
console.log(`Thresholds not present for key: ${key}`);
return false;
}
if (
element.hasOwnProperty("failureThreshold") &&
!Number.isInteger(element.failureThreshold)
) {
console.log(`Failure threshold is not an integer for key: ${key}`);
return false;
}
if (
element.hasOwnProperty("successThreshold") &&
!Number.isInteger(element.successThreshold)
) {
console.log(`Success threshold is not an integer for key: ${key}`);
return false;
}
if (element.hasOwnProperty("failureThreshold") && element.failureThreshold <= 0) {
console.log(`Failure threshold is less than or equal to 0 for key: ${key}`);
return false;
}
if (element.hasOwnProperty("successThreshold") && element.successThreshold <= 0) {
console.log(`Success threshold is less than or equal to 0 for key: ${key}`);
return false;
}
}
}
return true;
if (!alerts) {
console.log("Alerts object is not provided.");
return false;
}
// if down degraded not present return false
if (!alerts.hasOwnProperty("DOWN") && !alerts.hasOwnProperty("DEGRADED")) {
console.log("Alerts object does not have DOWN or DEGRADED properties.");
return false;
}
let statues = Object.keys(alerts);
//can be either DOWN or DEGRADED
let validKeys = ["DOWN", "DEGRADED"];
for (let i = 0; i < statues.length; i++) {
const keyName = statues[i];
if (!validKeys.includes(keyName)) {
console.log(`Invalid key found in alerts: ${keyName}`);
return false;
}
}
for (const key in alerts) {
if (Object.prototype.hasOwnProperty.call(alerts, key)) {
const element = alerts[key];
const triggers = element.triggers;
//if triggers not present return false
if (!element.hasOwnProperty("triggers")) {
console.log(`Triggers not present for key: ${key}`);
return false;
}
//if length of triggers is 0 return false
if (triggers.length === 0) {
console.log(`Triggers length is 0 for key: ${key}`);
return false;
}
if (!element.hasOwnProperty("failureThreshold") || !element.hasOwnProperty("successThreshold")) {
console.log(`Thresholds not present for key: ${key}`);
return false;
}
if (element.hasOwnProperty("failureThreshold") && !Number.isInteger(element.failureThreshold)) {
console.log(`Failure threshold is not an integer for key: ${key}`);
return false;
}
if (element.hasOwnProperty("successThreshold") && !Number.isInteger(element.successThreshold)) {
console.log(`Success threshold is not an integer for key: ${key}`);
return false;
}
if (element.hasOwnProperty("failureThreshold") && element.failureThreshold <= 0) {
console.log(`Failure threshold is less than or equal to 0 for key: ${key}`);
return false;
}
if (element.hasOwnProperty("successThreshold") && element.successThreshold <= 0) {
console.log(`Success threshold is less than or equal to 0 for key: ${key}`);
return false;
}
}
}
return true;
}
function GenerateRandomColor() {
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
return randomColor;
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
return randomColor;
}
//wait for x ms promise
function Wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
function MaskString(str) {
return "*".repeat(str.length - 4) + str.slice(-4);
return "*".repeat(str.length - 4) + str.slice(-4);
}
function GetDbType() {
//sqlite, postgresql, mysql
return knexOb.databaseType;
}
export {
IsValidURL,
IsValidHTTPMethod,
GetMinuteStartTimestampUTC,
GetNowTimestampUTC,
GetDayStartTimestampUTC,
GetMinuteStartNowTimestampUTC,
DurationInMinutes,
GetDayStartWithOffset,
BeginningOfDay,
IsStringURLSafe,
ValidateIpAddress,
checkIfDuplicateExists,
GetWordsStartingWithDollar,
StatusObj,
ParseUptime,
ParsePercentage,
IsValidHost,
IsValidRecordType,
IsValidNameServer,
ReplaceAllOccurrences,
GetRequiredSecrets,
ValidateMonitorAlerts,
GenerateRandomColor,
BeginningOfMinute,
Wait,
MaskString
IsValidURL,
IsValidHTTPMethod,
GetMinuteStartTimestampUTC,
GetNowTimestampUTC,
GetDayStartTimestampUTC,
GetMinuteStartNowTimestampUTC,
DurationInMinutes,
GetDayStartWithOffset,
BeginningOfDay,
IsStringURLSafe,
ValidateIpAddress,
checkIfDuplicateExists,
GetWordsStartingWithDollar,
StatusObj,
ParseUptime,
ParsePercentage,
IsValidHost,
IsValidRecordType,
IsValidNameServer,
ReplaceAllOccurrences,
GetRequiredSecrets,
ValidateMonitorAlerts,
GenerateRandomColor,
BeginningOfMinute,
Wait,
MaskString,
GetDbType,
};
+126 -125
View File
@@ -1,154 +1,155 @@
// @ts-nocheck
import {
GetMinuteStartNowTimestampUTC,
GetNowTimestampUTC,
GetMinuteStartTimestampUTC,
ParseUptime,
GetDayStartTimestampUTC
GetMinuteStartNowTimestampUTC,
GetNowTimestampUTC,
GetMinuteStartTimestampUTC,
ParseUptime,
GetDayStartTimestampUTC,
} from "./tool.js";
import db from "./db/db.js";
import { WEBHOOK } from "./constants.js";
import { GetMonitors, VerifyAPIKey } from "./controllers/controller.js";
const GetAllTags = async function () {
let tags = [];
let monitors = [];
try {
monitors = await GetMonitors({
status: "ACTIVE"
});
tags = monitors.map((monitor) => monitor.tag);
} catch (err) {
return [];
}
return tags;
let tags = [];
let monitors = [];
try {
monitors = await GetMonitors({
status: "ACTIVE",
});
tags = monitors.map((monitor) => monitor.tag);
} catch (err) {
return [];
}
return tags;
};
const CheckIfValidTag = async function (tag) {
let tags = [];
try {
let monitor = await db.getMonitorByTag(tag);
if (!!!monitor) {
throw new Error("not a valid tag");
}
if (monitor.status != "ACTIVE") {
throw new Error("monitor is not active");
}
} catch (err) {
return false;
}
return true;
let tags = [];
try {
let monitor = await db.getMonitorByTag(tag);
if (!!!monitor) {
throw new Error("not a valid tag");
}
if (monitor.status != "ACTIVE") {
throw new Error("monitor is not active");
}
} catch (err) {
return false;
}
return true;
};
const auth = async function (request) {
const authHeader = request.headers.get("authorization");
const authToken = authHeader?.replace("Bearer ", "");
const authHeader = request.headers.get("authorization");
const authToken = authHeader?.replace("Bearer ", "");
if ((await VerifyAPIKey(authToken)) === false) {
return new Error("invalid token");
}
if ((await VerifyAPIKey(authToken)) === false) {
return new Error("invalid token");
}
return null;
return null;
};
const store = async function (data) {
const tag = data.tag;
//remove Bearer from start in authHeader
const tag = data.tag;
//remove Bearer from start in authHeader
const resp = {};
if (data.status === undefined || ["UP", "DOWN", "DEGRADED"].indexOf(data.status) === -1) {
return { error: "status missing", status: 400 };
}
if (data.latency === undefined || isNaN(data.latency)) {
return { error: "latency missing or not a number", status: 400 };
}
if (data.timestampInSeconds !== undefined && isNaN(data.timestampInSeconds)) {
return { error: "timestampInSeconds not a number", status: 400 };
}
if (data.timestampInSeconds === undefined) {
data.timestampInSeconds = GetNowTimestampUTC();
}
data.timestampInSeconds = GetMinuteStartTimestampUTC(data.timestampInSeconds);
resp.status = data.status;
resp.latency = data.latency;
resp.type = "webhook";
let timestamp = GetMinuteStartNowTimestampUTC();
try {
//throw error if timestamp is future or older than 90days
if (data.timestampInSeconds > timestamp) {
throw new Error("timestampInSeconds is in future");
}
//past 90 days only
if (timestamp - data.timestampInSeconds > 90 * 24 * 60 * 60) {
throw new Error("timestampInSeconds is older than 90days");
}
} catch (err) {
return { error: err.message, status: 400 };
}
//check if tag is valid
let monitor = await db.getMonitorByTag(tag);
if (!!!monitor) {
return { error: "no monitor with tag found", status: 400 };
}
if (monitor.status != "ACTIVE") {
return { error: "monitor with the given tag is not active", status: 400 };
}
const resp = {};
if (data.status === undefined || ["UP", "DOWN", "DEGRADED"].indexOf(data.status) === -1) {
return { error: "status missing", status: 400 };
}
if (data.latency === undefined || isNaN(data.latency)) {
return { error: "latency missing or not a number", status: 400 };
}
if (data.timestampInSeconds !== undefined && isNaN(data.timestampInSeconds)) {
return { error: "timestampInSeconds not a number", status: 400 };
}
if (data.timestampInSeconds === undefined) {
data.timestampInSeconds = GetNowTimestampUTC();
}
data.timestampInSeconds = GetMinuteStartTimestampUTC(data.timestampInSeconds);
resp.status = data.status;
resp.latency = data.latency;
resp.type = WEBHOOK;
let timestamp = GetMinuteStartNowTimestampUTC();
try {
//throw error if timestamp is future or older than 90days
if (data.timestampInSeconds > timestamp) {
throw new Error("timestampInSeconds is in future");
}
//past 90 days only
if (timestamp - data.timestampInSeconds > 90 * 24 * 60 * 60) {
throw new Error("timestampInSeconds is older than 90days");
}
} catch (err) {
return { error: err.message, status: 400 };
}
//check if tag is valid
let monitor = await db.getMonitorByTag(tag);
if (!!!monitor) {
return { error: "no monitor with tag found", status: 400 };
}
if (monitor.status != "ACTIVE") {
return { error: "monitor with the given tag is not active", status: 400 };
}
//get the monitor object matching the tag
//get the monitor object matching the tag
await db.insertMonitoringData({
monitor_tag: tag,
timestamp: data.timestampInSeconds,
status: resp.status,
latency: resp.latency,
type: "webhook"
});
return { status: 200, message: "success at " + data.timestampInSeconds };
await db.insertMonitoringData({
monitor_tag: tag,
timestamp: data.timestampInSeconds,
status: resp.status,
latency: resp.latency,
type: "webhook",
});
return { status: 200, message: "success at " + data.timestampInSeconds };
};
const GetMonitorStatusByTag = async function (tag, timestamp) {
let monitor = await db.getMonitorByTag(tag);
if (!!!monitor) {
return { error: "no monitor with tag found", status: 400 };
}
if (monitor.status != "ACTIVE") {
return { error: "monitor with the given tag is not active", status: 400 };
}
const resp = {
status: null,
uptime: null,
last_updated_at: null
};
let monitor = await db.getMonitorByTag(tag);
if (!!!monitor) {
return { error: "no monitor with tag found", status: 400 };
}
if (monitor.status != "ACTIVE") {
return { error: "monitor with the given tag is not active", status: 400 };
}
const resp = {
status: null,
uptime: null,
last_updated_at: null,
};
const { include_degraded_in_downtime } = monitor;
const { include_degraded_in_downtime } = monitor;
let now = GetMinuteStartNowTimestampUTC();
if (timestamp !== null && timestamp !== undefined) {
now = timestamp;
}
let start = GetDayStartTimestampUTC(now);
let now = GetMinuteStartNowTimestampUTC();
if (timestamp !== null && timestamp !== undefined) {
now = timestamp;
}
let start = GetDayStartTimestampUTC(now);
let dayDataNew = await db.getMonitoringData(tag, start, now);
let ups = 0;
let downs = 0;
let degradeds = 0;
let lastData = dayDataNew[dayDataNew.length - 1];
let dayDataNew = await db.getMonitoringData(tag, start, now);
let ups = 0;
let downs = 0;
let degradeds = 0;
let lastData = dayDataNew[dayDataNew.length - 1];
for (let i = 0; i < dayDataNew.length; i++) {
let row = dayDataNew[i];
if (row.status == "UP") {
ups++;
} else if (row.status == "DEGRADED") {
degradeds++;
} else if (row.status == "DOWN") {
downs++;
}
}
let numerator = ups + degradeds;
if (include_degraded_in_downtime === "YES") {
numerator = ups;
}
for (let i = 0; i < dayDataNew.length; i++) {
let row = dayDataNew[i];
if (row.status == "UP") {
ups++;
} else if (row.status == "DEGRADED") {
degradeds++;
} else if (row.status == "DOWN") {
downs++;
}
}
let numerator = ups + degradeds;
if (include_degraded_in_downtime === "YES") {
numerator = ups;
}
resp.uptime = ParseUptime(numerator, ups + degradeds + downs);
resp.last_updated_at = Number(lastData.timestamp);
resp.status = lastData.status;
return { status: 200, ...resp };
resp.uptime = ParseUptime(numerator, ups + degradeds + downs);
resp.last_updated_at = Number(lastData.timestamp);
resp.status = lastData.status;
return { status: 200, ...resp };
};
export { store, auth, CheckIfValidTag, GetAllTags, GetMonitorStatusByTag };
+49 -3
View File
@@ -6,6 +6,7 @@
import Sun from "lucide-svelte/icons/sun";
import Menu from "lucide-svelte/icons/menu";
import X from "lucide-svelte/icons/x";
import Heart from "lucide-svelte/icons/heart";
import { clickOutsideAction, slide } from "svelte-legos";
import Moon from "lucide-svelte/icons/moon";
import { onMount } from "svelte";
@@ -56,6 +57,23 @@
if (window.innerWidth > 768) {
sideBarHidden = false;
}
const donationBanner = document.getElementById("donation-banner");
const closeButton = document.getElementById("close-donation");
// Check if we should show the banner today
const lastShown = localStorage.getItem("kener_donation_banner_closed");
const today = new Date().toDateString();
// Only show the banner if it hasn't been closed today
if (lastShown !== today) {
donationBanner.style.display = "block";
}
// Add close button handler
closeButton.addEventListener("click", () => {
donationBanner.style.display = "none";
localStorage.setItem("kener_donation_banner_closed", today);
});
});
</script>
@@ -92,7 +110,7 @@
<!-- Document Icon - Replace with your own logo -->
<img src="https://kener.ing/logo.png" class="h-8 w-8" alt="" />
<span class="text-xl font-medium">Kener Documentation</span>
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.1.4 </span>
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium"> 3.2.3 </span>
</a>
</div>
@@ -107,7 +125,6 @@
</a>
<a href="/api-reference" class="text-sm font-medium"> API Reference </a>
<a href="https://github.com/rajnandan1/kener/issues" class="text-sm font-medium"> Report Issue </a>
<a href="https://github.com/sponsors/rajnandan1" class="text-sm font-medium"> Sponsor </a>
</div>
</div>
@@ -148,6 +165,7 @@
<div class="">
{#each item.children as child}
<a
rel="external"
href={child.link.startsWith("/") ? base + child.link : child.link}
class="sidebar-item group flex items-center rounded-md px-3 py-2 text-sm font-medium {!!child.active
? 'active'
@@ -167,6 +185,34 @@
<main class="dark relative z-10 min-h-screen pt-16 md:ml-72">
<div class="mx-auto max-w-5xl px-4 py-10 sm:px-6 md:px-8 md:pr-64">
<!-- Content Header -->
<div
id="donation-banner"
style="display: none;"
class="mb-8 rounded-lg bg-gradient-to-r from-blue-900 to-indigo-900 p-4 shadow-lg"
>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="rounded-full bg-amber-500 p-2">
<Heart class="h-5 w-5 text-white" />
</div>
<div>
<h3 class="text-base font-medium text-white">Support Kener Development</h3>
<p class="text-sm text-blue-200">Help us keep this project running and improve it further</p>
</div>
</div>
<div class="flex items-center space-x-3">
<a
href="https://buymeacoffee.com/rajnandan1"
class="rounded-md bg-amber-500 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-amber-600"
>
Sponsor
</a>
<button id="close-donation" class="rounded p-1 text-blue-300 hover:bg-blue-800 hover:text-white">
<X class="h-5 w-5" />
</button>
</div>
</div>
</div>
<div
class="prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
@@ -181,7 +227,7 @@
{#each tableOfContents as item}
<a
href="#{item.id}"
class="block overflow-hidden text-ellipsis whitespace-nowrap text-xs text-muted-foreground hover:underline {item.level ==
class="rlink block overflow-hidden text-ellipsis whitespace-nowrap text-xs text-muted-foreground hover:underline {item.level ==
3
? 'ml-4'
: ''}"
+4 -4
View File
@@ -6,7 +6,7 @@
export let data;
let subHeadings = [];
let previousH2 = null;
let subPath = data.docFilePath.split(".md")[0];
function fillSubHeadings() {
subHeadings = [];
const headings = document.querySelectorAll("#markdown h2, #markdown h3");
@@ -48,14 +48,14 @@
<svg class="check-btn absolute left-0 top-0" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><path d="M20 6 9 17l-5-5"/></svg>`;
button.style = "margin-left: 10px;height:16px;width:16px background-color: transparent; cursor: pointer;";
button.onclick = () => {
navigator.clipboard.writeText(`https://kener.ing/docs${subPath}#` + heading.id);
navigator.clipboard.writeText(`https://kener.ing/docs${data.docFilePath.split(".md")[0]}#` + heading.id);
};
heading.appendChild(button);
});
}
function handleRightBarClick(e) {
if (e.target.tagName === "A") {
if (e.target.tagName === "A" && e.target.classList.contains("rlink")) {
e.preventDefault();
const id = e.target.getAttribute("href").slice(1);
scrollToId(id);
@@ -121,7 +121,7 @@
<svelte:head>
<title>{data.title}</title>
<meta name="description" content={data.description} />
<link rel="canonical" href="https://kener.ing/docs{subPath}" />
<link rel="canonical" href="https://kener.ing/docs{data.docFilePath.split('.md')[0]}" />
</svelte:head>
<div id="markdown">
+111 -38
View File
@@ -3,17 +3,22 @@
import "../../kener.css";
import Nav from "$lib/components/nav.svelte";
import { onMount } from "svelte";
import { Input } from "$lib/components/ui/input";
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import Sun from "lucide-svelte/icons/sun";
import Moon from "lucide-svelte/icons/moon";
import { Languages } from "lucide-svelte";
import { Languages, Globe } from "lucide-svelte";
import * as Popover from "$lib/components/ui/popover";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { analyticsEvent } from "$lib/boringOne";
import { setMode, mode, ModeWatcher } from "mode-watcher";
export let data;
import { l } from "$lib/i18n/client";
export let data;
let defaultLocaleKey = data.selectedLang;
let allTimezones = Intl.supportedValuesOf("timeZone");
let searchTzValue = "";
let defaultTheme = data.site.theme;
const allLocales = data.site.i18n?.locales.filter((locale) => locale.selected === true);
let hasConfiguredAnalytics =
@@ -52,15 +57,19 @@
location.reload();
}
let Analytics;
//set timezone
function setTz(tz) {
document.cookie = `localTz=${tz};max-age=${60 * 60 * 24 * 365 * 30};path=${base ? "base" : "/"};`;
location.reload();
}
let Analytics;
let myTimezone = data.localTz;
onMount(async () => {
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (localTz != data.localTz) {
myTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (data.localTz === "UTC") {
if (data.isBot === false) {
document.cookie =
"localTz=" + localTz + ";max-age=" + 60 * 60 * 24 * 365 * 30 + `;path=${base ? "base" : "/"};`;
location.reload();
setTz(myTimezone);
}
}
if (defaultTheme != "none") {
@@ -177,40 +186,104 @@
</div>
</footer>
{/if}
<div class="fixed bottom-4 right-4 z-20 rounded-md bg-background">
{#if data.site.i18n && data.site.i18n.locales && allLocales.length > 1}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" size="icon" class="flex">
<Languages class="h-[1.2rem] w-[1.2rem]" />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
{#each allLocales as locale}
<DropdownMenu.Item
class={defaultLocaleKey == locale.code ? "bg-input" : ""}
on:click={(e) => {
setLanguage(locale.code);
}}>{locale.name}</DropdownMenu.Item
>
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
<div class="fixed bottom-4 right-4 z-20 flex flex-col rounded-md bg-background">
{#if !!data.site.tzToggle && data.site.tzToggle === "YES"}
<div>
<Popover.Root>
<Popover.Trigger>
<Button variant="ghost" size="icon" class="flex">
<Globe class="h-[1.2rem] w-[1.2rem]" />
</Button>
</Popover.Trigger>
<Popover.Content class="px-0 py-2">
<div class="flex flex-col gap-2">
<div class="flex px-2 pb-1 text-xs font-semibold">
<span class="flex-1">
{@html l(data.lang, "Timezone set to %tz", {
tz: "<i>" + data.localTz + "</i>"
})}
</span>
</div>
<div class="flex flex-col gap-2">
<div class="border-b px-2 pb-2">
<Input
placeholder="Search for a timezone"
class="h-8 px-2 py-1 text-xs"
value={searchTzValue}
on:input={(e) => {
searchTzValue = e.target.value;
}}
/>
</div>
<div class="flex h-[200px] flex-col overflow-y-auto">
{#each allTimezones as tz}
{#if tz.toLowerCase().includes(searchTzValue.toLowerCase())}
<div class="text-xs font-semibold">
<Button
variant={tz === data.localTz ? "primary" : "ghost"}
class="h-8 w-full justify-start rounded-none text-xs text-muted-foreground"
on:click={() => setTz(tz)}
>
{tz}
</Button>
</div>
{/if}
{/each}
</div>
{#if myTimezone !== data.localTz}
<div class="px-2">
<Button
variant="outline"
class="h-6 w-full bg-transparent px-2 text-xs"
on:click={() => setTz(myTimezone)}
>
{l(data.lang, "Switch to your timezone")}
</Button>
</div>
{/if}
</div>
</div>
</Popover.Content>
</Popover.Root>
</div>
{/if}
{#if !!data.site.themeToggle}
<Button on:click={toggleMode} variant="ghost" size="icon" class="flex">
<Sun class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
{#if data.site.i18n && data.site.i18n.locales && allLocales.length > 1}
<div>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" size="icon" class="flex">
<Languages class="h-[1.2rem] w-[1.2rem]" />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="max-h-[250px] overflow-y-auto">
<DropdownMenu.Group>
{#each allLocales as locale}
<DropdownMenu.Item
class={defaultLocaleKey == locale.code ? "bg-input" : ""}
on:click={(e) => {
setLanguage(locale.code);
}}>{locale.name}</DropdownMenu.Item
>
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/if}
{#if !!data.site.themeToggle && data.site.themeToggle === "YES"}
<div>
<Button on:click={toggleMode} variant="ghost" size="icon" class="flex">
<Sun class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</div>
{/if}
</div>
{#if data.isLoggedIn}
<a href="{base}/manage/app/site" rel="external" class="button-77 fixed bottom-8 left-8 z-50" role="button">
Manage
</a>
<div class="fixed bottom-2 left-2 z-50 md:bottom-8 md:left-8">
<a href="{base}/manage/app/site" rel="external" class="button-77" role="button"> Manage </a>
</div>
{/if}
</main>
+8 -2
View File
@@ -72,7 +72,13 @@ export async function load({ parent, url }) {
include_degraded_in_downtime: monitor.include_degraded_in_downtime,
};
})
.filter((monitor) => !hiddenGroupedMonitorsTags.includes(monitor.tag));
.filter((monitor) => !hiddenGroupedMonitorsTags.includes(monitor.tag))
.filter((monitor) => {
if (pageType == "home") {
return monitor.category_name == "Home";
}
return true;
});
const parentData = await parent();
const siteData = parentData.site;
@@ -105,7 +111,7 @@ export async function load({ parent, url }) {
let isMonitorPage = pageType === "monitor";
let isGroupPage = pageType === "group";
if (isMonitorPage) {
if (isMonitorPage && monitorsActive.length > 0) {
pageTitle = monitorsActive[0].name + " - " + pageTitle;
pageDescription = monitorsActive[0].description;
canonical = canonical + "?monitor=" + monitorsActive[0].tag;
+13 -15
View File
@@ -63,11 +63,11 @@
</svelte:head>
<div class="mt-12"></div>
{#if data.hero}
<section class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<section class="section-hero mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center">
{#if data.hero.image}
<GMI src={data.site.hero.image} classList="m-auto mb-2 h-14 w-14" alt="" srcset="" />
<GMI src={data.hero.image} classList="m-auto mb-2 h-14 w-14" alt="" srcset="" />
{/if}
{#if data.hero.title}
<h1
@@ -86,7 +86,7 @@
</section>
{/if}
{#if data.pageType != "home"}
<section class="mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<section class="section-back mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<Button
variant="outline"
class="bounce-left h-8 justify-start pl-1.5"
@@ -107,10 +107,10 @@
{/if}
{#if data.allRecentIncidents.length + data.allRecentMaintenances.length > 0}
<section
class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
class="section-events mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
id=""
>
<div class="grid w-full grid-cols-2 gap-4">
<div class="mb-2 grid w-full grid-cols-2 gap-4">
<div class="col-span-2 flex gap-x-2 text-center md:text-left">
{#if kindFilter == "INCIDENT"}
{#if data.allRecentIncidents.length > 0}
@@ -137,11 +137,6 @@
{/if}
</div>
</div>
</section>
<section
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
id=""
>
<Card.Root class="w-full">
{#if kindFilter == "INCIDENT"}
<Card.Content class=" newincidents w-full overflow-hidden p-0">
@@ -159,7 +154,7 @@
{/if}
{#if data.monitors.length > 0}
<section
class="mx-auto mb-2 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
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">
@@ -189,9 +184,9 @@
</div>
</section>
<section
class="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 flex-1 flex-col items-start justify-center backdrop-blur-[2px] md:w-[655px]"
>
<Card.Root>
<Card.Root class="monitor-root">
<Card.Content class="monitors-card p-0">
{#each data.monitors as monitor}
<Monitor
@@ -208,7 +203,7 @@
{/if}
{#if data.site.categories && data.pageType == "home"}
<section
class="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-[890px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
>
{#each data.site.categories.filter((e) => e.name != "Home") as category}
<a href={`?category=${category.name}`} rel="external">
@@ -235,7 +230,10 @@
{/each}
</section>
{/if}
<section class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent" id="">
<section
class="section-browser-events mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<a
href="{base}/incidents/{format(new Date(), 'MMMM-yyyy')}"
rel="external"
@@ -0,0 +1,39 @@
// @ts-nocheck
// @ts-ignore
import { json } from "@sveltejs/kit";
import { RegisterHeartbeat } from "$lib/server/controllers/controller";
export async function GET({ request, params }) {
let signal = params.signal;
let signalData = signal.split(":");
if (signalData.length != 2) {
return json({ error: "Invalid url" }, { status: 400 });
}
let resp = await RegisterHeartbeat(signalData[0], signalData[1]);
if (!!!resp) {
return json({ error: "Invalid heartbeat url" }, { status: 400 });
}
return json(
{ status: "OK" },
{
status: 200,
},
);
}
export async function POST({ request, params }) {
let signal = params.signal;
let signalData = signal.split(":");
if (signalData.length != 2) {
return json({ error: "Invalid url" }, { status: 400 });
}
let resp = await RegisterHeartbeat(signalData[0], signalData[1]);
if (!!!resp) {
return json({ error: "Invalid heartbeat url" }, { status: 400 });
}
return json(
{ status: "OK" },
{
status: 200,
},
);
}
+55 -54
View File
@@ -6,68 +6,69 @@ import { CreateIncident, ParseIncidentToAPIResp } from "$lib/server/controllers/
import db from "$lib/server/db/db.js";
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
}
);
}
const payload = await request.json();
const authError = await auth(request);
if (authError !== null) {
return json(
{ error: authError.message },
{
status: 401,
},
);
}
let incident = {
title: payload.title,
start_date_time: payload.start_date_time
};
let incident = {
title: payload.title,
start_date_time: payload.start_date_time,
incident_source: "API",
};
try {
let incidentDb = await CreateIncident(incident);
return json(await ParseIncidentToAPIResp(incidentDb.incident_id), {
status: 201
});
} catch (e) {
return json(
{ error: e.message },
{
status: 400
}
);
}
try {
let incidentDb = await CreateIncident(incident);
return json(await ParseIncidentToAPIResp(incidentDb.incident_id), {
status: 201,
});
} catch (e) {
return json(
{ error: e.message },
{
status: 400,
},
);
}
}
export async function GET({ request, url }) {
const authError = await auth(request);
const authError = await auth(request);
if (authError !== null) {
return json(
{ error: authError.message },
{
status: 401
}
);
}
let filter = {
status: url.searchParams.get("status") || "OPEN"
};
if (authError !== null) {
return json(
{ error: authError.message },
{
status: 401,
},
);
}
let filter = {
status: url.searchParams.get("status") || "OPEN",
};
if (!!url.searchParams.get("start_date_time")) {
filter.start = url.searchParams.get("start_date_time");
}
if (!!url.searchParams.get("start_date_time")) {
filter.start = url.searchParams.get("start_date_time");
}
if (!!url.searchParams.get("end_date_time")) {
filter.end = url.searchParams.get("end_date_time");
}
if (!!url.searchParams.get("state")) {
filter.state = url.searchParams.get("state");
}
if (!!url.searchParams.get("end_date_time")) {
filter.end = url.searchParams.get("end_date_time");
}
if (!!url.searchParams.get("state")) {
filter.state = url.searchParams.get("state");
}
let page = url.searchParams.get("page") || 1;
let limit = url.searchParams.get("limit") || 10;
let page = url.searchParams.get("page") || 1;
let limit = url.searchParams.get("limit") || 10;
let incidents = await db.getIncidentsPaginatedDesc(page, limit, filter);
return json(incidents, {
status: 200
});
let incidents = await db.getIncidentsPaginatedDesc(page, limit, filter);
return json(incidents, {
status: 200,
});
}
+71 -77
View File
@@ -1,100 +1,94 @@
// @ts-nocheck
// @ts-ignore
import { json } from "@sveltejs/kit";
import {
GetMinuteStartNowTimestampUTC,
BeginningOfDay,
StatusObj,
ParseUptime
} from "$lib/server/tool.js";
import { GetMinuteStartNowTimestampUTC, BeginningOfDay, StatusObj, ParseUptime } from "$lib/server/tool.js";
import db from "$lib/server/db/db.js";
import {
GetAllSiteData,
GetIncidentsByIDS,
InterpolateData,
GetLastStatusBefore
GetAllSiteData,
GetIncidentsByIDS,
InterpolateData,
GetLastStatusBefore,
} from "$lib/server/controllers/controller.js";
export async function POST({ request }) {
const payload = await request.json();
const monitor = payload.monitor;
const localTz = payload.localTz;
const incidentIDs = payload.incidentIDs || [];
let _0Day = {};
const payload = await request.json();
const monitor = payload.monitor;
const localTz = payload.localTz;
const incidentIDs = payload.incidentIDs || [];
let _0Day = {};
const now = GetMinuteStartNowTimestampUTC() + 60;
const start = payload.startTs;
let end = Math.min(payload.startTs + 24 * 60 * 60, now);
const start = payload.startTs;
let end = payload.endTs;
for (let i = start; i < end; i += 60) {
_0Day[i] = {
timestamp: i,
status: "NO_DATA",
cssClass: StatusObj.NO_DATA,
index: (i - start) / 60
};
}
for (let i = start; i < end; i += 60) {
_0Day[i] = {
timestamp: i,
status: "NO_DATA",
cssClass: StatusObj.NO_DATA,
index: (i - start) / 60,
};
}
let dayData = await db.getMonitoringData(monitor.tag, payload.startTs, end);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
dayData = InterpolateData(dayData, payload.startTs, anchorStatus, end);
let siteData = await GetAllSiteData();
let dayData = await db.getMonitoringData(monitor.tag, payload.startTs, payload.endTs);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
dayData = InterpolateData(dayData, payload.startTs, anchorStatus, payload.endTs);
let siteData = await GetAllSiteData();
let ups = 0;
let downs = 0;
let degradeds = 0;
let ups = 0;
let downs = 0;
let degradeds = 0;
for (let i = 0; i < dayData.length; i++) {
let row = dayData[i];
let timestamp = row.timestamp;
let status = row.status;
let cssClass = StatusObj.UP;
for (let i = 0; i < dayData.length; i++) {
let row = dayData[i];
let timestamp = row.timestamp;
let status = row.status;
let cssClass = StatusObj.UP;
if (status == "DEGRADED") {
cssClass = StatusObj.DEGRADED;
}
if (status == "DOWN") {
cssClass = StatusObj;
}
if (_0Day[timestamp] !== undefined) {
_0Day[timestamp].status = status;
_0Day[timestamp].cssClass = StatusObj[status];
}
if (status == "DEGRADED") {
cssClass = StatusObj.DEGRADED;
}
if (status == "DOWN") {
cssClass = StatusObj;
}
if (_0Day[timestamp] !== undefined) {
_0Day[timestamp].status = status;
_0Day[timestamp].cssClass = StatusObj[status];
}
if (status == "UP") {
ups++;
} else if (status == "DOWN") {
downs++;
} else {
degradeds++;
}
}
if (status == "UP") {
ups++;
} else if (status == "DOWN") {
downs++;
} else if (status == "DEGRADED") {
degradeds++;
}
}
let total = ups + downs + degradeds;
let uptime = ParseUptime(ups + degradeds, total);
if (monitor.include_degraded_in_downtime === "YES") {
uptime = ParseUptime(ups, total);
}
let total = ups + downs + degradeds;
let uptime = ParseUptime(ups + degradeds, total);
if (monitor.include_degraded_in_downtime === "YES") {
uptime = ParseUptime(ups, total);
}
return json({ _0Day, uptime, incidents: await GetIncidentsByIDS(incidentIDs) });
return json({ _0Day, uptime, incidents: await GetIncidentsByIDS(incidentIDs) });
}
export async function GET({ request }) {
const payload = await request.json();
const monitor = payload.monitor;
const now = GetMinuteStartNowTimestampUTC();
const start = payload.startTs;
let end = Math.min(payload.startTs + 24 * 60 * 60, now);
let aggregatedData = db.getDataGroupByMinute(monitor.tag, start, end);
let ups = aggregatedData.UP;
let downs = aggregatedData.DOWN;
let degradeds = aggregatedData.DEGRADED;
const payload = await request.json();
const monitor = payload.monitor;
const now = GetMinuteStartNowTimestampUTC();
const start = payload.startTs;
let end = Math.min(payload.startTs + 24 * 60 * 60, now);
let aggregatedData = db.getDataGroupByMinute(monitor.tag, start, end);
let ups = aggregatedData.UP;
let downs = aggregatedData.DOWN;
let degradeds = aggregatedData.DEGRADED;
let total = ups + downs + degradeds;
let uptime = ParseUptime(ups + degradeds, total);
if (monitor.include_degraded_in_downtime === "YES") {
uptime = ParseUptime(ups, total);
}
let total = ups + downs + degradeds;
let uptime = ParseUptime(ups + degradeds, total);
if (monitor.include_degraded_in_downtime === "YES") {
uptime = ParseUptime(ups, total);
}
return json({ uptime });
return json({ uptime });
}
@@ -1,45 +1,41 @@
// @ts-nocheck
// @ts-ignore
import { json } from "@sveltejs/kit";
import {
GetMinuteStartNowTimestampUTC,
BeginningOfDay,
StatusObj,
ParseUptime
} from "$lib/server/tool.js";
import { GetMinuteStartNowTimestampUTC, BeginningOfDay, StatusObj, ParseUptime } from "$lib/server/tool.js";
import db from "$lib/server/db/db.js";
import { InterpolateData, GetLastStatusBefore } from "$lib/server/controllers/controller.js";
export async function POST({ request }) {
const payload = await request.json();
const monitor = payload.monitor;
const start = payload.startTs;
let end = GetMinuteStartNowTimestampUTC();
let rawData = await db.getMonitoringData(monitor.tag, start, end);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
rawData = InterpolateData(rawData, start, anchorStatus, rawData.length == 0 ? end : null);
const payload = await request.json();
const monitor = payload.monitor;
const start = payload.startTs;
const end = payload.endTs;
let aggregatedData = rawData.reduce(
(acc, row) => {
acc[row.status] = (acc[row.status] || 0) + 1;
return acc;
},
{
UP: 0,
DOWN: 0,
DEGRADED: 0
}
);
//covert all keys to uppercase
let ups = Number(aggregatedData.UP);
let downs = Number(aggregatedData.DOWN);
let degradeds = Number(aggregatedData.DEGRADED);
let rawData = await db.getMonitoringData(monitor.tag, start, end);
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
rawData = InterpolateData(rawData, start, anchorStatus, end);
let total = ups + downs + degradeds;
let uptime = ParseUptime(ups + degradeds, total);
if (monitor.include_degraded_in_downtime === "YES") {
uptime = ParseUptime(ups, total);
}
let aggregatedData = rawData.reduce(
(acc, row) => {
acc[row.status] = (acc[row.status] || 0) + 1;
return acc;
},
{
UP: 0,
DOWN: 0,
DEGRADED: 0,
},
);
//covert all keys to uppercase
let ups = Number(aggregatedData.UP);
let downs = Number(aggregatedData.DOWN);
let degradeds = Number(aggregatedData.DEGRADED);
return json({ uptime });
let total = ups + downs + degradeds;
let uptime = ParseUptime(ups + degradeds, total);
if (monitor.include_degraded_in_downtime === "YES") {
uptime = ParseUptime(ups, total);
}
return json({ uptime });
}
@@ -3,76 +3,71 @@ import { ParseUptime, GetMinuteStartNowTimestampUTC } from "$lib/server/tool.js"
import { makeBadge } from "badge-maker";
import moment from "moment";
import db from "$lib/server/db/db.js";
import { GetMonitors } from "$lib/server/controllers/controller.js";
import {
GetMonitors,
GetLastStatusBefore,
InterpolateData,
AggregateData,
} from "$lib/server/controllers/controller.js";
export async function GET({ params, url }) {
// @ts-ignore
let monitors = await GetMonitors({ status: "ACTIVE" });
const { name, tag, include_degraded_in_downtime } = monitors.find(
(monitor) => monitor.tag === params.tag
);
const query = url.searchParams;
let sinceLast = query.get("sinceLast");
if (sinceLast == undefined || isNaN(sinceLast) || sinceLast < 60) {
sinceLast = 90 * 24 * 60 * 60;
}
const rangeInSeconds = sinceLast;
const now = Math.floor(Date.now() / 1000);
const since = GetMinuteStartNowTimestampUTC() - rangeInSeconds;
let label = query.get("label") || name;
const hideDuration = query.get("hideDuration") === "true";
// @ts-ignore
let monitors = await GetMonitors({ status: "ACTIVE" });
const { name, tag, include_degraded_in_downtime } = monitors.find((monitor) => monitor.tag === params.tag);
const query = url.searchParams;
let sinceLast = query.get("sinceLast");
if (sinceLast == undefined || isNaN(sinceLast) || sinceLast < 60) {
sinceLast = 90 * 24 * 60 * 60;
}
const rangeInSeconds = sinceLast;
const now = Math.floor(Date.now() / 1000);
const since = GetMinuteStartNowTimestampUTC() - rangeInSeconds;
let label = query.get("label") || name;
const hideDuration = query.get("hideDuration") === "true";
//add all status up, degraded, down
let ups = 0;
let downs = 0;
let degradeds = 0;
const duration = moment.duration(rangeInSeconds, "seconds");
let formatted = "";
if (duration.days() > 0 || duration.minutes() < 1) {
formatted = `${Math.floor(rangeInSeconds / 86400)}d`;
} else if (duration.hours() > 0) {
formatted = `${duration.hours()}h`;
} else if (duration.minutes() > 0) {
formatted = `${duration.minutes()}m`;
}
const duration = moment.duration(rangeInSeconds, "seconds");
let formatted = "";
if (duration.days() > 0 || duration.minutes() < 1) {
formatted = `${Math.floor(rangeInSeconds / 86400)}d`;
} else if (duration.hours() > 0) {
formatted = `${duration.hours()}h`;
} else if (duration.minutes() > 0) {
formatted = `${duration.minutes()}m`;
}
let todayDataDb = await db.getMonitoringData(tag, since, now);
let anchorStatus = await GetLastStatusBefore(tag, since);
todayDataDb = InterpolateData(todayDataDb, since, anchorStatus, now);
let dbData = await db.getAggregatedMonitoringData(tag, since, now);
dbData = Object.keys(dbData).reduce((acc, key) => {
acc[key.toUpperCase()] = dbData[key];
return acc;
}, {});
dbData.UP = Number(dbData.UP);
dbData.DOWN = Number(dbData.DOWN);
dbData.DEGRADED = Number(dbData.DEGRADED);
let calculatedData = AggregateData(todayDataDb);
let numerator = dbData.UP + dbData.DEGRADED;
let denominator = dbData.UP + dbData.DEGRADED + dbData.DOWN;
if (include_degraded_in_downtime === "YES") {
numerator = dbData.UP;
}
let numerator = calculatedData.UPs + calculatedData.DEGRADEDs;
let denominator = calculatedData.total;
if (include_degraded_in_downtime === "YES") {
numerator = calculatedData.UPs;
}
let uptime = ParseUptime(numerator, denominator) + "%";
let uptime = ParseUptime(numerator, denominator) + "%";
const labelColor = query.get("labelColor") || "#333";
const color = query.get("color") || "#0079FF";
const style = query.get("style") || "flat";
const labelColor = query.get("labelColor") || "#333";
const color = query.get("color") || "#0079FF";
const style = query.get("style") || "flat";
label = label + (hideDuration ? "" : ` ${formatted}`);
label = label.trim();
label = label + (hideDuration ? "" : ` ${formatted}`);
label = label.trim();
const format = {
label: label,
message: uptime,
color: color,
labelColor: labelColor,
style: style
};
const svg = makeBadge(format);
const format = {
label: label,
message: uptime,
color: color,
labelColor: labelColor,
style: style,
};
const svg = makeBadge(format);
return new Response(svg, {
headers: {
"Content-Type": "image/svg+xml"
}
});
return new Response(svg, {
headers: {
"Content-Type": "image/svg+xml",
},
});
}
@@ -1,66 +1,57 @@
// @ts-nocheck
import {
GetMonitors,
GetIncidentsOpenHome,
GetIncidentsPage,
GetLocaleFromCookie
GetMonitors,
GetIncidentsOpenHome,
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,
addMonths,
parse,
subMonths,
getUnixTime,
startOfMonth,
endOfMonth
} from "date-fns";
import { format, addMonths, parse, subMonths, getUnixTime, startOfMonth, endOfMonth } from "date-fns";
import { f } from "$lib/i18n/client";
export async function load({ parent, url, params, cookies }) {
const parentData = await parent();
const siteData = parentData.site;
let monitors = await GetMonitors({ status: "ACTIVE" });
const query = url.searchParams;
let todayStart = BeginningOfDay({ timeZone: parentData.localTz });
let tomorrowStart = todayStart + 86400;
let locale = GetLocaleFromCookie(parentData.site, cookies);
let month = params.month; //January-2021
if (!!!month) {
month = format(new Date(), "MMMM-yyyy");
}
const parentData = await parent();
const siteData = parentData.site;
let monitors = await GetMonitors({ status: "ACTIVE" });
const query = url.searchParams;
let todayStart = BeginningOfDay({ timeZone: parentData.localTz });
let tomorrowStart = todayStart + 86400;
let locale = GetLocaleFromCookie(parentData.site, cookies);
let month = params.month; //January-2021
if (!!!month) {
month = format(new Date(), "MMMM-yyyy");
}
let monthStartTs = getUnixTime(startOfMonth(parse(month, "MMMM-yyyy", new Date())));
let monthStartTs = getUnixTime(startOfMonth(parse(month, "MMMM-yyyy", new Date())));
let monthEndTs = getUnixTime(endOfMonth(parse(month, "MMMM-yyyy", new Date())));
let monthEndTs = getUnixTime(endOfMonth(parse(month, "MMMM-yyyy", new Date())));
let incidents = await GetIncidentsPage(monthStartTs, monthEndTs);
let incidents = await GetIncidentsPage(monthStartTs, monthEndTs);
incidents = incidents.map((incident) => {
let incidentMonitors = incident.monitors;
let monitorTags = incidentMonitors.map((monitor) => monitor.monitor_tag);
let xm = monitors.filter((monitor) => monitorTags.includes(monitor.tag));
incidents = incidents.map((incident) => {
let incidentMonitors = incident.monitors;
let monitorTags = incidentMonitors.map((monitor) => monitor.monitor_tag);
let xm = monitors.filter((monitor) => monitorTags.includes(monitor.tag));
incident.monitors = xm.map((monitor) => {
return {
tag: monitor.tag,
name: monitor.name,
description: monitor.description,
image: monitor.image,
impact_type: incidentMonitors.filter((m) => m.monitor_tag === monitor.tag)[0]
.monitor_impact
};
});
return incident;
});
incident.monitors = xm.map((monitor) => {
return {
tag: monitor.tag,
name: monitor.name,
description: monitor.description,
image: monitor.image,
impact_type: incidentMonitors.filter((m) => m.monitor_tag === monitor.tag)[0].monitor_impact,
};
});
return incident;
});
return {
incidents: incidents,
nextMonthName: f(addMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en"),
prevMonthName: f(subMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en"),
thisMonthName: f(parse(month, "MMMM-yyyy", new Date()), "MMMM-yyyy", "en"),
canonical: siteData.siteURL + "/incidents/" + month
};
return {
incidents: incidents,
nextMonthName: f(addMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en", parentData.localTz),
prevMonthName: f(subMonths(parse(month, "MMMM-yyyy", new Date()), 1), "MMMM-yyyy", "en", parentData.localTz),
thisMonthName: f(parse(month, "MMMM-yyyy", new Date()), "MMMM-yyyy", "en", parentData.localTz),
canonical: siteData.siteURL + "/incidents/" + month,
};
}
+139 -159
View File
@@ -1,178 +1,158 @@
<script>
import { Badge } from "$lib/components/ui/badge";
import Incident from "$lib/components/IncidentNew.svelte";
import { Button } from "$lib/components/ui/button";
import { ArrowRight, ArrowLeft, CalendarCheck2, ChevronLeft } from "lucide-svelte";
import { base } from "$app/paths";
import { goto } from "$app/navigation";
import { l, f } from "$lib/i18n/client";
import { startOfDay, addDays, subDays, getUnixTime, parse } from "date-fns";
import { Badge } from "$lib/components/ui/badge";
import Incident from "$lib/components/IncidentNew.svelte";
import { Button } from "$lib/components/ui/button";
import { ArrowRight, ArrowLeft, CalendarCheck2, ChevronLeft } from "lucide-svelte";
import { base } from "$app/paths";
import { goto } from "$app/navigation";
import { l, f } from "$lib/i18n/client";
import { startOfDay, addDays, subDays, getUnixTime, parse } from "date-fns";
import { page } from "$app/stores";
export let data;
let selectedLang = data.selectedLang;
let incidents = data.incidents;
export let data;
let selectedLang = data.selectedLang;
let incidents = data.incidents;
let incidentSmartDates = {};
let incidentSmartDates = {};
function tsToDate(mnt) {
return mnt.unix();
}
function tsToDate(mnt) {
return mnt.unix();
}
incidents.forEach((incident) => {
let startTime = incident.start_date_time;
incidents.forEach((incident) => {
let startTime = incident.start_date_time;
const today = getUnixTime(startOfDay(new Date(startTime * 1000)));
const tomorrow = getUnixTime(startOfDay(addDays(new Date(startTime * 1000), 1)));
const dayAfterTomorrow = getUnixTime(startOfDay(addDays(new Date(startTime * 1000), 2)));
const yesterday = getUnixTime(startOfDay(subDays(new Date(startTime * 1000), 1)));
const dayBeforeYesterday = getUnixTime(startOfDay(subDays(new Date(startTime * 1000), 2)));
const today = getUnixTime(startOfDay(new Date(startTime * 1000)));
const tomorrow = getUnixTime(startOfDay(addDays(new Date(startTime * 1000), 1)));
const dayAfterTomorrow = getUnixTime(startOfDay(addDays(new Date(startTime * 1000), 2)));
const yesterday = getUnixTime(startOfDay(subDays(new Date(startTime * 1000), 1)));
const dayBeforeYesterday = getUnixTime(startOfDay(subDays(new Date(startTime * 1000), 2)));
if (!incidentSmartDates[today]) {
incidentSmartDates[today] = [];
}
if (!incidentSmartDates[tomorrow]) {
incidentSmartDates[tomorrow] = [];
}
if (!incidentSmartDates[dayAfterTomorrow]) {
incidentSmartDates[dayAfterTomorrow] = [];
}
if (!incidentSmartDates[yesterday]) {
incidentSmartDates[yesterday] = [];
}
if (!incidentSmartDates[dayBeforeYesterday]) {
incidentSmartDates[dayBeforeYesterday] = [];
}
if (!incidentSmartDates[today]) {
incidentSmartDates[today] = [];
}
if (!incidentSmartDates[tomorrow]) {
incidentSmartDates[tomorrow] = [];
}
if (!incidentSmartDates[dayAfterTomorrow]) {
incidentSmartDates[dayAfterTomorrow] = [];
}
if (!incidentSmartDates[yesterday]) {
incidentSmartDates[yesterday] = [];
}
if (!incidentSmartDates[dayBeforeYesterday]) {
incidentSmartDates[dayBeforeYesterday] = [];
}
incidentSmartDates[today].push(incident);
});
incidentSmartDates[today].push(incident);
});
//sort the incidentSmartDates ascending
let sortedIncidentSmartDates = Object.keys(incidentSmartDates).sort((a, b) => a - b);
let canonical = data.canonical;
//sort the incidentSmartDates ascending
let sortedIncidentSmartDates = Object.keys(incidentSmartDates).sort((a, b) => a - b);
let canonical = data.canonical;
</script>
<svelte:head>
<title>
{f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang)}
{l(data.lang, "Incident Updates")} |
{data.site.title}
</title>
<meta
name="description"
content="{l(data.lang, 'Incident Updates')} | {l(data.lang, 'Recent Incidents')} | {l(
data.lang,
'Recent Maintenances'
)} {f(parse(data.thisMonthName, 'MMMM-yyyy', new Date()), 'MMMM, yyyy', selectedLang)} |
<title>
{f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
{l(data.lang, "Incident Updates")} |
{data.site.title}
</title>
<meta
name="description"
content="{l(data.lang, 'Incident Updates')} | {l(data.lang, 'Recent Incidents')} | {l(
data.lang,
'Recent Maintenances'
)} {f(parse(data.thisMonthName, 'MMMM-yyyy', new Date()), 'MMMM, yyyy', selectedLang, $page.data.localTz)} |
{data.site.title}"
/>
<link rel="canonical" href={canonical} />
/>
<link rel="canonical" href={canonical} />
</svelte:head>
<div class="mt-12"></div>
<section class="mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<Button
variant="outline"
class="bounce-left h-8 justify-start pl-1.5"
on:click={() => {
return (window.location.href = `${base}/`);
}}
>
<ChevronLeft class="arrow mr-1 h-5 w-5" />
{l(data.lang, "Back")}
</Button>
<section class="section-back mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<Button
variant="outline"
class="bounce-left h-8 justify-start pl-1.5"
on:click={() => {
return (window.location.href = `${base}/`);
}}
>
<ChevronLeft class="arrow mr-1 h-5 w-5" />
{l(data.lang, "Back")}
</Button>
</section>
<section class="mx-auto mb-8 flex max-w-[655px] flex-1 flex-col items-start justify-center">
<div
class="mesh mx-auto min-w-full max-w-[655px] rounded-md px-4 py-12 lg:flex lg:items-center"
>
<div class="blurry-bg mx-auto max-w-3xl text-center text-muted">
<h1 class=" text-5xl font-extrabold leading-tight">
{f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang)}
</h1>
<p class="mx-auto mt-4 max-w-xl font-medium sm:text-xl">
{l(data.lang, "Incident Updates")}
</p>
</div>
</div>
<section class="section-month-banner mx-auto mb-8 flex max-w-[655px] flex-1 flex-col items-start justify-center">
<div class="mesh mx-auto min-w-full max-w-[655px] rounded-md px-4 py-12 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center text-muted">
<h1 class=" text-5xl font-extrabold leading-tight">
{f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
</h1>
<p class="mx-auto mt-4 max-w-xl font-medium sm:text-xl">
{l(data.lang, "Incident Updates")}
</p>
</div>
</div>
<div
class="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>
<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, "No Incident in %date", {
date: f(
parse(data.thisMonthName, "MMMM-yyyy", new Date()),
"MMMM, yyyy",
selectedLang
)
})}
</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)}
</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}
/>
</div>
{/each}
</div>
{/each}
</div>
<div
class="mx-auto mb-2 mt-4 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
>
<div class="flex w-full justify-between">
<Button
type="submit"
variant="secondary"
class="bounce-left"
on:click={() => {
window.location.href = `${base}/incidents/${data.prevMonthName}`;
}}
>
<ArrowLeft class="arrow mr-2 h-4 w-4" />
{f(parse(data.prevMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang)}
</Button>
<Button
variant="secondary"
class="bounce-right"
on:click={() => {
window.location.href = `${base}/incidents/${data.nextMonthName}`;
}}
>
{f(parse(data.nextMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang)}
<ArrowRight class="arrow ml-2 h-4 w-4" />
</Button>
</div>
</div>
<h1 class=" text-xl font-semibold leading-tight">
{l(data.lang, "No Incident in %date", {
date: f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)
})}
</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} />
</div>
{/each}
</div>
{/each}
</div>
<div class="mx-auto mb-2 mt-4 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]">
<div class="flex w-full justify-between">
<Button
type="submit"
variant="secondary"
class="bounce-left"
on:click={() => {
window.location.href = `${base}/incidents/${data.prevMonthName}`;
}}
>
<ArrowLeft class="arrow mr-2 h-4 w-4" />
{f(parse(data.prevMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
</Button>
<Button
variant="secondary"
class="bounce-right"
on:click={() => {
window.location.href = `${base}/incidents/${data.nextMonthName}`;
}}
>
{f(parse(data.nextMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang, $page.data.localTz)}
<ArrowRight class="arrow ml-2 h-4 w-4" />
</Button>
</div>
</div>
</section>
@@ -158,18 +158,18 @@
<!-- List -->
<ul class="flex flex-wrap items-center">
<li
class="before:size-[3px] relative inline-block pe-4 text-xs before:absolute before:end-1.5 before:top-1/2 before:-translate-y-1/2 before:rounded-full before:bg-gray-400 last:pe-0 last-of-type:before:hidden dark:text-neutral-500 dark:before:bg-neutral-600"
class="relative inline-block pe-4 text-xs before:absolute before:end-1.5 before:top-1/2 before:size-[3px] before:-translate-y-1/2 before:rounded-full before:bg-gray-400 last:pe-0 last-of-type:before:hidden dark:text-neutral-500 dark:before:bg-neutral-600"
>
<a
target="_blank"
class="text-xs font-medium text-muted-foreground hover:underline hover:decoration-1 focus:decoration-1 focus:outline-none"
href="https://github.com/sponsors/rajnandan1"
href="https://buymeacoffee.com/rajnandan1"
>
Donate
</a>
</li>
<li
class="before:size-[3px] relative inline-block pe-4 text-xs before:absolute before:end-1.5 before:top-1/2 before:-translate-y-1/2 before:rounded-full before:bg-gray-400 last:pe-0 last-of-type:before:hidden dark:text-neutral-500 dark:before:bg-neutral-600"
class="relative inline-block pe-4 text-xs before:absolute before:end-1.5 before:top-1/2 before:size-[3px] before:-translate-y-1/2 before:rounded-full before:bg-gray-400 last:pe-0 last-of-type:before:hidden dark:text-neutral-500 dark:before:bg-neutral-600"
>
<a
target="_blank"
@@ -180,7 +180,7 @@
</a>
</li>
<li
class="before:size-[3px] relative inline-block pe-4 text-xs before:absolute before:end-1.5 before:top-1/2 before:-translate-y-1/2 before:rounded-full before:bg-gray-400 last:pe-0 last-of-type:before:hidden dark:text-neutral-500 dark:before:bg-neutral-600"
class="relative inline-block pe-4 text-xs before:absolute before:end-1.5 before:top-1/2 before:size-[3px] before:-translate-y-1/2 before:rounded-full before:bg-gray-400 last:pe-0 last-of-type:before:hidden dark:text-neutral-500 dark:before:bg-neutral-600"
>
<a
target="_blank"
@@ -191,7 +191,7 @@
</a>
</li>
<li
class="before:size-[3px] relative inline-block pe-4 text-xs before:absolute before:end-1.5 before:top-1/2 before:-translate-y-1/2 before:rounded-full before:bg-gray-400 last:pe-0 last-of-type:before:hidden dark:text-neutral-500 dark:before:bg-neutral-600"
class="relative inline-block pe-4 text-xs before:absolute before:end-1.5 before:top-1/2 before:size-[3px] before:-translate-y-1/2 before:rounded-full before:bg-gray-400 last:pe-0 last-of-type:before:hidden dark:text-neutral-500 dark:before:bg-neutral-600"
>
<a
target="_blank"
@@ -62,6 +62,7 @@
})
});
let resp = await apiResp.json();
incidents = resp.incidents.map((incident) => {
let i = { ...incident };
if (!!!i.end_date_time) {
@@ -178,7 +179,7 @@
if (toPost.incident_type == "MAINTENANCE") {
toPost.state = "RESOLVED";
}
toPost.incident_source = "DASHBOARD";
formStateCreate = "loading";
try {
let data = await fetch(base + "/manage/app/api/", {
@@ -903,7 +904,7 @@
{#each comments as comment}
<div class="flex items-center justify-between gap-2 border-b py-2">
<div class="w-full rounded px-2 py-2 {newComment.id == comment.id ? 'bg-input' : ''}">
<p class="mb-2 text-xs font-medium">{comment.comment}</p>
<p class="mb-2 text-xs font-medium">{@html comment.comment}</p>
<div class="flex w-full justify-between gap-x-2">
<div class="text-xs font-semibold text-muted-foreground">
{#if currentIncident.incident_type == "INCIDENT"}
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+93
View File
@@ -0,0 +1,93 @@
Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

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