Compare commits

..

270 Commits

Author SHA1 Message Date
Raj Nandan Sharma e0cf568aa9 feat: implement custom handle function to modify response headers 2025-03-28 21:50:07 +05:30
Raj Nandan Sharma c12d7f8345 feat: add null check for document before dispatching analytics event 2025-03-28 21:18:26 +05:30
Raj Nandan Sharma 9ba115d5ab Merge pull request #366 from rajnandan1/release/3.2.11
release 3.2.11
2025-03-28 20:56:59 +05:30
Raj Nandan Sharma 3756bbfd06 chore: bump version to 3.2.11 in package.json 2025-03-28 20:54:50 +05:30
Raj Nandan Sharma 9096bcc641 feat: import analytics event module for enhanced tracking in incident page 2025-03-28 20:53:44 +05:30
Raj Nandan Sharma c4d04854e5 feat: enhance monitor deletion confirmation UI and add analytics event for incident back button click 2025-03-28 20:52:59 +05:30
Raj Nandan Sharma d95fc0005b feat: indicate Markdown support in incident summary and comment sections 2025-03-28 20:40:24 +05:30
Raj Nandan Sharma 2b9093184a feat: add support for Markdown language in events and enhance SEO documentation as requested in #359 2025-03-28 20:34:19 +05:30
Raj Nandan Sharma 831376054b feat: add analytics capture snippets and enhance event tracking also added plausible and ms clarity #355 2025-03-28 15:07:45 +05:30
Raj Nandan Sharma 885ee0b6d6 feat: implement monitor deletion functionality with confirmation 2025-03-26 11:47:53 +05:30
Raj Nandan Sharma 511d0f26dd feat: add email validation in CreateUpdateTrigger function 2025-03-26 10:16:36 +05:30
github-actions 5263698625 Auto-generate README.md with release versions 2025-03-25 17:45:05 +00:00
Raj Nandan Sharma 7859f9564e fix: bump version to 3.2.10 in package.json 2025-03-25 22:58:04 +05:30
github-actions 149456682d Auto-generate README.md with release versions 2025-03-25 17:20:55 +00:00
Raj Nandan Sharma 8c040997da fix: refactor monitorTriggers initialization into a separate function 2025-03-25 22:47:05 +05:30
Raj Nandan Sharma 868fed0176 fix: update tzdata version in Dockerfile to use latest available 2025-03-25 22:38:13 +05:30
Raj Nandan Sharma c7f44a48da fix: bump version to 3.2.9 in package.json 2025-03-25 22:31:57 +05:30
Raj Nandan Sharma 6300bff4e7 Merge pull request #363 from rajnandan1/fix/357
fix: update RBAC documentation and improve monitor data retrieval log…
2025-03-25 22:27:04 +05:30
Raj Nandan Sharma 3e6b1ffdbb fix: streamline query execution in DbImpl by removing redundant return statement 2025-03-25 22:24:10 +05:30
Raj Nandan Sharma 408fd5318b fix: update RBAC documentation and improve monitor data retrieval logic, fixes #357 and #361 2025-03-25 22:20:45 +05:30
Raj Nandan Sharma cfe419f6df Merge pull request #362 from rajnandan1/fix/360
Fix/360
2025-03-25 20:39:07 +05:30
Raj Nandan Sharma 5403726099 fix: replace parseInt with Number for timeout conversion in monitorSheet.svelte and ping.js 2025-03-25 20:38:23 +05:30
Raj Nandan Sharma 057c14909f fix #360 2025-03-25 20:29:30 +05:30
Raj Nandan Sharma 37b18c06bb fix: validate timeout input and ensure it's a number in monitorSheet.svelte 2025-03-25 20:28:39 +05:30
github-actions 3ad1eb9a03 Auto-generate README.md with release versions 2025-03-24 15:18:34 +00:00
Raj Nandan Sharma a1064771f8 fix: increment package version from 3.2.7 to 3.2.8 in package.json 2025-03-24 20:34:24 +05:30
Raj Nandan Sharma 463ee286bf fix: increment package version from 3.2.6 to 3.2.7 in package.json 2025-03-24 20:30:05 +05:30
Raj Nandan Sharma a7ae6e72ef fix: downgrade package version from 3.2.8 to 3.2.6 in package.json 2025-03-24 20:28:01 +05:30
github-actions 36c67f60dd Auto-generate README.md with release versions 2025-03-24 14:56:20 +00:00
Raj Nandan Sharma bb718b04a6 fix: update Dockerfile to use ARG variables for Node.js version tags 2025-03-24 20:10:13 +05:30
Raj Nandan Sharma b6ea060054 fix: update publish-images workflow and increment package version to 3.2.8 2025-03-24 20:01:53 +05:30
Raj Nandan Sharma 5dba54c048 fix: update publish-images workflow to set GH_TOKEN environment variable correctly 2025-03-24 19:53:29 +05:30
Raj Nandan Sharma 415276163f Merge pull request #356 from rajnandan1/release/3.2.8-x
fix: update Node.js version to 23 and increment package version to 3.2.7
2025-03-24 19:45:44 +05:30
Raj Nandan Sharma 017b9e2e84 fix: update Node.js version to 23 and increment package version to 3.2.7 2025-03-24 19:45:15 +05:30
Raj Nandan Sharma 5740991ee1 fix: update GitHub Actions workflow to use GH_PAT instead of GITHUB_TOKEN for authentication 2025-03-24 15:12:40 +05:30
Raj Nandan Sharma 06008bd719 fix: streamline Dockerfile by removing unnecessary paths and using variable references for base images 2025-03-24 12:11:03 +05:30
Raj Nandan Sharma b53cd485e5 Merge pull request #263 from kaffolder7/feature/dependabot-version-updates
feat: automate dependency updates 🤖
2025-03-24 11:59:41 +05:30
Raj Nandan Sharma 7c1fd39158 Merge branch 'main' into feature/dependabot-version-updates 2025-03-24 11:58:58 +05:30
Raj Nandan Sharma f7888f1ba4 fix: update package versions in Dockerfile for consistency and stability 2025-03-24 11:32:05 +05:30
Raj Nandan Sharma ece5ac37ad feat: update embed monitor URLs to use siteURL and base variables for improved flexibility 2025-03-24 11:19:51 +05:30
Raj Nandan Sharma a51183fb23 Merge pull request #348 from rajnandan1/rbac
Features role-based access control and user management
2025-03-24 10:45:39 +05:30
Raj Nandan Sharma 7875192fb0 feat: update package dependencies and improve invitation expiry date formatting 2025-03-24 10:17:21 +05:30
Raj Nandan Sharma eb128ad431 feat: add "View in detail" button for group monitors and update localization files 2025-03-23 22:44:11 +05:30
Raj Nandan Sharma ba61ed87ab feat: enhance incident month handling with improved date calculations and formatting, fixing #349 2025-03-23 21:51:51 +05:30
Raj Nandan Sharma 8b5348081d docs: update changelogs to include improvements in build time and component migration 2025-03-23 14:29:15 +05:30
Raj Nandan Sharma bb7e69ace5 fix: update icon imports to use specific paths from lucide-svelte 2025-03-23 14:28:04 +05:30
Raj Nandan Sharma 3b8bb60423 fix: update print width in configuration files and improve documentation links 2025-03-23 13:36:22 +05:30
Raj Nandan Sharma 93339ce9df feat: add support for self-signed certificates and update changelog, fixes #351 2025-03-23 12:32:36 +05:30
Raj Nandan Sharma d30d99d8c2 docs: update changelogs to include resolved GitHub issues and improve formatting 2025-03-22 22:57:26 +05:30
Raj Nandan Sharma 3468df16e1 fix: improve error handling and code structure in Minuter function #345 2025-03-22 22:50:17 +05:30
Raj Nandan Sharma ccdeff98dc feat: implement dynamic versioning in headers and documentation as reported in #346 2025-03-22 21:54:24 +05:30
Raj Nandan Sharma 7e182b1df2 fix: replace showModal with closeModal for better modal handling #350 2025-03-22 21:06:37 +05:30
Raj Nandan Sharma ff652ad435 fix: #238 #210 #309 #334 2025-03-22 16:42:21 +05:30
Raj Nandan Sharma 1fdafa7966 Adds badges and embed features
Implements badges for monitors, including status, uptime, and liveness, along with a dedicated management page.

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

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

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

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

Improves performance, security, UI, and developer experience.

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

Removes libcap related code from Dockerfile.
2025-03-18 23:08:51 +05:30
Raj Nandan Sharma 52286f26a5 fix: update version to 3.2.5 and reflect changes in User-Agent and documentation 2025-03-13 09:55:04 +05:30
github-actions 45e7567cb6 Auto-generate README.md with release versions 2025-03-12 17:48:41 +00:00
Raj Nandan Sharma c0edb59884 chore: update version to 3.2.4 and reflect changes in User-Agent and documentation 2025-03-12 23:16:48 +05:30
Raj Nandan Sharma 1e86c429ca fix: install libcap tools for setting capabilities in Dockerfile 2025-03-12 23:00:18 +05:30
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 8c95c94472 update: combine to avoid multiple processes
Combined to reduce spawning multiple `rm` processes.
2025-02-17 13:29:34 -05:00
Kyle Affolder 7dafb2eddc add: GHA job to confirm if Dependabot PRs exist
Adds job to check if any Dependabot PRs are open and if so, fail the Docker build (since we need to ensure OS packages exist and are in their correct versions when using pinned versions for security purposes).
2025-02-17 13:24:22 -05:00
Kyle Affolder 7c9f3eb87f add: add back pinned tzdata version
Necessary for Dependabot to track
2025-02-17 13:22:15 -05:00
Kyle ab34dd81f8 Merge branch 'rajnandan1:main' into feature/dependabot-version-updates 2025-02-17 13:04:23 -05:00
Kyle Affolder 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
Raj Nandan Sharma f43048d783 fix: update documentation version to 3.1.4 2025-02-17 07:06:35 +05:30
Raj Nandan Sharma b5ec332b4a Updates version to 3.1.3 and fixes group query
Updates the Kener version from 3.1.2 to 3.1.3.

Refactors the group query to use `havingRaw` for better compatibility across different database systems.

Adds database information to the bug report template.
2025-02-17 07:05:31 +05:30
github-actions dab1c44b18 Auto-generate README.md with release versions 2025-02-16 14:48:25 +00:00
Raj Nandan Sharma d9c0bff780 fix: update Dockerfile to remove specific tzdata version for compatibility 2025-02-16 19:56:15 +05:30
Raj Nandan Sharma beceace2c2 fix: update Dockerfile to correct documentation directory removal path 2025-02-16 19:23:45 +05:30
Raj Nandan Sharma 5373636704 Merge pull request #289 from rajnandan1/feature/category-status
Feature/category status
2025-02-16 19:09:22 +05:30
Raj Nandan Sharma e8d04eccf6 chore: bump version to 3.1.2 in package.json and update documentation layout 2025-02-16 19:08:01 +05:30
Raj Nandan Sharma d978c82263 Updates documentation and Dockerfile configuration
Updates documentation to reflect the new directory structure.
The documentation now correctly references images in the `/documentation` directory.
Removes the `src/static/documentation` directory in the Dockerfile.
2025-02-16 18:31:18 +05:30
Raj Nandan Sharma 883a458ed3 feat: add GroupCall service and integrate into monitoring logic #249 and #221 2025-02-16 12:23:58 +05:30
Raj Nandan Sharma 9009d9df99 refactor: streamline modal close logic and clear URL hash 2025-02-14 15:33:56 +05:30
Raj Nandan Sharma 81c0fa1243 fix: clear URL hash when modals are closed 2025-02-13 23:11:58 +05:30
github-actions 01221616ae Auto-generate README.md with release versions 2025-02-13 04:51:19 +00:00
Raj Nandan Sharma be75a0f5e9 Merge pull request #286 from rajnandan1/fix/seed-data-no-trackers
fix: update site metadata and image references for clarity
2025-02-13 10:05:46 +05:30
Raj Nandan Sharma 1b40839490 fix: update site metadata and image references for clarity 2025-02-13 10:02:18 +05:30
Raj Nandan Sharma 80f5780602 Merge pull request #284 from rajnandan1/feature/url-images
feat: Updates CSS and Svelte components
2025-02-12 22:23:16 +05:30
Raj Nandan Sharma dcf1817cd6 fix: add rel attribute to external link in monitor component 2025-02-12 22:21:41 +05:30
Raj Nandan Sharma ae251803ad refactor: remove unused sitemap generation code and update site metadata for clarity 2025-02-12 22:15:42 +05:30
Raj Nandan Sharma f6d9627ceb fix: update site title for clarity and conciseness 2025-02-12 22:07:58 +05:30
Raj Nandan Sharma 0cbc6b7b56 Update hero title for clearer branding
Updates hero title text to emphasize quick status page creation
and improves consistency in site configuration.
2025-02-12 22:05:51 +05:30
Raj Nandan Sharma f0af2d14e4 feat: Updates CSS and Svelte components
Enhances incident display with improved component structure.
2025-02-12 22:00:12 +05:30
github-actions b797039144 Auto-generate README.md with release versions 2025-02-12 02:49:23 +00:00
Kyle 86e29e5065 Merge pull request #283 from kaffolder7/fix/generate-readme-gha
fix(gha): generate-readme
2025-02-11 21:48:24 -05:00
Kyle Affolder fddef22e8e fix(gha): generate-readme
Was missing fallbacks in case repo variable was not set.
2025-02-11 21:47:52 -05:00
github-actions ad7551b7af Auto-generate README.md with release versions 2025-02-12 02:38:53 +00:00
Kyle Affolder a8be878a87 update: commented out node.js deps. monitoring
Not sure if we are wanting Dependabot to track Node.js packages, so for the time being, commenting this block out, but leaving for now w/ “TODO” to come back to at a later point.
2025-02-11 17:54:37 -05:00
Kyle Affolder 2b1849f1a3 update: grammar updates 2025-02-11 17:47:45 -05:00
Kyle Affolder 25a6590324 Merge branch 'feature/dependabot-version-updates' of https://github.com/kaffolder7/kener into feature/dependabot-version-updates 2025-02-11 17:31:48 -05:00
Kyle Affolder fa251dc07f update: README generation workflow 2025-02-11 17:31:33 -05:00
Kyle 4de93e7e95 Merge branch 'main' into feature/dependabot-version-updates 2025-02-11 17:25:28 -05:00
Kyle Affolder a5e5f33dc8 Merge branch 'main' into feature/dependabot-version-updates
# Conflicts:
#	.github/workflows/publish-images.yml
#	Dockerfile
2025-02-11 17:17:45 -05:00
Kyle f09eb18bcc Merge pull request #279 from kaffolder7/fix/readme-generation
fix: protect-readme workflow
2025-02-11 15:48:00 -05:00
Kyle 5c65e35f24 Merge branch 'rajnandan1:main' into fix/readme-generation 2025-02-11 15:46:45 -05:00
Kyle Affolder 8cbe859971 fix: protect-readme workflow 2025-02-11 15:46:05 -05:00
Kyle 67f38db847 Merge pull request #278 from kaffolder7/fix/readme-generation
fix: README generation
2025-02-11 15:41:12 -05:00
Kyle Affolder 28a72a3592 fix: README generation
Changed from trying to use artifacts and the GHA workflow failing to now using a simple `BUILD_VERSION` repository variable and automatically updating that when the `build-and-push-to-registries` workflow succeeds.

Other changes include:

* Added `workflow_run` trigger to `generate-readme.yml` so when that workflow recognizes the “Publish Docker Image to Registries” workflow runs and succeeds, it will automatically run the `generate-readme.yml` workflow (since a new Docker release will require Docker image variants table in README.md to have versioning updated)
* Generate major and major-minor versions from the `BUILD_VERSION` repository variable (more efficient than storing three separate variables from the `build-and-push-to-registries` workflow job)
2025-02-11 15:39:42 -05:00
Kyle dae334694d Merge pull request #274 from kaffolder7/feature/multi-arch-docker-build
manual readme update
2025-02-10 23:48:37 -05:00
Kyle e57fe3a046 Merge branch 'rajnandan1:main' into feature/multi-arch-docker-build 2025-02-10 23:48:04 -05:00
Kyle Affolder 54670c7845 fix: broken Docker latest ver. badge
Setting up to also pull latest ver. value from Mustache template
2025-02-10 23:47:28 -05:00
Kyle 3dbcef1500 Merge pull request #273 from kaffolder7/feature/multi-arch-docker-build
manual readme update
2025-02-10 23:36:18 -05:00
Kyle 24bbad05cb Merge branch 'rajnandan1:main' into feature/multi-arch-docker-build 2025-02-10 23:34:41 -05:00
Kyle Affolder cacbda3164 manual update
until I can get CI/CD pipeline debugged and fixed
2025-02-10 23:34:23 -05:00
Kyle da9b1a3a64 Merge pull request #272 from kaffolder7/feature/multi-arch-docker-build
fix: missing versions artifact
2025-02-10 23:25:38 -05:00
Kyle Affolder 394b911eca Merge branch 'feature/multi-arch-docker-build' of https://github.com/kaffolder7/kener into feature/multi-arch-docker-build 2025-02-10 23:25:03 -05:00
Kyle Affolder 220778e84f fix: missing versions artifact 2025-02-10 23:24:24 -05:00
Kyle c6e343da5d Merge pull request #271 from kaffolder7/feature/multi-arch-docker-build
fix: README build
2025-02-10 23:17:49 -05:00
Kyle 268e5b7478 Merge branch 'rajnandan1:main' into feature/multi-arch-docker-build 2025-02-10 23:17:13 -05:00
Kyle Affolder b9ccd16a02 fix: delete existing version artifact
* Delete existing version artifact if it exists
* Upload `versions.txt` only on first successful job
2025-02-10 23:16:20 -05:00
Kyle 302ad0e8a6 Merge pull request #270 from kaffolder7/feature/multi-arch-docker-build
fix: CI/CD README generation pipeline
2025-02-10 23:00:39 -05:00
Kyle Affolder e052f435db fix(README): docker versions table
Attempting to fix CI/CD pipeline so that README generation succeeds and versions are properly injected.
2025-02-10 22:58:55 -05:00
Kyle Affolder caa1330f39 remove: unnecessary attribute 2025-02-10 22:56:54 -05:00
Raj Nandan Sharma 870d19f566 docs: updated deployment docs 2025-02-11 08:39:09 +05:30
github-actions a28f787833 Auto-generate README.md with release versions 2025-02-11 01:55:21 +00:00
Raj Nandan Sharma 335a51b589 Merge pull request #265 from rajnandan1/release/3.0.13
feat: test monitors in manage monitor dashboard
2025-02-11 07:08:50 +05:30
Raj Nandan Sharma 72f9471486 chore: merged main with new release/3.1.0 2025-02-11 07:06:45 +05:30
Raj Nandan Sharma f0cd101af5 Merge branch 'main' into release/3.0.13 2025-02-11 07:04:12 +05:30
Raj Nandan Sharma 52876bd77e fix: fix #266 2025-02-11 07:01:17 +05:30
Raj Nandan Sharma aabe1926bd Merge pull request #268 from kaffolder7/feature/multi-arch-docker-build
fix: README generation
2025-02-11 06:55:28 +05:30
Kyle Affolder e785227064 Merge branch 'feature/multi-arch-docker-build' of https://github.com/kaffolder7/kener into feature/multi-arch-docker-build 2025-02-10 17:22:47 -05:00
Kyle Affolder 0cb88eec3b update: cleanup 2025-02-10 17:22:28 -05:00
Kyle 8cd2914f73 Merge branch 'rajnandan1:main' into feature/multi-arch-docker-build 2025-02-10 17:19:11 -05:00
Kyle Affolder f70e2ee8eb update: CI/CD pipeline
Changes include:

* Moving README generation to separate workflow (so that it can be trigger to run when any changes to `README.template.md` are pushed to `main` branch or a PR is opened with changes to template file
* GitHub Actions do not have privileges via `GITHUB_TOKEN` to commit to protected branches, thus, we need to take another approach and utilize a personal access token (which you’ll need to generate @rajnandan1) and add to the repository secrets (to avoid exposing that credential).
* Changes `publish-images` workflow to run now only when a new GitHub Release is created. (This will help prevent excessive workflow runs on merges into `main`)…in other words, @rajnandan1, you can merge freely into `main` now without excessive GitHub Actions usage.
2025-02-10 17:18:06 -05:00
Kyle c60a21ef32 Merge pull request #267 from kaffolder7/feature/multi-arch-docker-build
fix: multi-arch docker build
2025-02-10 16:19:01 -05:00
Kyle Affolder 77a57ee609 fix(docker): build issues
The following changes have been made:

* Ensured `package-lock.json` is up-to-date with latest dependencies from `package.json` - moved check to new workflow job and set as dependency for ‘build-and-push-to-registries’ job
* Updated branch-tagging for non-main branches (used when building Docker images)
* Restored pinned OS package versions in Dockerfile (for best-security)
* Restored “TODO” comments to Dockerfile (for tracking purposes and because I will revisit those items later this week)
* Added `—no-fund` tag to suppress npm package funding messages (helpful for CI/CD)
* Changed from `wget` to `curl` to resolve Debian package versioning issue between differing architectures (was one of the reasons causing the build to fail)
* As a part of the last comment, needed to then conditionalize container healthcheck logic
* Checked in newest `package-lock.json` file
* Fixed broken Docker badges in `README.template.md`
2025-02-10 16:15:08 -05:00
Kyle fc15f0e083 Merge branch 'rajnandan1:main' into feature/multi-arch-docker-build 2025-02-10 12:54:09 -05:00
Raj Nandan Sharma 6115beece3 fix: docker fix 2025-02-10 23:05:06 +05:30
Kyle Affolder 54434fbe78 fix: update package-lock.json
Hopefully will fix “npm error Missing: mustache@4.2.0 from lock file” error in broken GitHub Action docker build
2025-02-10 12:32:51 -05:00
Raj Nandan Sharma 83755bdd25 fix: docker fix 2025-02-10 22:59:07 +05:30
Raj Nandan Sharma 08f901c5f0 fix: docker fix 2025-02-10 22:53:30 +05:30
Raj Nandan Sharma 06910fbd4d fix: docker fix 2025-02-10 22:46:33 +05:30
Raj Nandan Sharma 154e7dd185 fix: docker fix 2025-02-10 22:41:18 +05:30
Raj Nandan Sharma 8bde3226bf Merge pull request #258 from kaffolder7/feature/multi-arch-docker-build
refactor(docker): multi-arch docker 🐳 build overhaul 🏗️
2025-02-10 22:04:28 +05:30
Kyle Affolder e5565145b5 fix(docker): action version number 2025-02-10 11:24:07 -05:00
Kyle Affolder 37a667daff fix(docker): dynamic README generation
🔄 Automate README Generation via Mustache Templating

- Use Mustache to dynamically generate `README.md` from `README.template.md`.
- Populate README with environment variables (e.g., `KENER_BUILD_FULL_VERSION`).
- Prevent direct edits to `README.md` by enforcing updates via the template.
- Enhance GitHub Actions workflow to auto-generate and commit the README.
- Add GitHub Action workflow (`protect-readme.yml`) to prevent others from direct updates to `README.md` via PR.
2025-02-10 11:21:22 -05:00
Kyle Affolder eda98bacfc update(docker): temporarily remove README updating
I caught an issue where the README will only auto-update listed Docker versions the first time. Commenting out for now (in case this PR gets merged before I have time to fix this). Will revisit this and fix this week.
2025-02-10 09:27:11 -05:00
Raj Nandan Sharma ee1ee52e13 feat: test monitors in manage monitor dashboard 2025-02-10 11:06:36 +05:30
Kyle Affolder 4060094404 add(dependabot): to automate dependency updates
Integrating Dependabot into the workflow ensures automatic dependency updates, improving security, reducing technical debt, and keeping packages up to date with minimal manual effort. This helps prevent vulnerabilities and maintain code stability over time.

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

1. Scans for Outdated Dependencies – It checks project dependency files (e.g., package.json, Dockerfile, .env.build, etc.) for outdated versions.
2. Fetches Latest Versions – When a newer version of a dependency is available, Dependabot retrieves it and updates the dependency files accordingly.
3. Opens a Pull Request – It then creates a PR with the updated dependency, detailing the changes and linking to release notes, changelogs, or security advisories.
4. Runs CI/CD Tests – If we end up setting up continuous integration (CI) tests, the PR will trigger the tests to check for breaking changes.
5. Security Updates – Dependabot also detects vulnerable dependencies and creates PRs to update them to a secure version.
6. Auto-Merging (Optional) – We might consider this at a later point, but if configured, it can automatically merge PRs when updates pass all tests and meet the requirements.
2025-02-09 22:08:06 -05:00
Kyle d03bf41ad1 Merge branch 'rajnandan1:main' into feature/multi-arch-docker-build 2025-02-09 21:56:54 -05:00
Raj Nandan Sharma ba2fe24629 Merge pull request #262 from rajnandan1/release/3.0.12
feat: adding hash params for modals in manage
2025-02-09 22:13:33 +05:30
Raj Nandan Sharma 547116090a feat: adding hash params for modals in manage 2025-02-09 22:11:07 +05:30
Raj Nandan Sharma 974826f42d Merge pull request #261 from rajnandan1/release/3.0.11
fix: handle ping migration
2025-02-09 19:49:46 +05:30
Raj Nandan Sharma 4914b029f9 fix: handle ping migration 2025-02-09 19:38:16 +05:30
Kyle Affolder ff864fbaab fix(README): restore accidentally removed tag
Noticed when doing some cleanup, that you had two awesome tags, but they both point to different URLs/repos. I added back in the one I had inadvertently removed.
2025-02-09 01:04:53 -05:00
Kyle Affolder f583ba4938 update(docker): README badge direct links
add direct links to filtered image(s) on Docker Hub, based on whether Debian or Alpine Linux variant badges are clicked
2025-02-09 00:44:28 -05:00
Kyle Affolder d552f541ac fix(docker): remove unnecessary files from build 2025-02-09 00:35:59 -05:00
Kyle d46d02e37a Merge branch 'main' into feature/multi-arch-docker-build 2025-02-09 00:26:21 -05:00
Raj Nandan Sharma 4e0c6e85da style: added more readme badges 2025-02-09 09:31:01 +05:30
Raj Nandan Sharma 1147808366 Merge pull request #256 from kaffolder7/feature/small-improvements
feat: small improvements
2025-02-09 09:04:13 +05:30
Kyle a7c27a60e0 Merge branch 'rajnandan1:main' into feature/multi-arch-docker-build 2025-02-08 15:32:48 -05:00
Kyle Affolder 974976bd90 update(docker): expanded on existing examples 2025-02-08 15:31:32 -05:00
Kyle Affolder 9bbe665984 update(docker): add TODO comments for future work 2025-02-08 14:22:16 -05:00
Kyle Affolder d0ea8551b6 update(docker): add TODO comments for future work 2025-02-08 13:56:11 -05:00
Kyle Affolder 089ee9bc07 update(formatting): made consistent
Aligned `.prettierrc` and `.editorconfig` files to same, best-practice & most-widely-adopted standards.
2025-02-08 13:39:41 -05:00
Raj Nandan Sharma 786c4f8207 fix: fixed cookie getting set in different paths from client side when language was changed^C 2025-02-08 21:37:18 +05:30
Raj Nandan Sharma 13879aefdd Merge pull request #253 from VincentDatrier/main
Add Norwegian (Bokmål) as an available locale
2025-02-08 17:01:07 +05:30
Kyle Affolder d03a8fd7a4 update(README): fix typo 2025-02-08 04:52:17 -05:00
Kyle Affolder c00aae5566 update(README): adjusted emoji spacing 2025-02-08 04:50:06 -05:00
Kyle Affolder e923f4d650 update(README): proofread and polished ☺️ 2025-02-08 04:48:30 -05:00
Kyle Affolder 654c07a364 fix(README): table subheading alignment 2025-02-08 04:19:34 -05:00
Kyle Affolder 820eeb0aac fix(README): table subheading alignment 2025-02-08 04:18:27 -05:00
Kyle Affolder 2a338baa26 fix(README): remove broken icons that were added 2025-02-08 04:17:16 -05:00
Kyle Affolder ec7351272f add(docker): missing entrypoint.sh file 2025-02-08 04:13:47 -05:00
Kyle Affolder 13dec43ef3 update(docker): expanded docker readme section
* Expanded upon existing Docker README section.
* Created table which will contains version placeholder variables that will be replaced by new GitHub workflow job: “update_readme”. Job automatically runs after new images have been built & pushed to container registries.
2025-02-08 04:12:50 -05:00
Kyle Affolder 43673e3349 add(workflow): allow admin to manually build 2025-02-08 04:08:45 -05:00
Kyle Affolder 1e77253a63 update(docker): healthcheck port & path + cleanup 2025-02-08 02:12:31 -05:00
Kyle Affolder 798af326a2 fix: update docs link to online documentation
Default documentation link in main nav won’t work because /docs are not included in built Docker images (to keep image smaller). Instead, changing seed data to point to the docs homepage. :)
2025-02-08 00:13:49 -05:00
Kyle Affolder d349a7591e update(docker): ver. pinning, healthcheck, etc.
* add: version pinning (better stability)
* remove: unnecessary KENER_BASE_PATH env. variable
* update: reduce permissions of /uploads and /database directories
* add: `entrypoint.sh` file
* add: properly map container timezone and localtime
* add: container healthcheck
* change: restrict to non-root “node” user
2025-02-08 00:04:19 -05:00
Kyle Affolder 8370145f6c update: change fonts API
Switching from Google Fonts to Bunny Fonts CDN. Bunny Fonts is an open-source, privacy-first web font platform. It is fully GDPR compliant (Google is not) and can act as a drop-in replacement for Google Fonts.
2025-02-07 20:14:45 -05:00
Kyle 304945acea Merge branch 'rajnandan1:main' into feature/small-improvements 2025-02-07 19:51:50 -05:00
Kyle Affolder 1dd05fee50 update: tweaked ordering of env variables 2025-02-07 14:58:33 -05:00
Kyle 374eda4103 Merge branch 'rajnandan1:main' into feature/multi-arch-docker-build 2025-02-07 14:55:05 -05:00
Kyle Affolder 52a8cca3e8 fix(docker): broken build & run as non-root user 2025-02-07 13:06:17 -05:00
Vincent Datrier 22ef4d75b6 Trigger GitHub Actions 2025-02-07 14:11:39 +01:00
Vincent Datrier 534e6a0ad3 Updated nb-NO locale with new strings 2025-02-07 13:46:32 +01:00
Vincent Datrier 80ed4fc5fa Merge branch 'main' of github.com:VincentDatrier/kener 2025-02-07 13:42:06 +01:00
Raj Nandan Sharma 445eb02386 chore: optimized seo 2025-02-07 10:24:06 +05:30
Raj Nandan Sharma 53ae0b89b0 chore: optimized seo 2025-02-07 10:16:49 +05:30
Raj Nandan Sharma c7376b4d8f Merge pull request #250 from rajnandan1/release/3.0.10
Release/3.0.10
2025-02-07 09:13:20 +05:30
Raj Nandan Sharma e3e59b7e24 feat: adding tcp monitor, reinstating ping as reported in #243 2025-02-07 09:12:15 +05:30
Raj Nandan Sharma 07f59ac581 feat: adding tcp monitor, reinstating ping as reported in #243 2025-02-07 09:02:28 +05:30
Kyle Affolder 7176c3d4f4 refactor(gha): update gh publish image workflow
Streamlined the GitHub `publishImage.yml` workflow with the following functionality:

* Handle both Alpine and Debian variants through matrix strategy
* Push to both Docker Hub and GitHub Container Registry
* Add comprehensive tagging strategy, handling both branches (aka release version, e.g. 1.0.0), semantic versions (major.minor and major), and latest versions (`latest` and `alpine`)
* Add security aspects (cosign signing, proper permissions)
* Add better caching and multi-platform build settings

With this revised workflow, the following Docker image variants will be built for every successful release. As an example, if the release version is “3.0.9”, then the following Docker image variants will be built:

Debian variants (default):
- `kener:3.0.9` (Semver of current release)
- `kener:latest` (Latest Debian release, ’latest’ label points to 3.0.9)
- `kener:3.0` (major.minor version, major.minor ‘3.0’ label points to 3.0.9)
- `kener:3` (major version, major ‘3’ label points to 3.0.9)

Alpine variants (smallest filesize):
- `kener:3.0.9-alpine` (Semver of current release)
- `kener:alpine` (Latest Alpine release, ‘alpine’ label points to 3.0.9)
- `kener:3.0-alpine` (major.minor version, major.minor ‘3.0-alpine’ label points to 3.0.9)
- `kener:3-alpine` (major version, major ‘3-alpine’ label points to 3.0.9)
2025-02-05 22:24:09 -05:00
Kyle Affolder 13366284c6 add(docker): docker-specific README badges 2025-02-05 21:03:18 -05:00
Kyle Affolder 0f0b447137 remove: forks badge from README 2025-02-05 20:47:51 -05:00
Kyle Affolder f7cc28c896 add: badges to README 2025-02-05 20:45:10 -05:00
Kyle Affolder fd790003d1 add: editorconfig to enforce consistent styles 2025-02-05 18:40:10 -05:00
Kyle Affolder fdad329148 update(docker): simplified variable name 2025-02-05 18:37:10 -05:00
Kyle Affolder 73bf5f3fbe refactor(docker): improve build w/ multistage
* Switch to multi-stage build pattern for smaller image size
* Add support for both Alpine and Debian variants via build args
* Change default image base to `node:23-slim` instead of using `node:23` (no need for full Debian base present in `node:23` since now prioritization is given to production-ready builds)
* Improve caching with --mount for npm dependencies
* Separate build and runtime dependencies
* Remove unnecessary Node.js packages in final stage
* Fix permissions on uploads/database directories
* Add proper scoping for build arguments
* Set NODE_ENV=production for better performance

This change reduces the final image size and improves build caching while adding flexibility to choose between Alpine and Debian base images.

Original: ~1.2GB
New Alpine: ~350MB
New Debian: ~450MB
2025-02-05 14:03:46 -05:00
Raj Nandan Sharma 103d64a659 fix: fix bug where incident status is not getting updated when adding comment #245 2025-02-05 22:43:33 +05:30
Raj Nandan Sharma 92c4d35992 fix: fix bug where incident status is not getting updated #246 2025-02-05 22:42:29 +05:30
Vincent Datrier 7f33f6ddfd Add Norwegian (Bokmål) as an available locale 2025-02-04 14:48:25 +01:00
307 changed files with 31320 additions and 22404 deletions
+3
View File
@@ -1,4 +1,7 @@
node_modules
.git
.github
.vscode
dist
build
.env
+58
View File
@@ -0,0 +1,58 @@
# EditorConfig helps maintain consistent coding styles between editors
root = true
# Default settings for all files (e.g. most common, best-practice standard across all filetypes)
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
# Svelte files
[*.svelte]
indent_style = space
indent_size = 2
trim_trailing_whitespace = false
# JavaScript and TypeScript
[*.{js,ts,tsx,cjs,mjs}]
indent_style = space
indent_size = 2
# JSON files (package.json, config files, etc.) - per JSON (RFC 8259) specification
[*.json,.prettierrc]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = false
# YAML files (e.g., GitHub Actions, Lint configs) - per YAML 1.2 (2009) specification
[*.{yaml,yml}]
indent_style = space
indent_size = 2
# CSS & PostCSS files
[*.{css,postcss}]
indent_style = space
indent_size = 2
# Markdown files
[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false
print_width = 180
# Dockerfile
[Dockerfile*]
indent_style = tab
indent_size = 4
insert_final_newline = false
# Ignore binary files
[*.{png,jpg,jpeg,gif,ico,svg,woff,woff2,eot,ttf,otf}]
charset = unset
trim_trailing_whitespace = false
insert_final_newline = false
+23 -9
View File
@@ -1,12 +1,26 @@
KENER_SECRET_KEY=please_change_me
NODE_ENV=production
PORT=3000
KENER_BASE_PATH=""
RESEND_API_KEY=
ORIGIN=http://localhost:3000
TZ=Etc/UTC
KENER_SECRET_KEY=please_change_me_to_something_secure
# For SQLite database...
DATABASE_URL=sqlite://./database/kener.sqlite.db
TZ=UTC
# For PostgreSQL database...
# DATABASE_URL=postgresql://db_user:db_password@localhost:5432/kener_db
# POSTGRES_PASSWORD=some_super_random_secure_password
# For MySQL database...
# DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/kener_db
# MYSQL_PASSWORD=some_super_random_secure_password
KENER_BASE_PATH=""
ORIGIN=http://localhost:3000
RESEND_API_KEY=""
RESEND_SENDER_EMAIL=Accounts <accounts@resend.dev>
# DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase
# DATABASE_URL=mysql://root:password@127.0.0.1:3306/kener
# Likely no need to change...
# NODE_ENV=production # already defined in container
# PORT=3000 # default port Kener service is exposed upon
# Add the below variable if you would like to white-label the product (aka. remove some of the attributions scattered throughout the app)
# WHITE_LABEL=true
+9 -11
View File
@@ -17,22 +17,20 @@ Steps to reproduce the behavior:
3. Scroll down to '....'
4. See error
**Version**
Which version of kener you are using.
**Environment**
Which environment you are using or where is it deployed. `docker`, `kubernetes`, `bare-metal`, `development`, `pm2` etc
**Database**
Which database you are using. `sqlite`, `mysql`, `postgres`
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
@@ -1,13 +0,0 @@
---
name: Create Incident Template
about: Create Incident Template
title: Title of Incident
labels: incident
assignees: ''
---
Your Incident Description goes here. Markdown Supported
[start_datetime:utcSeconds]
[end_datetime:utcSeconds]
+47
View File
@@ -0,0 +1,47 @@
version: 2
updates:
# Track base image versions via .env.build
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
file-patterns:
- ".env.build"
- "node:*" # Ensures Node.js images are correctly detected
# Monitor OS package versions in Dockerfile (Debian/Alpine)
- package-ecosystem: "gitsubmodule" # Alternative method to track OS packages in Dockerfile
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "os-packages"
commit-message:
prefix: "os"
include: "scope"
# Monitor Node.js dependencies from package.json
# TODO: Uncomment below if we want to begin letting Dependabot monitor & open PRs for Node.js project dependencies
# - package-ecosystem: "npm"
# directory: "/"
# schedule:
# interval: "weekly"
# labels:
# - "dependencies"
# - "npm"
# commit-message:
# prefix: "npm"
# include: "scope"
# Monitor GitHub Actions dependencies
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "actions"
include: "scope"
+83
View File
@@ -0,0 +1,83 @@
name: Generate README
on:
push:
branches:
- main
paths:
- 'README.template.md'
pull_request:
branches:
- main
paths:
- 'README.template.md'
workflow_dispatch: # Allows for manual execution
workflow_run: # Triggers this workflow to run when it recognizes 'publish-images' workflow has ran and successfully completed
workflows: ["Publish Docker Image to Registries"]
types:
- completed
permissions:
contents: write # Explicitly allow pushing changes
jobs:
generate-readme:
name: Generate README from template
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0 # Fetch full history, including tags
persist-credentials: false # We'll manually authenticate
- name: Configure Git
run: |
git config --global user.name "github-actions"
git config --global user.email "github-actions@github.com"
- name: Setup Node.js
uses: actions/setup-node@v4.2.0
with:
node-version: "20"
- name: Install Dependencies
run: npm install mustache dotenv
- name: Extract Major and Major-Minor Versions
run: |
VERSION="${{ vars.BUILD_VERSION }}"
# 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")
fi
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
echo "Full version: $VERSION"
echo "Major version: $MAJOR"
echo "Major-Minor version: $MAJOR_MINOR"
# Export all as environment variables
echo "LATEST_VERSION=$VERSION" >> $GITHUB_ENV
echo "LATEST_MAJOR_VERSION=$MAJOR" >> $GITHUB_ENV
echo "LATEST_MAJOR_MINOR_VERSION=$MAJOR_MINOR" >> $GITHUB_ENV
- name: Generate README.md
env:
BUILD_FULL_VERSION: ${{ env.LATEST_VERSION }} # e.g., 1.2.3
BUILD_MAJOR_VERSION: ${{ env.LATEST_MAJOR_VERSION}} # e.g., 1
BUILD_MAJOR_MINOR_VERSION: ${{ env.LATEST_MAJOR_MINOR_VERSION }} # e.g., 1.2
run: node scripts/generate-readme.js
- name: Commit and Push Changes
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
git add README.md
git commit -m "Auto-generate README.md with release versions" || echo "No changes to commit"
git push https://x-access-token:${{ secrets.GH_PAT }}@github.com/${{ github.repository }}.git HEAD:main
+21
View File
@@ -0,0 +1,21 @@
name: Prevent Direct README Changes
on:
pull_request:
paths:
- "README.md"
jobs:
check-readme:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4.2.2
- name: Detect direct README changes
run: |
if git diff --name-only origin/main | grep -q "README.md"; then
echo "❌ Direct modifications to README.md are not allowed!"
echo "Please update README.template.md instead."
exit 1
fi
+156
View File
@@ -0,0 +1,156 @@
name: Publish Docker Image to Registries
on:
release:
types:
- published # Runs only when a GitHub Release is published
workflow_dispatch: # Allows for manual execution
env:
ALPINE_VERSION: "23-alpine"
DEBIAN_VERSION: "23-slim"
# Registry URLs
DOCKERHUB_REGISTRY: docker.io
GITHUB_REGISTRY: ghcr.io
# Docker Hub image name (using Docker Hub username)
DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
# GitHub image name (formatted as `account/repo`)
GITHUB_IMAGE_NAME: ${{ github.repository }}
jobs:
check-lockfile:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '23' # Adjust as needed
- name: Install dependencies
run: npm ci
- name: Check for lock-file changes
run: |
git diff --exit-code package-lock.json || (
echo "🫥 package-lock.json is outdated or missing. Please run 'npm install' and commit the updated lockfile."
exit 1
)
build-and-push-to-registries:
needs: check-lockfile # Runs only after `check-lockfile` completes successfully
name: Push Docker images to Docker Hub and GitHub Container Registry
strategy:
matrix:
variant: [alpine, debian]
runs-on: ubuntu-latest
permissions:
actions: write
contents: write
packages: write
# This is used to complete the identity challenge with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4.2.2
# Install the cosign tool (except on PR)
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.8.0
with:
cosign-release: 'v2.2.4'
# Set up BuildKit Docker container builder to be able to build multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.8.0
# Log in to Docker Hub (except on PR)
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Log in to GitHub Container Registry (except on PR)
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Combined metadata extraction for both registries
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.6.1
with:
images: |
${{ env.DOCKERHUB_IMAGE_NAME }}
${{ env.GITHUB_REGISTRY }}/${{ env.GITHUB_IMAGE_NAME }}
tags: |
# 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' }}
# 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
# Build and push Docker image with Buildx to both registries (don't push on PR)
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6.13.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VARIANT=${{ matrix.variant }}
ALPINE_VERSION=${{ env.ALPINE_VERSION }}
DEBIAN_VERSION=${{ env.DEBIAN_VERSION }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digests
- name: Sign the published Docker images
if: ${{ github.event_name != 'pull_request' }}
env:
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: |
echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
# For use in other workflows (e.g. 'generate-readme', etc.)
- name: Save Build Version to Repository Variable
if: matrix.variant == 'debian' && github.run_attempt == 1
run: |
# 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
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"
env:
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
-61
View File
@@ -1,61 +0,0 @@
---
name: Publish Docker image to Dockerhub and GHCR
on:
push:
branches:
- main
tags:
- "*.*.*"
paths-ignore:
- '**/*.md'
- 'docs/**'
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
if: ${{ vars.DOCKERHUB_IMAGE_NAME != '' || vars.GHCR_IMAGE_NAME != '' }}
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Log in to Docker Hub
if: ${{ github.event_name != 'pull_request' && vars.DOCKERHUB_IMAGE_NAME != ''
}}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ github.event_name != 'pull_request' && vars.GHCR_IMAGE_NAME != '' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3
with:
images: |
${{ vars.DOCKERHUB_IMAGE_NAME }}
${{ vars.GHCR_IMAGE_NAME }}
tags: |
type=raw,value=latest,enable=${{ endsWith(github.ref, 'main') }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, 'main') }}
type=semver,pattern={{version}}
flavor: |
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' && !env.ACT}}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
+1
View File
@@ -9,6 +9,7 @@ config/server.yaml
/src/lib/.kener
/package
.env
.vscode
.env.*
!.env.example
vite.config.js.timestamp-*
+69 -7
View File
@@ -1,9 +1,71 @@
{
"useTabs": true,
"semi": true,
"tabWidth": 4,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"useTabs": false,
"semi": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte",
"useTabs": false,
"semi": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 120
}
},
{
"files": ["*.js", "*.ts", "*.tsx", "*.cjs", "*.mjs"],
"options": {
"useTabs": false,
"semi": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 120
}
},
{
"files": ["*.json", ".prettierrc"],
"options": {
"useTabs": false,
"semi": false,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 120
}
},
{
"files": ["*.yaml", "*.yml"],
"options": {
"useTabs": false,
"semi": false,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 80
}
},
{
"files": "*.md",
"options": {
"useTabs": false,
"semi": false,
"tabWidth": 4,
"trailingComma": "none",
"printWidth": 180
}
},
{
"files": "Dockerfile",
"options": {
"useTabs": true,
"tabWidth": 4,
"semi": false,
"trailingComma": "none",
"printWidth": 120
}
}
]
}
+128 -32
View File
@@ -1,52 +1,148 @@
FROM node:23
# syntax=docker/dockerfile:1
# Install necessary packages including tzdata for timezone setting
# Global build arguments (defined default values in case `.env.build` isn't loaded)
ARG ALPINE_VERSION_TAG=23.7.0-alpine3.21
ARG DEBIAN_VERSION_TAG=23.7.0-bookworm-slim
ARG VARIANT=debian
#==========================================================#
# STAGE 1: BUILD STAGE #
#==========================================================#
FROM node:${DEBIAN_VERSION_TAG} AS builder-debian
RUN apt-get update && apt-get install -y \
build-essential \
libnode108 \
nodejs \
python3 \
sqlite3 \
libsqlite3-dev \
make \
node-gyp \
g++ \
tzdata \
iputils-ping && \
build-essential=12.9 \
python3=3.11.2-1+b1 \
sqlite3=3.40.1-2+deb12u1 \
libsqlite3-dev=3.40.1-2+deb12u1 \
make=4.3-4.1 \
node-gyp=9.3.0-2 \
g++=4:12.2.0-3 \
tzdata \
iputils-ping=3:20221126-1+deb12u1 && \
rm -rf /var/lib/apt/lists/*
# Set the timezone environment variable and the application environment
ARG KENER_BASE_PATH=
ENV TZ=Etc/UTC
ENV KENER_BASE_PATH=${KENER_BASE_PATH}
FROM node:${ALPINE_VERSION_TAG} AS builder-alpine
RUN apk add --no-cache --update \
build-base=0.5-r3 \
python3=3.12.9-r0 \
py3-pip=24.3.1-r0 \
make=4.4.1-r2 \
g++=14.2.0-r4 \
sqlite=3.48.0-r0 \
sqlite-dev=3.48.0-r0 \
tzdata \
iputils=20240905-r0
FROM builder-${VARIANT} AS builder
# Set environment variables
ENV NPM_CONFIG_LOGLEVEL=error \
VITE_BUILD_ENV=production
# Set the working directory
WORKDIR /app
# Copy package files and install dependencies
# Copy package files for dependency installation
COPY package*.json ./
RUN npm install && npm cache clean --force
# Copy the rest of the application code
# Install all dependencies, including `devDependencies` (cache enabled for faster builds)
# TODO: Possibly add `--no-audit` flag to `npm ci` to prevent `npm` from running a security audit on installed packages. (By default, `npm install` performs an audit to check for vulnerabilities in dependencies, which can slow down installation. Adding this flag would skip the audit, thus making `npm install` significantly faster for the CI/CD pipeline.)
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-fund && \
npm cache clean --force
# Copy application source code
COPY . .
# remove dir src/routes/(docs)
RUN rm -rf src/routes/\(docs\)
# TODO: Reevaluate permissions (possibly reduce?)...
# Remove docs directory and ensure required directories exist
RUN rm -rf src/routes/\(docs\) \
static/documentation \
static/fonts/lato/full && \
mkdir -p uploads database && \
chmod -R 750 uploads database
# Ensure /app/uploads and /app/database have rw permissions
RUN mkdir -p /app/uploads /app/database && \
chmod -R 777 /app/uploads /app/database
# Build the application and remove `devDependencies`
RUN npm run build && \
npm prune --omit=dev
# Build the application
RUN npm run build
#==========================================================#
# STAGE 2: PRODUCTION/FINAL STAGE #
#==========================================================#
# Argument for the port
ARG PORT=3000
# Set the environment variable for the port
ENV PORT=$PORT
FROM node:${DEBIAN_VERSION_TAG} AS final-debian
# TODO: Consider adding `--no-install-recommends`, but will need testing (may further help reduce final build size)
RUN apt-get update && apt-get install -y \
iputils-ping=3:20221126-1+deb12u1 \
sqlite3=3.40.1-2+deb12u1 \
tzdata \
# TODO: Is it ok to change to `curl` here so that we don't have to maintain `wget` version mismatch between Debian architectures? (`curl` is only used for the container healthcheck and because there is an Alpine variant (best!) we probably don't care if the Debian image ends up building bigger due to `curl`.)
curl && \
rm -rf /var/lib/apt/lists/*
FROM node:${ALPINE_VERSION_TAG} AS final-alpine
RUN apk add --no-cache --update \
iputils=20240905-r0 \
sqlite=3.48.0-r0 \
tzdata
FROM final-${VARIANT} AS final
ARG PORT=3000 \
USERNAME=node
# Set environment variables
ENV HEALTHCHECK_PORT=$PORT \
HEALTHCHECK_PATH= \
NODE_ENV=production \
NPM_CONFIG_LOGLEVEL=error \
PORT=$PORT \
TZ=Etc/UTC
# Set the working directory
WORKDIR /app
# Copy package files build artifacts, and necessary files from builder stage
COPY --chown=node:node --from=builder /app/src/lib/ ./src/lib/
COPY --chown=node:node --from=builder /app/build ./build
COPY --chown=node:node --from=builder /app/uploads ./uploads
COPY --chown=node:node --from=builder /app/database ./database
# TODO: Consider changing from copying `node_modules` to instead letting `npm ci --omit=dev` handle production dependencies. Right now, copying `node_modules` is leading to a smaller image, whereas letting `npm ci` handle the install in final image is slightly faster, but leads to larger image size. IMO, having a slightly longer build time (e.g. ~10 sec.) is better in the end to have a smaller image.
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
COPY --chown=node:node --from=builder /app/migrations ./migrations
COPY --chown=node:node --from=builder /app/seeds ./seeds
COPY --chown=node:node --from=builder /app/static ./static
COPY --chown=node:node --from=builder /app/entrypoint.sh ./entrypoint.sh
COPY --chown=node:node --from=builder /app/knexfile.js ./knexfile.js
COPY --chown=node:node --from=builder /app/main.js ./main.js
COPY --chown=node:node --from=builder /app/openapi.json ./openapi.json
COPY --chown=node:node --from=builder /app/openapi.yaml ./openapi.yaml
COPY --chown=node:node --from=builder /app/package.json ./package.json
# Ensure necessary directories are writable
VOLUME ["/uploads", "/database"]
# 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
# Expose the application port
EXPOSE $PORT
# Set the command to run the application
# 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'
# 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)
USER $USERNAME
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["node", "main"]
+130 -47
View File
@@ -7,9 +7,27 @@
<p align="center">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/rajnandan1/kener?label=Star%20Repo&style=social">
<a href="https://github.com/ivbeg/awesome-status-pages"><img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome status page" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener"><img src="https://img.shields.io/docker/pulls/rajnandan1/kener" alt="Docker Kener" /></a>
<a href="https://awesome-selfhosted.net/tags/status--uptime-pages.html#kener"><img src="https://awesome.re/mentioned-badge.svg" alt="Awesome self hosted" /></a>
</p>
<p align="center">
<a href="https://hub.docker.com/r/rajnandan1/kener"><img src="https://img.shields.io/docker/pulls/rajnandan1/kener" alt="Docker Kener" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/latest?logo=docker&logoColor=white&label=debian" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/alpine?logo=docker&logoColor=white&label=alpine" /></a>
</p>
<p align="center">
<a href="https://github.com/rajnandan1/kener/actions/workflows/publish-images.yml"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/rajnandan1/kener/publish-images.yml" /></a>
<a href="https://github.com/rajnandan1/kener/commit/HEAD"><img src="https://img.shields.io/github/last-commit/rajnandan1/kener/main" alt="" /></a>
<a href="https://github.com/rajnandan1/kener/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/rajnandan1/kener.svg" /></a>
</p>
<p align="center">
<a href="https://www.producthunt.com/posts/kener-2" target="_blank">
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=kener-2&theme=light" alt="Kener on Product Hunt">
</a>
</p>
<p align="center">
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f514/512.webp" type="image/webp">
@@ -30,13 +48,19 @@
## What is 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** is a sleek and lightweight status page system built with **SvelteKit** and **NodeJS**. Its not here to replace heavyweights like Datadog or Atlassian but rather to offer a simple, modern, and hassle-free way to set up a great-looking status page with minimal effort.
It is carefully crafted to be easy to use and customize.
Designed with **ease of use** and **customization in mind**, Kener provides all the essential features youd expect from a status page—without unnecessary complexity.
It comes with all the basic asks for a status page. It is open-source and free to use.
### Why 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.
✅ &nbsp;Minimal overhead &ndash; Set up quickly with a clean, modern UI<br>
✅ &nbsp;Customizable &ndash; Easily tailor it to match your brand<br>
✅ &nbsp;Open-source & free &ndash; Because great tools should be accessible to everyone
### What's in a Name?
“Kener” is inspired by the Assamese word _“Kene”_, meaning _“hows it going?”_. The _.ing_ was added because, well… that domain was available. 😄
## Installation
@@ -48,14 +72,78 @@ git clone https://github.com/rajnandan1/kener.git
cd kener
npm install
cp .env.example .env
npm run build
npm run dev
```
### Docker
The latest image is available on DockerHub at [`rajnandan1/kener:latest`](https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest).
Download and use the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.10"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.10-blue" alt="Kener latest stable version: 3.2.10" /></a>
#### Available Tags
<table>
<tr>
<th>Image Tag</th>
<th>Description</th>
</tr>
<tr>
<td align="left" colspan="2" style="color:#A81D33;text-align:left;">Debian 12 <small>(Bookwork Slim)</small> w/ Node.js v23.7.0 &nbsp;<strong><em>(default)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
<td>Latest stable release (aka 3.2.10)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.10" target="_blank"><code>3.2.10</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2" target="_blank"><code>3.2</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.10) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.10) release within that major version (3.x.x)</td>
</tr>
<tr>
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 &nbsp;<strong><em>(smallest image size)</em></strong></td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine" target="_blank"><code>alpine</code></td>
<td>Latest stable release (aka 3.2.10)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.10-alpine" target="_blank"><code>3.2.10-alpine</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2-alpine" target="_blank"><code>3.2-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.10) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.10) release within that major version (3.x.x)</td>
</tr>
</table>
#### Usage
Pull the latest stable version:
```sh
docker pull rajnandan1/kener:latest
```
Or use the smaller, Alpine-based variant:
```sh
docker pull rajnandan1/kener:alpine
```
For a production setup, refer to the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
This keeps things clean, structured, and easy to read while preserving all the details.
### One Click
@@ -65,69 +153,64 @@ Download and use the sample [docker-compose.yml](https://github.com/rajnandan1/k
Here are some of the features that you get out of the box. Please read the documentation to know how to use them.
### Monitoring and Tracking
### 📊 &nbsp;Monitoring and Tracking
- Advanced application performance monitoring tools
- Real-time network monitor software capabilities
- Polls HTTP endpoint or Push data to monitor using Rest APIs
- Adjusts 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
- Automatically adjusts timezones for visitors
- Advanced **application performance monitoring** tools
- **Real-time network monitoring** capabilities
- Supports **polling HTTP endpoints** or **pushing data** via REST APIs
- **Timezone auto-adjustment** for visitors
- Organize monitors into **custom sections**
- **Cron-based scheduling** (minimum: **every minute**)
- **Create complex API polls** (chaining, secrets, etc.)
- Set a **default status** for monitors
- Supports **base path hosting in Kubernetes (k8s)**
- **Pre-built Docker images** for easy deployment
### Customization and Branding
### 🎨 &nbsp;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
- Fully **customizable status page**
- **Badge generation** for status and uptime tracking
- Support for **custom domains**
- Embed monitors as **iframes or widgets**
- **Light & Dark Mode**
- **Internationalization (i18n) support**
- **Sleek, beautifully crafted UI**
### Incident Management
### 🚨 &nbsp;Incident Management
- Incident Management
- Incident Communication
- Comprehensive APIs for Incident Management
- **Incident tracking & communication** tools
- **Comprehensive APIs** for incident management
### User Experience and Design
- Good Accessibility Score
- Easy installation and setup
- User-friendly interface
- Responsive design for various devices
- Auto SEO and Social Media ready
- Server Side Rendering
### 🧑‍💻 &nbsp;User Experience and Design
- **Accessible & user-friendly interface**
- **Quick & easy installation**
- **Responsive design** for all devices
- **Auto SEO & Social Media ready**
- **Server-Side Rendering (SSR) for better performance**
<div align="left">
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
</div>
## Technologies used
## 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.
If youre enjoying Kener and want to support its development, consider sponsoring me on GitHub or treating me to a coffee. Your support helps keep the project growing! 🚀
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
☕ &nbsp;[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
![image](https://badges.pufler.dev/visits/rajnandan1/kener)
## Contributing
If you want to contribute to Kener, please read the [Contributing Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
If you want to contribute to Kener, please read the [Contribution Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
## Star History
+217
View File
@@ -0,0 +1,217 @@
# Kener - Stunning Status Pages
<p align="center">
<img src="https://kener.ing/newbg.png?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
</p>
<p align="center">
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/rajnandan1/kener?label=Star%20Repo&style=social">
<a href="https://github.com/ivbeg/awesome-status-pages"><img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome status page" /></a>
<a href="https://awesome-selfhosted.net/tags/status--uptime-pages.html#kener"><img src="https://awesome.re/mentioned-badge.svg" alt="Awesome self hosted" /></a>
</p>
<p align="center">
<a href="https://hub.docker.com/r/rajnandan1/kener"><img src="https://img.shields.io/docker/pulls/rajnandan1/kener" alt="Docker Kener" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/latest?logo=docker&logoColor=white&label=debian" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=alpine"><img alt="Docker Image Size" src="https://img.shields.io/docker/image-size/rajnandan1/kener/alpine?logo=docker&logoColor=white&label=alpine" /></a>
</p>
<p align="center">
<a href="https://github.com/rajnandan1/kener/actions/workflows/publish-images.yml"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/rajnandan1/kener/publish-images.yml" /></a>
<a href="https://github.com/rajnandan1/kener/commit/HEAD"><img src="https://img.shields.io/github/last-commit/rajnandan1/kener/main" alt="" /></a>
<a href="https://github.com/rajnandan1/kener/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/rajnandan1/kener.svg" /></a>
</p>
<p align="center">
<a href="https://www.producthunt.com/posts/kener-2" target="_blank">
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=kener-2&theme=light" alt="Kener on Product Hunt">
</a>
</p>
<p align="center">
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f514/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f514/512.gif" alt="🔔" width="32" height="32">
</picture>
<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>
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f6a7/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f6a7/512.gif" alt="🚧" width="32" height="32">
</picture>
</p>
| [🌍 Live Server](https://kener.ing) | [🎉 Quick Start](https://kener.ing/docs/quick-start) | [🗄 Documentation](https://kener.ing/docs/home) |
| ----------------------------------- | ---------------------------------------------------- | ----------------------------------------------- |
## What is Kener?
**Kener** is a sleek and lightweight status page system built with **SvelteKit** and **NodeJS**. Its not here to replace heavyweights like Datadog or Atlassian but rather to offer a simple, modern, and hassle-free way to set up a great-looking status page with minimal effort.
Designed with **ease of use** and **customization in mind**, Kener provides all the essential features youd expect from a status page—without unnecessary complexity.
### Why Kener?
✅ &nbsp;Minimal overhead &ndash; Set up quickly with a clean, modern UI<br>
✅ &nbsp;Customizable &ndash; Easily tailor it to match your brand<br>
✅ &nbsp;Open-source & free &ndash; Because great tools should be accessible to everyone
### What's in a Name?
“Kener” is inspired by the Assamese word _“Kene”_, meaning _“hows it going?”_. The _.ing_ was added because, well… that domain was available. 😄
## Installation
### Manual
```shell
# Clone the repository
git clone https://github.com/rajnandan1/kener.git
cd kener
npm install
cp .env.example .env
npm run dev
```
### Docker
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={{kener_full_version}}"><img src="https://img.shields.io/badge/Latest_Stable_Release-{{kener_full_version}}-blue" alt="Kener latest stable version: {{kener_full_version}}" /></a>
#### Available Tags
<table>
<tr>
<th>Image Tag</th>
<th>Description</th>
</tr>
<tr>
<td align="left" colspan="2" style="color:#A81D33;text-align:left;">Debian 12 <small>(Bookwork Slim)</small> w/ Node.js v23.7.0 &nbsp;<strong><em>(default)</em></strong></td>
</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 {{kener_full_version}})</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_full_version}}" target="_blank"><code>{{kener_full_version}}</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={{kener_major_minor_version}}" target="_blank"><code>{{kener_major_minor_version}}</code></a></td>
<td>Major-minor version tag pointing to the latest patch ({{kener_full_version}}) release within that minor version ({{kener_major_minor_version}}.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_version}}" target="_blank"><code>{{kener_major_version}}</code></a></td>
<td>Major version tag pointing to the latest stable ({{kener_full_version}}) release within that major version ({{kener_major_version}}.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 {{kener_full_version}})</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_full_version}}-alpine" target="_blank"><code>{{kener_full_version}}-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={{kener_major_minor_version}}-alpine" target="_blank"><code>{{kener_major_minor_version}}-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch ({{kener_full_version}}) release within that minor version ({{kener_major_minor_version}}.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name={{kener_major_version}}-alpine" target="_blank"><code>{{kener_major_version}}-alpine</code></a></td>
<td>Major version tag pointing to the latest stable ({{kener_full_version}}) release within that major version ({{kener_major_version}}.x.x)</td>
</tr>
</table>
#### Usage
Pull the latest stable version:
```sh
docker pull rajnandan1/kener:latest
```
Or use the smaller, Alpine-based variant:
```sh
docker pull rajnandan1/kener:alpine
```
For a production setup, refer to the sample [docker-compose.yml](https://github.com/rajnandan1/kener/blob/main/docker-compose.yml).
This keeps things clean, structured, and easy to read while preserving all the details.
### One Click
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
## Features
Here are some of the features that you get out of the box. Please read the documentation to know how to use them.
### 📊 &nbsp;Monitoring and Tracking
- Advanced **application performance monitoring** tools
- **Real-time network monitoring** capabilities
- Supports **polling HTTP endpoints** or **pushing data** via REST APIs
- **Timezone auto-adjustment** for visitors
- Organize monitors into **custom sections**
- **Cron-based scheduling** (minimum: **every minute**)
- **Create complex API polls** (chaining, secrets, etc.)
- Set a **default status** for monitors
- Supports **base path hosting in Kubernetes (k8s)**
- **Pre-built Docker images** for easy deployment
### 🎨 &nbsp;Customization and Branding
- Fully **customizable status page**
- **Badge generation** for status and uptime tracking
- Support for **custom domains**
- Embed monitors as **iframes or widgets**
- **Light & Dark Mode**
- **Internationalization (i18n) support**
- **Sleek, beautifully crafted UI**
### 🚨 &nbsp;Incident Management
- **Incident tracking & communication** tools
- **Comprehensive APIs** for incident management
### 🧑‍💻 &nbsp;User Experience and Design
- **Accessible & user-friendly interface**
- **Quick & easy installation**
- **Responsive design** for all devices
- **Auto SEO & Social Media ready**
- **Server-Side Rendering (SSR) for better performance**
<div align="left">
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
</div>
## Technologies Used
- [SvelteKit](https://kit.svelte.dev/)
- [shadcn-svelte](https://www.shadcn-svelte.com/)
## Support Me
If youre enjoying Kener and want to support its development, consider sponsoring me on GitHub or treating me to a coffee. Your support helps keep the project growing! 🚀
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
☕ &nbsp;[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
![image](https://badges.pufler.dev/visits/rajnandan1/kener)
## Contributing
If you want to contribute to Kener, please read the [Contribution Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=rajnandan1/kener&type=Date)](https://star-history.com/#rajnandan1/kener&Date)
+51 -16
View File
@@ -1,26 +1,61 @@
# Docker Compose Configuration
# Description: This file sets up a multi-container environment for Kener (https://github.com/rajnandan1/kener).
# Last Updated: 2025-02-08
# Docker Compose Version: 3.8
# Notes: Ensure that you specify a random value for the `KENER_SECRET_KEY` environment variable before running `docker-compose up -d`.
version: '3.8'
services:
kener:
image: rajnandan1/kener:latest
image: rajnandan1/kener:latest # Change to 'rajnandan1/kener:alpine' for an even smaller image! 😁🚀
container_name: kener
#env_file: .env #uncomment this, if you are using .env file
# env_file: custom.env # Uncomment this if you are needing to export environment variables from a custom environment file. By default, Docker will import any variables that exist in `.env`
environment:
- TZ=Etc/UTC
- NODE_ENV=production
#- PORT=3000
#- KENER_BASE_PATH=
#- RESEND_API_KEY=
#- ORIGIN=
#- KENER_SECRET_KEY=
#- RESEND_SENDER_EMAIL=
#- DATABASE_URL=
TZ: Etc/UTC
KENER_SECRET_KEY: replace_me_with_a_random_string # Keep private!! - best to define in `.env` file or through Docker Secret
# DATABASE_URL: custom_db_url # By default, a SQLite database is used - you may override the database url/type here
# RESEND_API_KEY:
# RESEND_SENDER_EMAIL:
### You most likely will NOT need to change anything below this line. Be sure you know what you're doing!! (https://kener.ing/docs/deployment/#docker-environment-variables)
### Most likely DO NOT need to change anything below this ###
#- PORT=3000 Port app listens on IN CONTAINER
# PORT: 3000 # Port that app listens on in the container
# KENER_BASE_PATH: # By default, Kener runs at `/`. You may change this to be, e.g. `/status`, etc. Do NOT add a trailing slash!! (more info here: https://kener.ing/docs/deployment/#docker-environment-variables)
# ORIGIN: http://localhost:3000
# NODE_ENV: production # This is already set to "production" by default within the container
ports:
- '3000:3000/tcp'
volumes:
- '.:/app/database:rw'
- '.:/app/uploads:rw'
- data:/app/database # We suggest using a Docker named volume, which is more performant for databases
- $(pwd)/uploads:/app/uploads
# read_only: true # Uncommenting this fortifies security by marking the container's filesystem as read-only (aka no data can be written to the container's filesystem except for explicitly defined writable volumes and bind mounts, an exception has already been defined for `/database` and `/uploads`)
restart: unless-stopped
# depends_on: # <-- Uncomment if you would like to use PostgreSQL or MySQL
# - postgres # ...instead of SQLite
# - mysql #
# Only use below section if you would like to utilize PostgreSQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=postgresql://db_user:db_password@localhost:5432/kener_db`)
postgres:
image: postgres:alpine
name: kener_db
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
POSTGRES_DB: kener_db
restart: unless-stopped
# Only use below section if you would like to utilize MySQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=mysql://db_user:db_password@localhost:3306/kener_db`)
mysql:
image: mariadb:11
name: kener_db
environment:
MYSQL_USER: user
MYSQL_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
MYSQL_DATABASE: kener_db
MYSQL_RANDOM_ROOT_PASSWORD: true
restart: unless-stopped
volumes:
data:
name: kener_db
+20 -20
View File
@@ -64,24 +64,24 @@ Body of the webhook will be sent as below:
```json
{
"id": "mockoon-9",
"alert_name": "Mockoon DOWN",
"severity": "critical",
"status": "TRIGGERED",
"source": "Kener",
"timestamp": "2024-11-27T04:55:00.369Z",
"description": "🚨 **Service Alert**: Check the details below",
"details": {
"metric": "Mockoon",
"current_value": 1,
"threshold": 1
},
"actions": [
{
"text": "View Monitor",
"url": "https://kener.ing/monitor-mockoon"
}
]
"id": "mockoon-9",
"alert_name": "Mockoon DOWN",
"severity": "critical",
"status": "TRIGGERED",
"source": "Kener",
"timestamp": "2024-11-27T04:55:00.369Z",
"description": "🚨 **Service Alert**: Check the details below",
"details": {
"metric": "Mockoon",
"current_value": 1,
"threshold": 1
},
"actions": [
{
"text": "View Monitor",
"url": "https://kener.ing/monitor-mockoon"
}
]
}
```
@@ -108,7 +108,7 @@ The discord message when alert is `TRIGGERED` will look like this
The discord message when alert is `RESOLVED` will look like this
![Discord](/discord_resolved.png)
![Discord](/documentation/discord_resolved.png)
### Slack
@@ -118,7 +118,7 @@ The slack message when alert is `TRIGGERED` will look like this
The slack message when alert is `RESOLVED` will look like this
![Slack](/slack_resolved.png)
![Slack](/documentation/slack_resolved.png)
### Add Alerts to Monitors
+263 -45
View File
@@ -7,6 +7,224 @@ description: Changelogs for Kener
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
## v3.2.5
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- Added code editing for all JavaScript and JSON fields for trigger custom body and monitor `evals`.
- Syntax highlighting and error checking for monitor evaluation functions
- Added support for custom Discord and Slack body templates in triggers
- Improved variable templating for all notification types
- Added comprehensive user management with 3 roles: admin, editor, and member. Read more [here](/docs/rbac)
- Self-service profile management for all users
- User activation/deactivation controls for admins
- Added dedicated Badges management page in the admin dashboard
- Site-wide status badges to show overall service health
- Version information automatically pulled from package.json
### Improvements
- Improved build time by migrating lucid-svelte to individual components
### Github Issues Resolved
- Feature: Enhancement of Webhook Custom Body Functionality [#238](https://github.com/rajnandan1/kener/issues/238)
- Feature: Add LDAP for authentication [#210](https://github.com/rajnandan1/kener/issues/210)
- the event created by tigger can't update affects. [#309](https://github.com/rajnandan1/kener/issues/309)
- Multiple account or RBAC [#334](https://github.com/rajnandan1/kener/issues/334)
- The event that has been edited isn't clickable after you save it [#350](https://github.com/rajnandan1/kener/issues/350)
- /app/package.json ERROR on fresh install [#346](https://github.com/rajnandan1/kener/issues/346)
- Site stops when monitor processing throws an error [#345](https://github.com/rajnandan1/kener/issues/345)
- Support for Self Signed Certificates [#351](https://github.com/rajnandan1/kener/issues/351)
- Wrong month is showing up after clicking on the 'Browse Events' link [#349](https://github.com/rajnandan1/kener/issues/349)
### Migration Notes
This update is fully backward compatible with existing installations. User accounts created before this update will retain admin privileges by default.
## v3.2.1
<picture>
<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>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- Added TCP monitors
### Breaking Changes
- Ping monitors will break.
### Fixes
- Bug fixes in the UI
## v3.0.9
<picture>
@@ -16,16 +234,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
@@ -38,8 +256,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
@@ -52,18 +270,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
@@ -76,14 +294,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
@@ -100,27 +318,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
+3 -3
View File
@@ -50,7 +50,7 @@ i18n:
vi: "Tiếng Việt"
pattern: "squares"
font:
cssSrc: "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
cssSrc: "https://fonts.bunny.net/css?family=lato:100,100i,300,300i,400,400i,700,700i,900,900i&display=swap"
family: '"Lato", sans-serif'
analytics:
- id: "G-QsFT"
@@ -315,11 +315,11 @@ You can set the background pattern of the site. It can be `squares`, `dots`, `no
## font
You can set the font of the site. You can use google fonts or any other font.
You can set the font of the site. You can use google fonts, [bunny fonts](https://fonts.bunny.net/) (which is a fully GDPR-compliant, zero-tracking/no-logging CDN font service), or any other font.
```yaml
font:
cssSrc: "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
cssSrc: "https://fonts.bunny.net/css?family=lato:100,100i,300,300i,400,400i,700,700i,900,900i&display=swap"
family: '"Lato", sans-serif'
```
+22 -2
View File
@@ -56,7 +56,7 @@ ghcr.io/rajnandan1/kener:latest
[Environment variables](/docs/environment-vars) can be passed with `-e` An example `docker run` command:
Make sure `./database` directory is present in the root directory.
Make sure `./database` and `./uploads` directories are present in the root directory.
### Examples
@@ -65,7 +65,10 @@ This example is for sqlite. You can also use postgres. Read more about it [here]
#### sqlite
```bash
mkdir database
mkdir database uploads
```
```bash
docker run \
-v $(pwd)/database:/app/database \
-v $(pwd)/uploads:/app/uploads \
@@ -79,6 +82,10 @@ docker run \
You can also use a .env file
```bash
mkdir database uploads
```
```bash
docker run \
-v $(pwd)/database:/app/database \
@@ -92,6 +99,12 @@ Or use **Docker Compose** with the example [docker-compose.yaml](https://raw.git
#### Postgres
```bash
mkdir uploads
```
Database folder is not required for postgres.
```bash
docker run \
-p 3000:3000 \
@@ -104,6 +117,12 @@ docker run \
#### MySQL
```bash
mkdir uploads
```
Database folder is not required for mysql.
```bash
docker run \
-p 3000:3000 \
@@ -122,6 +141,7 @@ By default kener runs on `/` but you can change it to `/status` or any other pat
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
- Important: This env variable should be present during both build and run time
- Important: You will have to build the frontend with the same base path
</div>
+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
+3 -3
View File
@@ -21,7 +21,7 @@ A small text that will be shown below the title.
<div class="border rounded-md">
![Hero Section](/home_1.png)
![Hero Section](/documentation/home_1.png)
</div>
@@ -31,7 +31,7 @@ A small text that will be shown below the title.
You can navigation links to other urls. You can add as many as you want.
![Nav Section](/home_2.png)
![Nav Section](/documentation/home_2.png)
### Icon
@@ -45,7 +45,7 @@ The title of the link.
The URL to redirect to when the link is clicked.
![Nav Section](/home_3.png)
![Nav Section](/documentation/home_3.png)
---
+30 -74
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,85 +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
- 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
+82 -60
View File
@@ -9,7 +9,7 @@ API monitors are used to monitor APIs. You can use API monitors to monitor the u
<div class="border rounded-md">
![Monitors API](/m_api.png)
![Monitors API](/documentation/m_api.png)
</div>
@@ -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,76 +64,98 @@ 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";
let status = "DOWN"
//if the status code is 2XX then the status is UP
if (/^[2]\d{2}$/.test(statusCode)) {
status = "UP";
if (responseTime > 2000) {
status = "DEGRADED";
}
}
//if the status code is 2XX then the status is UP
if (/^[2]\d{2}$/.test(statusCode)) {
status = "UP"
if (responseTime > 2000) {
status = "DEGRADED"
}
}
//if the status code is 5XX then the status is DOWN
if (/^[5]\d{2}$/.test(statusCode)) status = "DOWN";
//if the status code is 5XX then the status is DOWN
if (/^[5]\d{2}$/.test(statusCode)) status = "DOWN"
if (resp.includes("Unknown Error")) {
status = "DOWN";
}
if (responseRaw.includes("Unknown Error")) {
status = "DOWN"
}
return {
status: status,
latency: responseTime
};
});
return {
status: status,
latency: responseTime
}
})
```
### 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);
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g);
if (scriptTags && scriptTags.length >= 2) {
// Extract the second last script tag's src attribute
const secondLastScript = scriptTags[scriptTags.length - 2];
const srcMatch = secondLastScript.match(/src="([^"]+)"/);
const secondLastScriptSrc = srcMatch ? srcMatch[1] : null;
```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
const secondLastScript = scriptTags[scriptTags.length - 2]
const srcMatch = secondLastScript.match(/src="([^"]+)"/)
const secondLastScriptSrc = srcMatch ? srcMatch[1] : null
let jsResp = await fetch(secondLastScriptSrc); //api call
let jsRespText = await jsResp.text();
//check if heading":"No recent issues" exists
let noRecentIssues = jsRespText.indexOf('heading":"No recent issues"');
if (noRecentIssues != -1) {
return {
status: "UP",
latency: responseTime
};
}
}
return {
status: "DOWN",
latency: responseTime
};
});
let jsResp = await fetch(secondLastScriptSrc) //api call
let jsRespText = await jsResp.text()
//check if heading":"No recent issues" exists
let noRecentIssues = jsRespText.indexOf('heading":"No recent issues"')
if (noRecentIssues != -1) {
return {
status: "UP",
latency: responseTime
}
}
}
return {
status: "DOWN",
latency: responseTime
}
})
```
### 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
@@ -153,7 +175,7 @@ This is an example to monitor google every 5 minute.
<div class="border rounded-md">
![Monitors API](/m_ex_website.png)
![Monitors API](/documentation/m_ex_website.png)
</div>
@@ -180,7 +202,7 @@ export SOME_TOKEN=some-token-example
<div class="border rounded-md p-1">
![Monitors API](/m_ex_2.png)
![Monitors API](/documentation/m_ex_2.png)
</div>
@@ -201,7 +223,7 @@ Example showing setting up a POST request every minute with a timeout of 2 secon
<div class="border rounded-md p-1">
![Monitors API](/m_ex_3.png)
![Monitors API](/documentation/m_ex_3.png)
</div>
@@ -228,7 +250,7 @@ export SERVICE_SECRET=secret2_secret
<div class="border rounded-md p-1">
![Monitors API](/m_ex_4.png)
![Monitors API](/documentation/m_ex_4.png)
</div>
+1 -1
View File
@@ -9,7 +9,7 @@ DNS monitors are used to monitor DNS servers. Verify DNS queries for your server
<div class="border rounded-md">
![Monitors Ping](/m_dns.png)
![Monitors Ping](/documentation/m_dns.png)
</div>
+40
View File
@@ -0,0 +1,40 @@
---
title: Group Monitors | Kener
description: Learn how to set up and work with Group monitors in kener.
---
# Group Monitors
Group monitors are used to monitor multiple monitors at once. You can use Group monitors to monitor multiple monitors at once and get notified when they are down.
<div class="border rounded-md">
![Monitors Group](/documentation/m_group.png)
</div>
## Timeout
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The timeout is used to define the time in milliseconds after which the group monitor should timeout.
Let us say the group monitor runs every minute, it will expect in the same minute all the other monitors to finish. It will wait till the timeout for them to complete. If not completed within that timeout, it will be marked as down.
## Monitors
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
You can add as many monitors as you want to monitor. The minimum number of monitors required is 2. The monitor can be any type of monitor.
## Hide
You can hide the monitors that are part of the group monitor. If you hide the monitors, the monitors inside the group will not be shown in the home page.
<div class="note">
The group status will be the worst status of the monitors in the group.
</div>
+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.
+80 -93
View File
@@ -9,29 +9,18 @@ Ping monitors are used to monitor livenees of your servers. You can use Ping mon
<div class="border rounded-md">
![Monitors Ping](/m_ping.png)
![Monitors Ping](/documentation/m_ping.png)
</div>
## Host V4
## Hosts
You can add as many IP addresses as you want to monitor. The IP address should be a valid IPv4 address.
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.
Example of IP4 address is `106.12.43.232`, with the port number `80`. The port number is optional and defaults to `80`.
Example of IP4 address with port number is `66.51.120.219:465`.
Example of domain name with port number is `www.rajnandan.com:80`.
## Host V6
You can add as many IP addresses as you want to monitor. The IP address should be a valid IPv6 address. Example of IP6 address is `2001:0db8:85a3:0000:0000:8a2e:0370`.
Example of IP6 address with port number is `2606:50c0:8000::153:443`.
<p class="note danger">
Please note that at least one of the Host V4 or Host V6 is required.
<p>
- 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.
- Timeout: Enter the timeout in milliseconds for each ping request of each host. For IP6 timeout is not supported.
- Count: The number of pings you want to do for each host. The average latency of all the pings will be used to evaluate the response.
## Eval
@@ -43,102 +32,100 @@ 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));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
let alive = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true;
} else {
return false;
}
}, true);
let alive = arrayOfPings.reduce((acc, ping) => {
return acc && ping.alive
}, true)
return {
status: alive ? "UP" : "DOWN",
latency: parseInt(latencyTotal / arrayOfPings.length)
};
});
return {
status: alive ? "UP" : "DOWN",
latency: latencyTotal / arrayOfPings.length
}
})
```
- `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
/*
[
{
"host": "smtp.resend.com",
"port": 587,
"type": "IP4",
"status": "open",
"latency": 36.750917
},
{
"host": "66.51.120.219",
"port": 465,
"type": "IP4",
"status": "open",
"latency": 27.782792
},
{
"host": "2606:4700:4700::1111",
"port": 443,
"status": "open",
"type": "IP6",
"latency": 5.684375
}
{
alive: true,
min: '145.121',
max: '149.740',
avg: '147.430',
latencies: [ 145.121, 149.74 ],
latency: 145.121,
host: '45.91.169.49',
type: 'IP4'
},
{
alive: true,
min: '145.348',
max: '149.143',
avg: '147.245',
latencies: [ 145.348, 149.143 ],
latency: 145.348,
host: 'kener.ing',
type: 'DOMAIN'
},
{
alive: true,
min: '57.696',
max: '58.330',
avg: '58.013',
latencies: [ 57.696, 58.33 ],
latency: 57.696,
host: '2404:6800:4003:c1c::66',
type: 'IP6'
}
]
*/
```
### Understanding the Input
- `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.
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.
- `max` is a string. It is the maximum latency of the pings.
- `avg` is a string. It is the average latency of the pings.
- `latencies` is an array of numbers. It is the latency of each ping.
- `latency` is a number. It is the average latency of the pings.
- `host` is a string. It is the host that was pinged.
- `type` is a string. It is the type of the host. It can be either `IP4` or `IP6` or `DOMAIN`.
### 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));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true;
} else {
return false;
}
}, true);
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
return acc && ping.alive
}, true)
let avgLatency = latencyTotal / arrayOfPings.length;
let avgLatency = latencyTotal / arrayOfPings.length
if (areAllOpen && avgLatency > 10) {
return {
status: "DEGRADED",
latency: avgLatency
};
}
if (areAllOpen && avgLatency > 10) {
return {
status: "DEGRADED",
latency: avgLatency
}
}
return {
status: areAllOpen ? "UP" : "DOWN",
latency: avgLatency
};
});
return {
status: areAllOpen ? "UP" : "DOWN",
latency: avgLatency
}
})
```
+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>
+130
View File
@@ -0,0 +1,130 @@
---
title: TCP Monitors | Kener
description: Learn how to set up and work with TCP monitors in kener.
---
# TCP Monitors
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">
![Monitors TCP](/documentation/m_tcp.png)
</div>
## Hosts
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
## Eval
The eval is used to define the JavaScript code that should be used to evaluate the response. It is optional and has be a valid JavaScript code.
This is an anonymous JS function, it should return a **Promise**, that resolves or rejects to `{status, latency}`, by default it looks like this.
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and latency(number)
> `{status:"DEGRADED", latency: 200}`.
```javascript
(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
let alive = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true
} else {
return false
}
}, true)
return {
status: alive ? "UP" : "DOWN",
latency: latencyTotal / arrayOfPings.length
}
})
```
- `arrayOfPings` **REQUIRED** is an array of TCP Response Objects as shown below.
```json
/*
[
{
status: 'open',
latency: 31.93,
host: '66.51.120.219',
port: 465,
type: 'IP4'
},
{
status: 'open',
latency: 47.041417,
host: '2404:6800:4003:c1c::66',
port: 80,
type: 'IP6'
},
{
status: 'open',
latency: 82.1865,
host: 'rajnandan.com',
port: 80,
type: 'DOMAIN'
}
]
*/
```
### Understanding the Input
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.
### 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 (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency
}, 0)
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true
} else {
return false
}
}, true)
let avgLatency = latencyTotal / arrayOfPings.length
if (areAllOpen && avgLatency > 10) {
return {
status: "DEGRADED",
latency: avgLatency
}
}
return {
status: areAllOpen ? "UP" : "DOWN",
latency: avgLatency
}
})
```
+22 -11
View File
@@ -13,7 +13,7 @@ Click on the to add a monitor.
<div class="border rounded-md">
![Monitors Main](/m_main.png)
![Monitors Main](/documentation/m_main.png)
</div>
@@ -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.
+134
View File
@@ -0,0 +1,134 @@
---
title: Kener Role Based Access Control (RBAC)
description: Role-Based Access Control (RBAC) in Kener
---
# Role-Based Access Control (RBAC) in Kener
Kener includes a comprehensive role-based access control system that allows you to manage user permissions and access to various features. This document explains the available roles, their permissions, and how to manage users effectively.
## Available Roles
Kener offers three different roles with varying levels of permissions:
| Role | Description |
| ------ | ------------------------------------------------------------------------------ |
| Admin | Full access to all features including user management and system configuration |
| Editor | Can create and edit monitors, triggers, incidents, and other operational data |
| Member | Read-only access with limited interaction capabilities |
## Role Permissions
### Admin
Admins have unrestricted access to the entire system:
- **User Management**:
- Create, update, and deactivate users
- Change user roles
- Reset passwords for other users
- Send verification emails
- **System Configuration**:
- Configure all site settings
- Manage API keys
- Set up triggers and integrations
- **Operational Access**:
- Full access to create and manage monitors
- Create and update incidents
- Run tests on monitors and triggers
- Configure all notification channels
### Editor
Editors can manage most operational aspects but cannot administer users:
- **Content Management**:
- Create and edit monitors
- Configure monitor settings
- Create and manage triggers
- Create and update incidents
- Add incident updates and comments
- **Limited Access**:
- Cannot manage users
- Cannot change role assignments
- Cannot access certain system-level configurations
### Member
Members have read-only access with minimal interaction capabilities:
- **View Access**:
- View all monitors and their status
- View incidents and their history
- See system configuration (but cannot modify)
- **Limited Interactions**:
- Can test existing triggers but cannot create or edit them
- Cannot create or update incidents
- Cannot modify any system configuration
## Managing Users
### Adding New Users
Only Admins and Editors can add new users to the system:
1. Navigate to the **Users** page in the management dashboard
2. Click the **Add New User** button
3. Fill in the required information:
- Name
- Email
- Password
- Role (Member or Editor)
4. Click **Add User** to create the account
When a new user is added, a verification email can be sent to confirm their email address if email sending is configured.
### User Settings
Admins can manage user accounts through the user settings page:
- **Email Verification**: Send verification emails to users
- **Password Reset**: Reset a user's password
- **Role Management**: Change a user's role between Member and Editor
- **Account Status**: Activate or deactivate user accounts
### Best Practices
1. **Follow the Principle of Least Privilege**:
- Assign the minimum necessary permissions for users to perform their job
- Start users with the Member role and elevate as needed
2. **Regular Access Reviews**:
- Periodically review user access and roles
- Remove access for users who no longer need it
3. **Admin Accounts**:
- Limit the number of Admin accounts
- Use strong passwords for Admin accounts
- Consider using email verification for all users
## Role Limitations
- The Admin role can only be assigned during initial setup
- Members cannot create or modify content
- Users cannot modify their own role (only an Admin can change roles)
## Email Verification
When email sending is configured, users can verify their email addresses:
1. Admins can send verification emails from the user management interface
2. Users receive an email with a verification link
3. After clicking the link, the user's email is marked as verified
Email verification improves security and ensures that users have provided valid email addresses for notifications.
+1 -1
View File
@@ -40,4 +40,4 @@ description: Roadmap for Kener
---
Request a feature or start a discussion [here](https://github.com/rajnandan1/kener/discussions/119)
Request a feature or start a discussion [here](https://github.com/rajnandan1/kener/discussions/new/choose)
+11 -1
View File
@@ -21,7 +21,17 @@ Add API key of your Amplitude account
### Mixpanel
Add token of your Mixpanel account.
Add project token of your Mixpanel account.
### Plausible
- Add domain of your Plausible account. Example: `example.com`
- Add API key of your Plausible account. Default is `https://plausible.io/api/event`
- Add Script Source of your Plausible account. Default is `https://plausible.io/js/script.pageview-props.tagged-events.js`
### Microsoft Clarity
Add Project ID of your Microsoft Clarity account.
## Search Engine Optimization
+3 -3
View File
@@ -19,11 +19,11 @@ Example: `Kener - Open-Source and Modern looking Node.js Status Page for Effortl
```html
<title>
Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management
Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management
</title>
```
![Site Title](/ms_1.png)
![Site Title](/documentation/ms_1.png)
## Site Name
@@ -33,7 +33,7 @@ Example: `Kener - Open-Source and Modern looking Node.js Status Page for Effortl
This will be shown as a brand name on the status page on the nav bar top left.
![Site Name](/s_2.png)
![Site Name](/documentation/s_2.png)
## Home Location
+188 -157
View File
@@ -1,161 +1,192 @@
{
"sidebar": [
{
"sectionTitle": "Getting Started",
"children": [
{
"title": "Introduction",
"link": "/docs/home",
"file": "/home.md"
},
{
"title": "Get Started",
"link": "/docs/quick-start",
"file": "/quick-start.md"
},
{
"title": "Concepts",
"link": "/docs/concepts",
"file": "/concepts.md"
},
"sidebar": [
{
"sectionTitle": "Getting Started",
"children": [
{
"title": "Introduction",
"link": "/docs/home",
"file": "/home.md"
},
{
"title": "Get Started",
"link": "/docs/quick-start",
"file": "/quick-start.md"
},
{
"title": "Concepts",
"link": "/docs/concepts",
"file": "/concepts.md"
},
{
"title": "Deployment",
"link": "/docs/deployment",
"file": "/deployment.md"
},
{
"title": "Databases",
"link": "/docs/database",
"file": "/database.md"
}
]
},
{
"sectionTitle": "Guides",
"children": [
{
"title": "Setup Environment",
"link": "/docs/environment-vars",
"file": "/environment-vars.md"
},
{
"title": "Use Badges",
"link": "/docs/status-badges",
"file": "/status-badges.md"
},
{
"title": "Setup Monitors",
"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": "DNS Monitor",
"link": "/docs/monitors-dns",
"file": "/monitors-dns.md"
},
{
"title": "Setup Triggers",
"link": "/docs/triggers",
"file": "/triggers.md"
},
{
"title": "Setup Site",
"link": "/docs/site",
"file": "/site.md"
},
{
"title": "Setup Github",
"link": "/docs/gh-setup",
"file": "/gh-setup.md"
},
{
"title": "Setup SEO",
"link": "/docs/seo",
"file": "/seo.md"
},
{
"title": "Setup Home",
"link": "/docs/home-page",
"file": "/home-page.md"
},
{
"title": "Setup Theme",
"link": "/docs/theme",
"file": "/theme.md"
},
{
"title": "View Alerts",
"link": "/docs/alerts",
"file": "/alerts.md"
},
{
"title": "API Keys",
"link": "/docs/apikeys",
"file": "/apikeys.md"
},
{
"title": "Deployment",
"link": "/docs/deployment",
"file": "/deployment.md"
},
{
"title": "Databases",
"link": "/docs/database",
"file": "/database.md"
},
{
"title": "Access Control",
"link": "/docs/rbac",
"file": "/rbac.md"
}
]
},
{
"sectionTitle": "Guides",
"children": [
{
"title": "Setup Environment",
"link": "/docs/environment-vars",
"file": "/environment-vars.md"
},
{
"title": "Use Badges",
"link": "/docs/status-badges",
"file": "/status-badges.md"
},
{
"title": "Setup Monitors",
"link": "/docs/monitors",
"file": "/monitors.md"
},
{
"title": "Incident Management",
"link": "/docs/incident-management",
"file": "/incident-management.md"
},
{
"title": "Embed",
"link": "/docs/embed",
"file": "/embed.md"
},
{
"title": "Custom JS/CSS",
"link": "/docs/custom-js-css-guide",
"file": "/custom-js-css-guide.md"
},
{
"title": "Internationalization",
"link": "/docs/i18n",
"file": "/i18n.md"
}
]
},
{
"sectionTitle": "API Reference",
"children": [
{
"title": "Kener APIs",
"link": "/docs/kener-apis",
"file": "/kener-apis.md"
}
]
},
{
"sectionTitle": "Help",
"children": [
{
"title": "Fonts",
"link": "/docs/custom-fonts",
"file": "/custom-fonts.md"
},
{
"title": "Changelogs",
"link": "/docs/changelogs",
"file": "/changelogs.md"
},
{
"title": "Roadmap",
"link": "/docs/roadmap",
"file": "/roadmap.md"
}
]
}
]
{
"title": "Setup Triggers",
"link": "/docs/triggers",
"file": "/triggers.md"
},
{
"title": "Setup Site",
"link": "/docs/site",
"file": "/site.md"
},
{
"title": "Setup SEO",
"link": "/docs/seo",
"file": "/seo.md"
},
{
"title": "Setup Home",
"link": "/docs/home-page",
"file": "/home-page.md"
},
{
"title": "Setup Theme",
"link": "/docs/theme",
"file": "/theme.md"
},
{
"title": "View Alerts",
"link": "/docs/alerts",
"file": "/alerts.md"
},
{
"title": "API Keys",
"link": "/docs/apikeys",
"file": "/apikeys.md"
},
{
"title": "Incident Management",
"link": "/docs/incident-management",
"file": "/incident-management.md"
},
{
"title": "Embed",
"link": "/docs/embed",
"file": "/embed.md"
},
{
"title": "Custom JS/CSS",
"link": "/docs/custom-js-css-guide",
"file": "/custom-js-css-guide.md"
},
{
"title": "Internationalization",
"link": "/docs/i18n",
"file": "/i18n.md"
}
]
},
{
"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": [
{
"title": "Kener APIs",
"link": "/docs/kener-apis",
"file": "/kener-apis.md"
}
]
},
{
"sectionTitle": "Help",
"children": [
{
"title": "Fonts",
"link": "/docs/custom-fonts",
"file": "/custom-fonts.md"
},
{
"title": "Changelogs",
"link": "/docs/changelogs",
"file": "/changelogs.md"
},
{
"title": "Roadmap",
"link": "/docs/roadmap",
"file": "/roadmap.md"
}
]
}
]
}
+7 -7
View File
@@ -11,7 +11,7 @@ Kener provides you with the ability to customize the theme of your status page.
## Home Page Pattern
Kener can show a subtle pattern in all your pages. It is either sqaure or dots. Right now you cannot modify the color of the pattern. However, you can disable it by choosing none
Kener can show a subtle pattern in all your pages. It is either square or dots. Right now you cannot modify the color of the pattern. However, you can disable it by choosing none
---
@@ -35,13 +35,13 @@ You can change how the bars and summary of a monitor looks like.
The status bar will be a gradient from green to red/yellow based on the status of the monitor.
![Trigger API](/x1.png)
![Trigger API](/documentation/x1.png)
#### Full
The status bar will be a solid color based on the status of the monitor.
![Trigger API](/x2.png)
![Trigger API](/documentation/x2.png)
---
@@ -51,11 +51,11 @@ Adjust the roundness of the status bar.
#### SHARP
![Trigger API](/x4.png)
![Trigger API](/documentation/x4.png)
#### ROUNDED
![Trigger API](/x3.png)
![Trigger API](/documentation/x3.png)
---
@@ -81,7 +81,7 @@ You can change the colors of UP/DEGRADED/DOWN states of the monitor. You show ha
## Font
You can change the font of the status page. You can choose from a list of google fonts.
You can change the font of the status page. You can choose from a list of google, or [bunny fonts](https://fonts.bunny.net/) (which is a fully GDPR-compliant, zero-tracking/no-logging CDN font service).
### Font URL
@@ -97,7 +97,7 @@ You can add custom CSS to your status page. This will be added to the head of th
```css
.my-class {
color: red;
color: red;
}
```
+254 -72
View File
@@ -3,13 +3,13 @@ title: Triggers | Kener
description: Learn how to set up and work with triggers in kener.
---
# Triggers
## Triggers
Triggers are used to trigger actions based on the status of your monitors. You can use triggers to send notifications, or call webhooks when a monitor goes down or up.
<div class="border rounded-md">
![Trigger API](/trig_1.png)
![Trigger API](/documentation/trig_1.png)
</div>
@@ -27,10 +27,36 @@ The description is used to define the description of the webhook. It is optional
Kener supports the following triggers:
- [Webhook](#webhook)
- [Discord](#discord)
- [Slack](#slack)
- [Email](#email)
- [Webhook](#webhook)
- [Discord](#discord)
- [Slack](#slack)
- [Email](#email)
### Trigger Variables
You can use the following variables in the triggers using mustache syntax.
| Key | Description | Mustache Variable |
| ------------- | ------------------------------------------------------------ | ----------------- |
| site_name | [string] Name of your site | {{site_name}} |
| logo_url | [string] Logo of your site | {{logo_url}} |
| site_url | [string] Url of your site | {{site_url}} |
| alert_name | [string] Name of the alert | {{alert_name}} |
| status | [string] Status of the alert. Can be `TRIGGERED`, `RESOLVED` | {{status}} |
| is_resolved | [bool] True if alert is `RESOLVED` | {{is_resolved}} |
| is_triggered | [bool] True if alert is `TRIGGERED` | {{is_triggered}} |
| description | [string] Description of the alert. | {{description}} |
| action_text | [string] CTA for next action on the alert | {{action_text}} |
| action_url | [string] URL for next action on the alert | {{action_url}} |
| metric | [string] Name of the monitor | {{metric}} |
| severity | [string] Severity of the alert. Can be `critical`, `warn` | {{severity}} |
| id | [string] Id of the alert | {{id}} |
| current_value | [number] Current count of failures | {{current_value}} |
| threshold | [number] Threshold Failue | {{threshold}} |
| source | [string] Source of the alert | {{source}} |
| timestamp | [string] Timestamp **ISO 8601** formatted | {{timestamp}} |
---
## Webhook
@@ -38,7 +64,7 @@ Webhook triggers are used to send a HTTP POST request to a URL when a monitor go
<div class="border rounded-md">
![Trigger API](/trig_web.png)
![Trigger API](/documentation/trig_web.png)
</div>
@@ -72,45 +98,86 @@ Body of the webhook will be sent as below:
```json
{
"id": "mockoon-9",
"alert_name": "Mockoon DOWN",
"severity": "critical",
"status": "TRIGGERED",
"source": "Kener",
"timestamp": "2024-11-27T04:55:00.369Z",
"description": "🚨 **Service Alert**: Check the details below",
"details": {
"metric": "Mockoon",
"current_value": 1,
"threshold": 1
},
"actions": [
{
"text": "View Monitor",
"url": "https://kener.ing/monitor-mockoon"
}
]
"id": "mockoon-9",
"alert_name": "Mockoon DOWN",
"severity": "critical",
"status": "TRIGGERED",
"source": "Kener",
"timestamp": "2024-11-27T04:55:00.369Z",
"description": "🚨 **Service Alert**: Check the details below",
"details": {
"metric": "Mockoon",
"current_value": 1,
"threshold": 1
},
"actions": [
{
"text": "View Monitor",
"url": "https://kener.ing/monitor-mockoon"
}
]
}
```
| Key | Description | Variable |
| --------------------- | ----------------------------------------------------------- | ----------------------------- |
| id | Unique ID of the alert | ${id} |
| alert_name | Name of the alert | ${alert_name} |
| severity | Severity of the alert. Can be `critical`, `warn` | ${severity} |
| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` | ${status} |
| source | Source of the alert. Can be `Kener` | ${source} |
| timestamp | Timestamp of the alert | ${timestamp} |
| description | Description of the alert. This you can customize. See below | ${description} |
| details | Details of the alert. | - |
| details.metric | Name of the monitor | ${metric} |
| details.current_value | Current value of the monitor | ${current_value} |
| details.threshold | Alert trigger threshold of the monitor | ${threshold} |
| actions | Actions to be taken. Link to view the monitor. | ${action_text}, ${action_url} |
### Custom Body
You can customize the body of the webhook. You can use the variables mentioned above. If you are not using a json body then please make sure you are using the right content-type by setting custom headers. See examples below.
Using [mustache variables](#triggers-trigger-variables) you can customize the body of the webhook. If you are not using a json body then please make sure you are using the right content-type by setting custom headers.
### Examples
#### 1. Telegram
You can use the webhook trigger to send a message to a telegram channel. Enable `Use a custom webhook body`.
Set the URL to `https://api.telegram.org/bot[BOT_TOKEN]/sendMessage`. Replace [BOT_TOKEN] with your bot token.
```json
{
"chat_id": "[CHAT_ID]", // Replace [CHAT_ID] with your chat id
"text": "<b>{{alert_name}}</b>\n\n<b>Severity:</b> <code>{{severity}}</code>\n<b>Status:</b> {{status}}\n<b>Source:</b> Kener\n<b>Time:</b> {{timestamp}}\n\n📌 <b>Details:</b>\n- <b>Metric:</b>{{metric}}\n- <b>Current Value:</b> <code>{{current_value}}</code>\n- <b>Threshold:</b> <code>{{threshold}}</code>\n\n🔍 <a href=\"{{action_url}}\">{{action_text}}</a>",
"parse_mode": "HTML"
}
```
If you want to send a message to a group, then replace `[CHAT_ID]` with the group id.
You can also use environment variables to store the bot token and chat id. In that case the URL will be `https://api.telegram.org/bot$BOT_TOKEN/sendMessage`. In the body you can use `"chat_id": "$CHAT_ID"`. Make sure you have set the `BOT_TOKEN` and `CHAT_ID` in the <a href="/docs/environment-vars#secrets">environment variables</a>.
#### 2. Conditional Webhook
You can use `is_triggered` and `is_resolved` to create conditional webhooks.
```json
{
"title": "Monitor Alert for {{metric}} has been {{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}. Current value is {{current_value}} and threshold set is {{threshold}}"
}
```
The output will be
```json
{
"title": "Monitor Alert for Mockoon has been 🔴 Triggered. Current value is 1 and threshold set is 1"
}
```
```json
{
"title": "Monitor Alert for Mockoon has been 🟢 Resolved. Current value is 0 and threshold set is 1"
}
```
#### 3. Combing mustache with env variables
Combine the above example with `NODE_ENV` environment variable.
```json
{
"title": "Monitor Alert for {{metric}} in environment $NODE_ENV has been {{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}. Current value is {{current_value}} and threshold set is {{threshold}}."
}
```
---
## Discord
@@ -118,7 +185,7 @@ Discord triggers are used to send a message to a discord channel when a monitor
<div class="border rounded-md">
![Trigger API](/trig_2.png)
![Trigger API](/documentation/trig_2.png)
</div>
@@ -147,7 +214,61 @@ The discord message when alert is `TRIGGERED` will look like this
The discord message when alert is `RESOLVED` will look like this
![Discord](/discord_resolved.png)
![Discord](/documentation/discord_resolved.png)
### Modify Discord Message
You can modify the discord message by using [mustache variables](#triggers-trigger-variables).
```json
{
"username": "{{site_name}}",
"avatar_url": "{{{logo_url}}}",
"content": "## {{alert_name}}\n{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}\n{{description}}\nClick [MY CTA]({{{action_url}}}) for more.",
"embeds": [
{
"title": "❌{{alert_name}}❌",
"description": "{{description}}",
"url": "{{{action_url}}}",
"color": "{{#is_triggered}}13250616{{/is_triggered}}{{#is_resolved}}5156244{{/is_resolved}}",
"fields": [
{
"name": "Monitor",
"value": "{{metric}}",
"inline": false
},
{
"name": "Severity",
"value": "{{severity}}",
"inline": false
},
{
"name": "Alert ID",
"value": "{{id}}",
"inline": false
},
{
"name": "Current Value",
"value": "{{current_value}}",
"inline": true
},
{
"name": "Threshold",
"value": "{{threshold}}",
"inline": true
}
],
"footer": {
"text": "{{source}}",
"icon_url": "{{{logo_url}}}"
},
"timestamp": "{{timestamp}}"
}
]
}
```
---
## Slack
@@ -155,7 +276,7 @@ Slack triggers are used to send a message to a slack channel when a monitor goes
<div class="border rounded-md">
![Trigger API](/trig_3.png)
![Trigger API](/documentation/trig_3.png)
</div>
@@ -185,7 +306,82 @@ The slack message when alert is `TRIGGERED` will look like this
The slack message when alert is `RESOLVED` will look like this
![Slack](/slack_resolved.png)
![Slack](/documentation/slack_resolved.png)
### Modify Slack Message
You can modify the slack message by using [mustache variables](#triggers-trigger-variables).
```json
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Hello Alert {{alert_name}}",
"emoji": true
}
},
{
"type": "header",
"text": {
"type": "plain_text",
"text": "{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "{{description}}\n*Source:* {{source}}\n*Severity:* {{severity}}\n*Status:* {{status}}"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Metric:*\n{{metric}}"
},
{
"type": "mrkdwn",
"text": "*Current Value:*\n{{current_value}}"
},
{
"type": "mrkdwn",
"text": "*Threshold:*\n{{threshold}}"
},
{
"type": "mrkdwn",
"text": "*Environment:*\n$_NODE_ENV"
},
{
"type": "mrkdwn",
"text": "*Timestamp:*\n<!date^{{timestamp_unix}}^{date} at {time}|{{timestamp}}>"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "{{action_text}}"
},
"url": "{{{action_url}}}",
"style": "primary"
}
]
}
]
}
```
---
## Email
@@ -193,7 +389,7 @@ Email triggers are used to send an email when a monitor goes down or up. Kener s
<div class="border rounded-md">
![Trigger API](/trig_4.png)
![Trigger API](/documentation/trig_4.png)
</div>
@@ -205,10 +401,10 @@ To send emails using Resend you just need to set `RESEND_API_KEY` in the environ
To send emails using SMTP, please enter
- Host: SMTP server host
- Port: SMTP server port
- User: SMTP server username
- Password: SMTP server password
- Host: SMTP server host
- Port: SMTP server port
- User: SMTP server username
- Password: SMTP server password
<div class="note danger">
@@ -216,6 +412,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">
@@ -250,11 +452,11 @@ Subject of the email when `RESOLVED`
The emaik message when alert is `TRIGGERED` will look like this
![Slack](/em_t.png)
![Slack](/documentation/em_t.png)
The emaik message when alert is `RESOLVED` will look like this
![Slack](/em_r.png)
![Slack](/documentation/em_r.png)
---
@@ -267,23 +469,3 @@ Click on the ⚙️ to edit the trigger.
You can deactivate the trigger by switching the toggle to off. You cannot send message to a deactivated trigger. Any monitor with this trigger will not send any notifications.
---
## Examples
### Telegram
You can use the webhook trigger to send a message to a telegram channel. Enable `Use a custom webhook body`.
Set the URL to `https://api.telegram.org/bot[BOT_TOKEN]/sendMessage`. Replace [BOT_TOKEN] with your bot token.
```json
{
"chat_id": "[CHAT_ID]", // Replace [CHAT_ID] with your chat id
"text": "<b>${alert_name}</b>\n\n<b>Severity:</b> <code>${severity}</code>\n<b>Status:</b> ${status}\n<b>Source:</b> Kener\n<b>Time:</b> ${timestamp}\n\n📌 <b>Details:</b>\n- <b>Metric:</b>${metric}\n- <b>Current Value:</b> <code>${current_value}</code>\n- <b>Threshold:</b> <code>${threshold}</code>\n\n🔍 <a href=\"${action_url}\">${action_text}</a>",
"parse_mode": "HTML"
}
```
If you want to send a message to a group, then replace `[CHAT_ID]` with the group id.
You can also use environment variables to store the bot token and chat id. In that case the URL will be `https://api.telegram.org/bot$BOT_TOKEN/sendMessage`. In the body you can use `"chat_id": "$CHAT_ID"`. Make sure you have set the `BOT_TOKEN` and `CHAT_ID` in the <a href="/docs/environment-vars#secrets">environment variables</a>.
+16 -17
View File
@@ -1,20 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
background-color: #111;
}
</style>
</head>
<body>
Hello world
<script
async
src="http://localhost:3000/embed/monitor-earth/js?theme=dark&bgc=111&locale=hi&monitor=http://localhost:3000/embed/monitor-earth"
></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
background-color: #111;
}
</style>
</head>
<body>
<script
async
src="http://localhost:3000/embed/monitor-bitbuckettcp/js?bgc=212226&locale=en&theme=dark&monitor=http://localhost:3000/embed/monitor-bitbuckettcp"
></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
set -e
# Automatically set PUBLIC_WHITE_LABEL based on WHITE_LABEL
export PUBLIC_WHITE_LABEL="${WHITE_LABEL}"
# Replace shell with the given command (from CMD or runtime args)
exec "$@"
+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");
});
}
@@ -0,0 +1,38 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export function up(knex) {
return knex.schema.createTable("invitations", (table) => {
// Primary key
table.increments("id").primary();
// Core invitation fields
table.string("invitation_token").unique().notNullable();
table.string("invitation_type").notNullable();
table.integer("invited_user_id").nullable();
table.integer("invited_by_user_id").notNullable();
// Additional data fields
table.text("invitation_meta").nullable(); // For storing JSON or other metadata
table.timestamp("invitation_expiry").notNullable();
table.string("invitation_status").notNullable().defaultTo("PENDING");
// Timestamps
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Indexes
table.index("invitation_status");
table.index("invitation_expiry");
table.index(["invited_by_user_id", "invitation_status"]);
});
}
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export function down(knex) {
return knex.schema.dropTableIfExists("invitations");
}
+9061 -7893
View File
File diff suppressed because it is too large Load Diff
+120 -109
View File
@@ -1,111 +1,122 @@
{
"name": "kener",
"version": "3.0.9",
"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.",
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
"keywords": [
"Node.js application",
"Open-source status page",
"Service monitoring tool",
"Real-time incident management",
"Customizable reporting",
"Service outage tracker",
"User-friendly dashboard",
"Incident communication platform",
"Scalable monitoring solution",
"Community-driven software",
"Website status tracker",
"Incident response tool",
"System status monitoring",
"Service reliability management",
"Incident alert system"
],
"repository": {
"type": "git",
"url": "https://github.com/rajnandan1/kener.git"
},
"scripts": {
"build": "vite build",
"preview": "vite preview",
"configure": "node build.js",
"migrate": "npx knex migrate:latest",
"preseed": "npx knex migrate:latest",
"seed": "npx knex seed:run",
"predev": "npm run seed",
"devschedule": "node src/lib/server/startup.js",
"schedule": "node src/lib/server/startup.js",
"development": "vite dev",
"dev": "npm-run-all --parallel devschedule development",
"prettify": "prettier --write .",
"start": "node main.js"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/kit": "^1.27.4",
"@tailwindcss/typography": "^0.5.10",
"@zerodevx/svelte-toast": "^0.9.6",
"autoprefixer": "^10.4.14",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"date-picker-svelte": "^2.15.1",
"postcss": "^8.4.24",
"postcss-load-config": "^4.0.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.5.14",
"svelte": "^4.0.5",
"svelte-awesome-color-picker": "^3.1.4",
"svelte-check": "^3.6.0",
"svelte-dnd-action": "^0.9.55",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.0",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
"analytics": "^0.8.14",
"axios": "^1.6.2",
"badge-maker": "^3.3.1",
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.5.0",
"bits-ui": "^0.9.9",
"clsx": "^2.0.0",
"croner": "^7.0.5",
"date-fns": "^4.1.0",
"dns2": "^2.1.0",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"figlet": "^1.8.0",
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lucide-svelte": "^0.292.0",
"marked": "^11.1.1",
"mode-watcher": "^0.4.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"mysql2": "^3.12.0",
"node-cache": "^5.1.2",
"nodemailer": "^6.10.0",
"npm-run-all": "^4.1.5",
"pg": "^8.13.1",
"pg-pool": "^3.7.0",
"ping": "^0.4.4",
"queue": "^7.0.0",
"randomstring": "^1.3.0",
"resend": "^4.0.1",
"svelte-legos": "^0.2.5",
"tailwind-merge": "^2.0.0",
"tailwind-variants": "^0.1.18"
},
"engines": {
"node": ">=20.0.0"
}
"name": "kener",
"version": "3.2.11",
"private": false,
"license": "MIT",
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
"keywords": [
"Node.js application",
"Open-source status page",
"Service monitoring tool",
"Real-time incident management",
"Customizable reporting",
"Service outage tracker",
"User-friendly dashboard",
"Incident communication platform",
"Scalable monitoring solution",
"Community-driven software",
"Website status tracker",
"Incident response tool",
"System status monitoring",
"Service reliability management",
"Incident alert system"
],
"repository": {
"type": "git",
"url": "https://github.com/rajnandan1/kener.git"
},
"scripts": {
"build": "vite build",
"preview": "vite preview",
"configure": "node build.js",
"migrate": "npx knex migrate:latest",
"preseed": "npx knex migrate:latest",
"seed": "npx knex seed:run",
"predev": "npm run seed",
"devschedule": "node src/lib/server/startup.js",
"schedule": "node src/lib/server/startup.js",
"development": "vite dev",
"generate-readme": "node scripts/generate-readme.js",
"dev": "npm-run-all --parallel devschedule development",
"prettify": "prettier --write .",
"start": "node main.js"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/kit": "^1.27.4",
"@tailwindcss/typography": "^0.5.10",
"@zerodevx/svelte-toast": "^0.9.6",
"autoprefixer": "^10.4.14",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"date-picker-svelte": "^2.15.1",
"postcss": "^8.4.24",
"postcss-load-config": "^4.0.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.5.14",
"svelte": "^4.0.5",
"svelte-awesome-color-picker": "^3.1.4",
"svelte-check": "^3.6.0",
"svelte-dnd-action": "^0.9.55",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.0",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
"@uiw/codemirror-theme-github": "^4.23.10",
"analytics": "^0.8.14",
"axios": "^1.6.2",
"badge-maker": "^3.3.1",
"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",
"figlet": "^1.8.0",
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lucide-svelte": "^0.483.0",
"marked": "^11.1.1",
"mode-watcher": "^0.4.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"mustache": "^4.2.0",
"mysql2": "^3.12.0",
"node-cache": "^5.1.2",
"nodemailer": "^6.10.0",
"npm-run-all": "^4.1.5",
"pg": "^8.13.1",
"pg-pool": "^3.7.0",
"ping": "^0.4.4",
"queue": "^7.0.0",
"randomstring": "^1.3.0",
"resend": "^4.0.1",
"svelte-codemirror-editor": "^1.4.1",
"svelte-legos": "^0.2.5",
"tailwind-merge": "^2.0.0",
"tailwind-variants": "^0.1.18",
"vite-plugin-package-version": "^1.1.0"
},
"engines": {
"node": ">=20.0.0"
}
}
+31
View File
@@ -0,0 +1,31 @@
import fs from "fs";
import Mustache from "mustache";
import dotenv from "dotenv";
import { fileURLToPath } from "url";
import path from "path";
// Load environment variables from .env file
dotenv.config();
// Resolve paths correctly in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load the template
const templatePath = path.resolve(__dirname, "../README.template.md");
const template = fs.readFileSync(templatePath, "utf-8");
// Load environment variables and provide default values
const data = {
kener_full_version: process.env.BUILD_FULL_VERSION || "N/A",
kener_major_version: process.env.BUILD_MAJOR_VERSION || "N/A",
kener_major_minor_version: process.env.BUILD_MAJOR_MINOR_VERSION || "N/A",
};
// Render README.md
const output = Mustache.render(template, data);
// Write to README.md
fs.writeFileSync(path.resolve(__dirname, "../README.md"), output);
console.log("✅ README.md generated successfully!");
-45
View File
@@ -1,45 +0,0 @@
//read from process.env.PUBLIC_KENER_FOLDER / site.json
//read from process.env.PUBLIC_KENER_FOLDER / monitors.json
// create sitemap.xml
import fs from "fs-extra";
let siteMap = "";
import dotenv from "dotenv";
dotenv.config();
const site = fs.readJSONSync("./database/site.json", "utf8");
const monitors = fs.readJSONSync("./database/monitors.json", "utf8");
if (site.siteURL !== undefined && site.siteURL !== null && site.siteURL !== "") {
if (monitors.length > 0) {
siteMap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.sitemaps.org/schemas/sitemap/0.9
https://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
${monitors
.map((monitor) => {
return `
<url>
<loc>${site.siteURL}/incident/${monitor.tag}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>`;
})
.join("\n")}
${monitors
.map((monitor) => {
return `
<url>
<loc>${site.siteURL}/monitor-${encodeURIComponent(monitor.tag)}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>`;
})
.join("\n")}
</urlset>`;
}
}
//export default siteMap
export default siteMap;
+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;
}
}
+37 -23
View File
@@ -1,69 +1,83 @@
code:not([class^="language-"]) {
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs dark:bg-gray-800;
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs dark:bg-gray-800;
}
.sidebar-item.active,
.sidebar-item:hover {
color: #ed702d;
color: #ed702d;
}
.w-585px {
width: 585px;
width: 585px;
}
main {
scroll-behavior: smooth;
scroll-behavior: smooth;
}
.kener-home-links a {
text-decoration: none;
background-color: var(--bg-background);
text-decoration: none;
background-color: var(--bg-background);
}
.kener-home-links > div:hover {
transition: all 0.3s;
box-shadow: 0 0 8px 1.5px #3e9a4b;
.kener-home-links > a:hover {
transition: all 0.3s;
border-color: #ed702d;
background-color: #ed702d;
}
.accm input:checked ~ div {
display: block;
display: block;
}
.accm input ~ div {
display: none;
display: none;
}
.accm input {
visibility: hidden;
visibility: hidden;
}
.accmt {
background-color: hsl(223, 10%, 14%);
background-color: hsl(223, 10%, 14%);
}
.accm input:checked ~ .showaccm span:first-child {
display: none;
display: none;
}
.accm input:checked ~ .showaccm span:last-child {
display: block;
display: block;
}
.accm input ~ .showaccm span:first-child {
display: block;
display: block;
}
.accm input ~ .showaccm span:last-child {
display: none;
display: none;
}
.hljs {
background: transparent !important;
background: transparent !important;
}
.note {
@apply mt-4 rounded-md border bg-background p-3 text-sm shadow-sm;
@apply mt-4 rounded-md border bg-background p-3 text-sm shadow-sm;
}
.note.danger {
border: 1px solid #e3342f;
color: #e3342f;
border: 1px solid #e3342f;
color: #e3342f;
}
.note.info {
border: 1px solid #3490dc;
color: #3490dc;
border: 1px solid #3490dc;
color: #3490dc;
}
.copybtn .copy-btn {
transform: scale(1);
}
.copybtn .check-btn {
transform: scale(0);
}
.copybtn:focus .copy-btn {
transform: scale(0);
}
.copybtn:focus .check-btn {
transform: scale(1);
}
+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;
}
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event);
response.headers.delete("Link");
return response;
}
+703 -606
View File
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
<script>
import { onMount, onDestroy, beforeUpdate } from "svelte";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
export let monitor;
export let theme = "light";
export let bgc = "transparent";
export let locale = "en";
let iframe, listeners;
let containerId = `embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
let isMounted = false;
let prevMonitor, prevTheme, prevBgc, prevLocale;
onMount(() => {
if (!monitor) return;
isMounted = true;
prevMonitor = monitor;
prevTheme = theme;
prevBgc = bgc;
prevLocale = locale;
const container = document.getElementById(containerId);
if (!container) return;
const uid = "KENER_" + ~~(new Date().getTime() / 86400000);
const uriEmbedded = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
iframe = document.createElement("iframe");
iframe.src = uriEmbedded;
iframe.id = uid;
iframe.width = "0%";
iframe.height = "0";
iframe.frameBorder = "0";
iframe.allowTransparency = true;
iframe.sandbox =
"allow-modals allow-forms allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation allow-downloads";
iframe.allow = "midi; geolocation; microphone; camera; display-capture; encrypted-media;";
container.appendChild(iframe);
const setHeight = (data) => {
if (data.height !== undefined) {
iframe.height = data.height;
}
};
const setWidth = (data) => {
if (data.width !== undefined) {
iframe.width = data.width;
}
};
listeners = (event) => {
if (event.data && event.data.height !== undefined) {
setHeight(event.data);
}
if (event.data && event.data.width !== undefined) {
setWidth(event.data);
}
};
window.addEventListener("message", listeners, false);
});
beforeUpdate(() => {
if (
isMounted &&
iframe &&
(monitor !== prevMonitor || theme !== prevTheme || bgc !== prevBgc || locale !== prevLocale)
) {
iframe.src = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
// Update previous values
prevMonitor = monitor;
prevTheme = theme;
prevBgc = bgc;
prevLocale = locale;
dispatch("update", {
monitor: prevMonitor,
theme: prevTheme,
bgc: prevBgc,
locale: prevLocale
});
}
});
onDestroy(() => {
if (iframe) {
window.removeEventListener("message", listeners);
iframe.remove();
}
});
</script>
<!-- Dynamic container with unique ID -->
<div id={containerId}></div>
+194
View File
@@ -0,0 +1,194 @@
export const DiscordJSONTemplate = JSON.stringify(
{
username: "{{site_name}}",
avatar_url: "{{{logo_url}}}",
content:
"## {{alert_name}}\n{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}\n{{description}}\nClick [{{action_text}}]({{{action_url}}}) for more.",
embeds: [
{
title: "{{alert_name}}",
description: "{{description}}",
url: "{{{action_url}}}",
color: "{{#is_triggered}}13250616{{/is_triggered}}{{#is_resolved}}5156244{{/is_resolved}}",
fields: [
{
name: "Monitor",
value: "{{metric}}",
inline: false,
},
{
name: "Severity",
value: "{{severity}}",
inline: false,
},
{
name: "Alert ID",
value: "{{id}}",
inline: false,
},
{
name: "Current Value",
value: "{{current_value}}",
inline: true,
},
{
name: "Threshold",
value: "{{threshold}}",
inline: true,
},
],
footer: {
text: "{{source}}",
icon_url: "{{{logo_url}}}",
},
timestamp: "{{timestamp}}",
},
],
},
null,
2,
);
export const WebhookJSONTemplate = JSON.stringify(
{
id: "{{id}}",
alert_name: "{{alert_name}}",
severity: "{{severity}}",
status: "{{status}}",
source: "{{source}}",
timestamp: "{{timestamp}}",
description: "{{description}}",
details: {
metric: "{{metric}}",
current_value: "{{current_value}}",
threshold: "{{threshold}}",
},
actions: [
{
text: "{{action_text}}",
url: "{{action_url}}",
},
],
},
null,
2,
);
export const SlackJSONTemplate = JSON.stringify(
{
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "{{alert_name}}",
emoji: true,
},
},
{
type: "header",
text: {
type: "plain_text",
text: "{{#is_triggered}}🔴 Triggered{{/is_triggered}}{{#is_resolved}}🟢 Resolved{{/is_resolved}}",
emoji: true,
},
},
{
type: "section",
text: {
type: "mrkdwn",
text: "{{description}}\n*Source:* {{source}}\n*Severity:* {{severity}}\n*Status:* {{status}}",
},
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: "*Metric:*\n{{metric}}",
},
{
type: "mrkdwn",
text: "*Current Value:*\n{{current_value}}",
},
{
type: "mrkdwn",
text: "*Threshold:*\n{{threshold}}",
},
{
type: "mrkdwn",
text: "*Timestamp:*\n<!date^{{timestamp_unix}}^{date} at {time}|{{timestamp}}>",
},
],
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "{{action_text}}",
},
url: "{{{action_url}}}",
style: "primary",
},
],
},
],
},
null,
2,
);
export const DefaultAPIEval = `(async function (statusCode, responseTime, responseRaw, modules) {
let statusCodeShort = Math.floor(statusCode/100);
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
return {
status: 'UP',
latency: responseTime,
}
}
return {
status: 'DOWN',
latency: responseTime,
}
})`;
export const DefaultPingEval = `(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let alive = arrayOfPings.reduce((acc, ping) => {
return acc && ping.alive;
}, true);
return {
status: alive ? 'UP' : 'DOWN',
latency: latencyTotal / arrayOfPings.length,
}
})`;
export const DefaultTCPEval = `(async function (arrayOfPings) {
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let alive = arrayOfPings.reduce((acc, ping) => {
return acc && ping.status === "open";
}, true);
return {
status: alive ? 'UP' : 'DOWN',
latency: latencyTotal / arrayOfPings.length,
}
})`;
export const ErrorSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="60" viewBox="0 0 120 60" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="30" cy="24" r="10"/>
<path d="M26 27h8"/>
<path d="M26 21h2"/>
<path d="M32 21h2"/>
<text x="80" y="29" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" fill="currentColor" font-weight="300">Not Found</text>
</svg>`;
+13 -10
View File
@@ -1,13 +1,16 @@
// @ts-ignore
export const analyticsEvent = (event, data) => {
// Do something with the event and data
document.dispatchEvent(
new CustomEvent("analyticsEvent", {
bubbles: true,
detail: {
event,
data
}
})
);
// Do something with the event and data
event = "kener:" + event;
if (typeof document !== "undefined") {
document.dispatchEvent(
new CustomEvent("analyticsEvent", {
bubbles: true,
detail: {
event,
data,
},
}),
);
}
};
+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}(:\d{1,5})?$/;
if (ipv4Regex.test(input)) {
return "IPv4";
}
// 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}|:))(?::\d{1,5})?$/;
if (ipv6Regex.test(input)) {
return "IPv6";
}
// 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,}(:\d{1,5})?$/;
if (domainRegex.test(input)) {
return "Domain Name";
}
// 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,
};
+220 -163
View File
@@ -1,175 +1,232 @@
<script>
import { formatDistanceToNow, formatDistance } from "date-fns";
import { formatDistanceToNow, formatDistance } from "date-fns";
import Settings from "lucide-svelte/icons/settings";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import { analyticsEvent } from "$lib/boringOne";
import * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
import { Button } from "$lib/components/ui/button";
import { Tooltip } from "bits-ui";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { marked } from "marked";
export let incident;
export let index;
export let lang;
let startTime = new Date(incident.start_date_time * 1000);
let endTime = new Date();
let nowTime = new Date();
if (incident.end_date_time) {
endTime = new Date(incident.end_date_time * 1000);
}
let incidentType = incident.incident_type;
const lastedFor = fd(startTime, endTime, $page.data.selectedLang);
const remainingTime = fd(nowTime, endTime, $page.data.selectedLang);
const startedAt = fdn(startTime, $page.data.selectedLang);
import * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
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();
if (incident.end_date_time) {
endTime = new Date(incident.end_date_time * 1000);
}
let incidentType = incident.incident_type;
const lastedFor = fd(startTime, endTime, selectedLang);
const startedAt = fdn(startTime, selectedLang);
let incidentTimeStatus = "";
if (nowTime < startTime) {
incidentTimeStatus = "YET_TO_START";
} else if (nowTime >= startTime && nowTime <= endTime) {
incidentTimeStatus = "ONGOING";
} else if (nowTime > endTime) {
incidentTimeStatus = "COMPLETED";
}
let isFuture = false;
//is future incident
if (nowTime < startTime) {
isFuture = true;
}
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", {
startedAt
});
maintenanceBadge = "Maintenance in Progress";
maintenanceBadgeColor = "text-maintenance-in-progress";
} else if (!isFuture && incident.state == "RESOLVED") {
incidentDateSummary = l(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";
}
let incidentDateSummary = "";
let maintenanceBadge = "";
let maintenanceBadgeColor = "";
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
});
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";
}
</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.Trigger class="px-4 hover:bg-muted hover:no-underline">
<div class="w-full text-left hover:no-underline">
{#if incidentType == "INCIDENT"}
<p
class="scroll-m-20 text-xs font-semibold leading-5 tracking-normal badge-{incident.state}"
>
{l(lang, incident.state)}
</p>
{:else if incidentType == "MAINTENANCE"}
<p
class="{maintenanceBadgeColor} scroll-m-20 text-xs font-semibold leading-5 tracking-normal"
>
{l(lang, maintenanceBadge)}
</p>
{/if}
<div class="col-span-12">
<Accordion.Root bind:value={index} class="accor">
<Accordion.Item value={accordionValue}>
<Accordion.Trigger
class="rounded-md px-4 hover:bg-muted hover:no-underline"
on:click={() => analyticsEvent("incident_open", { incident_title: incident.title })}
>
<div class="w-full text-left hover:no-underline">
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
<span class="badge-{incident.state}">
{l($page.data.lang, incident.state)}
</span>
{:else if incidentType == "MAINTENANCE"}
<span class="{maintenanceBadgeColor} ">
{l($page.data.lang, maintenanceBadge)}
</span>
{/if}
{#if $page.data.isLoggedIn}
<Button
href="{base}/manage/app/events#{incident.id}"
class=" rotate-once h-5 p-0 text-muted-foreground hover:text-primary"
variant="link"
>
<Settings class="h-4 w-4 " />
</Button>
{/if}
</p>
<p class="scroll-m-20 text-lg font-medium tracking-tight">
{incident.title}
</p>
{#if !!incidentDateSummary}
<p
class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground"
>
{incidentDateSummary}
</p>
{/if}
</div>
</Accordion.Trigger>
<Accordion.Content>
<div class="px-4 pt-2">
{#if incident.monitors.length > 0}
<div class="flex 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
class="bg-api-{monitor.impact_type.toLowerCase()} rounded px-1.5 py-1 text-xs font-semibold text-primary-foreground"
>
{monitor.impact_type}
</div>
{#if monitor.image}
<img src={base + monitor.image} class="mt-1 h-4 w-4" />
{/if}
<div class="mt-0.5 font-medium">
{monitor.name}
</div>
</div>
{/each}
</div>
{/if}
<p class="my-3 text-xs font-semibold uppercase text-muted-foreground">
{l(lang, "Updates")}
</p>
{#if incident.comments.length > 0}
{#if incidentType == "INCIDENT"}
<ol class="relative mt-2 pl-14">
{#each incident.comments as comment}
<li
class="relative border-l pb-4 pl-[4.5rem] last:border-0"
>
<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)}
</div>
<p class="font-medium">
{incident.title}
</p>
{#if !!incidentDateSummary}
<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 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
class="bg-api-{monitor.impact_type.toLowerCase()} rounded px-1.5 py-1 text-xs font-semibold text-primary-foreground"
>
{monitor.impact_type}
</div>
{#if monitor.image}
<GMI src={monitor.image} classList="mt-1 h-4 w-4" />
{/if}
<div class="mt-0.5 font-medium">
{monitor.name}
</div>
</div>
{/each}
</div>
{/if}
<p class="my-3 text-xs font-semibold uppercase text-muted-foreground">
{l($page.data.lang, "Updates")}
</p>
{#if incident.comments.length > 0}
{#if incidentType == "INCIDENT"}
<ol class="relative mt-2 pl-14">
{#each incident.comments as comment}
<li class="relative border-l pb-4 pl-[4.5rem] last:border-0">
<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($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
)}
</time>
<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",
$page.data.selectedLang,
$page.data.localTz
)}
</time>
<p class="mb-4 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
{:else if incidentType == "MAINTENANCE"}
<ol class="relative mt-2 pl-0">
{#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
)}
</time>
<div class="mb-4 text-sm font-normal">
<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"
>
{@html marked.parse(comment.comment)}
</div>
</div>
</li>
{/each}
</ol>
{:else if incidentType == "MAINTENANCE"}
<ol class="relative mt-2 pl-0">
{#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",
$page.data.selectedLang,
$page.data.localTz
)}
</time>
<p class="mb-2 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
{/if}
{:else}
<p class="text-sm font-medium">
{l(lang, "No Updates Yet")}
</p>
{/if}
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</div>
<p class="mb-2 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
{/if}
{:else}
<p class="text-sm font-medium">
{l($page.data.lang, "No Updates Yet")}
</p>
{/if}
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</div>
</div>
+11
View File
@@ -0,0 +1,11 @@
<script>
import { base } from "$app/paths";
export let src = "";
export let alt = "GMI";
export let classList = "";
export let style = "";
export let srcset = "";
if (!src.startsWith("http")) src = `${base}${src}`;
</script>
<img {src} {alt} class={classList} {style} {srcset} />
+197 -201
View File
@@ -1,209 +1,205 @@
<script>
import { onMount } from "svelte";
import { base } from "$app/paths";
import moment from "moment";
import { Loader, ChevronLeft, ChevronRight } from "lucide-svelte";
import { onMount } from "svelte";
import { base } from "$app/paths";
import moment from "moment";
import Loader from "lucide-svelte/icons/loader";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import ChevronRight from "lucide-svelte/icons/chevron-right";
export let data;
let pageNo = 1;
let limit = 20;
let alerts = [];
let totalPages = 0;
let loadingData = false;
export let data;
let pageNo = 1;
let limit = 20;
let alerts = [];
let totalPages = 0;
let loadingData = false;
let incidentURL = "";
if (data.siteData.github && data.siteData.github.owner && data.siteData.github.repo) {
incidentURL = `https://github.com/${data.siteData.github.owner}/${data.siteData.github.repo}/issues`;
}
let incidentURL = "";
if (data.siteData.github && data.siteData.github.owner && data.siteData.github.repo) {
incidentURL = `https://github.com/${data.siteData.github.owner}/${data.siteData.github.repo}/issues`;
}
async function loadData() {
loadingData = true;
try {
let apiResp = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "getAllAlertsPaginated",
data: { page: pageNo, limit }
})
});
let resp = await apiResp.json();
alerts = resp.alerts;
totalPages = Math.ceil(resp.total.count / limit);
} catch (error) {
alert("Error: " + error);
} finally {
loadingData = false;
}
}
async function loadData() {
loadingData = true;
try {
let apiResp = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "getAllAlertsPaginated",
data: { page: pageNo, limit }
})
});
let resp = await apiResp.json();
alerts = resp.alerts;
totalPages = Math.ceil(resp.total.count / limit);
} catch (error) {
alert("Error: " + error);
} finally {
loadingData = false;
}
}
onMount(async () => {
await loadData();
});
onMount(async () => {
await loadData();
});
</script>
<div class="mt-4">
<div class="mb-4 flex justify-between">
<div>
{#if loadingData}
<Loader class="mt-6 h-4 w-4 animate-spin" />
{/if}
</div>
<!-- Pagination -->
{#if totalPages > 1}
<nav class="flex max-w-2xl items-center gap-x-1" aria-label="Pagination">
<button
disabled={pageNo === 1}
type="button"
class="inline-flex min-h-[38px] min-w-[38px] items-center justify-center gap-x-2 rounded-lg border border-transparent px-2.5 py-2 text-sm text-gray-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
aria-label="Previous"
on:click={() => {
pageNo--;
loadData();
}}
>
<ChevronLeft class="size-3.5 shrink-0" />
<span class="sr-only">Previous</span>
</button>
<div class="flex max-w-[380px] items-center gap-x-1 overflow-x-auto">
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
<button
on:click={() => {
pageNo = page;
loadData();
}}
type="button"
class="flex min-h-[38px] min-w-[38px] items-center justify-center rounded-lg border {page ==
pageNo
? 'border-gray-200'
: 'border-transparent'} px-3 py-2 text-sm text-gray-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
>
{page}
</button>
{/each}
</div>
<button
type="button"
disabled={pageNo === totalPages}
class="inline-flex min-h-[38px] min-w-[38px] items-center justify-center gap-x-2 rounded-lg border border-transparent px-2.5 py-2 text-sm text-gray-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
aria-label="Next"
on:click={() => {
pageNo++;
loadData();
}}
>
<span class="sr-only">Next</span>
<ChevronRight class="size-3.5 shrink-0" />
</button>
</nav>
{/if}
<!-- End Pagination -->
</div>
<div class="flex flex-col">
<div class="-m-1.5 overflow-x-auto">
<div class="inline-block min-w-full p-1.5 align-middle">
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
<thead>
<tr>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>ID</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Type</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Monitor</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Status</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Checks</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Incident</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Created At</th
>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
{#each alerts as alert}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium">
{alert.id}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
{#if alert.monitor_status === "DOWN"}
<span class="text-red-500">
{alert.monitor_status}
</span>
{:else}
<span class="text-yellow-500">
{alert.monitor_status}
</span>
{/if}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
{alert.monitor_tag}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
{#if alert.alert_status === "RESOLVED"}
<span class="text-blue-500">
{alert.alert_status}
</span>
{:else}
<span class="text-fuchsia-500">
{alert.alert_status}
</span>
{/if}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
{alert.health_checks}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
{#if !!alert.incident_number}
<a
target="_blank"
href="{incidentURL}/{alert.incident_number}"
class="text-cyan-500 hover:underline focus:outline-none"
>
{alert.incident_number}
</a>
{:else}
-
{/if}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
{moment
.utc(alert.created_at, "YYYY-MM-DD HH:mm:ss")
.local()
.format("YYYY-MM-DD HH:mm:ss")}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="mb-4 flex justify-between">
<div>
{#if loadingData}
<Loader class="mt-6 h-4 w-4 animate-spin" />
{/if}
</div>
<!-- Pagination -->
{#if totalPages > 1}
<nav class="flex max-w-2xl items-center gap-x-1" aria-label="Pagination">
<button
disabled={pageNo === 1}
type="button"
class="inline-flex min-h-[38px] min-w-[38px] items-center justify-center gap-x-2 rounded-lg border border-transparent px-2.5 py-2 text-sm text-gray-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
aria-label="Previous"
on:click={() => {
pageNo--;
loadData();
}}
>
<ChevronLeft class="size-3.5 shrink-0" />
<span class="sr-only">Previous</span>
</button>
<div class="flex max-w-[380px] items-center gap-x-1 overflow-x-auto">
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
<button
on:click={() => {
pageNo = page;
loadData();
}}
type="button"
class="flex min-h-[38px] min-w-[38px] items-center justify-center rounded-lg border {page == pageNo
? 'border-gray-200'
: 'border-transparent'} px-3 py-2 text-sm text-gray-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
>
{page}
</button>
{/each}
</div>
<button
type="button"
disabled={pageNo === totalPages}
class="inline-flex min-h-[38px] min-w-[38px] items-center justify-center gap-x-2 rounded-lg border border-transparent px-2.5 py-2 text-sm text-gray-800 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none disabled:pointer-events-none disabled:opacity-50 dark:border-transparent dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10"
aria-label="Next"
on:click={() => {
pageNo++;
loadData();
}}
>
<span class="sr-only">Next</span>
<ChevronRight class="size-3.5 shrink-0" />
</button>
</nav>
{/if}
<!-- End Pagination -->
</div>
<div class="flex flex-col">
<div class="-m-1.5 overflow-x-auto">
<div class="inline-block min-w-full p-1.5 align-middle">
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
<thead>
<tr>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500">ID</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Type</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Monitor</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Status</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Checks</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Incident</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Created At</th
>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
{#each alerts as alert}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium">
{alert.id}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
{#if alert.monitor_status === "DOWN"}
<span class="text-red-500">
{alert.monitor_status}
</span>
{:else}
<span class="text-yellow-500">
{alert.monitor_status}
</span>
{/if}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
{alert.monitor_tag}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
{#if alert.alert_status === "RESOLVED"}
<span class="text-blue-500">
{alert.alert_status}
</span>
{:else}
<span class="text-fuchsia-500">
{alert.alert_status}
</span>
{/if}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
{alert.health_checks}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
{#if !!alert.incident_number}
<a
rel="external"
href="{base}/manage/app/events#{alert.incident_number}"
class="text-cyan-500 hover:underline focus:outline-none"
>
{alert.incident_number}
</a>
{:else}
-
{/if}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
{moment.utc(alert.created_at, "YYYY-MM-DD HH:mm:ss").local().format("YYYY-MM-DD HH:mm:ss")}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
+233 -253
View File
@@ -1,271 +1,251 @@
<script>
import { onMount } from "svelte";
import { base } from "$app/paths";
import moment from "moment";
import { Button } from "$lib/components/ui/button";
import { Plus, X, Settings, Bell, Loader, Copy, Check } from "lucide-svelte";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { onMount } from "svelte";
import { base } from "$app/paths";
import moment from "moment";
import { Button } from "$lib/components/ui/button";
import Plus from "lucide-svelte/icons/plus";
import X from "lucide-svelte/icons/x";
import Settings from "lucide-svelte/icons/settings";
import Bell from "lucide-svelte/icons/bell";
import Loader from "lucide-svelte/icons/loader";
import Copy from "lucide-svelte/icons/copy";
import Check from "lucide-svelte/icons/check";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { page } from "$app/stores";
let apiKeys = [];
let loaderLoadingAll = false;
let loaderCreateNew = false;
let newAPIKeyName = "";
let newKeyResp = {};
let showCreateModal = false;
let apiKeys = [];
let loaderLoadingAll = false;
let loaderCreateNew = false;
let newAPIKeyName = "";
let newKeyResp = {};
let showCreateModal = false;
async function loadAPIKeys() {
loaderLoadingAll = true;
try {
let apiResp = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "getAPIKeys",
data: {}
})
});
let resp = await apiResp.json();
apiKeys = resp;
} catch (error) {
alert("Error: " + error);
} finally {
loaderLoadingAll = false;
}
}
async function loadAPIKeys() {
loaderLoadingAll = true;
try {
let apiResp = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "getAPIKeys",
data: {}
})
});
let resp = await apiResp.json();
apiKeys = resp;
} catch (error) {
alert("Error: " + error);
} finally {
loaderLoadingAll = false;
}
}
async function createNew() {
newKeyResp = {};
loaderCreateNew = true;
try {
let apiResp = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "createNewApiKey",
data: {
name: newAPIKeyName
}
})
});
newKeyResp = await apiResp.json();
loadAPIKeys();
showCreateModal = false;
} catch (error) {
alert("Error: " + error);
} finally {
loaderCreateNew = false;
}
}
async function createNew() {
newKeyResp = {};
loaderCreateNew = true;
try {
let apiResp = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "createNewApiKey",
data: {
name: newAPIKeyName
}
})
});
newKeyResp = await apiResp.json();
loadAPIKeys();
showCreateModal = false;
} catch (error) {
alert("Error: " + error);
} finally {
loaderCreateNew = false;
}
}
onMount(() => {
loadAPIKeys();
});
onMount(() => {
loadAPIKeys();
});
function copyKey() {
navigator.clipboard.writeText(newKeyResp.apiKey);
}
function copyKey() {
navigator.clipboard.writeText(newKeyResp.apiKey);
}
function updateStatus(apiKey) {
apiKey.status = apiKey.status == "ACTIVE" ? "INACTIVE" : "ACTIVE";
fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "updateApiKeyStatus",
data: {
id: apiKey.id,
status: apiKey.status
}
})
});
}
function updateStatus(apiKey) {
apiKey.status = apiKey.status == "ACTIVE" ? "INACTIVE" : "ACTIVE";
fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "updateApiKeyStatus",
data: {
id: apiKey.id,
status: apiKey.status
}
})
});
}
</script>
<div class="mt-4">
<div class="mb-4 flex justify-between">
<div>
{#if loaderLoadingAll}
<Loader class="mt-6 h-4 w-4 animate-spin" />
{/if}
</div>
<div>
<Button
on:click={(e) => {
showCreateModal = true;
}}
>
<Plus class="mr-2 h-4 w-4" /> Create New API Key
</Button>
</div>
</div>
<div class="mb-4 flex justify-between">
<div>
{#if loaderLoadingAll}
<Loader class="mt-6 h-4 w-4 animate-spin" />
{/if}
</div>
<div>
{#if $page.data.user.role != "member"}
<Button
on:click={(e) => {
showCreateModal = true;
}}
>
<Plus class="mr-2 h-4 w-4" /> Create New API Key
</Button>
{/if}
</div>
</div>
</div>
{#if !!newKeyResp && !!newKeyResp.apiKey}
<div class="my-4 rounded-lg border border-green-700 bg-green-800 bg-opacity-20 p-4">
<p class="font-medium">
<picture class="mr-1 inline-block">
<source
srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.webp"
type="image/webp"
/>
<img
src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif"
alt="🎉"
width="24"
height="24"
/>
</picture>
API Key Created
</p>
<p
class="relative my-2 rounded-sm border bg-card px-4 py-2 pr-8 font-mono text-sm font-medium"
>
{newKeyResp.apiKey}
<div class="my-4 rounded-lg border border-green-700 bg-green-800 bg-opacity-20 p-4">
<p class="font-medium">
<picture class="mr-1 inline-block">
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.webp" type="image/webp" />
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif" alt="🎉" width="24" height="24" />
</picture>
API Key Created
</p>
<p class="relative my-2 rounded-sm border bg-card px-4 py-2 pr-8 font-mono text-sm font-medium">
{newKeyResp.apiKey}
<Button
size="icon"
variant="ghost"
class="copybtn absolute right-2 top-2 h-5 w-5 p-1"
on:click={copyKey}
>
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
</Button>
</p>
<p class="text-xs text-muted-foreground">
Your new API key has been created. It will be <b class="uppercase underline"
>not be shown again</b
>, so make sure to save it.
</p>
</div>
<Button size="icon" variant="ghost" class="copybtn absolute right-2 top-2 h-5 w-5 p-1" on:click={copyKey}>
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
</Button>
</p>
<p class="text-xs text-muted-foreground">
Your new API key has been created. It will be <b class="uppercase underline">not be shown again</b>, so make sure
to save it.
</p>
</div>
{/if}
<div class="flex flex-col">
<div class="-m-1.5 overflow-x-auto">
<div class="inline-block min-w-full p-1.5 align-middle">
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
<thead>
<tr>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Name</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Key</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Created At</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
{#each apiKeys as apiKey}
<tr>
<td
class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200"
>
{apiKey.name}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
>
{apiKey.masked_key.slice(-32)}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200"
>
{moment
.utc(apiKey.created_at, "YYYY-MM-DD HH:mm:ss")
.local()
.format("YYYY-MM-DD HH:mm:ss")}
</td>
<td
class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200"
>
<label class="inline-flex cursor-pointer items-center">
<input
type="checkbox"
value=""
class="peer sr-only"
checked={apiKey.status == "ACTIVE"}
on:change={() => {
updateStatus(apiKey);
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"
></div>
</label>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
<div class="-m-1.5 overflow-x-auto">
<div class="inline-block min-w-full p-1.5 align-middle">
<div class="overflow-hidden rounded-lg border dark:border-neutral-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
<thead>
<tr>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500">Name</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500">Key</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
>Created At</th
>
<th
scope="col"
class="px-6 py-3 text-start text-xs font-medium uppercase text-gray-500 dark:text-neutral-500"
></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
{#each apiKeys as apiKey}
<tr>
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-800 dark:text-neutral-200">
{apiKey.name}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200">
{apiKey.masked_key.slice(-32)}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-800 dark:text-neutral-200">
{moment.utc(apiKey.created_at, "YYYY-MM-DD HH:mm:ss").local().format("YYYY-MM-DD HH:mm:ss")}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold text-gray-800 dark:text-neutral-200">
<label class="inline-flex cursor-pointer items-center">
<input
type="checkbox"
disabled={$page.data.user.role == "member"}
value=""
class="peer sr-only"
checked={apiKey.status == "ACTIVE"}
on:change={() => {
updateStatus(apiKey);
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 peer-disabled:opacity-55 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</div>
{#if showCreateModal}
<div
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
>
<div
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
>
<Button
variant="ghost"
on:click={() => {
showCreateModal = false;
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
<X class="h-4 w-4 text-muted-foreground" />
</Button>
<div class="content px-4 py-4">
<h2 class="text-lg font-semibold">Create a new API Key</h2>
<p class="text-xs text-muted-foreground">
API keys are used to authenticate your requests to the API. They are unique to
your account and should be kept secret.
</p>
<hr class="my-4" />
<form on:submit|preventDefault={createNew}>
<div class="flex flex-col gap-4">
<div>
<Label for="newAPIKeyName">Name</Label>
<Input
bind:value={newAPIKeyName}
class="mt-2"
type="text"
id="newAPIKeyName"
placeholder="eg. My API Key"
/>
</div>
</div>
<div class="mt-4 flex justify-end">
<Button variant="secondary" type="submit" disabled={loaderCreateNew}>
Create
{#if loaderCreateNew}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</div>
</div>
</div>
<div class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm">
<div
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
>
<Button
variant="ghost"
on:click={() => {
showCreateModal = false;
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
<X class="h-4 w-4 text-muted-foreground" />
</Button>
<div class="content px-4 py-4">
<h2 class="text-lg font-semibold">Create a new API Key</h2>
<p class="text-xs text-muted-foreground">
API keys are used to authenticate your requests to the API. They are unique to your account and should be kept
secret.
</p>
<hr class="my-4" />
<form on:submit|preventDefault={createNew}>
<div class="flex flex-col gap-4">
<div>
<Label for="newAPIKeyName">Name</Label>
<Input
bind:value={newAPIKeyName}
class="mt-2"
type="text"
id="newAPIKeyName"
required
placeholder="eg. My API Key"
/>
</div>
</div>
<div class="mt-4 flex justify-end">
<Button variant="secondary" type="submit" disabled={loaderCreateNew}>
Create
{#if loaderCreateNew}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</div>
</div>
</div>
{/if}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+324 -205
View File
@@ -1,221 +1,340 @@
<script>
import * as Card from "$lib/components/ui/card";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Plus, X } from "lucide-svelte";
import autoAnimate from "@formkit/auto-animate";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import { base } from "$app/paths";
import { Loader, Info } from "lucide-svelte";
import { Tooltip } from "bits-ui";
import * as Card from "$lib/components/ui/card";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import autoAnimate from "@formkit/auto-animate";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { base } from "$app/paths";
import Plus from "lucide-svelte/icons/plus";
import X from "lucide-svelte/icons/x";
import Check from "lucide-svelte/icons/check";
import Loader from "lucide-svelte/icons/loader";
import Info from "lucide-svelte/icons/info";
import { Tooltip } from "bits-ui";
export let data;
export let data;
let metaTags = [];
function addNewRow() {
metaTags = [
...metaTags,
{
key: "",
value: ""
}
];
}
let analyticsSupported = [
{
id: "",
type: "GA",
name: "Google Analytics",
script: "https://unpkg.com/@analytics/google-analytics@1.0.7/dist/@analytics/google-analytics.min.js"
},
{
id: "",
type: "AMPLITUDE",
name: "Amplitude",
script: "https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js"
},
{
id: "",
type: "MIXPANEL",
name: "MixPanel",
script: "https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js"
}
];
function removeRow(i) {
metaTags = metaTags.filter((_, index) => index !== i);
}
let metaTags = [];
function addNewRow() {
metaTags = [
...metaTags,
{
key: "",
value: ""
}
];
}
metaTags = siteDataExtractFromDb(data.siteData, {
metaTags: JSON.stringify(metaTags)
}).metaTags;
function removeRow(i) {
metaTags = metaTags.filter((_, index) => index !== i);
}
let analytics = siteDataExtractFromDb(data.siteData, {
analytics: []
}).analytics;
metaTags = siteDataExtractFromDb(data.siteData, {
metaTags: JSON.stringify(metaTags)
}).metaTags;
//merge analyticsSupported with analytics
analytics = analyticsSupported.map((supported) => {
let found = analytics.find((a) => a.type === supported.type);
if (found) {
return found;
}
return supported;
});
let formState = "idle";
let formStateAnalytics = "idle";
let formState = "idle";
let formStateAnalytics = "idle";
async function formSubmit() {
formState = "loading";
let resp = await storeSiteData({
metaTags: JSON.stringify(metaTags)
});
//print data
let data = await resp.json();
formState = "idle";
if (data.error) {
alert(data.error);
return;
}
}
let metaError = "";
async function formSubmit() {
metaError = "";
formState = "loading";
let resp = await storeSiteData({
metaTags: JSON.stringify(metaTags)
});
//print data
let data = await resp.json();
formState = "idle";
if (data.error) {
metaError = data.error;
return;
}
}
let seoError = "";
async function formSubmitAnalytics() {
seoError = "";
formStateAnalytics = "loading";
async function formSubmitAnalytics() {
formStateAnalytics = "loading";
let resp = await storeSiteData({
analytics: JSON.stringify(analytics)
});
//print data
let data = await resp.json();
formStateAnalytics = "idle";
if (data.error) {
alert(data.error);
return;
}
}
let dataToSave = {};
selectedAnalytics.requirements.forEach((req) => {
dataToSave[req.label] = req.value;
});
let finalData = {};
finalData[selectedAnalytics.key] = JSON.stringify({
requirements: dataToSave,
isEnabled: selectedAnalytics.isEnabled
});
let resp = await storeSiteData(finalData);
//print data
let data = await resp.json();
formStateAnalytics = "idle";
if (data.error) {
seoError = data.error;
return;
} else {
selectedAnalytics.activeInSite = selectedAnalytics.isEnabled;
//update activeInSite in analyticsDropdownItems also
let index = analyticsDropdownItems.findIndex((item) => item.label === selectedAnalytics.label);
analyticsDropdownItems[index].activeInSite = true;
}
}
let selectedAnalytics;
let analyticsDropdownItems = [
{
label: "Google Analytics",
logo: `/ga.png`,
key: "analytics.googleTagManager",
isEnabled: false,
requirements: [
{
label: "Measurement ID",
type: "text",
placeholder: "G-S05E5E5E5E5",
required: true,
value: ""
}
]
},
{
label: "Plausible",
logo: `/plausible.png`,
key: "analytics.plausible",
isEnabled: false,
requirements: [
{
label: "Domain",
type: "text",
placeholder: "kener.ing",
required: true,
value: new URL(data.siteData.siteURL).hostname
},
{
label: "API",
type: "text",
placeholder: "https://plausible.io/api/event",
required: true,
value: "https://plausible.io/api/event"
},
{
label: "Script Source",
type: "text",
placeholder: "https://plausible.io/js/script.pageview-props.tagged-events.js",
required: true,
value: "https://plausible.io/js/script.pageview-props.tagged-events.js"
}
]
},
{
label: "MixPanel",
logo: `/mixpanel.png`,
key: "analytics.mixpanel",
isEnabled: false,
requirements: [
{
label: "Project Token",
type: "text",
placeholder: "YOUR_PROJECT_TOKEN",
required: true,
value: ""
}
]
},
{
label: "Amplitude",
logo: `/amplitude.png`,
key: "analytics.amplitude",
isEnabled: false,
requirements: [
{
label: "Amplitude API Key",
type: "text",
placeholder: "API key for your Amplitude project",
required: true,
value: ""
}
]
},
{
label: "Microsoft Clarity",
logo: `/clarity.png`,
key: "analytics.clarity",
isEnabled: false,
requirements: [
{
label: "Project ID",
type: "text",
placeholder: "Project ID for your Microsoft Clarity project",
required: true,
value: ""
}
]
}
];
analyticsDropdownItems = analyticsDropdownItems.map((item) => {
let dbData = data.siteData[item.key];
if (!!dbData) {
item.isEnabled = dbData.isEnabled;
item.requirements.forEach((req) => {
req.value = dbData.requirements[req.label];
});
item.activeInSite = true;
}
return item;
});
selectedAnalytics = analyticsDropdownItems[0];
</script>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Analytics</Card.Title>
<Card.Description>
Add your analytics ID/Key here. You can add multiple analytics providers.
</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitAnalytics}>
<div class="grid grid-cols-2 gap-2">
{#each analytics as analytic}
<div class="col-span-1">
<Label for={analytic.type}>
{analytic.name}
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-gray-800 p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Add your {analytic.name} ID/Key here.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={analytic.id}
class="mt-2"
type="text"
id={analytic.type}
placeholder="{analytic.type} ID/Key"
/>
</div>
{/each}
</div>
<Card.Header class="border-b">
<Card.Title>Analytics</Card.Title>
<Card.Description>Add your analytics ID/Key here. You can add multiple analytics providers.</Card.Description>
</Card.Header>
<Card.Content>
<div class="mt-4 grid grid-cols-4 overflow-hidden rounded-md border">
<div class="col-span-1 flex flex-col border-r">
{#each analyticsDropdownItems as item}
<Button
variant={!!selectedAnalytics && selectedAnalytics.label === item.label ? "primary" : "ghost"}
class="flex items-center justify-between gap-x-2 rounded-none border-b text-sm last:border-none"
on:click={() => (selectedAnalytics = item)}
>
<div class="flex flex-row gap-x-3">
<img src={base + item.logo} class="w-5" alt={item.label} />
<span>{item.label}</span>
</div>
{#if item.activeInSite && item.isEnabled}
<div class="rounded-sm">
<span class="relative flex size-2">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
></span>
<span class="relative inline-flex size-2 rounded-full bg-green-500"></span>
</span>
</div>
{/if}
</Button>
{/each}
</div>
<div class="flex w-full justify-end">
<Button type="submit" disabled={formStateAnalytics === "loading"}>
Save
{#if formStateAnalytics === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
<form on:submit|preventDefault={formSubmitAnalytics} class="col-span-3 flex flex-col justify-between p-3 py-2">
{#if !!selectedAnalytics}
<div>
<div class="mb-2 text-sm font-medium text-muted-foreground">
Add details for your {selectedAnalytics.label} account
</div>
</div>
<div class="grid grid-cols-4 gap-4">
{#each selectedAnalytics.requirements as req}
<div class="col-span-2 flex flex-col gap-y-2">
<Label for={req.label}>{req.label}</Label>
<Input
bind:value={req.value}
class=""
type={req.type}
placeholder={req.placeholder}
bind:required={req.required}
/>
</div>
{/each}
</div>
<div class="mt-2 flex flex-row justify-between">
<div class="flex flex-row gap-x-2">
<label class="inline-flex cursor-pointer items-center">
<input
type="checkbox"
class="peer sr-only"
checked={selectedAnalytics.isEnabled}
on:change={() => {
selectedAnalytics.isEnabled = !selectedAnalytics.isEnabled;
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
<div class="pt-2.5 text-sm font-medium text-muted-foreground">
{selectedAnalytics.label} is
<i class="underline">{!selectedAnalytics.isEnabled ? "disabled" : "enabled"}</i> for your site
</div>
</div>
<div>
<Button type="submit" disabled={formStateAnalytics === "loading"}>
Save Changes for {selectedAnalytics.label}
{#if formStateAnalytics === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
{#if !!seoError}
<div class="py-2 text-sm font-medium text-destructive">{seoError}</div>
{/if}
{/if}
</form>
</div>
</Card.Content>
</Card.Root>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Search Engine Optimization</Card.Title>
<Card.Description>
Add your meta tags here. You can add multiple meta tags.
</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" use:autoAnimate on:submit|preventDefault={formSubmit}>
{#each metaTags as metaTag, i}
<div class="flex w-full flex-row justify-between gap-2">
<div class="grid grid-cols-12 gap-x-2">
<div class="col-span-4">
<Label for="key">
Meta Name
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
&#x3C;meta name=&#x22;{metaTag.key}&#x22; content=&#x22;{metaTag.value}&#x22;&#x3E;
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={metaTag.key}
class="mt-2"
type="text"
id="key"
placeholder="eg. kener"
/>
</div>
<div class="col-span-7">
<Label for="value">Content</Label>
<Input
bind:value={metaTag.value}
class="mt-2"
type="text"
id="value"
placeholder="eg. kener"
/>
</div>
<div class="col-span-1">
<Button variant="ghost" class="mt-8" on:click={() => removeRow(i)}>
<X class="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/each}
<div class="relative">
<hr class="border-1 border-border-input relative top-4 h-px border-dashed" />
<Card.Header class="border-b">
<Card.Title>Search Engine Optimization</Card.Title>
<Card.Description>Add your meta tags here. You can add multiple meta tags.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" use:autoAnimate on:submit|preventDefault={formSubmit}>
{#each metaTags as metaTag, i}
<div class="flex w-full flex-row justify-between gap-2">
<div class="grid grid-cols-12 gap-x-2">
<div class="col-span-4">
<Label for="key">
Meta Name
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
&#x3C;meta name=&#x22;{metaTag.key}&#x22; content=&#x22;{metaTag.value}&#x22;&#x3E;
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input bind:value={metaTag.key} class="mt-2" type="text" id="key" placeholder="eg. kener" />
</div>
<div class="col-span-7">
<Label for="value">Content</Label>
<Input bind:value={metaTag.value} class="mt-2" type="text" id="value" placeholder="eg. kener" />
</div>
<div class="col-span-1">
<Button variant="ghost" class="mt-8" on:click={() => removeRow(i)}>
<X class="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/each}
<div class="relative">
<hr class="border-1 border-border-input relative top-4 h-px border-dashed" />
<Button
on:click={addNewRow}
variant="secondary"
class="absolute left-1/2 h-8 w-8 -translate-x-1/2 p-0 "
>
<Plus class="h-4 w-4 " />
</Button>
</div>
<div class="flex w-full justify-end" style="margin-top:32px">
<Button type="submit" disabled={formState === "loading"}>
Save
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
<Button on:click={addNewRow} variant="secondary" class="absolute left-1/2 h-8 w-8 -translate-x-1/2 p-0 ">
<Plus class="h-4 w-4 " />
</Button>
</div>
<div class="flex w-full justify-end gap-x-2" style="margin-top:32px">
{#if !!metaError}
<div class="py-2 text-sm font-medium text-destructive">{metaError}</div>
{/if}
<Button type="submit" disabled={formState === "loading"}>
Save Meta Tags
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
</Card.Root>
+423 -352
View File
@@ -1,381 +1,452 @@
<script>
import * as Card from "$lib/components/ui/card";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import { tooltipAction } from "svelte-legos";
import { base } from "$app/paths";
import { Loader, Info, X } from "lucide-svelte";
import { Tooltip } from "bits-ui";
import * as Card from "$lib/components/ui/card";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import { tooltipAction } from "svelte-legos";
import { base } from "$app/paths";
import Loader from "lucide-svelte/icons/loader";
import Info from "lucide-svelte/icons/info";
import X from "lucide-svelte/icons/x";
import * as Alert from "$lib/components/ui/alert";
import { Tooltip } from "bits-ui";
import * as Select from "$lib/components/ui/select";
import GMI from "$lib/components/gmi.svelte";
export let data;
export let data;
let siteInformation = {
title: "",
siteName: "",
siteURL: "",
home: "",
logo: "",
favicon: ""
};
let siteInformation = {
title: "",
siteName: "",
siteURL: "",
home: "",
logo: "",
favicon: ""
};
let logoFile;
let logoFile;
let formErrorMessage = "";
let formErrorMessage = "";
siteInformation = siteDataExtractFromDb(data.siteData, siteInformation);
siteInformation = siteDataExtractFromDb(data.siteData, siteInformation);
let formState = "idle";
async function formSubmit() {
formErrorMessage = "";
formState = "loading";
let resp = await storeSiteData(siteInformation);
//print data
let data = await resp.json();
formState = "idle";
if (data.error) {
formErrorMessage = data.error;
return;
}
}
let formState = "idle";
async function formSubmit() {
formErrorMessage = "";
formState = "loading";
let resp = await storeSiteData(siteInformation);
//print data
let data = await resp.json();
formState = "idle";
if (data.error) {
formErrorMessage = data.error;
return;
}
}
function handleFileChangeFavicon(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
siteInformation.favicon = reader.result;
uploadFunction(reader.result);
};
reader.readAsDataURL(file);
}
}
let uploadLogoStatus = "";
let uploadingLogo = false;
async function handleFileChangeLogo(event) {
event.preventDefault();
function handleFileChangeFavicon(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
siteInformation.favicon = reader.result;
uploadFunction(reader.result);
};
reader.readAsDataURL(file);
}
}
let uploadLogoStatus = "";
let uploadingLogo = false;
async function handleFileChangeLogo(event) {
event.preventDefault();
uploadLogoStatus = "";
const file = event.target.files[0];
if (!file) {
uploadLogoStatus = "Please select a file to upload.";
return;
}
uploadLogoStatus = "";
const file = event.target.files[0];
if (!file) {
uploadLogoStatus = "Please select a file to upload.";
return;
}
if (file.size > 100000) {
uploadLogoStatus = "File size should be less than 100KB.";
return;
}
if (file.size > 102400) {
uploadLogoStatus = "File size should be less than 100KB.";
return;
}
const formData = new FormData();
formData.append("image", file);
uploadingLogo = true;
try {
const response = await fetch(base + "/manage/app/upload", {
method: "POST",
body: formData
});
const formData = new FormData();
formData.append("image", file);
uploadingLogo = true;
try {
const response = await fetch(base + "/manage/app/upload", {
method: "POST",
body: formData
});
if (response.ok) {
const result = await response.json();
siteInformation.logo = "/uploads/" + result.filename;
} else {
uploadLogoStatus = "Failed to upload file.";
}
} catch (error) {
uploadLogoStatus = "An error occurred while uploading the file.";
} finally {
uploadingLogo = false;
}
}
if (response.ok) {
const result = await response.json();
siteInformation.logo = "/uploads/" + result.filename;
} else {
uploadLogoStatus = "Failed to upload file.";
}
} catch (error) {
uploadLogoStatus = "An error occurred while uploading the file.";
} finally {
uploadingLogo = false;
}
}
let uploadFaviconStatus = "";
let uploadingFavicon = false;
async function handleFaviconChangeLogo(event) {
event.preventDefault();
let uploadFaviconStatus = "";
let uploadingFavicon = false;
async function handleFaviconChangeLogo(event) {
event.preventDefault();
uploadFaviconStatus = "";
const file = event.target.files[0];
if (!file) {
uploadFaviconStatus = "Please select a file to upload.";
return;
}
uploadFaviconStatus = "";
const file = event.target.files[0];
if (!file) {
uploadFaviconStatus = "Please select a file to upload.";
return;
}
if (file.size > 20000) {
uploadFaviconStatus = "File size should be less than 20KB.";
return;
}
if (file.size > 20480) {
uploadFaviconStatus = "File size should be less than 20KB.";
return;
}
const formData = new FormData();
formData.append("image", file);
uploadingFavicon = true;
try {
const response = await fetch(base + "/manage/app/upload", {
method: "POST",
body: formData
});
const formData = new FormData();
formData.append("image", file);
uploadingFavicon = true;
try {
const response = await fetch(base + "/manage/app/upload", {
method: "POST",
body: formData
});
if (response.ok) {
const result = await response.json();
siteInformation.favicon = "/uploads/" + result.filename;
} else {
uploadFaviconStatus = "Failed to upload file.";
}
} catch (error) {
uploadFaviconStatus = "An error occurred while uploading the file.";
} finally {
uploadingFavicon = false;
}
}
if (response.ok) {
const result = await response.json();
siteInformation.favicon = "/uploads/" + result.filename;
} else {
uploadFaviconStatus = "Failed to upload file.";
}
} catch (error) {
uploadFaviconStatus = "An error occurred while uploading the file.";
} finally {
uploadingFavicon = false;
}
}
let typeOfLogoUpload = siteInformation.logo.startsWith("http") ? "URL" : "FILE";
let typeOfFaviconUpload = siteInformation.favicon.startsWith("http") ? "URL" : "FILE";
</script>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Site Configuration</Card.Title>
<Card.Description>Configure your site information here.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmit}>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="title" class="">
Site Title
<span class="text-red-500">*</span>
<Card.Header class="border-b">
<Card.Title>Site Configuration</Card.Title>
<Card.Description>Configure your site information here.</Card.Description>
</Card.Header>
<Card.Content>
{#if !!base}
<Alert.Root class="my-2">
<Alert.Title>Sub path</Alert.Title>
<Alert.Description>Your site is running in a subpath <span class="">{base}</span></Alert.Description>
</Alert.Root>
{/if}
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmit}>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="title" class="">
Site Title
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Site Title will be the page title of your site.
&lt;title&gt;&lt;/title&gt;
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={siteInformation.title}
class="mt-2"
type="text"
id="title"
placeholder="eg. kener"
required
/>
</div>
<div class="w-full">
<Label for="siteName">
Site Name
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Site Title will be the page title of your site. &lt;title&gt;&lt;/title&gt;
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={siteInformation.title}
class="mt-2"
type="text"
id="title"
placeholder="eg. kener"
required
/>
</div>
<div class="w-full">
<Label for="siteName">
Site Name
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
This is the name of your site.<br /> It will be shown in the nav
bar as brand name.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={siteInformation.siteName}
class="mt-2"
type="text"
id="siteName"
placeholder="eg. Kener.ing"
required
/>
</div>
</div>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="siteURL">
Site URL
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
The URL your kener is hosted on.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={siteInformation.siteURL}
class="mt-2"
type="text"
id="siteURL"
placeholder="eg. https://status.example.com"
required
/>
</div>
</div>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="siteURL">
Home Location
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Where to go when someone clicks on the brand logo in the navbar.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
This is the name of your site.<br /> It will be shown in the nav bar as brand name.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={siteInformation.siteName}
class="mt-2"
type="text"
id="siteName"
placeholder="eg. Kener.ing"
required
/>
</div>
</div>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="siteURL">
Site URL
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
The URL your kener is hosted on.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={siteInformation.siteURL}
class="mt-2"
type="text"
id="siteURL"
placeholder="eg. https://status.example.com"
required
/>
</div>
</div>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="siteURL">
Home Location
<span class="text-red-500">*</span>
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-input p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Where to go when someone clicks on the brand logo in the navbar.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={siteInformation.home}
class="mt-2"
type="text"
id="home"
placeholder="/ or https://example.com"
required
/>
</div>
</div>
<Input
bind:value={siteInformation.home}
class="mt-2"
type="text"
id="home"
placeholder="/ or https://example.com"
required
/>
</div>
</div>
<div class="flex w-full flex-col justify-evenly gap-2">
<div>
<Label for="logo">Logo</Label>
</div>
<div class="flex gap-x-2">
{#if !!siteInformation.logo}
<div class="relative">
<Label
for="logo"
class="inline-block h-[60px] w-[60px] cursor-pointer rounded-sm border p-1"
>
<img
src={base + siteInformation.logo}
class="w-fit hover:scale-95"
alt=""
/>
</Label>
<div class="flex w-full flex-col justify-evenly gap-2">
<div>
<Label for="logo">Logo</Label>
</div>
<div class="flex gap-x-4">
{#if !!siteInformation.logo}
<div class="relative mt-2.5">
<Label for="logo" class="inline-block h-[60px] w-[60px] cursor-pointer rounded-sm border p-1">
<GMI src={siteInformation.logo} classList="w-fit hover:scale-95" alt="" />
</Label>
<div class="absolute -right-2 -top-2">
<Button
variant="secondary"
class="h-5 w-5 rounded-full border p-1"
size="icon"
on:click={() => (siteInformation.logo = "")}
>
<X class="h-4 w-4" />
</Button>
</div>
{#if uploadingLogo}
<div
class="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform rounded-sm bg-[rgba(0,0,0,0.5)] p-0.5"
>
<Loader class="h-4 w-4 animate-spin" />
</div>
{/if}
</div>
{/if}
<div>
<Input
class=""
id="logo"
type="file"
accept=".jpg, .jpeg, .png"
disabled={uploadingLogo}
on:change={handleFileChangeLogo}
/>
<p class="mt-1 text-xs font-medium">
Please upload a square image of max size 100KB
</p>
<p class="mt-1 text-xs font-medium text-destructive">
{uploadLogoStatus}
</p>
</div>
</div>
<div>
<Label for="favicon">Favicon</Label>
</div>
<div class="flex gap-x-2">
{#if !!siteInformation.favicon}
<div class="relative">
<Label
for="favicon"
class="inline-block h-[40px] w-[40px] cursor-pointer rounded-sm border p-1"
>
<img
src={base + siteInformation.favicon}
class="w-fit hover:scale-95"
alt=""
/>
</Label>
<div class="absolute -right-2 -top-2">
<Button
variant="secondary"
class="h-5 w-5 rounded-full border p-1"
size="icon"
on:click={() => (siteInformation.logo = "")}
>
<X class="h-4 w-4" />
</Button>
</div>
{#if uploadingLogo}
<div
class="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform rounded-sm bg-[rgba(0,0,0,0.5)] p-0.5"
>
<Loader class="h-4 w-4 animate-spin" />
</div>
{/if}
</div>
{/if}
<div class="flex gap-x-2">
<div>
<Label for="typeOfLogoUpload" class="text-sm font-medium">Method</Label>
<Select.Root
portal={null}
onSelectedChange={(e) => {
typeOfLogoUpload = e.value;
}}
selected={{
value: typeOfLogoUpload,
label: typeOfLogoUpload
}}
>
<Select.Trigger class="mt-2 w-[200px]" id="typeOfLogoUpload">
<Select.Value bind:value={typeOfLogoUpload} placeholder="Logo Type" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Logo Type</Select.Label>
<Select.Item value="URL" label="URL" class="text-sm font-medium">URL</Select.Item>
<Select.Item value="FILE" label="FILE" class="text-sm font-medium">FILE</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</div>
{#if typeOfLogoUpload === "URL"}
<div>
<Label for="logo" class="text-sm font-medium">Please provide a URL of the image</Label>
<Input
bind:value={siteInformation.logo}
class="mt-2"
type="text"
id="logo"
placeholder="https://example.com/logo.png"
/>
</div>
{:else}
<div>
<Label for="logo" class="text-sm font-medium">Please upload a square image of max size 100KB</Label>
<Input
class="mt-2"
id="logo"
type="file"
accept=".jpg, .jpeg, .png, .svg"
disabled={uploadingLogo}
on:change={handleFileChangeLogo}
/>
<div class="absolute -right-2 -top-2">
<Button
variant="secondary"
class="h-5 w-5 rounded-full border p-1"
size="icon"
on:click={() => (siteInformation.favicon = "")}
>
<X class="h-4 w-4" />
</Button>
</div>
{#if uploadingFavicon}
<div
class="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform rounded-sm bg-[rgba(0,0,0,0.5)] p-0.5"
>
<Loader class="h-4 w-4 animate-spin" />
</div>
{/if}
</div>
{/if}
<div>
<Input
class=""
id="logo"
type="file"
accept=".jpg, .jpeg, .png, .ico"
disabled={uploadingFavicon}
on:change={handleFaviconChangeLogo}
/>
<p class="mt-1 text-xs font-medium">
Please upload a square image of max size 20KB
</p>
<p class="mt-1 text-xs font-medium text-destructive">
{uploadFaviconStatus}
</p>
</div>
</div>
</div>
<div class="flex w-full justify-end">
<p class="px-4 pt-3 text-sm">
<span class="text-red-500">
{formErrorMessage}
</span>
</p>
<Button type="submit" disabled={formState === "loading"}>
Save
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
<p class="mt-1 text-xs font-medium text-destructive">
{uploadLogoStatus}
</p>
</div>
{/if}
</div>
</div>
<div>
<Label for="favicon">Favicon</Label>
</div>
<div class="flex gap-x-4">
{#if !!siteInformation.favicon}
<div class="relative mt-8">
<Label for="favicon" class="inline-block h-[40px] w-[40px] cursor-pointer rounded-sm border p-1">
<GMI src={siteInformation.favicon} classList="w-fit hover:scale-95" alt="" />
</Label>
<div class="absolute -right-2 -top-2">
<Button
variant="secondary"
class="h-5 w-5 rounded-full border p-1"
size="icon"
on:click={() => (siteInformation.favicon = "")}
>
<X class="h-4 w-4" />
</Button>
</div>
{#if uploadingFavicon}
<div
class="absolute left-1/2 top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform rounded-sm bg-[rgba(0,0,0,0.5)] p-0.5"
>
<Loader class="h-4 w-4 animate-spin" />
</div>
{/if}
</div>
{/if}
<div class="flex gap-x-2">
<div>
<Label for="typeOfFaviconUpload" class="text-sm font-medium">Method</Label>
<Select.Root
portal={null}
onSelectedChange={(e) => {
typeOfFaviconUpload = e.value;
}}
selected={{
value: typeOfFaviconUpload,
label: typeOfFaviconUpload
}}
>
<Select.Trigger class="mt-2 w-[200px]" id="typeOfFaviconUpload">
<Select.Value bind:value={typeOfFaviconUpload} placeholder="Favicon Type" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Favicon Type</Select.Label>
<Select.Item value="URL" label="URL" class="text-sm font-medium">URL</Select.Item>
<Select.Item value="FILE" label="FILE" class="text-sm font-medium">FILE</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</div>
{#if typeOfFaviconUpload === "URL"}
<div>
<Label for="favicon" class="text-sm font-medium">Please provide a URL of the image</Label>
<Input
bind:value={siteInformation.favicon}
class="mt-2"
type="text"
id="favicon"
placeholder="https://example.com/favicon.png"
/>
</div>
{:else}
<div>
<Label for="favicon" class="text-sm font-medium">Please upload a square image of max size 20KB</Label>
<Input
class="mt-2"
id="favicon"
type="file"
accept=".jpg, .jpeg, .png, .svg"
disabled={uploadingFavicon}
on:change={handleFaviconChangeLogo}
/>
<p class="mt-1 text-xs font-medium text-destructive">
{uploadFaviconStatus}
</p>
</div>
{/if}
</div>
</div>
<div class="flex w-full justify-end gap-x-2">
<p class="py-2 text-sm font-medium text-destructive">
{formErrorMessage}
</p>
<Button type="submit" disabled={formState === "loading"}>
Save
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
</form>
</Card.Content>
</Card.Root>
<!-- GitHub Settings -->
+352 -368
View File
@@ -1,381 +1,365 @@
<script>
import * as Card from "$lib/components/ui/card";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Plus, X, Info } from "lucide-svelte";
import autoAnimate from "@formkit/auto-animate";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import { base } from "$app/paths";
import { Loader } from "lucide-svelte";
import * as RadioGroup from "$lib/components/ui/radio-group";
import { Tooltip } from "bits-ui";
import ColorPicker from "svelte-awesome-color-picker";
import * as Card from "$lib/components/ui/card";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import Plus from "lucide-svelte/icons/plus";
import X from "lucide-svelte/icons/x";
import Info from "lucide-svelte/icons/info";
import Loader from "lucide-svelte/icons/loader";
import autoAnimate from "@formkit/auto-animate";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import { base } from "$app/paths";
import * as RadioGroup from "$lib/components/ui/radio-group";
import { Tooltip } from "bits-ui";
import ColorPicker from "svelte-awesome-color-picker";
export let data;
export let data;
let formState = "idle";
let formState = "idle";
let themeError = "";
let themeData = {
pattern: "none",
theme: "light",
themeToggle: "YES",
barStyle: "PARTIAL",
barRoundness: "ROUNDED",
summaryStyle: "CURRENT",
colorsJ: {
UP: "#4ead94",
DOWN: "#ca3038",
DEGRADED: "#e6ca61"
},
fontJ: {
cssSrc: "https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@0,100..900;1,100..900&display=swap",
family: "Albert Sans"
},
colors: "",
font: "",
customCSS: ""
};
let themeData = {
pattern: "none",
theme: "light",
themeToggle: "YES",
barStyle: "PARTIAL",
barRoundness: "ROUNDED",
summaryStyle: "CURRENT",
colorsJ: {
UP: "#4ead94",
DOWN: "#ca3038",
DEGRADED: "#e6ca61"
},
fontJ: {
cssSrc:
"https://fonts.bunny.net/css?family=albert-sans:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i&display=swap",
family: "Albert Sans"
},
colors: "",
font: "",
customCSS: ""
};
themeData = siteDataExtractFromDb(data.siteData, themeData);
themeData = siteDataExtractFromDb(data.siteData, themeData);
if (data.siteData.colors) {
themeData.colorsJ = data.siteData.colors;
}
if (data.siteData.font) {
themeData.fontJ = data.siteData.font;
}
if (data.siteData.colors) {
themeData.colorsJ = data.siteData.colors;
}
if (data.siteData.font) {
themeData.fontJ = data.siteData.font;
}
async function formSubmit() {
formState = "loading";
let themeDataAPI = { ...themeData };
themeDataAPI.colors = JSON.stringify(themeDataAPI.colorsJ);
themeDataAPI.font = JSON.stringify(themeDataAPI.fontJ);
delete themeDataAPI.colorsJ;
delete themeDataAPI.fontJ;
let resp = await storeSiteData(themeDataAPI);
//print data
let data = await resp.json();
formState = "idle";
if (data.error) {
alert(data.error);
return;
}
}
async function formSubmit() {
formState = "loading";
themeError = "";
let themeDataAPI = { ...themeData };
themeDataAPI.colors = JSON.stringify(themeDataAPI.colorsJ);
themeDataAPI.font = JSON.stringify(themeDataAPI.fontJ);
delete themeDataAPI.colorsJ;
delete themeDataAPI.fontJ;
let resp = await storeSiteData(themeDataAPI);
//print data
let data = await resp.json();
formState = "idle";
if (data.error) {
themeError = data.error;
return;
}
}
</script>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Theme Customization</Card.Title>
<Card.Description>
You can customize the theme of your site here. You can change the pattern, theme,
colors, font, and monitor style.
</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmit}>
<p class="font-medium">
Home Page Pattern
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the pattern for the home page. You can choose between squares,
dots, or none.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<Card.Header class="border-b">
<Card.Title>Theme Customization</Card.Title>
<Card.Description>
You can customize the theme of your site here. You can change the pattern, theme, colors, font, and monitor style.
</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmit}>
<p class="font-medium">
Home Page Pattern
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the pattern for the home page. You can choose between squares, dots, or none.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex">
<RadioGroup.Root
bind:value={themeData.pattern}
onValueChange={(e) => (themeData.pattern = e)}
>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="squares" id="rsq" />
<Label for="rsq">Squares</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="dots" id="rdot" />
<Label for="rdot">Dots</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="none" id="rnone" />
<Label for="rnone">None</Label>
</div>
</RadioGroup.Root>
</div>
<hr />
<p class="font-medium">
Default Theme
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the default theme for the site. You can choose between light,
dark, or system.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex">
<RadioGroup.Root
bind:value={themeData.theme}
onValueChange={(e) => (themeData.theme = e)}
>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="none" id="none_theme" />
<Label for="none_theme">None</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="light" id="light_theme" />
<Label for="light_theme">Light</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="dark" id="dark_theme" />
<Label for="dark_theme">Dark</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="system" id="system_theme" />
<Label for="system_theme">System</Label>
</div>
</RadioGroup.Root>
</div>
<p class="font-medium">
<label>
<input
on:change={(e) => {
themeData.themeToggle = e.target.checked === true ? "YES" : "NO";
}}
type="checkbox"
checked={themeData.themeToggle === "YES"}
/>
Let Users toggle theme between light and dark
</label>
</p>
<hr />
<p class="font-medium">Monitor Style</p>
<div class="flex">
<RadioGroup.Root bind:value={themeData.pattern} onValueChange={(e) => (themeData.pattern = e)}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="squares" id="rsq" />
<Label for="rsq">Squares</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="dots" id="rdot" />
<Label for="rdot">Dots</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="none" id="rnone" />
<Label for="rnone">None</Label>
</div>
</RadioGroup.Root>
</div>
<hr />
<p class="font-medium">
Default Theme
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the default theme for the site. You can choose between light, dark, or system.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex">
<RadioGroup.Root bind:value={themeData.theme} onValueChange={(e) => (themeData.theme = e)}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="none" id="none_theme" />
<Label for="none_theme">None</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="light" id="light_theme" />
<Label for="light_theme">Light</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="dark" id="dark_theme" />
<Label for="dark_theme">Dark</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="system" id="system_theme" />
<Label for="system_theme">System</Label>
</div>
</RadioGroup.Root>
</div>
<p class="font-medium">
<label>
<input
on:change={(e) => {
themeData.themeToggle = e.target.checked === true ? "YES" : "NO";
}}
type="checkbox"
checked={themeData.themeToggle === "YES"}
/>
Let Users toggle theme between light and dark
</label>
</p>
<hr />
<p class="font-medium">Monitor Style</p>
<div class="flex gap-x-24">
<RadioGroup.Root
bind:value={themeData.barStyle}
onValueChange={(e) => (themeData.barStyle = e)}
>
<p class="text-sm font-medium text-muted-foreground">
Status bar
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the style for the monitor. You can choose between partial
or full.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="PARTIAL" id="barStylePARTIAL" />
<Label for="barStylePARTIAL">PARTIAL</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="FULL" id="barStyleFULL" />
<Label for="barStyleFULL">FULL</Label>
</div>
</RadioGroup.Root>
<RadioGroup.Root
bind:value={themeData.barRoundness}
onValueChange={(e) => (themeData.barRoundness = e)}
>
<p class="text-sm font-medium text-muted-foreground">
Roundness
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the roundness for the monitor. You can choose between
rounded or sharp.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="SHARP" id="barRoundnessSHARP" />
<Label for="barRoundnessSHARP">SHARP</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="ROUNDED" id="barRoundnessRound" />
<Label for="barRoundnessRound">ROUNDED</Label>
</div>
</RadioGroup.Root>
<RadioGroup.Root
bind:value={themeData.summaryStyle}
onValueChange={(e) => (themeData.summaryStyle = e)}
>
<p class="text-sm font-medium text-muted-foreground">
Summary Type
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the summary type for the monitor. You can choose between
current or day.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="CURRENT" id="summaryStyle1" />
<Label for="summaryStyle1">CURRENT</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="DAY" id="summaryStyle2" />
<Label for="summaryStyle2">DAY</Label>
</div>
</RadioGroup.Root>
</div>
<hr />
<p class="font-medium">
Colors
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the colors for the monitor. You can choose between UP, DEGRADED,
and DOWN.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="relative w-full">
<ColorPicker
bind:hex={themeData.colorsJ.UP}
position="responsive"
label="UP"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.UP = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<ColorPicker
bind:hex={themeData.colorsJ.DEGRADED}
position="responsive"
label="DEGRADED"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DEGRADED = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<ColorPicker
bind:hex={themeData.colorsJ.DOWN}
position="responsive"
label="DOWN"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DOWN = event.detail.hex;
}}
/>
</div>
</div>
<hr />
<p class="font-medium">
Font
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the font for the monitor. You can choose the font URL and font
family name.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="cssSrc" class="text-sm text-muted-foreground">Font URL</Label>
<Input
bind:value={themeData.fontJ.cssSrc}
class="mt-2"
type="text"
id="cssSrc"
placeholder="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
/>
</div>
</div>
<div class="flex w-full flex-row justify-start gap-2">
<div class="w-48">
<Label for="fontName" class="text-sm text-muted-foreground"
>Font Family Name</Label
>
<Input
bind:value={themeData.fontJ.family}
class="mt-2"
type="text"
id="fontName"
placeholder="Lato"
/>
</div>
</div>
<hr />
<p class="font-medium">Custom CSS</p>
<div class="flex w-full flex-row justify-start gap-2">
<div class="w-full">
<Label for="customCSS" class="text-sm text-muted-foreground">
You can add custom CSS to style your site. Do not include style tags.
</Label>
<textarea
bind:value={themeData.customCSS}
id="customCSS"
class="mt-1 h-48 w-full rounded-sm border p-2"
placeholder=".className&#123;color: red;&#125;"
></textarea>
</div>
</div>
<hr />
<div class="flex w-full justify-end">
<Button type="submit" disabled={formState === "loading"}>
Save
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
<div class="flex gap-x-24">
<RadioGroup.Root bind:value={themeData.barStyle} onValueChange={(e) => (themeData.barStyle = e)}>
<p class="text-sm font-medium text-muted-foreground">
Status bar
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the style for the monitor. You can choose between partial or full.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="PARTIAL" id="barStylePARTIAL" />
<Label for="barStylePARTIAL">PARTIAL</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="FULL" id="barStyleFULL" />
<Label for="barStyleFULL">FULL</Label>
</div>
</RadioGroup.Root>
<RadioGroup.Root bind:value={themeData.barRoundness} onValueChange={(e) => (themeData.barRoundness = e)}>
<p class="text-sm font-medium text-muted-foreground">
Roundness
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the roundness for the monitor. You can choose between rounded or sharp.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="SHARP" id="barRoundnessSHARP" />
<Label for="barRoundnessSHARP">SHARP</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="ROUNDED" id="barRoundnessRound" />
<Label for="barRoundnessRound">ROUNDED</Label>
</div>
</RadioGroup.Root>
<RadioGroup.Root bind:value={themeData.summaryStyle} onValueChange={(e) => (themeData.summaryStyle = e)}>
<p class="text-sm font-medium text-muted-foreground">
Summary Type
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the summary type for the monitor. You can choose between current or day.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="CURRENT" id="summaryStyle1" />
<Label for="summaryStyle1">CURRENT</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="DAY" id="summaryStyle2" />
<Label for="summaryStyle2">DAY</Label>
</div>
</RadioGroup.Root>
</div>
<hr />
<p class="font-medium">
Colors
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the colors for the monitor. You can choose between UP, DEGRADED, and DOWN.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="relative w-full">
<ColorPicker
bind:hex={themeData.colorsJ.UP}
position="responsive"
label="UP"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.UP = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<ColorPicker
bind:hex={themeData.colorsJ.DEGRADED}
position="responsive"
label="DEGRADED"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DEGRADED = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<ColorPicker
bind:hex={themeData.colorsJ.DOWN}
position="responsive"
label="DOWN"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DOWN = event.detail.hex;
}}
/>
</div>
</div>
<hr />
<p class="font-medium">
Font
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the font for the monitor. You can choose the font URL and font family name.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="cssSrc" class="text-sm text-muted-foreground">Font URL</Label>
<Input
bind:value={themeData.fontJ.cssSrc}
class="mt-2"
type="text"
id="cssSrc"
placeholder="https://fonts.bunny.net/css?family=lato:100,100i,300,300i,400,400i,700,700i,900,900i&display=swap"
/>
</div>
</div>
<div class="flex w-full flex-row justify-start gap-2">
<div class="w-48">
<Label for="fontName" class="text-sm text-muted-foreground">Font Family Name</Label>
<Input bind:value={themeData.fontJ.family} class="mt-2" type="text" id="fontName" placeholder="Lato" />
</div>
</div>
<p class="text-xs text-muted-foreground">
Want to upload and use custom font? Read more about it <a
href="https://kener.ing/docs/custom-fonts/"
class="text-card-foreground"
target="_blank">here</a
>.
</p>
<hr />
<p class="font-medium">Custom CSS</p>
<div class="flex w-full flex-row justify-start gap-2">
<div class="w-full">
<Label for="customCSS" class="text-sm text-muted-foreground">
You can add custom CSS to style your site. Do not include style tags.
</Label>
<textarea
bind:value={themeData.customCSS}
id="customCSS"
class="mt-1 h-48 w-full rounded-sm border p-2"
placeholder=".className&#123;color: red;&#125;"
></textarea>
</div>
</div>
<hr />
<div class="flex w-full justify-end gap-x-2">
{#if !!themeError}
<div class="py-2 text-sm font-medium text-destructive">{themeError}</div>
{/if}
<Button type="submit" disabled={formState === "loading"}>
Save
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
</Card.Root>
File diff suppressed because it is too large Load Diff
+401 -388
View File
@@ -1,408 +1,421 @@
<script>
import * as Popover from "$lib/components/ui/popover";
import { onMount } from "svelte";
import { Badge } from "$lib/components/ui/badge";
import { Button } from "$lib/components/ui/button";
import { base } from "$app/paths";
import { sub, startOfDay, getUnixTime } from "date-fns";
import * as Popover from "$lib/components/ui/popover";
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";
import { page } from "$app/stores";
import { analyticsEvent } from "$lib/boringOne";
import Share2 from "lucide-svelte/icons/share-2";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import Settings from "lucide-svelte/icons/settings";
import TrendingUp from "lucide-svelte/icons/trending-up";
import Loader from "lucide-svelte/icons/loader";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { buttonVariants } from "$lib/components/ui/button";
import { createEventDispatcher } from "svelte";
import { afterUpdate } from "svelte";
import axios from "axios";
import { l, summaryTime, f } from "$lib/i18n/client";
import { hoverAction, clickOutsideAction, slide } from "svelte-legos";
import LoaderBoxes from "$lib/components/loaderbox.svelte";
import NumberFlow from "@number-flow/svelte";
import Incident from "$lib/components/IncidentNew.svelte";
import {
Share2,
Link,
CopyCheck,
Code,
TrendingUp,
Percent,
Loader,
ChevronLeft,
ChevronRight
} from "lucide-svelte";
import { buttonVariants } from "$lib/components/ui/button";
import { createEventDispatcher } from "svelte";
import { afterUpdate } from "svelte";
import axios from "axios";
import { l, summaryTime, f } from "$lib/i18n/client";
import { hoverAction, clickOutsideAction, slide } from "svelte-legos";
import LoaderBoxes from "$lib/components/loaderbox.svelte";
import NumberFlow from "@number-flow/svelte";
import Incident from "$lib/components/IncidentNew.svelte";
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
export let monitor;
export let localTz;
export let lang;
export let embed = false;
export let selectedLang = "en";
export let monitor;
let _0Day = {};
let _90Day = monitor.pageData._90Day;
let uptime90Day = monitor.pageData.uptime90Day;
let incidents = {};
let dayIncidentsFull = [];
export let localTz;
export let lang;
export let embed = false;
export let selectedLang = "en";
function loadIncidents() {
axios
.post(`${base}/api/today/incidents`, {
tag: monitor.tag,
startTs: monitor.pageData.midnight90DaysAgo,
endTs: monitor.pageData.maxDateTodayTimestamp,
localTz: localTz
})
.then((response) => {
if (response.data) {
incidents = response.data;
}
})
.catch((error) => {
console.log(error);
});
}
let _0Day = {};
let _90Day = monitor.pageData._90Day;
let uptime90Day = monitor.pageData.uptime90Day;
let incidents = {};
let dayIncidentsFull = [];
function getToday(startTs, incidentIDs) {
let endTs = Math.min(startTs + 86400, monitor.pageData.maxDateTodayTimestamp);
function loadIncidents() {
axios
.post(`${base}/api/today/incidents`, {
tag: monitor.tag,
startTs: monitor.pageData.midnight90DaysAgo,
endTs: monitor.pageData.midnightTomorrow,
localTz: localTz
})
.then((response) => {
if (response.data) {
incidents = response.data;
}
})
.catch((error) => {
console.log(error);
});
}
axios
.post(`${base}/api/today`, {
monitor: monitor,
localTz: localTz,
endTs: endTs,
startTs: startTs,
incidentIDs: incidentIDs
})
.then((response) => {
if (response.data) {
_0Day = response.data._0Day;
dayUptime = response.data.uptime;
dayIncidentsFull = response.data.incidents;
}
loadingDayData = false;
})
.catch((error) => {
console.log(error);
loadingDayData = false;
});
}
function getToday(startTs, incidentIDs) {
axios
.post(`${base}/api/today`, {
monitor: monitor,
localTz: localTz,
startTs: startTs,
incidentIDs: incidentIDs
})
.then((response) => {
if (response.data) {
_0Day = response.data._0Day;
dayUptime = response.data.uptime;
dayIncidentsFull = response.data.incidents;
}
loadingDayData = false;
})
.catch((error) => {
console.log(error);
loadingDayData = false;
});
}
function scrollToRight() {
setTimeout(() => {
let divs = document.querySelectorAll(".daygrid90");
divs.forEach((div) => {
div.scrollLeft = div.scrollWidth;
});
}, 1000 * 0.2);
}
let uptimesRollers = [
{
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 }))),
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "30 Days")}`,
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 }))),
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: `${l(lang, "7 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 }))),
endTs: monitor.pageData.maxDateTodayTimestamp
},
{
text: l(lang, "Today"),
startTs: getUnixTime(startOfDay(new Date())),
endTs: monitor.pageData.maxDateTodayTimestamp
}
];
function scrollToRight() {
setTimeout(() => {
let divs = document.querySelectorAll(".daygrid90");
divs.forEach((div) => {
div.scrollLeft = div.scrollWidth;
});
}, 1000 * 0.2);
}
let uptimesRollers = [
{
text: `${l(lang, "90 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 90 }))),
value: uptime90Day
},
{
text: `${l(lang, "60 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 59 })))
},
{
text: `${l(lang, "30 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 29 })))
},
{
text: `${l(lang, "14 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 13 })))
},
{
text: `${l(lang, "7 Days")}`,
startTs: getUnixTime(startOfDay(sub(new Date(), { days: 6 })))
},
{
text: l(lang, "Today"),
startTs: getUnixTime(startOfDay(new Date()))
}
];
//start of the week moment
let rolledAt = 0;
let rollerLoading = false;
async function rollSummary(r) {
let newRolledAt = r;
analyticsEvent("monitor_interval_switch", {
tag: monitor.tag,
interval: uptimesRollers[newRolledAt].text
});
//start of the week moment
let rolledAt = 0;
let rollerLoading = false;
async function rollSummary(r) {
let newRolledAt = (rolledAt + r) % uptimesRollers.length;
if (uptimesRollers[newRolledAt].value === undefined) {
rollerLoading = true;
uptimesRollers[newRolledAt].loading = true;
if (uptimesRollers[newRolledAt].value === undefined) {
rollerLoading = true;
uptimesRollers[newRolledAt].loading = true;
let resp = await axios.post(`${base}/api/today/aggregated`, {
monitor: monitor,
startTs: uptimesRollers[newRolledAt].startTs,
endTs: uptimesRollers[newRolledAt].endTs
});
uptimesRollers[newRolledAt].value = resp.data.uptime;
rollerLoading = false;
}
rolledAt = newRolledAt;
for (const key in _90Day) {
if (Object.prototype.hasOwnProperty.call(_90Day, key)) {
const element = _90Day[key];
if (key >= uptimesRollers[rolledAt].startTs) {
_90Day[key].border = true;
} else {
_90Day[key].border = false;
}
}
}
}
let resp = await axios.post(`${base}/api/today/aggregated`, {
monitor: monitor,
startTs: uptimesRollers[newRolledAt].startTs
});
uptimesRollers[newRolledAt].value = resp.data.uptime;
rollerLoading = false;
}
rolledAt = newRolledAt;
for (const key in _90Day) {
if (Object.prototype.hasOwnProperty.call(_90Day, key)) {
const element = _90Day[key];
if (key >= uptimesRollers[rolledAt].startTs) {
_90Day[key].border = true;
} else {
_90Day[key].border = false;
}
}
}
}
onMount(async () => {
scrollToRight();
loadIncidents();
});
afterUpdate(() => {
dispatch("heightChange", {});
});
function show90Inline(e, bar) {
if (e.detail.hover) {
_90Day[bar.timestamp].showDetails = true;
} else {
_90Day[bar.timestamp].showDetails = false;
}
}
let showDailyDataModal = false;
let dateFetchedFor = "";
let dayUptime = "NA";
let loadingDayData = false;
onMount(async () => {
scrollToRight();
loadIncidents();
});
afterUpdate(() => {
dispatch("heightChange", {});
});
function show90Inline(e, bar) {
if (e.detail.hover) {
_90Day[bar.timestamp].showDetails = true;
} else {
_90Day[bar.timestamp].showDetails = false;
}
}
let showDailyDataModal = false;
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);
showDailyDataModal = true;
loadingDayData = true;
dayIncidentsFull = [];
setTimeout(() => {
getToday(bar.timestamp, incidentIDs);
}, 750);
}
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, $page.data.localTz);
showDailyDataModal = true;
loadingDayData = true;
dayIncidentsFull = [];
analyticsEvent("monitor_day_data", {
tag: monitor.tag,
data_date: dateFetchedFor
});
setTimeout(() => {
getToday(bar.timestamp, incidentIDs);
}, 50);
}
</script>
<div class="monitor relative grid w-full grid-cols-12 gap-2 pb-2 pt-0 md:w-[655px]">
{#if !!!embed}
<div class="col-span-12 md:w-[546px]">
<div class="pt-0">
<div class="scroll-m-20 pr-5 text-xl font-medium tracking-tight">
{#if monitor.image}
<img
src={base + monitor.image}
class="absolute left-6 top-6 inline h-5 w-5"
alt={monitor.name}
srcset=""
/>
{/if}
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
{monitor.name}
</p>
{#if !!!embed}
<div class="col-span-12 md:w-[546px]">
<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 hidden md:block"
alt={monitor.name}
srcset=""
/>
{/if}
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
{monitor.name}
</p>
<p class="mt-1 text-xs font-medium text-muted-foreground">
{#if !!monitor.description}
{@html monitor.description}
{/if}
</p>
<div class="absolute right-14 top-5">
<Button
class="wiggle h-5 p-0 text-muted-foreground hover:text-primary"
variant="link"
on:click={(e) => {
dispatch("show_shareMenu", {
monitor: monitor
});
}}
>
<Share2 class="h-4 w-4 " />
</Button>
</div>
</div>
</div>
</div>
{/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>
{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>
</div>
<div class="pt-0.5 text-right">
<div
class="text-api-up truncate text-xs font-semibold text-{monitor.pageData
.summaryColorClass}"
>
{monitor.pageData.summaryStatus}
</div>
</div>
</div>
<div
class="relative col-span-12 mt-1"
use:clickOutsideAction
on:clickoutside={(e) => {
showDailyDataModal = false;
}}
>
<div
class="daygrid90 flex min-h-[60px] justify-start overflow-x-auto overflow-y-hidden py-1"
>
{#each Object.entries(_90Day) as [ts, bar]}
<a
data-ts={ts}
use:hoverAction
on:hover={(e) => {
show90Inline(e, bar);
}}
on:click={(e) => {
dailyDataGetter(e, bar, incidents[ts]);
}}
href="#"
class="oneline h-[34px] w-[6px]
<p class="mt-1 text-xs font-medium text-muted-foreground">
{#if !!monitor.description}
{@html monitor.description}
{/if}
</p>
<div class="absolute right-14 top-5 flex gap-x-2">
{#if $page.data.isLoggedIn}
<Button
href="{base}/manage/app/monitors#{monitor.tag}"
class=" rotate-once h-5 p-0 text-muted-foreground hover:text-primary"
variant="link"
rel="external"
>
<Settings class="h-4 w-4 " />
</Button>
{/if}
<Button
class="wiggle h-5 p-0 text-muted-foreground hover:text-primary"
variant="link"
on:click={(e) => {
dispatch("show_shareMenu", {
monitor: monitor
});
}}
>
<Share2 class="h-4 w-4 " />
</Button>
</div>
</div>
</div>
</div>
{/if}
<div class="col-span-12 min-h-[94px] pt-2 md:w-[546px]">
<div class="col-span-12">
<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}
</Button>
</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="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-0.5"
use:clickOutsideAction
on:clickoutside={(e) => {
showDailyDataModal = false;
}}
>
<div class="daygrid90 flex min-h-[60px] justify-start overflow-x-auto overflow-y-hidden py-1">
{#each Object.entries(_90Day) as [ts, bar]}
<a
data-ts={ts}
use:hoverAction
on:hover={(e) => {
show90Inline(e, bar);
}}
on:click={(e) => {
dailyDataGetter(e, bar, incidents[ts]);
}}
href="#"
class="oneline h-[34px] w-[6px]
{bar.border ? 'opacity-100' : 'opacity-20'} pb-1"
>
<div
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto w-[4px] rounded-{monitor.pageData.barRoundness.toUpperCase() ==
'SHARP'
? 'none'
: 'sm'}"
></div>
{#if !!incidents[ts]}
<div
style="transition-delay: {Math.floor(
Math.random() * (1500 - 500 + 1)
) + 500}ms;"
class="bg-api-{incidents[
ts
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] left-[1px] h-[4px] w-[4px] rounded-full"
></div>
{/if}
</a>
{#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
)}
-
{bar.summaryStatus}
</div>
</div>
{/if}
{/each}
</div>
{#if showDailyDataModal}
<div
transition:slide={{ direction: "bottom" }}
class="absolute -left-2 top-10 z-10 mx-auto rounded-sm border bg-card px-[7px] py-[7px] shadow-lg md:w-[560px]"
>
<div class="mb-2 flex justify-between text-xs font-semibold">
<span>{dateFetchedFor}</span>
{#if !loadingDayData}
<span>
<TrendingUp class="mx-1 inline" size={12} />
{dayUptime}%</span
>
{/if}
</div>
{#if dayIncidentsFull.length > 0}
<div class="-mx-2 mb-4 grid grid-cols-1">
<div class="col-span-1 px-2">
<Badge variant="outline" class="border-0 pl-0">
{l(lang, "Incident Updates")}
</Badge>
</div>
{#each dayIncidentsFull as incident, index}
<div class="col-span-1">
<Incident {incident} {lang} index="incident-{index}" />
</div>
{/each}
</div>
{/if}
<div class="flex flex-wrap">
{#if loadingDayData}
<LoaderBoxes />
{:else}
{#each Object.entries(_0Day) as [ts, bar]}
<div
data-index={bar.index}
class="bg-{bar.cssClass} today-sq m-[1px] h-[10px] w-[10px]"
></div>
<div class="hiddenx relative">
<div
data-index={ts.index}
class="message rounded border bg-black p-2 text-xs font-semibold text-white"
>
<p>
<span class="text-{bar.cssClass}"></span>
>
<div
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto w-[4px] rounded-{monitor.pageData.barRoundness.toUpperCase() ==
'SHARP'
? 'none'
: 'sm'}"
></div>
{#if !!incidents[ts]}
<div
class="bg-api-{incidents[
ts
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] left-[1px] h-[4px] w-[4px] rounded-full"
></div>
{/if}
</a>
{#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, $page.data.localTz)}
-
{bar.summaryStatus}
</div>
</div>
{/if}
{/each}
</div>
{#if monitor.monitor_type === "GROUP" && !!!embed}
<div class="-mt-4 flex justify-end">
<Button
variant="secondary"
href="{base}?group={monitor.tag}"
rel="external"
class="bounce-right h-8 text-xs"
on:click={scrollToRight}
>
{l(lang, "View in detail")}
<ArrowRight class="arrow ml-1.5 h-4 w-4" />
</Button>
</div>
{/if}
{f(
new Date(bar.timestamp * 1000),
"hh:mm a",
selectedLang
)}
</p>
{#if bar.status != "NO_DATA"}
<p class="pl-2">
{l(lang, bar.status)}
</p>
{:else}
<p class="pl-2">-</p>
{/if}
</div>
</div>
{/each}
{/if}
</div>
</div>
{/if}
</div>
</div>
</div>
{#if showDailyDataModal}
<div
transition:slide={{ direction: "bottom" }}
class="absolute -left-2 top-10 z-10 mx-auto rounded-sm border bg-card px-[7px] py-[7px] shadow-lg md:w-[560px]"
>
<div class="mb-2 flex justify-between text-xs font-semibold">
<span>{dateFetchedFor}</span>
{#if !loadingDayData}
<span>
<TrendingUp class="mx-1 inline" size={12} />
{dayUptime}%</span
>
{/if}
</div>
{#if dayIncidentsFull.length > 0}
<div class="-mx-2 mb-4 grid grid-cols-1">
<div class="col-span-1 px-2">
<Badge variant="outline" class="border-0 pl-0">
{l(lang, "Incident Updates")}
</Badge>
</div>
{#each dayIncidentsFull as incident, index}
<div class="col-span-1">
<Incident {incident} {lang} index="incident-{index}" />
</div>
{/each}
</div>
{/if}
<div class="flex flex-wrap">
{#if loadingDayData}
<LoaderBoxes />
{:else}
{#each Object.entries(_0Day) as [ts, bar]}
<div data-index={bar.index} class="bg-{bar.cssClass} today-sq m-[1px] h-[10px] w-[10px]"></div>
<div class="hiddenx relative">
<div
data-index={ts.index}
class="message rounded border bg-black p-2 text-xs font-semibold text-white"
>
<p>
<span class="text-{bar.cssClass}"></span>
{f(new Date(bar.timestamp * 1000), "hh:mm a", selectedLang, $page.data.localTz)}
</p>
{#if bar.status != "NO_DATA"}
<p class="pl-2">
{l(lang, bar.status)}
</p>
{:else}
<p class="pl-2">-</p>
{/if}
</div>
</div>
{/each}
{/if}
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
+61 -57
View File
@@ -1,64 +1,68 @@
<script>
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { Languages, Menu } from "lucide-svelte";
import { base } from "$app/paths";
import { analyticsEvent } from "$lib/boringOne";
export let data;
let defaultPattern = data.site?.pattern || "squares";
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import Languages from "lucide-svelte/icons/languages";
import Menu from "lucide-svelte/icons/menu";
import { base } from "$app/paths";
import { analyticsEvent } from "$lib/boringOne";
import GMI from "$lib/components/gmi.svelte";
export let data;
let defaultPattern = data.site?.pattern || "squares";
</script>
<div class="{defaultPattern}-pattern"></div>
<header class="blurry-bg sticky top-0 z-50 mx-auto md:mt-2">
<div class="container flex h-14 max-w-[820px] items-center border bg-card px-3 md:rounded-md">
<a href={data.site.home ? data.site.home : base} class="mr-6 flex items-center space-x-2">
{#if data.site.logo}
<img src={base + data.site.logo} class="w-8" alt={data.site.title} srcset="" />
{/if}
{#if data.site.siteName}
<span class=" inline-block text-[15px] font-bold lg:text-base">
{data.site.siteName}
</span>
{/if}
</a>
<div class="flex w-full justify-end">
{#if data.site.nav}
<nav class=" hidden flex-wrap items-center text-sm font-medium md:flex">
{#each data.site.nav as navItem}
<a
href={navItem.url}
class="flex rounded-md px-3 py-2 text-card-foreground transition-all ease-linear hover:bg-background"
on:click={() => analyticsEvent("nav", navItem.name)}
>
{#if navItem.iconURL}
<img
src={base + navItem.iconURL}
class="mr-1.5 mt-0.5 inline h-4"
alt={navItem.name}
/>
{/if}
<span>{navItem.name}</span>
</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 href={navItem.url}> {navItem.name} </a>
</DropdownMenu.Item>
</DropdownMenu.Group>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}
</div>
</div>
<div class="container flex h-14 max-w-[820px] items-center border bg-card px-3 md:rounded-md">
<a rel="external" href={data.site.home ? data.site.home : base} class="mr-6 flex items-center space-x-2">
{#if data.site.logo}
<GMI src={data.site.logo} classList="w-8" alt={data.site.title} srcset="" />
{/if}
{#if data.site.siteName}
<span class=" inline-block text-[15px] font-bold lg:text-base">
{data.site.siteName}
</span>
{/if}
</a>
<div class="flex w-full justify-end">
{#if data.site.nav}
<nav class=" hidden flex-wrap items-center text-sm font-medium md:flex">
{#each data.site.nav as navItem}
<a
rel="external"
href={navItem.url}
class="flex rounded-md px-3 py-2 text-card-foreground transition-all ease-linear hover:bg-background"
on:click={() =>
analyticsEvent("navigation", {
name: navItem.name
})}
>
{#if navItem.iconURL}
<GMI src={navItem.iconURL} classList="mr-1.5 mt-0.5 inline h-4" alt={navItem.name} />
{/if}
<span>{navItem.name}</span>
</a>
{/each}
</nav>
<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>
</header>
+268 -265
View File
@@ -1,293 +1,296 @@
<script>
import { Share2, Link, CopyCheck, Code, TrendingUp, Percent, Loader } from "lucide-svelte";
import { Button } from "$lib/components/ui/button";
import { base } from "$app/paths";
import { onMount } from "svelte";
import { analyticsEvent } from "$lib/boringOne";
import { l, summaryTime } from "$lib/i18n/client";
import * as RadioGroup from "$lib/components/ui/radio-group";
import { Label } from "$lib/components/ui/label";
import Share2 from "lucide-svelte/icons/share-2";
import Link from "lucide-svelte/icons/link";
import CopyCheck from "lucide-svelte/icons/copy-check";
import Code from "lucide-svelte/icons/code";
import TrendingUp from "lucide-svelte/icons/trending-up";
import Percent from "lucide-svelte/icons/percent";
import Loader from "lucide-svelte/icons/loader";
import ExternalLink from "lucide-svelte/icons/external-link";
import { Button } from "$lib/components/ui/button";
import { base } from "$app/paths";
import { onMount } from "svelte";
import { analyticsEvent } from "$lib/boringOne";
import { l, summaryTime } from "$lib/i18n/client";
import * as RadioGroup from "$lib/components/ui/radio-group";
import { Label } from "$lib/components/ui/label";
import GMI from "$lib/components/gmi.svelte";
export let monitor;
export let lang;
export let selectedLang = "en";
let theme = "light";
let copiedLink = false;
let embedType = "js";
let copiedEmbed = false;
let copiedBadgeStatus = false;
let copiedBadgeUptime = false;
let copiedBadgeDotStandard = false;
let copiedBadgeDotPing = false;
export let monitor;
export let lang;
export let selectedLang = "en";
let theme = "light";
let copiedLink = false;
let embedType = "js";
let copiedEmbed = false;
let copiedBadgeStatus = false;
let copiedBadgeUptime = false;
let copiedBadgeDotStandard = false;
let copiedBadgeDotPing = false;
let protocol;
let domain;
let protocol;
let domain;
let pathMonitorLink;
function copyLinkToClipboard() {
analyticsEvent("monitor_link_copied", {
tag: monitor.tag
});
navigator.clipboard.writeText(pathMonitorLink);
copiedLink = true;
setTimeout(function () {
copiedLink = false;
}, 1500);
}
let pathMonitorLink;
function copyLinkToClipboard() {
analyticsEvent("monitor_link_copied", {
tag: monitor.tag
});
navigator.clipboard.writeText(pathMonitorLink);
copiedLink = true;
setTimeout(function () {
copiedLink = false;
}, 1500);
}
let pathMonitorBadgeUptime;
function copyUptimeBadge() {
analyticsEvent("monitor_uptime_badge_copied", {
tag: monitor.tag
});
navigator.clipboard.writeText(pathMonitorBadgeUptime);
copiedBadgeUptime = true;
setTimeout(function () {
copiedBadgeUptime = false;
}, 1500);
}
let pathMonitorBadgeUptime;
function copyUptimeBadge() {
analyticsEvent("monitor_uptime_badge_copied", {
tag: monitor.tag
});
navigator.clipboard.writeText(pathMonitorBadgeUptime);
copiedBadgeUptime = true;
setTimeout(function () {
copiedBadgeUptime = false;
}, 1500);
}
let pathMonitorBadgeStatus;
function copyStatusBadge() {
analyticsEvent("monitor_status_badge_copied", {
tag: monitor.tag
});
navigator.clipboard.writeText(pathMonitorBadgeStatus);
copiedBadgeStatus = true;
setTimeout(function () {
copiedBadgeStatus = false;
}, 1500);
}
let pathMonitorBadgeStatus;
function copyStatusBadge() {
analyticsEvent("monitor_status_badge_copied", {
tag: monitor.tag
});
navigator.clipboard.writeText(pathMonitorBadgeStatus);
copiedBadgeStatus = true;
setTimeout(function () {
copiedBadgeStatus = false;
}, 1500);
}
let pathMonitorBadgeDot;
function copyDotStandard() {
analyticsEvent("monitor_svg_standard_copied", {
tag: monitor.tag
});
let pathMonitorBadgeDot;
function copyDotStandard() {
analyticsEvent("monitor_svg_standard_copied", {
tag: monitor.tag
});
navigator.clipboard.writeText(pathMonitorBadgeDot);
copiedBadgeDotStandard = true;
setTimeout(function () {
copiedBadgeDotStandard = false;
}, 1500);
}
navigator.clipboard.writeText(pathMonitorBadgeDot);
copiedBadgeDotStandard = true;
setTimeout(function () {
copiedBadgeDotStandard = false;
}, 1500);
}
let pathMonitorBadgeDotPing;
function copyDotPing() {
analyticsEvent("monitor_svg_pinging_copied", {
tag: monitor.tag
});
navigator.clipboard.writeText(pathMonitorBadgeDotPing);
copiedBadgeDotPing = true;
setTimeout(function () {
copiedBadgeDotPing = false;
}, 1500);
}
let pathMonitorBadgeDotPing;
function copyDotPing() {
analyticsEvent("monitor_svg_pinging_copied", {
tag: monitor.tag
});
navigator.clipboard.writeText(pathMonitorBadgeDotPing);
copiedBadgeDotPing = true;
setTimeout(function () {
copiedBadgeDotPing = false;
}, 1500);
}
function copyScriptTagToClipboard() {
//get domain with port number
function copyScriptTagToClipboard() {
//get domain with port number
analyticsEvent("monitor_embed_copied", {
tag: monitor.tag,
type: embedType
});
analyticsEvent("monitor_embed_copied", {
tag: monitor.tag,
type: embedType
});
let path = `${base}/embed/monitor-${monitor.tag}`;
let scriptTag =
`<script async src="${protocol + "//" + domain + path}/js?theme=${theme}&monitor=${protocol + "//" + domain + path}"><` +
"/script>";
let path = `${base}/embed/monitor-${monitor.tag}`;
let scriptTag =
`<script async src="${protocol + "//" + domain + path}/js?theme=${theme}&monitor=${protocol + "//" + domain + path}"><` +
"/script>";
if (embedType == "iframe") {
scriptTag = `<iframe src="${protocol + "//" + domain + path}?theme=${theme}" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>`;
}
navigator.clipboard.writeText(scriptTag);
copiedEmbed = true;
setTimeout(function () {
copiedEmbed = false;
}, 1500);
}
if (embedType == "iframe") {
scriptTag = `<iframe src="${protocol + "//" + domain + path}?theme=${theme}" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>`;
}
navigator.clipboard.writeText(scriptTag);
copiedEmbed = true;
setTimeout(function () {
copiedEmbed = false;
}, 1500);
}
onMount(async () => {
protocol = window.location.protocol;
domain = window.location.host;
pathMonitorLink = `${protocol}//${domain}${base}/?monitor=${monitor.tag}`;
pathMonitorBadgeUptime = `${protocol}//${domain}${base}/badge/${monitor.tag}/uptime`;
pathMonitorBadgeStatus = `${protocol}//${domain}${base}/badge/${monitor.tag}/status`;
pathMonitorBadgeDot = `${protocol}//${domain}${base}/badge/${monitor.tag}/dot`;
pathMonitorBadgeDotPing = `${protocol}//${domain}${base}/badge/${monitor.tag}/dot?animate=ping`;
analyticsEvent("monitor_share_menu_open", {
tag: monitor.tag
});
});
onMount(async () => {
protocol = window.location.protocol;
domain = window.location.host;
pathMonitorLink = `${protocol}//${domain}${base}/?monitor=${monitor.tag}`;
pathMonitorBadgeUptime = `${protocol}//${domain}${base}/badge/${monitor.tag}/uptime`;
pathMonitorBadgeStatus = `${protocol}//${domain}${base}/badge/${monitor.tag}/status`;
pathMonitorBadgeDot = `${protocol}//${domain}${base}/badge/${monitor.tag}/dot`;
pathMonitorBadgeDotPing = `${protocol}//${domain}${base}/badge/${monitor.tag}/dot?animate=ping`;
analyticsEvent("monitor_share_menu_open", {
tag: monitor.tag
});
});
</script>
<div
class="relative mb-2 scroll-m-20 px-4 pt-4 {!!monitor.image
? 'pl-10'
: ''} text-xl font-medium tracking-tight"
>
{#if !!monitor.image}
<img
src={monitor.image.startsWith("/") ? base + monitor.image : monitor.image}
class="absolute left-4 top-5 inline h-5 w-5"
alt={monitor.name}
srcset=""
/>
{/if}
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
{monitor.name}
</p>
<div class="relative mb-2 scroll-m-20 px-4 pt-4 {!!monitor.image ? 'pl-10' : ''} text-xl font-medium tracking-tight">
{#if !!monitor.image}
<GMI src={monitor.image} classList="absolute left-4 top-5 inline h-5 w-5" alt={monitor.name} srcset="" />
{/if}
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
{monitor.name}
</p>
</div>
<div class="px-4">
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "Share")}
</h2>
<p class="mb-2 text-xs text-muted-foreground">
{l(lang, "Share this monitor using a link with others")}
</p>
<Button
class="h-8 px-2 pr-4 text-xs font-semibold"
variant="secondary"
on:click={copyLinkToClipboard}
>
{#if !copiedLink}
<Link class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Copy Link")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Link Copied")}
</span>
{/if}
</Button>
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "Share")}
</h2>
<p class="mb-2 text-xs text-muted-foreground">
{l(lang, "Share this monitor using a link with others")}
</p>
<Button class="h-8 px-2 pr-4 text-xs font-semibold" variant="secondary" on:click={copyLinkToClipboard}>
{#if !copiedLink}
<Link class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Copy Link")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Link Copied")}
</span>
{/if}
</Button>
<Button
href={pathMonitorLink}
target="_blank"
variant="link"
class="h-8 px-3 text-xs font-semibold text-muted-foreground"
>
<ExternalLink class="inline" size={12} />
</Button>
</div>
<hr class="my-4" />
<div class="px-4">
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "Embed")}
</h2>
<p class="mb-1 text-xs text-muted-foreground">
{l(lang, "Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.")}
</p>
<div class="mb-4 grid grid-cols-2 gap-2">
<div class="col-span-1">
<h3 class="mb-2 text-xs">
{l(lang, "Theme")}
</h3>
<RadioGroup.Root bind:value={theme} class=" flex">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="light" id="light-theme" />
<Label class="text-xs" for="light-theme">{l(lang, "Light")}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="dark" id="dark-theme" />
<Label class="text-xs" for="dark-theme">{l(lang, "Dark")}</Label>
</div>
<RadioGroup.Input name="theme" />
</RadioGroup.Root>
</div>
<div class="col-span-1 pl-2">
<h3 class="mb-2 text-xs">
{l(lang, "Mode")}
</h3>
<RadioGroup.Root bind:value={embedType} class="flex">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="js" id="js-embed" />
<Label class="text-xs" for="js-embed">&#x3C;script&#x3E;</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="iframe" id="iframe-embed" />
<Label class="text-xs" for="iframe-embed">&#x3C;iframe&#x3E;</Label>
</div>
<RadioGroup.Input name="embed" />
</RadioGroup.Root>
</div>
</div>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyScriptTagToClipboard}>
{#if !copiedEmbed}
<Code class="mr-2 inline" size={12} />
<span class=" font-semibold">
{l(lang, "Copy Code")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Code Copied")}
</span>
{/if}
</Button>
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "Embed")}
</h2>
<p class="mb-1 text-xs text-muted-foreground">
{l(lang, "Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.")}
</p>
<div class="mb-4 grid grid-cols-2 gap-2">
<div class="col-span-1">
<h3 class="mb-2 text-xs">
{l(lang, "Theme")}
</h3>
<RadioGroup.Root bind:value={theme} class=" flex">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="light" id="light-theme" />
<Label class="text-xs" for="light-theme">{l(lang, "Light")}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="dark" id="dark-theme" />
<Label class="text-xs" for="dark-theme">{l(lang, "Dark")}</Label>
</div>
<RadioGroup.Input name="theme" />
</RadioGroup.Root>
</div>
<div class="col-span-1 pl-2">
<h3 class="mb-2 text-xs">
{l(lang, "Mode")}
</h3>
<RadioGroup.Root bind:value={embedType} class="flex">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="js" id="js-embed" />
<Label class="text-xs" for="js-embed">&#x3C;script&#x3E;</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="iframe" id="iframe-embed" />
<Label class="text-xs" for="iframe-embed">&#x3C;iframe&#x3E;</Label>
</div>
<RadioGroup.Input name="embed" />
</RadioGroup.Root>
</div>
</div>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyScriptTagToClipboard}>
{#if !copiedEmbed}
<Code class="mr-2 inline" size={12} />
<span class=" font-semibold">
{l(lang, "Copy Code")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Code Copied")}
</span>
{/if}
</Button>
</div>
<hr class="my-4" />
<div class="px-4">
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "Badge")}
</h2>
<p class="mb-2 text-xs text-muted-foreground">
{l(lang, "Get SVG badge for this monitor")}
</p>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyStatusBadge}>
{#if !copiedBadgeStatus}
<TrendingUp class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Status")}
{l(lang, "Badge")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Badge Copied")}
</span>
{/if}
</Button>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyUptimeBadge}>
{#if !copiedBadgeUptime}
<Percent class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Uptime")}
{l(lang, "Badge")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Badge Copied")}
</span>
{/if}
</Button>
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "Badge")}
</h2>
<p class="mb-2 text-xs text-muted-foreground">
{l(lang, "Get SVG badge for this monitor")}
</p>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyStatusBadge}>
{#if !copiedBadgeStatus}
<TrendingUp class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Status")}
{l(lang, "Badge")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Badge Copied")}
</span>
{/if}
</Button>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyUptimeBadge}>
{#if !copiedBadgeUptime}
<Percent class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Uptime")}
{l(lang, "Badge")}
</span>
{:else}
<CopyCheck class="mr-2 inline" size={12} />
<span class="font-semibold">
{l(lang, "Badge Copied")}
</span>
{/if}
</Button>
</div>
<hr class="my-4" />
<div class="mb-4 px-4">
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "LIVE Status")}
</h2>
<p class="mb-2 text-xs text-muted-foreground">
{l(lang, "Get a LIVE Status for this monitor")}
</p>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyDotStandard}>
{#if !copiedBadgeDotStandard}
<img src={pathMonitorBadgeDot} class="mr-1 inline h-5" alt="status" />
<span class="font-semibold">
{l(lang, "Standard")}
</span>
{:else}
<CopyCheck class="mr-2 inline h-5 w-5" />
<span class="font-semibold">
{l(lang, "Standard")}
</span>
{/if}
</Button>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyDotPing}>
{#if !copiedBadgeDotPing}
<img src={pathMonitorBadgeDotPing} class="mr-1 inline h-5" alt="status" />
<span class="font-semibold">
{l(lang, "Pinging")}
</span>
{:else}
<CopyCheck class="mr-2 inline h-5 w-5" />
<span class="font-semibold">
{l(lang, "Pinging")}
</span>
{/if}
</Button>
<h2 class="mb-1 text-sm font-semibold">
{l(lang, "LIVE Status")}
</h2>
<p class="mb-2 text-xs text-muted-foreground">
{l(lang, "Get a LIVE Status for this monitor")}
</p>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyDotStandard}>
{#if !copiedBadgeDotStandard}
<img src={pathMonitorBadgeDot} class="mr-1 inline h-5" alt="status" />
<span class="font-semibold">
{l(lang, "Standard")}
</span>
{:else}
<CopyCheck class="mr-2 inline h-5 w-5" />
<span class="font-semibold">
{l(lang, "Standard")}
</span>
{/if}
</Button>
<Button class="h-8 px-2 pr-4 text-xs" variant="secondary" on:click={copyDotPing}>
{#if !copiedBadgeDotPing}
<img src={pathMonitorBadgeDotPing} class="mr-1 inline h-5" alt="status" />
<span class="font-semibold">
{l(lang, "Pinging")}
</span>
{:else}
<CopyCheck class="mr-2 inline h-5 w-5" />
<span class="font-semibold">
{l(lang, "Pinging")}
</span>
{/if}
</Button>
</div>
@@ -1,26 +1,26 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { ChevronDown } from "lucide-svelte";
import { cn } from "$lib/utils";
import { Accordion as AccordionPrimitive } from "bits-ui";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from "$lib/utils";
type $$Props = AccordionPrimitive.TriggerProps;
type $$Events = AccordionPrimitive.TriggerEvents;
type $$Props = AccordionPrimitive.TriggerProps;
type $$Events = AccordionPrimitive.TriggerEvents;
let className: $$Props["class"] = undefined;
export let level: AccordionPrimitive.HeaderProps["level"] = 3;
export { className as class };
let className: $$Props["class"] = undefined;
export let level: AccordionPrimitive.HeaderProps["level"] = 3;
export { className as class };
</script>
<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
class={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...$$restProps}
on:click
>
<slot />
<ChevronDown class="h-4 w-4 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
<AccordionPrimitive.Trigger
class={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...$$restProps}
on:click
>
<slot />
<ChevronDown class="h-4 w-4 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
@@ -1,35 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { Check } from "lucide-svelte";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import Check from "lucide-svelte/icons/check";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
bind:checked
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
</DropdownMenuPrimitive.CheckboxItem>
@@ -1,35 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { Circle } from "lucide-svelte";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import Circle from "lucide-svelte/icons/circle";
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<DropdownMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
</DropdownMenuPrimitive.RadioItem>
@@ -1,32 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { ChevronRight } from "lucide-svelte";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import ChevronRight from "lucide-svelte/icons/chevron-right";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
@@ -1,28 +1,28 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import { Circle } from "lucide-svelte";
import { cn } from "$lib/utils";
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils";
type $$Props = RadioGroupPrimitive.ItemProps;
type $$Events = RadioGroupPrimitive.ItemEvents;
type $$Props = RadioGroupPrimitive.ItemProps;
type $$Events = RadioGroupPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<RadioGroupPrimitive.Item
{value}
class={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
on:click
{value}
class={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
on:click
>
<div class="flex items-center justify-center">
<RadioGroupPrimitive.ItemIndicator>
<Circle class="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.ItemIndicator>
</div>
<div class="flex items-center justify-center">
<RadioGroupPrimitive.ItemIndicator>
<Circle class="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.ItemIndicator>
</div>
</RadioGroupPrimitive.Item>
+30 -30
View File
@@ -1,38 +1,38 @@
<script lang="ts">
import { cn } from "$lib/utils";
import { Select as SelectPrimitive } from "bits-ui";
import { Check } from "lucide-svelte";
import { cn } from "$lib/utils";
import { Select as SelectPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
type $$Props = SelectPrimitive.ItemProps;
type $$Events = SelectPrimitive.ItemEvents;
type $$Props = SelectPrimitive.ItemProps;
type $$Events = SelectPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export let label: $$Props["label"] = undefined;
export let disabled: $$Props["disabled"] = undefined;
export { className as class };
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export let label: $$Props["label"] = undefined;
export let disabled: $$Props["disabled"] = undefined;
export { className as class };
</script>
<SelectPrimitive.Item
{value}
{disabled}
{label}
class={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
{value}
{disabled}
{label}
class={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check class="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<slot />
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check class="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<slot />
</SelectPrimitive.Item>
@@ -1,27 +1,27 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { ChevronDown } from "lucide-svelte";
import { cn } from "$lib/utils";
import { Select as SelectPrimitive } from "bits-ui";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from "$lib/utils";
type $$Props = SelectPrimitive.TriggerProps;
type $$Events = SelectPrimitive.TriggerEvents;
type $$Props = SelectPrimitive.TriggerProps;
type $$Events = SelectPrimitive.TriggerEvents;
let className: $$Props["class"] = undefined;
export { className as class };
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<SelectPrimitive.Trigger
class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
let:builder
on:click
on:keydown
class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
let:builder
on:click
on:keydown
>
<slot {builder} />
<div>
<ChevronDown class="h-4 w-4 opacity-50" />
</div>
<slot {builder} />
<div>
<ChevronDown class="h-4 w-4 opacity-50" />
</div>
</SelectPrimitive.Trigger>
+48 -46
View File
@@ -1,70 +1,72 @@
// @ts-nocheck
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr } from "date-fns/locale";
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
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 };
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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",
"View in detail": "Details anzeigen"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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",
"View in detail": "Vis i detaljer"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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",
"View in detail": "View in detail"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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",
"View in detail": "Voir en détail"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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": "अपटाइम",
"View in detail": "विस्तार में देखें"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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": "稼働時間",
"View in detail": "詳細を表示"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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": "업타임",
"View in detail": "자세히 보기"
}
+4
View File
@@ -50,5 +50,9 @@
{
"code": "tr",
"name": "Türkçe"
},
{
"code": "nb-NO",
"name": "Norsk Bokmål"
}
]
+65
View File
@@ -0,0 +1,65 @@
{
"%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",
"View in detail": "Vis detaljer"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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",
"View in detail": "Bekijk in detail"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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",
"View in detail": "Ver em detalhes"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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": "Время работы",
"View in detail": "Подробный просмотр"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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",
"View in detail": "Detaylı Görüntüle"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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",
"View in detail": "Xem chi tiết"
}
+63 -59
View File
@@ -1,61 +1,65 @@
{
"%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": "正常运行时间",
"View in detail": "详细查看"
}
+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;
+112 -83
View File
@@ -8,92 +8,121 @@ 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"
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",
};
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,
};
// Export the constants
export { MONITOR, UP, DOWN, SITE, DEGRADED, API_TIMEOUT, ENV, AnalyticsProviders, AllRecordTypes };
const REALTIME = "realtime";
const TIMEOUT = "timeout";
const ERROR = "error";
const MANUAL = "manual";
const WEBHOOK = "webhook";
const DEFAULT_STATUS = "default_status";
const SIGNAL = "signal";
const INVITE_VERIFY_EMAIL = "invite_verify_email";
export {
MONITOR,
UP,
DOWN,
SITE,
DEGRADED,
API_TIMEOUT,
ENV,
AnalyticsProviders,
AllRecordTypes,
REALTIME,
TIMEOUT,
ERROR,
MANUAL,
NO_DATA,
WEBHOOK,
DEFAULT_STATUS,
SIGNAL,
INVITE_VERIFY_EMAIL,
};
File diff suppressed because it is too large Load Diff
+134 -446
View File
@@ -1,13 +1,9 @@
// @ts-nocheck
import axios from "axios";
import { Ping, ExtractIPv6HostAndPort } from "./ping.js";
import { UP, DOWN, DEGRADED } from "./constants.js";
import {
GetMinuteStartNowTimestampUTC,
ReplaceAllOccurrences,
GetRequiredSecrets,
Wait
} from "./tool.js";
import { Ping, ExtractIPv6HostAndPort, TCP } from "./ping.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";
import alerting from "./alerting.js";
import Queue from "queue";
@@ -19,473 +15,165 @@ 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)
autostart: true // Automatically start the queue (optional)
concurrency: 10, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true, // Automatically start the queue (optional)
});
const apiQueue = new Queue({
concurrency: 10, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true // Automatically start the queue (optional)
concurrency: 10, // Number of tasks that can run concurrently
timeout: 2 * 60 * 1000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true, // Automatically start the queue (optional)
});
const defaultEval = `(async function (statusCode, responseTime, responseData) {
let statusCodeShort = Math.floor(statusCode/100);
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
return {
status: 'UP',
latency: responseTime,
}
}
return {
status: 'DOWN',
latency: responseTime,
}
})`;
const defaultPingEval = `(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let alive = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true;
} else {
return false;
}
}, true);
return {
status: alive ? 'UP' : 'DOWN',
latency: parseInt(latencyTotal / arrayOfPings.length),
}
})`;
async function manualIncident(monitor) {
let startTs = GetMinuteStartNowTimestampUTC();
let incidentArr = await db.getIncidentsByMonitorTagRealtime(monitor.tag, startTs);
let maintenanceArr = await db.getMaintenanceByMonitorTagRealtime(monitor.tag, startTs);
let startTs = GetMinuteStartNowTimestampUTC();
let incidentArr = await db.getIncidentsByMonitorTagRealtime(monitor.tag, startTs);
let maintenanceArr = await db.getMaintenanceByMonitorTagRealtime(monitor.tag, startTs);
let impactArr = incidentArr.concat(maintenanceArr);
let impactArr = incidentArr.concat(maintenanceArr);
let impact = "";
if (impactArr.length == 0) {
return {};
}
let impact = "";
if (impactArr.length == 0) {
return {};
}
for (let i = 0; i < impactArr.length; i++) {
const element = impactArr[i];
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 (element.monitor_impact === "DOWN") {
impact = "DOWN";
break;
}
if (element.monitor_impact === "DEGRADED") {
impact = "DEGRADED";
}
}
if (!!autoIncidents) {
continue;
}
if (impact === "") {
return {};
}
if (element.monitor_impact === "DOWN") {
impact = "DOWN";
break;
}
if (element.monitor_impact === "DEGRADED") {
impact = "DEGRADED";
}
}
let manualData = {
[startTs]: {
status: impact,
latency: 0,
type: MANUAL,
},
};
if (impact === "") {
return {};
}
let manualData = {
[startTs]: {
status: impact,
latency: 0,
type: MANUAL
}
};
return manualData;
}
const pingCall = async (hostsV4, hostsV6, pingEval, tag) => {
if (hostsV4 === undefined) hostsV4 = [];
if (hostsV6 === undefined) hostsV6 = [];
let arrayOfPings = [];
for (let i = 0; i < hostsV4.length; i++) {
const hostFull = hostsV4[i].trim();
let res;
let splitHost = hostFull.split(":");
let host = splitHost[0];
let port = 80;
if (splitHost.length > 1) {
port = parseInt(splitHost[1]);
}
try {
res = await Ping(host, port, 3000);
} catch (error) {
console.log(`Error in pingCall IP4 for ${hostFull}`, error);
} finally {
arrayOfPings.push({
host: host,
port: port,
type: "IP4",
status: res.status,
latency: res.latency
});
}
}
for (let i = 0; i < hostsV6.length; i++) {
const hostFull = hostsV6[i].trim();
let { host, port } = ExtractIPv6HostAndPort(hostFull);
if (!!!port) {
port = 80;
}
let res;
try {
res = await Ping(host, port, 3000);
} catch (error) {
console.log(`Error in pingCall IP6 for ${hostFull}`, error);
} finally {
arrayOfPings.push({
host: host,
port: port,
status: res.status,
type: "IP6",
latency: res.latency
});
}
}
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString("base64");
let evalResp = undefined;
try {
evalResp = await eval(pingEval + `("${respBase64}")`);
} catch (error) {
console.log(`Error in pingEval for ${tag}`, error.message);
}
//reduce to get the status
return {
status: evalResp.status,
latency: evalResp.latency,
type: REALTIME
};
};
const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorEval, tag) => {
let axiosHeaders = {};
axiosHeaders["User-Agent"] = "Kener/3.0.2";
axiosHeaders["Accept"] = "*/*";
const start = Date.now();
//replace all secrets
for (let i = 0; i < envSecrets.length; i++) {
const secret = envSecrets[i];
if (!!body) {
body = ReplaceAllOccurrences(body, secret.find, secret.replace);
}
if (!!url) {
url = ReplaceAllOccurrences(url, secret.find, secret.replace);
}
if (!!headers) {
headers = ReplaceAllOccurrences(headers, secret.find, secret.replace);
}
}
if (!!headers) {
headers = JSON.parse(headers);
headers = headers.reduce((acc, header) => {
acc[header.key] = header.value;
return acc;
}, {});
axiosHeaders = { ...axiosHeaders, ...headers };
}
const options = {
method: method,
headers: headers,
timeout: timeout,
transformResponse: (r) => r
};
if (!!headers) {
options.headers = headers;
}
if (!!body) {
options.data = body;
}
let statusCode = 500;
let latency = 0;
let resp = "";
let timeoutError = false;
try {
let data = await axios(url, options);
statusCode = data.status;
resp = data.data;
} catch (err) {
console.log(`Error in apiCall ${tag}`, err.message);
if (err.message.startsWith("timeout of") && err.message.endsWith("exceeded")) {
timeoutError = true;
}
if (err.response !== undefined && err.response.status !== undefined) {
statusCode = err.response.status;
}
if (err.response !== undefined && err.response.data !== undefined) {
resp = err.response.data;
} else {
resp = JSON.stringify(resp);
}
} finally {
const end = Date.now();
latency = end - start;
if (resp === undefined || resp === null) {
resp = "";
}
}
resp = Buffer.from(resp).toString("base64");
let evalResp = undefined;
try {
evalResp = await eval(monitorEval + `(${statusCode}, ${latency}, "${resp}")`);
} catch (error) {
console.log(`Error in monitorEval for ${tag}`, error.message);
}
if (evalResp === undefined || evalResp === null) {
evalResp = {
status: DOWN,
latency: latency,
type: ERROR
};
} else if (
evalResp.status === undefined ||
evalResp.status === null ||
[UP, DOWN, DEGRADED].indexOf(evalResp.status) === -1
) {
evalResp = {
status: DOWN,
latency: latency,
type: ERROR
};
} else {
evalResp.type = REALTIME;
}
let toWrite = {
status: DOWN,
latency: latency,
type: ERROR
};
if (evalResp.status !== undefined && evalResp.status !== null) {
toWrite.status = evalResp.status;
}
if (evalResp.latency !== undefined && evalResp.latency !== null) {
toWrite.latency = evalResp.latency;
}
if (evalResp.type !== undefined && evalResp.type !== null) {
toWrite.type = evalResp.type;
}
if (timeoutError) {
toWrite.type = TIMEOUT;
}
return toWrite;
};
async function dsnChecker(dnsResolver, host, recordType, matchType, values) {
try {
let queryStartTime = Date.now();
let dnsRes = await dnsResolver.getRecord(host, recordType);
let latency = Date.now() - queryStartTime;
if (dnsRes[recordType] === undefined) {
return {
status: DOWN,
latency: latency,
type: REALTIME
};
}
let data = dnsRes[recordType];
let dnsData = data.map((d) => d.data);
if (matchType === "ALL") {
for (let i = 0; i < values.length; i++) {
if (dnsData.indexOf(values[i].trim()) === -1) {
return {
status: DOWN,
latency: latency,
type: REALTIME
};
}
}
return {
status: UP,
latency: latency,
type: REALTIME
};
} else if (matchType === "ANY") {
for (let i = 0; i < values.length; i++) {
if (dnsData.indexOf(values[i].trim()) !== -1) {
return {
status: UP,
latency: latency,
type: REALTIME
};
}
}
return {
status: DOWN,
latency: latency,
type: REALTIME
};
}
} catch (error) {
console.log("Error in dnsChecker", error);
return {
status: DOWN,
latency: 0,
type: REALTIME
};
}
return manualData;
}
const Minuter = async (monitor) => {
let realTimeData = {};
let manualData = {};
try {
let realTimeData = {};
let manualData = {};
const startOfMinute = GetMinuteStartNowTimestampUTC();
if (monitor.monitor_type === "API") {
let envSecrets = GetRequiredSecrets(
`${monitor.type_data.url} ${monitor.type_data.body} ${JSON.stringify(monitor.type_data.headers)}`
);
const startOfMinute = GetMinuteStartNowTimestampUTC();
const serviceClient = new Service(monitor);
if (monitor.monitor_type === "API") {
let apiResponse = await serviceClient.execute();
if (monitor.type_data.eval === "") {
monitor.type_data.eval = defaultEval;
}
realTimeData[startOfMinute] = apiResponse;
let apiResponse = await apiCall(
envSecrets,
monitor.type_data.url,
monitor.type_data.method,
JSON.stringify(monitor.type_data.headers),
monitor.type_data.body,
monitor.type_data.timeout,
monitor.type_data.eval,
monitor.tag
);
//if timeout, retry after 500ms
if (apiResponse.type === TIMEOUT) {
apiQueue.push(async (cb) => {
await Wait(500); //wait for 500ms
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
serviceClient.execute().then(async (data) => {
await db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: startOfMinute,
status: data.status,
latency: data.latency,
type: data.type,
});
cb();
});
});
}
} else if (monitor.monitor_type === "PING") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "TCP") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "DNS") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "GROUP") {
realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
} else if (monitor.monitor_type === "SSL") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "SQL") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "HEARTBEAT") {
realTimeData[startOfMinute] = await serviceClient.execute();
}
realTimeData[startOfMinute] = apiResponse;
if (apiResponse.type === TIMEOUT) {
apiQueue.push(async (cb) => {
await Wait(500); //wait for 500ms
console.log(
"Retrying api call for " +
monitor.name +
" at " +
startOfMinute +
" due to timeout"
);
apiCall(
envSecrets,
monitor.type_data.url,
monitor.type_data.method,
JSON.stringify(monitor.type_data.headers),
monitor.type_data.body,
monitor.type_data.timeout,
monitor.type_data.eval,
monitor.tag
).then(async (data) => {
await db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: startOfMinute,
status: data.status,
latency: data.latency,
type: data.type
});
cb();
});
});
}
} else if (monitor.monitor_type === "PING") {
if (!!!monitor.type_data.pingEval) {
monitor.type_data.pingEval = defaultPingEval;
}
let pingResponse = await pingCall(
monitor.type_data.hostsV4,
monitor.type_data.hostsV6,
monitor.type_data.pingEval,
monitor.tag
);
realTimeData[startOfMinute] = pingResponse;
} else if (monitor.monitor_type === "DNS") {
const dnsResolver = new DNSResolver(monitor.type_data.nameServer);
let dnsResponse = await dsnChecker(
dnsResolver,
monitor.type_data.host,
monitor.type_data.lookupRecord,
monitor.type_data.matchType,
monitor.type_data.values
);
realTimeData[startOfMinute] = dnsResponse;
}
manualData = await manualIncident(monitor);
manualData = await manualIncident(monitor);
//merge noData, apiData, webhookData, dayData
let mergedData = {};
//merge noData, apiData, webhookData, dayData
let mergedData = {};
if (monitor.default_status !== undefined && monitor.default_status !== null) {
if ([UP, DOWN, DEGRADED].indexOf(monitor.default_status) !== -1) {
mergedData[startOfMinute] = {
status: monitor.default_status,
latency: 0,
type: "default_status"
};
}
}
if (monitor.default_status !== undefined && monitor.default_status !== null) {
if ([UP, DOWN, DEGRADED].indexOf(monitor.default_status) !== -1) {
mergedData[startOfMinute] = {
status: monitor.default_status,
latency: 0,
type: DEFAULT_STATUS,
};
}
}
for (const timestamp in realTimeData) {
mergedData[timestamp] = realTimeData[timestamp];
}
for (const timestamp in realTimeData) {
if (!!realTimeData[timestamp] && !!realTimeData[timestamp].status) {
mergedData[timestamp] = realTimeData[timestamp];
}
}
for (const timestamp in manualData) {
mergedData[timestamp] = manualData[timestamp];
}
for (const timestamp in manualData) {
if (!!manualData[timestamp] && !!manualData[timestamp].status) {
mergedData[timestamp] = manualData[timestamp];
}
}
for (const timestamp in mergedData) {
const element = mergedData[timestamp];
db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: parseInt(timestamp),
status: element.status,
latency: element.latency,
type: element.type
});
}
alertingQueue.push(async (cb) => {
setTimeout(async () => {
await alerting(monitor);
cb();
}, 1042);
});
for (const timestamp in mergedData) {
const element = mergedData[timestamp];
db.insertMonitoringData({
monitor_tag: monitor.tag,
timestamp: parseInt(timestamp),
status: element.status,
latency: element.latency,
type: element.type,
});
}
} catch (e) {
console.log(`[Error] in Running Monitor name: ${monitor.name}, tag: ${monitor.tag} error:`, e);
}
alertingQueue.push(async (cb) => {
setTimeout(async () => {
try {
await alerting(monitor);
cb();
} catch (e) {
console.log(`[Error] in Running Alerting name: ${monitor.name}, tag: ${monitor.tag} error:`, e);
cb();
}
}, 1042);
});
};
alertingQueue.start((err) => {
if (err) {
console.error("Error occurred:", err);
process.exit(1);
}
if (err) {
console.error("Error occurred:", err);
process.exit(1);
}
});
apiQueue.start((err) => {
if (err) {
console.error("Error occurred:", err);
process.exit(1);
}
if (err) {
console.error("Error occurred:", err);
process.exit(1);
}
});
export { Minuter };

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