Compare commits

..

196 Commits

Author SHA1 Message Date
Raj Nandan Sharma 65f039b7bd refactor(docker): enhance build process and organize template files fixes #429 2025-08-05 22:36:31 +05:30
Raj Nandan Sharma 5c7b62e9f7 Merge pull request #451 from moaazbhnas228/fix/heartbeat-grace-period
Fix: Heartbeat monitor respects cron-based grace periods
2025-08-05 22:21:37 +05:30
Raj Nandan Sharma 5e6e891ea6 Merge pull request #473 from rajnandan1/fix/441
Fix/441
2025-08-05 22:00:06 +05:30
Raj Nandan Sharma 82c904608b refactor(subscriptions): simplify condition for subscription trigger ID check 2025-08-05 21:59:22 +05:30
Raj Nandan Sharma f5260ceb86 chore(package): update version to 3.2.19 2025-08-05 21:18:32 +05:30
Raj Nandan Sharma abb32fdd6f fix(subscriptions): improve status toggle handling
Refactors the subscription status toggle to use a dedicated handler, ensuring correct behavior when toggling status for both new and existing triggers. Enhances user experience by automatically saving and reloading data when needed.
2025-08-05 21:17:09 +05:30
Raj Nandan Sharma 97aef5c5f5 Merge pull request #461 from antonio-sessa/fix/issue_456
fix: enhance monitor rearrangement with scrollable container
2025-08-05 11:06:47 +05:30
Raj Nandan Sharma c3eb8ae7f4 Merge pull request #467 from Baw-Appie/patch-1
Update Korean translations for clarity and consistency
2025-08-05 11:06:15 +05:30
Raj Nandan Sharma 72f74f9164 Merge pull request #469 from faridvatani/feature/persian-support
feat(i18n): add Persian language support
2025-08-05 11:05:50 +05:30
Farid Vatani 958676e5a7 feat(i18n): add Persian language support 2025-07-21 14:27:42 +03:30
JiHun Oh 51134670e5 Update Korean translations for clarity and consistency 2025-07-19 11:02:39 +00:00
JiHun Oh ecea2b220d Update ko.json 2025-07-19 19:51:27 +09:00
antonio-sessa 212f4d5e7a fix: enhance monitor rearrangement with scrollable container 2025-07-14 09:30:41 +02:00
Raj Nandan Sharma 2edb09bd13 Merge pull request #455 from xgenos/fix-issue-332
fixed the issue#332 with more optimized ORIGIN env calls
2025-07-11 10:10:07 +05:30
Biswa Kalyan Bhuyan 6b9f073cb7 fix: fixed the issue#332 with more optimized ORIGIN env calls 2025-07-02 19:15:52 +05:30
Moaaz Bhnas 9f933a0f1c Fixed returned status/type on heartbeat error 2025-07-02 09:21:58 +03:00
Moaaz Bhnas 8a6f721bb4 previousRun() 2025-07-01 09:03:50 +03:00
Moaaz Bhnas eeadcca6ed Used croner instead of cron-parser 2025-07-01 08:59:04 +03:00
Moaaz Bhnas 8e89c10906 Small change 2025-07-01 08:53:46 +03:00
Moaaz Bhnas 833d16351c FIX: heartbeat latency 2025-07-01 08:49:32 +03:00
Raj Nandan Sharma 0c10552fa7 Merge pull request #448 from federico-it/main
feat(i18n): add Italian language support
2025-06-30 21:30:05 +05:30
Raj Nandan Sharma 443afe2f6e Merge pull request #447 from sendmiche/patch-1
The translation has been made more suitable for the Russian language.
2025-06-30 21:29:33 +05:30
Federico db48d98da7 feat(i18n): add Italian language support 2025-06-20 17:44:28 +02:00
sendmiche bf3150434a The translation has been made more suitable for the Russian language. 2025-06-20 12:30:59 +03:00
github-actions 71c4bd26d2 Auto-generate README.md with release versions 2025-06-12 04:47:55 +00:00
Raj Nandan Sharma ca4f7040e8 refactor(themeInfo): remove maintenance status migration and update default value handling 2025-06-12 09:54:03 +05:30
Raj Nandan Sharma 1cfa861d7e Merge pull request #431 from rajnandan1/release/18
Release/18
2025-06-12 09:21:40 +05:30
Raj Nandan Sharma 007baefe80 Merge pull request #426 from ToxykAuBleu/feat/scheduled-downtime
Add maintenance visibility to monitor' status
2025-06-02 09:31:36 +05:30
Raj Nandan Sharma 7d77f4d996 chore: update version to 3.2.18 2025-05-28 10:01:15 +05:30
Raj Nandan Sharma 3d5a62c085 feat(subscription): add status toggle and restrict config editing #418
Adds support for toggling the subscription trigger's active/inactive status from the UI and updates backend/controller logic to allow dynamic status changes. Restricts editing of subscription event configuration and user subscriptions to when the trigger is active, improving admin control and preventing unintended changes when subscriptions are inactive.
2025-05-28 10:00:32 +05:30
Raj Nandan Sharma 24ffef997b feat(docker): copy server templates to build output #429 2025-05-28 08:52:37 +05:30
ToxykAuBleu 7d5275152c Fixing responsiveness of "Availability per Component", above monitors. 2025-05-26 12:08:03 +00:00
ToxykAuBleu 93167c2358 Reverting commit about recurring maintenance (forgotten). 2025-05-26 11:22:30 +00:00
ToxykAuBleu 734d3ad280 Adding translations for all translations. 2025-05-26 09:57:59 +00:00
ToxykAuBleu 753668dfa5 Adding documentation related to new maintenance status. 2025-05-26 06:58:03 +00:00
ToxykAuBleu 351817338d Reverting commits about recurring maintenance (moved to new branch). 2025-05-26 06:47:17 +00:00
ToxykAuBleu 7c12218489 Changing default color of maintenance' status. 2025-05-22 06:50:24 +00:00
ToxykAuBleu 7c71298227 Adding maintenance calculation & message to system data. 2025-05-22 06:41:10 +00:00
ToxykAuBleu 167db6d7e6 Adding visibility of this new status to Home page.
Adding MAINTENANCE to affected monitors (Events page).
2025-05-21 07:08:35 +00:00
Raj Nandan Sharma 74834f3559 Merge pull request #417 from chunchet-ng/main
fix: remove VOLUME declarations to prevent unintended anonymous volumes
2025-05-21 09:24:23 +05:30
ToxykAuBleu e13ca5c639 Adding visibility of this new type of maintenance into Events page. 2025-05-15 12:08:29 +00:00
Chun Chet a51806d62b fix: remove VOLUME declarations to prevent unintended anonymous volumes 2025-05-15 11:18:23 +08:00
ToxykAuBleu 57238dfb72 Adding input validation for maintenance duration.
Fixing some labels + flex layout.
2025-05-13 07:02:42 +00:00
ToxykAuBleu a2be642af0 Updating controller to accept new input data from Incident creation page.
Updating last migration to create new columns.
2025-05-13 07:01:14 +00:00
Raj Nandan Sharma aec2d23c2f Merge pull request #416 from OzkrOssa/feature/spanish-support
feat(i18n): add Spanish language support
2025-05-13 09:56:31 +05:30
Oscar Ossa 11a11162da feat(i18n): add Spanish language support 2025-05-12 09:48:06 -05:00
ToxykAuBleu 12a7be497e Editing incident creation page to include maintenance strategy option.
Removing unused properties inside newIncident.
2025-05-12 08:25:26 +00:00
github-actions 9b69da66a5 Auto-generate README.md with release versions 2025-05-06 17:27:39 +00:00
Raj Nandan Sharma ee831f96bb Merge pull request #413 from rajnandan1/css-fix-1
chore: update version to 3.2.17 and adjust layout properties in monit…
2025-05-06 22:40:06 +05:30
Raj Nandan Sharma 5542790145 chore: update version to 3.2.17 and adjust layout properties in monitor component 2025-05-06 22:39:09 +05:30
github-actions 3b1091a34f Auto-generate README.md with release versions 2025-05-06 01:54:32 +00:00
Raj Nandan Sharma eebe6541c3 Merge pull request #408 from ToxykAuBleu/fix/gamedig-call
Fixing missing call of any gamedig monitors.
2025-05-06 07:05:59 +05:30
Raj Nandan Sharma e308089cfa refactor(triggerInfo): remove timeout for test loaders and improve image tag formatting 2025-05-06 07:05:04 +05:30
Raj Nandan Sharma 5132d6894e Merge pull request #407 from ToxykAuBleu/fix/compiler-warnings
Fixing compiler warnings.
2025-05-06 07:02:19 +05:30
Raj Nandan Sharma 006d1e1fa0 Merge pull request #411 from rajnandan1/fix/ui-cycle
fix(notifications): improve error handling and UI feedback
2025-05-06 07:01:05 +05:30
Raj Nandan Sharma 4c6dc24355 fix(notifications): improve error handling and UI feedback
Returns structured error messages from notification providers, updates API logic to surface errors to users, and enhances UI to display error or success states during trigger tests. Also adjusts monitor API request payload and corrects data range calculation.

Relates to improved reliability and user experience.
2025-05-06 06:57:12 +05:30
ToxykAuBleu acf227a947 Fixing missing call of any gamedig monitors. 2025-05-05 09:57:39 +00:00
ToxykAuBleu 3b05ca73e5 Adding validation to this new status.
Adding migration + seed for this new status.
2025-05-05 09:05:16 +00:00
ToxykAuBleu 236d7ea585 Adding new maintenance status to theme page. 2025-05-05 09:04:37 +00:00
ToxykAuBleu 87408d22c4 Merge branch 'main' into fix/compiler-warnings 2025-05-05 06:54:14 +00:00
Raj Nandan Sharma ea6754775d feat: update localization files with new duration options for multiple languages 2025-05-05 11:16:38 +05:30
Raj Nandan Sharma 3210f4d406 Merge pull request #406 from rajnandan1/cycle-changer
feat: add configurable home data range per device
2025-05-05 10:50:15 +05:30
Raj Nandan Sharma 16ac97a9c9 feat: document hidden categories and configurable data range in changelogs 2025-05-05 10:48:08 +05:30
Raj Nandan Sharma 8857a51dde chore: update version to 3.2.16 in package.json 2025-05-05 10:44:23 +05:30
Raj Nandan Sharma fc2abe7f16 refactor: remove commented code and clean up dropdown labels in homePage and monitor components 2025-05-05 10:31:15 +05:30
Raj Nandan Sharma f04a930e13 feat: add configurable home data range per device
Introduces per-device configuration for maximum data range and selectable days on the homepage, allowing separate settings for desktop and mobile. Updates UI, server logic, and data model to support these options, and uses user agent detection to apply the correct configuration. Improves flexibility for data display across devices.

Relates to #105
2025-05-05 10:28:02 +05:30
Raj Nandan Sharma 1de1e2d6ba Merge pull request #405 from rajnandan1/hide-cate
refactor: rename visibility property to isHidden and update related l…
2025-05-04 12:47:32 +05:30
Raj Nandan Sharma 0208e4baf9 refactor: rename visibility property to isHidden and update related logic #321 2025-05-04 12:46:11 +05:30
Raj Nandan Sharma 21af2a6d02 Merge pull request #399 from rohith1222004/feature/monitorVisibility
feat: implement visibility control for monitors
2025-05-04 12:11:37 +05:30
github-actions 2fbe301a51 Auto-generate README.md with release versions 2025-04-30 17:53:33 +00:00
Raj Nandan Sharma 6d5382982c Merge pull request #402 from rajnandan1/fix0embed
feat(embed): add timezone support to embed parameters
2025-04-30 23:03:05 +05:30
Raj Nandan Sharma a20b4ee570 feat(embed): add timezone support to embed parameters
Introduces a 'tz' parameter for specifying timezone in embed scripts and iframe URLs, improving localization for embedded monitors.
Updates documentation and refactors parameter handling to prioritize explicit timezone over browser-detected values.

Relates to improved internationalization and user experience.
2025-04-30 23:02:10 +05:30
Raj Nandan Sharma 909ff9e070 Merge pull request #330 from Aj7ay7/main
docker/fix-compose-configuration
2025-04-30 19:58:05 +05:30
github-actions 7d3e265ece Auto-generate README.md with release versions 2025-04-30 14:18:34 +00:00
Raj Nandan Sharma 78d8f311c0 Merge pull request #400 from ToxykAuBleu/fix/398-docker-not-starting 2025-04-30 17:50:14 +05:30
ToxykAuBleu c52ba95fd0 Removing bad import of "@sveltejs/kit" in gamedigCall.js. 2025-04-30 11:37:43 +00:00
Rohith Kumar 9462a0331a feat: implement visibility control for monitors
- Private monitors are visible only to logged-in users
- Public monitors are accessible to all
2025-04-30 15:46:08 +05:30
ToxykAuBleu 4e3d1838c0 Fixing compiler warnings. 2025-04-30 06:43:21 +00:00
Raj Nandan Sharma aad7b5c6ea chore: update Dockerfile to use latest package versions for alpine build 2025-04-30 07:37:47 +05:30
Raj Nandan Sharma abec24d65b Merge pull request #397 from ToxykAuBleu/fix/396-gamedig-install
Updating Gamedig to 5.3.0 + fix build app.
2025-04-30 07:32:50 +05:30
Raj Nandan Sharma e0c3d1864c chore: remove specific version for iputils in Dockerfile 2025-04-30 07:14:54 +05:30
ToxykAuBleu bd25c6fe05 Updating Gamedig to 5.3.0.
Moving Gamedig to dependencies (not devDependencies).
2025-04-29 20:34:13 +00:00
Raj Nandan Sharma 4ec5e14431 chore: update changelog for version 3.2.14 with new features and bug fixes 2025-04-29 22:58:09 +05:30
Raj Nandan Sharma c9b8c5bec3 chore: update changelogs for version 3.2.14 with new features and bug fixes 2025-04-29 22:50:28 +05:30
Raj Nandan Sharma 223883adf7 Merge pull request #385 from rajnandan1/event-subscription
Event subscription
2025-04-29 22:21:22 +05:30
Raj Nandan Sharma 83c2d7ff79 fix: improve subscription UX and email code handling
Enhances subscription menu by showing a default icon when no image is available.
Replaces hardcoded verification code in email template with a variable.
Prevents background scrolling when subscription menu is open.
Removes redundant new subscriber response in API.
Removes unused documentation button from subscriptions page.
Adjusts checkbox styling for better UI consistency.
2025-04-29 22:07:39 +05:30
Raj Nandan Sharma cf33f5a2f6 refactor: improve downtime duration calculation in closure comment #387 2025-04-29 21:20:04 +05:30
Raj Nandan Sharma 92b4596924 refactor: centralize monitoring data insertion logic
Introduces a unified function for monitoring data insertion,
adds support for group monitor status updates, and ensures
consistency by routing all monitoring data writes through the
new logic. Removes duplicated validation and streamlines
group monitor handling for improved reliability.

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

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

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

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

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

fix: adjust delete monitor confirmation form layout for better UX

style: refine positioning of monitor action buttons for improved alignment

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

fix: remove unnecessary line breaks in layout server file
2025-03-29 22:33:17 +05:30
github-actions 3476ec2b29 Auto-generate README.md with release versions 2025-03-28 16:39:18 +00:00
Raj Nandan Sharma e0cf568aa9 feat: implement custom handle function to modify response headers 2025-03-28 21:50:07 +05:30
Raj Nandan Sharma c12d7f8345 feat: add null check for document before dispatching analytics event 2025-03-28 21:18:26 +05:30
Raj Nandan Sharma 9ba115d5ab Merge pull request #366 from rajnandan1/release/3.2.11
release 3.2.11
2025-03-28 20:56:59 +05:30
Raj Nandan Sharma 3756bbfd06 chore: bump version to 3.2.11 in package.json 2025-03-28 20:54:50 +05:30
Raj Nandan Sharma 9096bcc641 feat: import analytics event module for enhanced tracking in incident page 2025-03-28 20:53:44 +05:30
Raj Nandan Sharma c4d04854e5 feat: enhance monitor deletion confirmation UI and add analytics event for incident back button click 2025-03-28 20:52:59 +05:30
Raj Nandan Sharma d95fc0005b feat: indicate Markdown support in incident summary and comment sections 2025-03-28 20:40:24 +05:30
Raj Nandan Sharma 2b9093184a feat: add support for Markdown language in events and enhance SEO documentation as requested in #359 2025-03-28 20:34:19 +05:30
Raj Nandan Sharma 831376054b feat: add analytics capture snippets and enhance event tracking also added plausible and ms clarity #355 2025-03-28 15:07:45 +05:30
Raj Nandan Sharma 885ee0b6d6 feat: implement monitor deletion functionality with confirmation 2025-03-26 11:47:53 +05:30
Raj Nandan Sharma 511d0f26dd feat: add email validation in CreateUpdateTrigger function 2025-03-26 10:16:36 +05:30
github-actions 5263698625 Auto-generate README.md with release versions 2025-03-25 17:45:05 +00:00
Raj Nandan Sharma 7859f9564e fix: bump version to 3.2.10 in package.json 2025-03-25 22:58:04 +05:30
github-actions 149456682d Auto-generate README.md with release versions 2025-03-25 17:20:55 +00:00
Raj Nandan Sharma 8c040997da fix: refactor monitorTriggers initialization into a separate function 2025-03-25 22:47:05 +05:30
Raj Nandan Sharma 868fed0176 fix: update tzdata version in Dockerfile to use latest available 2025-03-25 22:38:13 +05:30
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
Aj7ay7 1288b463f1 Aj7ay7/fix/docker-compose-config
hotfix: updated container_name in compose
2025-02-28 23:21:02 +05:30
Aj7ay7 1aed6d0703 hotfix: updated container_name in compose 2025-02-28 23:20:38 +05:30
Aj7ay7 69f1e69140 Merge pull request #2 from Aj7ay7/revert-1-fix/docker-compose-config-validation
Revert "hotfix: Docker Compose Configuration Issues"
2025-02-28 23:17:52 +05:30
Aj7ay7 ab4ecd29c6 Revert "hotfix: Docker Compose Configuration Issues" 2025-02-28 23:17:40 +05:30
Aj7ay7 1d76c8a3d3 fix: docker-compose-config-validation
hotfix: Docker Compose Configuration Issues
2025-02-28 23:13:49 +05:30
Aj7ay7 b1c2cedbac hotfix: Docker Compose Configuration Issues 2025-02-28 23:11:13 +05:30
Kyle Affolder 8c95c94472 update: combine to avoid multiple processes
Combined to reduce spawning multiple `rm` processes.
2025-02-17 13:29:34 -05:00
Kyle Affolder 7dafb2eddc add: GHA job to confirm if Dependabot PRs exist
Adds job to check if any Dependabot PRs are open and if so, fail the Docker build (since we need to ensure OS packages exist and are in their correct versions when using pinned versions for security purposes).
2025-02-17 13:24:22 -05:00
Kyle Affolder 7c9f3eb87f add: add back pinned tzdata version
Necessary for Dependabot to track
2025-02-17 13:22:15 -05:00
Kyle ab34dd81f8 Merge branch 'rajnandan1:main' into feature/dependabot-version-updates 2025-02-17 13:04:23 -05:00
Kyle Affolder a8be878a87 update: commented out node.js deps. monitoring
Not sure if we are wanting Dependabot to track Node.js packages, so for the time being, commenting this block out, but leaving for now w/ “TODO” to come back to at a later point.
2025-02-11 17:54:37 -05:00
Kyle Affolder 2b1849f1a3 update: grammar updates 2025-02-11 17:47:45 -05:00
Kyle Affolder 25a6590324 Merge branch 'feature/dependabot-version-updates' of https://github.com/kaffolder7/kener into feature/dependabot-version-updates 2025-02-11 17:31:48 -05:00
Kyle Affolder fa251dc07f update: README generation workflow 2025-02-11 17:31:33 -05:00
Kyle 4de93e7e95 Merge branch 'main' into feature/dependabot-version-updates 2025-02-11 17:25:28 -05:00
Kyle Affolder a5e5f33dc8 Merge branch 'main' into feature/dependabot-version-updates
# Conflicts:
#	.github/workflows/publish-images.yml
#	Dockerfile
2025-02-11 17:17:45 -05:00
Kyle Affolder 4060094404 add(dependabot): to automate dependency updates
Integrating Dependabot into the workflow ensures automatic dependency updates, improving security, reducing technical debt, and keeping packages up to date with minimal manual effort. This helps prevent vulnerabilities and maintain code stability over time.

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

1. Scans for Outdated Dependencies – It checks project dependency files (e.g., package.json, Dockerfile, .env.build, etc.) for outdated versions.
2. Fetches Latest Versions – When a newer version of a dependency is available, Dependabot retrieves it and updates the dependency files accordingly.
3. Opens a Pull Request – It then creates a PR with the updated dependency, detailing the changes and linking to release notes, changelogs, or security advisories.
4. Runs CI/CD Tests – If we end up setting up continuous integration (CI) tests, the PR will trigger the tests to check for breaking changes.
5. Security Updates – Dependabot also detects vulnerable dependencies and creates PRs to update them to a secure version.
6. Auto-Merging (Optional) – We might consider this at a later point, but if configured, it can automatically merge PRs when updates pass all tests and meet the requirements.
2025-02-09 22:08:06 -05:00
152 changed files with 9621 additions and 1483 deletions
+47
View File
@@ -0,0 +1,47 @@
version: 2
updates:
# Track base image versions via .env.build
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
file-patterns:
- ".env.build"
- "node:*" # Ensures Node.js images are correctly detected
# Monitor OS package versions in Dockerfile (Debian/Alpine)
- package-ecosystem: "gitsubmodule" # Alternative method to track OS packages in Dockerfile
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "os-packages"
commit-message:
prefix: "os"
include: "scope"
# Monitor Node.js dependencies from package.json
# TODO: Uncomment below if we want to begin letting Dependabot monitor & open PRs for Node.js project dependencies
# - package-ecosystem: "npm"
# directory: "/"
# schedule:
# interval: "weekly"
# labels:
# - "dependencies"
# - "npm"
# commit-message:
# prefix: "npm"
# include: "scope"
# Monitor GitHub Actions dependencies
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "actions"
include: "scope"
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20' # Adjust as needed
node-version: '23' # Adjust as needed
- name: Install dependencies
run: npm ci
@@ -153,4 +153,4 @@ jobs:
echo "Setting BUILD_VERSION to $VERSION"
gh variable set BUILD_VERSION --body "$VERSION"
env:
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
GH_TOKEN: ${{ secrets.GH_PAT }} # Needs to be PAT w/ Read access to metadata and secrets & Read and Write access to actions, actions variables, and code
+1
View File
@@ -9,6 +9,7 @@ config/server.yaml
/src/lib/.kener
/package
.env
.vscode
.env.*
!.env.example
vite.config.js.timestamp-*
+25 -26
View File
@@ -1,15 +1,15 @@
# syntax=docker/dockerfile:1
# Global build arguments
ARG ALPINE_VERSION=23.7.0-alpine3.21
ARG DEBIAN_VERSION=23.7.0-bookworm-slim
# Global build arguments (defined default values in case `.env.build` isn't loaded)
ARG ALPINE_VERSION_TAG=23.7.0-alpine3.21
ARG DEBIAN_VERSION_TAG=23.7.0-bookworm-slim
ARG VARIANT=debian
#==========================================================#
# STAGE 1: BUILD STAGE #
#==========================================================#
FROM node:${DEBIAN_VERSION} AS builder-debian
FROM node:${DEBIAN_VERSION_TAG} AS builder-debian
RUN apt-get update && apt-get install -y \
build-essential=12.9 \
python3=3.11.2-1+b1 \
@@ -18,21 +18,21 @@ RUN apt-get update && apt-get install -y \
make=4.3-4.1 \
node-gyp=9.3.0-2 \
g++=4:12.2.0-3 \
tzdata=2024b-0+deb12u1 \
tzdata \
iputils-ping=3:20221126-1+deb12u1 && \
rm -rf /var/lib/apt/lists/*
FROM node:${ALPINE_VERSION} AS builder-alpine
FROM node:${ALPINE_VERSION_TAG} AS builder-alpine
RUN apk add --no-cache --update \
build-base=0.5-r3 \
python3=3.12.9-r0 \
py3-pip=24.3.1-r0 \
python3 \
py3-pip \
make=4.4.1-r2 \
g++=14.2.0-r4 \
sqlite=3.48.0-r0 \
sqlite-dev=3.48.0-r0 \
sqlite \
sqlite-dev \
tzdata \
iputils=20240905-r0
iputils
FROM builder-${VARIANT} AS builder
@@ -58,35 +58,36 @@ COPY . .
# TODO: Reevaluate permissions (possibly reduce?)...
# Remove docs directory and ensure required directories exist
RUN rm -rf src/routes/\(docs\) \
static/documentation \
static/fonts/lato/full && \
mkdir -p uploads database && \
# TODO: Consider changing below to `chmod -R u-rwX,g=rX,o= uploads database`
static/documentation \
static/fonts/lato/full && \
mkdir -p uploads database && \
chmod -R 750 uploads database
# Build the application and remove `devDependencies`
# Build the application and copy templates to build directory
RUN npm run build && \
mkdir -p build/server/templates && \
cp -r src/lib/server/templates/* build/server/templates/ && \
npm prune --omit=dev
#==========================================================#
# STAGE 2: PRODUCTION/FINAL STAGE #
#==========================================================#
FROM node:${DEBIAN_VERSION} AS final-debian
FROM node:${DEBIAN_VERSION_TAG} AS final-debian
# TODO: Consider adding `--no-install-recommends`, but will need testing (may further help reduce final build size)
RUN apt-get update && apt-get install -y \
iputils-ping=3:20221126-1+deb12u1 \
sqlite3=3.40.1-2+deb12u1 \
tzdata=2024b-0+deb12u1 \
tzdata \
# TODO: Is it ok to change to `curl` here so that we don't have to maintain `wget` version mismatch between Debian architectures? (`curl` is only used for the container healthcheck and because there is an Alpine variant (best!) we probably don't care if the Debian image ends up building bigger due to `curl`.)
curl=7.88.1-10+deb12u8 && \
curl && \
rm -rf /var/lib/apt/lists/*
FROM node:${ALPINE_VERSION} AS final-alpine
FROM node:${ALPINE_VERSION_TAG} AS final-alpine
RUN apk add --no-cache --update \
iputils=20240905-r0 \
sqlite=3.48.0-r0 \
tzdata
iputils \
sqlite \
tzdata
FROM final-${VARIANT} AS final
@@ -109,6 +110,7 @@ COPY --chown=node:node --from=builder /app/src/lib/ ./src/lib/
COPY --chown=node:node --from=builder /app/build ./build
COPY --chown=node:node --from=builder /app/uploads ./uploads
COPY --chown=node:node --from=builder /app/database ./database
COPY --chown=node:node --from=builder /app/src/lib/server/templates ./build/server/templates
# TODO: Consider changing from copying `node_modules` to instead letting `npm ci --omit=dev` handle production dependencies. Right now, copying `node_modules` is leading to a smaller image, whereas letting `npm ci` handle the install in final image is slightly faster, but leads to larger image size. IMO, having a slightly longer build time (e.g. ~10 sec.) is better in the end to have a smaller image.
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
COPY --chown=node:node --from=builder /app/migrations ./migrations
@@ -121,9 +123,6 @@ COPY --chown=node:node --from=builder /app/openapi.json ./openapi.json
COPY --chown=node:node --from=builder /app/openapi.yaml ./openapi.yaml
COPY --chown=node:node --from=builder /app/package.json ./package.json
# Ensure necessary directories are writable
VOLUME ["/uploads", "/database"]
# Set container timezone and make entrypoint script executable
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
chmod +x ./entrypoint.sh
+9 -9
View File
@@ -79,7 +79,7 @@ npm run dev
Official Docker images for **Kener** are available on [Docker Hub](https://hub.docker.com/r/rajnandan1/kener). Multiple versions are maintained to support different use cases.
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.4"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.4-blue" alt="Kener latest stable version: 3.2.4" /></a>
<a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.18"><img src="https://img.shields.io/badge/Latest_Stable_Release-3.2.18-blue" alt="Kener latest stable version: 3.2.18" /></a>
#### Available Tags
@@ -93,38 +93,38 @@ Official Docker images for **Kener** are available on [Docker Hub](https://hub.d
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=latest" target="_blank"><code>latest</code></td>
<td>Latest stable release (aka 3.2.4)</td>
<td>Latest stable release (aka 3.2.18)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.4" target="_blank"><code>3.2.4</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.18" target="_blank"><code>3.2.18</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2" target="_blank"><code>3.2</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.4) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.18) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3" target="_blank"><code>3</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.4) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.18) release within that major version (3.x.x)</td>
</tr>
<tr>
<td align="left" colspan="2" style="color:#0D597F;text-align:left;">Alpine Linux 3.21 w/ Node.js v23.7.0 &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.4)</td>
<td>Latest stable release (aka 3.2.18)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.4-alpine" target="_blank"><code>3.2.4-alpine</code></a></td>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2.18-alpine" target="_blank"><code>3.2.18-alpine</code></a></td>
<td>Specific release version</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3.2-alpine" target="_blank"><code>3.2-alpine</code></a></td>
<td>Major-minor version tag pointing to the latest patch (3.2.4) release within that minor version (3.2.x)</td>
<td>Major-minor version tag pointing to the latest patch (3.2.18) release within that minor version (3.2.x)</td>
</tr>
<tr>
<td><a href="https://hub.docker.com/r/rajnandan1/kener/tags?page=1&ordering=last_updated&name=3-alpine" target="_blank"><code>3-alpine</code></a></td>
<td>Major version tag pointing to the latest stable (3.2.4) release within that major version (3.x.x)</td>
<td>Major version tag pointing to the latest stable (3.2.18) release within that major version (3.x.x)</td>
</tr>
</table>
+3 -3
View File
@@ -38,7 +38,7 @@ services:
# Only use below section if you would like to utilize PostgreSQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=postgresql://db_user:db_password@localhost:5432/kener_db`)
postgres:
image: postgres:alpine
name: kener_db
container_name: postgres
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
@@ -48,7 +48,7 @@ services:
# Only use below section if you would like to utilize MySQL instead of Kener's default SQLite database. (Don't forget to set `DATABASE_URL` in `kener` service to be: `DATABASE_URL=mysql://db_user:db_password@localhost:3306/kener_db`)
mysql:
image: mariadb:11
name: kener_db
container_name: mysql
environment:
MYSQL_USER: user
MYSQL_PASSWORD: some_super_random_secure_password # Best to define this in `.env` or via Docker Secret!!
@@ -58,4 +58,4 @@ services:
volumes:
data:
name: kener_db
name: kener_db
+29 -257
View File
@@ -7,6 +7,35 @@ description: Changelogs for Kener
Here are the changelogs for Kener. Changelogs are only published when there are new features or breaking changes.
## v3.2.16
### ✨ Features
- **Hidden Categories:** You can now create hidden categories that will not show up for users that are not logged in. All the monitors will also not show up. Some one however can directly get to see them if they visit the monitor link directly
- **Configure data rage:** Now you can configure the data range in home page between 1 and 180 days. You can set different ranges for mobile and desktop devices.
## v3.2.14
### ✨ Features
- **Gamedig Monitor Functionality:** Introduced a new monitor type using Gamedig to monitor over 320+ games and services. Includes options for host, port, timeout, game/service selection, and documentation updates. ([#389](https://github.com/rajnandan1/kener/pull/389) by ToxykAuBleu)
- **Event Subscription:** Added functionality for event subscriptions, involving new database tables (`subscribers`, `subscriptions`, `subscription_triggers`), UI enhancements, a comprehensive game list, and package version update (3.2.13 to 3.2.14). ([#385](https://github.com/rajnandan1/kener/pull/385) by rajnandan1)
- **CRUD API for Monitors:** Implemented API endpoints for creating, reading, updating, and deleting monitors (`GET /api/monitor`, `POST /api/monitor`, `GET /api/monitor/[monitor_id]`, `PUT /api/monitor/[monitor_id]`, `DELETE /api/monitor/[monitor_id]`). Includes OpenAPI spec updates. ([#384](https://github.com/rajnandan1/kener/pull/384) by jensvandenreyt)
- **Modify Monitor Data:** Added the ability for users to modify historical monitoring data via a new UI option and backend API, useful for correcting past statuses (e.g., false positives). ([#380](https://github.com/rajnandan1/kener/pull/380) by mcorbin)
- **Site Status Banner:** Now you can choose to show a site status banner in the main home page. The setting has to be turned on in the Home section of the kener portal.
- **Upcoming Maintenance**: Dedicated Page for upcoming maintenance events.
- **Event Page**: Dedicated page for each event (incident/maintenance)
- **Admin Portal UI Update**: Revamped kener management portal for better accessibility.
### 🐛 Bug Fixes
- **SMTP Secure Variable Fix:** Corrected the evaluation of the `SMTP_SECURE` environment variable to properly handle values like '0' or empty strings, preventing SSL errors with STARTTLS. ([#392](https://github.com/rajnandan1/kener/pull/392) by myned)
- **Group Monitor Fix:** Fix group monitor using queues
### 🌍 Internationalization
- **Polish Translation:** Added Polish language support (`pl.json`) and integrated it into the application's localization framework. ([#386](https://github.com/rajnandan1/kener/pull/386) by lolen)
## v3.2.5
<picture>
@@ -85,260 +114,3 @@ This update is fully backward compatible with existing installations. User accou
- **Documentation**
- Added comprehensive documentation for Heartbeat Monitors
- Updated API documentation with new endpoints
## v3.2.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **Improved Monitor Evaluation Functions**
- Replaced unsafe `eval()` with secure `Function()` constructors across all monitor types
- Added support for using modules (like cheerio) directly in evaluation functions
- **Enhanced API Monitors**
- Raw response data is now passed directly to eval functions instead of base64 encoded
- Added `modules` parameter with access to cheerio for HTML parsing
- Updated default evaluation function to use the new parameter structure
- **Improved TCP & Ping Monitors**
- Simplified evaluation functions with direct access to ping/TCP data
- Removed unnecessary base64 encoding/decoding steps
- Better error handling for invalid evaluation functions
- **Documentation Updates**
- Updated all examples and documentation for the new evaluation function signatures
- Added more detailed explanations of input parameters
- Improved examples showing usage with the new parameter structure
### Breaking Changes
- **Monitor Evaluation Functions**
- Custom evaluation functions will need to be updated to the new parameter structure
- API monitors: `(statusCode, responseTime, responseRaw, modules)` instead of `(statusCode, responseTime, responseDataBase64)`
- TCP/Ping monitors: `(arrayOfPings)` instead of `(responseDataBase64)`
### Fixes
- Fixed "cheerio is undefined" errors in API monitor evaluations
- Improved error handling and logging for monitor evaluation failures
- Security enhancements by removing `eval()` usage
### Migration
If you're using custom evaluation functions in your monitors, you'll need to update them to the new format:
#### API Monitors
```javascript
// Old format
;(async function (statusCode, responseTime, responseDataBase64) {
const resp = atob(responseDataBase64)
// Your logic here
})
```
```javascript
// New format
;(async function (statusCode, responseTime, responseRaw, modules) {
// responseRaw is the direct response - no need to decode
// Access cheerio with modules.cheerio
// Your logic here
})
```
#### TCP/Ping Monitors
```javascript
// Old format
;(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64))
// Your logic here
})
```
```javascript
// New format
;(async function (arrayOfPings) {
// arrayOfPings is directly available - no need to decode
// Your logic here
})
```
## v3.1.8
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- **Timezone Support & UI Toggle**
- Added timezone support using `date-fns-tz` for improved date formatting.
- Introduced a UI toggle in settings to switch between different timezones.
- **Enhanced Incident Management**
- Improved incident filtering to prevent duplicate auto incidents when creating manual incidents.
- Added support for incident sources to refine incident handling.
- **Dynamic Cron Job Scheduling**
- Cron jobs are now dynamically added and removed based on active monitors.
- Ensures jobs are triggered in the correct order and prevents duplicate incidents.
- **Monitor Component Improvements**
- Refactored monitor component for better data display and interaction.
- Improved uptime calculations.
- Added a dropdown to select time ranges for better visibility.
### Fixes & Improvements
- **Refactored Incident Handling & Scheduling** for better reliability and performance.
- **UI Responsiveness Fixes** to improve the experience on smaller screens.
- **Dependency Updates** to support new timezone functionality and ensure stability.
## v3.0.10
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- Added TCP monitors
### Breaking Changes
- Ping monitors will break.
### Fixes
- Bug fixes in the UI
## v3.0.9
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
### Features
- Support of SMTP for email notifications. Read more [here](/docs/triggers/#email-smtp)
- Introduction of event type `MAINTENANCE` for incidents.
- You can write eval function for ping now in monitors. Read more [here](/docs/monitors-ping/#eval)
- Added category filter for monitor management.
### Fixes
- Support longer TLD in `siteURL` example `https://example.network`
- Remove googleapis preconnect and preload
- Fixed wrong action url in webhook.
## v3.0.1
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- Support for i18n in dates.
- Support of i18n in monitor embeds. Read more [here](/docs/embed#javascript-parameters)
## v3.0.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- New APIs for creating incidents and pushing updates. Read more [here](/docs/kener-apis)
- Incident management is now part of the admin UI and removed from the config file.
- The UI colors have been updated to be more muted.
- Email Notifications for incidents using [resend](https://resend.com).
- New Kener management portal. No monitors.yaml or site.yaml needed anymore
- Login Page and Setup Page
- Remove Github dependency
- Options to disable square or dot pattern
- Support for new languages
- Multiple DB support (mysql, postgres, sqlite3)
- New API reference
- New documentation site
## v2.0.0
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- Added support for sqlite3 and removed dependency on file system
- Added support for postgres database. Read more [here](/docs/database)
- Added support for alerting. Read more [here](/docs/alerting)
- Added color customization. Read more [here](/docs/customize-site#color)
- Added three new customizations for home page. Read more [here](/docs/customize-site#barstyle)
- `barStyle`
- `barRoundness`
- `summaryStyle`
### Migration
Kener will automatically migrate your data from file system to sqlite3. If you are using a custom domain, you need to update the `site.yaml` file with the new `siteURL` field. Read more [here](/docs/customize-site#siteURL)
## v0.0.16
<picture>
<source srcset="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.webp" type="image/webp">
<img src="https://fonts.gstatic.com/s/e/notoemoji/latest/1f680/512.gif" alt="🚀" width="32" height="32">
</picture>
Here are the changes in this release
### Features
- Added support for `hideURLForGet` in monitors. Read more [here](/docs/monitors)
- New SVG badges for LIVE status. Read more [here](/docs/status-badges#live)
- `[Breaking Change]` Removed dependency on Environment variable `PUBLIC_KENER_FOLDER`. Read more [here](#v0-0-16-migration)
- Simplified build and deploy process
- Added support for fonts. Read more [here](/docs/customize-site#font)
- Added support for home page pattern. Read more [here](/docs/customize-site#pattern)
- Added support for adding your analytics provider. Read more [here](/docs/site-analytics)
- New Documentation Site
- Addes support for `sqaures` pattern in home page. Read more [here](/docs/customize-site#pattern)
- Redesigned the UI for better consistency
- Embed now supports background color using a parameter `bgc`. Read more [here](/docs/embed#javascript-parameters)
- Now title in `site.yaml` is `<title>` and `siteName` is actually the name of the site. Read more [here](/docs/customize-site#siteName)
### Migration
#### Source
- Move data from `PUBLIC_KENER_FOLDER` to `/database` file.
- Move `site.yaml` to `/config` folder
- Move `monitors.yaml` to `/config` folder
#### Docker
- Use `-v $(pwd)/database:/app/database` and `-v $(pwd)/config:/app/config` in your docker run command
+44 -1
View File
@@ -33,10 +33,53 @@ To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS the
```css
.my-class {
color: red;
color: red;
}
```
<div class="note danger">
Do not include &#x3C;style&#x3E; tags.
</div>
## Grid Layout
To change from a column layout to a grid layout in your Kener instance, you can use the following CSS:
```css
@media (min-width: 1330px) {
.section-monitors,
.section-categories,
.section-hero,
.section-back,
.section-events,
.section-legend,
.section-categories,
.section-browser-events {
width: 1330px !important;
max-width: 1330px !important;
}
.section-monitors .monitor-root {
background-color: transparent;
border: none !important;
box-shadow: none;
}
.section-monitors .monitor-root .monitors-card {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}
.section-monitors .monitor-root .monitors-card .monitor {
grid-column: span 1;
border-radius: 0.375rem;
background-color: hsl(var(--card) / var(--tw-bg-opacity, 1));
border-width: 1px;
}
.section-monitors .monitor-root .monitors-card .monitor:last-child {
border-bottom: inherit;
border-bottom-width: 1px !important;
}
}
```
+12 -31
View File
@@ -12,29 +12,24 @@ There are two ways to embed your monitor in your website
You can embed your monitor in your website using javascript. We recommend using this method as it takes care of the height of the embedded monitor.
```html
<script
async
src="http://[hostname]/embed/monitor-[tag]/js?theme=light&monitor=http://[hostname]/embed/monitor-[tag]"
></script>
<script async src="http://[hostname]/embed/monitor-[tag]/js?theme=light&monitor=http://[hostname]/embed/monitor-[tag]"></script>
```
Here is an example
```html
<script
async
src="https://kener.ing/embed/monitor-earth/js?theme=light&monitor=https://kener.ing/embed/monitor-earth"
></script>
<script async src="https://kener.ing/embed/monitor-earth/js?theme=light&monitor=https://kener.ing/embed/monitor-earth"></script>
```
### Parameters
You can pass the following parameters to the embed code
- `theme`: You can pass `light` or `dark` theme
- `monitor`: The monitor url
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
- `theme`: You can pass `light` or `dark` theme
- `monitor`: The monitor url
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
- `tz`: Timezone. Example `Asia/Calcutta`, `Asia/Tokyo`
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
@@ -49,27 +44,13 @@ Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
This is the simplest way to embed your monitor in your website. You can use the following code to embed your monitor in your website.
```html
<iframe
src="http://[hostname]/embed/monitor-[tag]?theme=light"
width="100%"
height="200"
allowfullscreen="allowfullscreen"
allowpaymentrequest
frameborder="0"
></iframe>
<iframe src="http://[hostname]/embed/monitor-[tag]?theme=light" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
```
Here is an example
```html
<iframe
src="https://kener.ing/embed/monitor-earth?theme=light"
width="100%"
height="100"
allowfullscreen="allowfullscreen"
allowpaymentrequest
frameborder="0"
></iframe>
<iframe src="https://kener.ing/embed/monitor-earth?theme=light" width="100%" height="100" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
```
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
@@ -78,9 +59,9 @@ Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
You can pass the following parameters to the embed code
- `theme`: You can pass `light` or `dark` theme
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
- `theme`: You can pass `light` or `dark` theme
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
### Demo
+9 -2
View File
@@ -25,6 +25,13 @@ Set this to the origin of your website(protocl + hostname + port if there). This
export ORIGIN=http://localhost:3000
```
If your host is at let us say `https://example.com`, then set the ORIGIN to `https://example.com`. This is required for CORS.
If you are using a reverse proxy like nginx, make sure to set the `ORIGIN` to the domain name of your website.
```bash
export ORIGIN=https://example.com
```
## PORT
Defaults to 3000 if not specified
@@ -37,8 +44,8 @@ export PORT=4242
By default kener runs on `/` but you can change it to `/status` or any other path.
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
- Important: This env variable should be present during both build and run time
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
- Important: This env variable should be present during both build and run time
```bash
export KENER_BASE_PATH=/status
+29 -1
View File
@@ -29,17 +29,26 @@ Copy and update the translation file in the `locales` folder. The translation fi
```json
{
"%status for %duration": "%status for %duration",
"120 Days": "120 Days",
"14 Days": "14 Days",
"150 Days": "150 Days",
"180 Days": "180 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
"7 Days": "7 Days",
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"All Systems are Degraded": "All Systems are Degraded",
"All Systems are Down": "All Systems are Down",
"All Systems are Operational": "All Systems are Operational",
"All Systems are in Maintenance": "All Systems are in Maintenance",
"Back": "Back",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Change Email": "Change Email",
"Code Copied": "Code Copied",
"Confirm Login": "Confirm Login",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"DEGRADED": "DEGRADED",
@@ -48,6 +57,9 @@ Copy and update the translation file in the `locales` folder. The translation fi
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Enter the code": "Enter the code",
"Enter your email": "Enter your email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
@@ -57,10 +69,12 @@ Copy and update the translation file in the `locales` folder. The translation fi
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"Login": "Login",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Manage Subscription": "Manage Subscription",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
@@ -69,11 +83,16 @@ Copy and update the translation file in the `locales` folder. The translation fi
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"Please select specific monitors to receive updates from.": "Please select specific monitors to receive updates from.",
"RESOLVED": "RESOLVED",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Some Systems are not working as expected": "Some Systems are not working as expected",
"Some Systems Degraded": "Some Systems Degraded",
"Some Systems Down": "Some Systems Down",
"Some Systems in Maintenance": "Some Systems in Maintenance",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
@@ -82,14 +101,23 @@ Copy and update the translation file in the `locales` folder. The translation fi
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Subscribe",
"Subscribe to Updates": "Subscribe to Updates",
"Subscribe to all monitors": "Subscribe to all monitors",
"Switch to your timezone": "Switch to your timezone",
"Theme": "Theme",
"There are no upcoming maintenances": "There are no upcoming maintenances",
"Timezone set to %tz": "Timezone set to %tz",
"Today": "Today",
"UP": "UP",
"Update Subscription": "Update Subscription",
"Upcoming Maintenance": "Upcoming Maintenance",
"Upcoming Maintenances": "Upcoming Maintenances",
"Updates": "Updates",
"Uptime": "Uptime"
"Uptime": "Uptime",
"View in detail": "View in detail",
"We have sent a code to your email. Please enter it below to confirm your login": "We have sent a code to your email. Please enter it below to confirm your login",
"You are logged in as %email": "You are logged in as %email"
}
```
+1 -1
View File
@@ -73,7 +73,7 @@ To close an incident, you need to add a message and set the status to `RESOLVED`
## Add Monitors
To add monitors to the incident, you need to add the monitor tag to the incident. This will automatically add the monitor to the incident. You will also get the status of the monitor in the incident. This can be `DEGRADED` or `DOWN`.
To add monitors to the incident, you need to add the monitor tag to the incident. This will automatically add the monitor to the incident. You will also get the status of the monitor in the incident. For any event type, this can be `DEGRADED` or `DOWN`. For maintenance event type, this can also be `MAINTENANCE`.
## Maintenance
+68
View File
@@ -0,0 +1,68 @@
---
title: Gamedig Monitors | Kener
description: Monitor games and services
---
# Gamedig Monitors
Gamedig monitors are used to monitor games and services. Currently, 320+ games and some services are available. You can view the entire list [here](https://github.com/gamedig/node-gamedig/blob/HEAD/GAMES_LIST.md). More than just the state of the game/service, it provides information about your game/service, such as players list, world name, connection method, and so on.
<div class="border rounded-md">
![Monitors Gamedig](/documentation/m_gamedig.png)
</div>
## Host
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The host is the domain name, IPv4 or IPv6 of the game/service you want to monitor. The host should be valid.
## Port
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The port is the query port of the game/service you want to monitor. Some games use a port, different from the one to connect, to accept queries, such as Minecraft with `query-port`.
If you do not know the query port, you can let the monitor guess the port by checking the option `Guess port`.
## Timeout
<span class="text-red-500 text-xs font-semibold">
REQUIRED
</span>
The timeout is used to define the time in milliseconds after which the monitor should timeout. If the monitor does not respond within the timeout period, the monitor will be marked as down. For example, 10000 will set the timeout to 10 seconds. It is required and has to be a number greater than 2000 (or 2 seconds).
## Request additional rules
Valve games can provide additional 'rules' to Gamedig monitors. If checked, they will be available in `reponseRaw.raw` (see [Eval](#eval)), beware that it may increase query time.
## Eval
The eval is used to define the JavaScript code that should be used to evaluate the response. It is optional and has be a valid JavaScript code.
This is an anonymous JS function, it should return a **Promise**, that resolves or rejects to `{status, latency}`, by default it looks like this.
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and latency(number)
> `{status: "DEGRADED", latency: 200}`.
```javascript
(async function (responseTime, responseRaw) {
return {
status: "UP",
latency: responseTime
}
})
```
- `responseTime` **REQUIRED** is a number. It is the latency in milliseconds
- `responseRaw` **REQUIRED** is the raw response of the game/service. It varies from game by game, we recommend first printing responseRaw with a simple `console.log`.
## Additional notes
Some games/services need more configuration on their part to accept correctly any query of the monitor. You can consult these notes [here](https://github.com/gamedig/node-gamedig/blob/HEAD/GAMES_LIST.md#games-with-additional-notes).
+4
View File
@@ -103,6 +103,10 @@ To monitor the DNS records, you need to provide the domain name and the record t
To monitor the ping, you need to provide the IP address or the domain name. If the ping is not successful, the monitor will be marked as down. You can read more about PING monitoring [here](/docs/monitors-ping).
### GAMEDIG
To monitor game/service, you need to provide the game or service to monitor, the IP address or the domain name and the port. If the query is not successful, the monitor will be marked as down. You can read more about GAMEDIG monitoring [here](/docs/monitors-gamedig).
---
## Triggers
+1 -1
View File
@@ -119,7 +119,7 @@ Admins can manage user accounts through the user settings page:
## Role Limitations
- The Admin role can only be assigned during initial setup or by existing Admins
- 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)
+11 -1
View File
@@ -21,7 +21,17 @@ Add API key of your Amplitude account
### Mixpanel
Add token of your Mixpanel account.
Add project token of your Mixpanel account.
### Plausible
- Add domain of your Plausible account. Example: `example.com`
- Add API key of your Plausible account. Default is `https://plausible.io/api/event`
- Add Script Source of your Plausible account. Default is `https://plausible.io/js/script.pageview-props.tagged-events.js`
### Microsoft Clarity
Add Project ID of your Microsoft Clarity account.
## Search Engine Optimization
+5
View File
@@ -155,6 +155,11 @@
"title": "Heartbeat Monitor",
"link": "/docs/monitors-heartbeat",
"file": "/monitors-heartbeat.md"
},
{
"title": "Gamedig Monitor",
"link": "/docs/monitors-gamedig",
"file": "/monitors-gamedig.md"
}
]
},
+1 -1
View File
@@ -75,7 +75,7 @@ Shows the day's impact. Example: If it was down at 10:00 AM and it is 11:00 AM,
## Colors
You can change the colors of UP/DEGRADED/DOWN states of the monitor. You show have a shade of green, yellow, and red respectively.
You can change the colors of UP/DEGRADED/DOWN/MAINTENANCE states of the monitor. By default, it shows a shade of green, yellow, red and blue respectively.
---
+1 -1
View File
@@ -13,7 +13,7 @@
<body>
<script
async
src="http://localhost:3000/embed/monitor-bitbuckettcp/js?bgc=212226&locale=en&theme=dark&monitor=http://localhost:3000/embed/monitor-bitbuckettcp"
src="http://localhost:3000/embed/monitor-earth/js?theme=light&monitor=http://localhost:3000/embed/monitor-earth&locale=fr&tz=Asia/Tokyo"
></script>
</body>
</html>
+2 -1
View File
@@ -9,7 +9,8 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"noImplicitAny": false
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
+4
View File
@@ -15,6 +15,10 @@ const base = process.env.KENER_BASE_PATH || "";
const app = express();
const db = knex(knexOb);
if (process.env.ORIGIN) {
process.env.SVELTEKIT_ORIGIN = process.env.ORIGIN;
}
app.use((req, res, next) => {
if (req.path.startsWith("/embed")) {
res.setHeader("Content-Security-Policy", "frame-ancestors *");
@@ -0,0 +1,49 @@
export function up(knex) {
return knex.schema
.createTable("subscribers", (table) => {
table.increments("id").primary();
table.string("subscriber_send").notNullable();
table.text("subscriber_meta").nullable();
table.string("subscriber_type").notNullable();
table.string("subscriber_status").notNullable();
table.datetime("created_at").defaultTo(knex.fn.now());
table.datetime("updated_at").defaultTo(knex.fn.now());
// Add unique constraint on subscriber_send and subscriber_type
table.unique(["subscriber_send", "subscriber_type"]);
// Add index on subscriber_send for better query performance
table.index(["subscriber_send"]);
})
.createTable("subscriptions", (table) => {
table.increments("id").primary();
table.integer("subscriber_id").unsigned().notNullable();
table.string("subscriptions_status").notNullable();
table.string("subscriptions_monitors").notNullable();
table.text("subscriptions_meta").nullable();
table.datetime("created_at").defaultTo(knex.fn.now());
table.datetime("updated_at").defaultTo(knex.fn.now());
// Add unique constraint on subscriber_id and subscriptions_monitors
// This constraint also creates an index that will be used for queries
table.unique(["subscriber_id", "subscriptions_monitors"]);
// Add index to optimize queries filtering by status and monitors
table.index(["subscriptions_status", "subscriptions_monitors"]);
})
.createTable("subscription_triggers", (table) => {
table.increments("id").primary();
table.string("subscription_trigger_type").notNullable().unique();
table.string("subscription_trigger_status").notNullable();
table.text("config").nullable();
table.datetime("created_at").defaultTo(knex.fn.now());
table.datetime("updated_at").defaultTo(knex.fn.now());
});
}
export function down(knex) {
return knex.schema
.dropTableIfExists("subscription_triggers")
.dropTableIfExists("subscriptions")
.dropTableIfExists("subscribers");
}
+901 -3
View File
@@ -25,6 +25,10 @@
"name": "Monitors",
"description": "APIs to interact with monitors"
},
{
"name": "MonitorStatus",
"description": "APIs to interact with monitor status"
},
{
"name": "Incidents",
"description": "APIs to integrate incidents"
@@ -457,7 +461,7 @@
"operationId": "updateMonitorStatus",
"summary": "Update status of a monitor",
"description": "Update status of a monitor at a given timestamp UTC",
"tags": ["Monitors"],
"tags": ["MonitorStatus"],
"security": [
{
"bearerAuth": []
@@ -516,7 +520,7 @@
"operationId": "getMonitorStatus",
"summary": "Get status of a monitor",
"description": "get status of a monitor at timestamp",
"tags": ["Monitors"],
"tags": ["MonitorStatus"],
"security": [
{
"bearerAuth": []
@@ -1097,6 +1101,900 @@
}
]
}
}
},
"/api/monitor": {
"get": {
"summary": "Search Monitor",
"description": "Search Monitor",
"operationId": "searchMonitor",
"tags": ["Monitors"],
"parameters": [
{
"name": "tag",
"in": "query",
"schema": {
"type": "string",
"example": "{{tag}}"
},
"description": "Optional tag to filter results"
}
],
"responses": {
"200": {
"description": "Response200",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T11:14:49.822Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 15
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "SSL"
},
"name": {
"type": "string",
"example": "Github SSL Certificate"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "GithubSSL"
},
"type_data": {
"type": "string",
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T12:05:01.941Z"
}
}
},
"example": [
{
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T12:05:01.941Z"
}
]
},
"examples": {
"Response200": {
"value": [
{
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T12:05:01.941Z"
}
]
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"post": {
"summary": "Create Monitor",
"description": "API to create monitors",
"operationId": "createMonitor",
"tags": ["Monitors"],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-22T10:10:00.546Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 15
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "API"
},
"name": {
"type": "string",
"example": "Github"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "Github"
},
"type_data": {
"type": "string",
"example": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}"
},
"updated_at": {
"type": "string",
"example": "2025-04-22T10:10:00.546Z"
}
}
},
"examples": {
"Create Monitor": {
"value": {
"category_name": "Home",
"created_at": "2025-04-22T10:10:00.546Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "API",
"name": "Github",
"status": "ACTIVE",
"tag": "Github",
"type_data": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}",
"updated_at": "2025-04-22T10:10:00.546Z"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Response201",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T12:35:21.030Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 16
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "API"
},
"name": {
"type": "string",
"example": "Github"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "Github"
},
"type_data": {
"type": "string",
"example": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T12:35:21.030Z"
}
}
},
"examples": {
"Response201": {
"value": {
"category_name": "Home",
"created_at": "2025-04-23T12:35:21.030Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 16,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "API",
"name": "Github",
"status": "ACTIVE",
"tag": "Github",
"type_data": "{\"url\":\"https://github.com/\",\"method\":\"GET\",\"headers\":[],\"body\":\"\",\"timeout\":10000,\"eval\":\"(async function (statusCode, responseTime, responseRaw, modules) {\\n\\tlet statusCodeShort = Math.floor(statusCode/100);\\n if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {\\n return {\\n\\t\\t\\tstatus: 'UP',\\n\\t\\t\\tlatency: responseTime,\\n }\\n } \\n\\treturn {\\n\\t\\tstatus: 'DOWN',\\n\\t\\tlatency: responseTime,\\n\\t}\\n})\",\"hideURLForGet\":\"NO\",\"allowSelfSignedCert\":false}",
"updated_at": "2025-04-23T12:35:21.030Z"
}
}
}
}
}
},
"400": {
"description": "Response400",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "monitor id must be empty or 0"
}
}
},
"examples": {
"Response400": {
"value": {
"error": "monitor id must be empty or 0"
}
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"/api/monitor/{monitorId}": {
"get": {
"summary": "Get Monitor",
"description": "API to get a specified monitor",
"operationId": "getMonitor",
"tags": ["Monitors"],
"responses": {
"200": {
"description": "Response200",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T11:14:49.822Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 15
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "SSL"
},
"name": {
"type": "string",
"example": "Github SSL Certificate"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "GithubSSL"
},
"type_data": {
"type": "string",
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T12:05:01.941Z"
}
}
},
"examples": {
"Response200": {
"value": {
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T12:05:01.941Z"
}
}
}
}
}
},
"404": {
"description": "Response404",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "monitor does not exist"
}
}
},
"examples": {
"Response404": {
"value": {
"error": "monitor does not exist"
}
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"put": {
"summary": "Update Monitor",
"description": "API to update a specified monitor",
"operationId": "updateMonitor",
"tags": ["Monitors"],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T11:14:49.822Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 20
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "SSL"
},
"name": {
"type": "string",
"example": "Github SSL Certificate"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "GithubSSL"
},
"type_data": {
"type": "string",
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T12:05:01.941Z"
}
}
},
"examples": {
"Update Monitor": {
"value": {
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 20,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T12:05:01.941Z"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Response200",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"category_name": {
"type": "string",
"example": "Home"
},
"created_at": {
"type": "string",
"example": "2025-04-23T11:14:49.822Z"
},
"cron": {
"type": "string",
"example": "* * * * *"
},
"day_degraded_minimum_count": {
"type": "number",
"example": 1
},
"day_down_minimum_count": {
"type": "number",
"example": 1
},
"default_status": {
"type": "string",
"example": "NONE"
},
"degraded_trigger": {
"nullable": true,
"example": null
},
"description": {
"type": "string",
"example": ""
},
"down_trigger": {
"nullable": true,
"example": null
},
"id": {
"type": "number",
"example": 15
},
"image": {
"type": "string",
"example": ""
},
"include_degraded_in_downtime": {
"type": "string",
"example": "NO"
},
"monitor_type": {
"type": "string",
"example": "SSL"
},
"name": {
"type": "string",
"example": "Github SSL Certificate"
},
"status": {
"type": "string",
"example": "ACTIVE"
},
"tag": {
"type": "string",
"example": "GithubSSL"
},
"type_data": {
"type": "string",
"example": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}"
},
"updated_at": {
"type": "string",
"example": "2025-04-23T13:38:22.965Z"
}
}
},
"examples": {
"Response200": {
"value": {
"category_name": "Home",
"created_at": "2025-04-23T11:14:49.822Z",
"cron": "* * * * *",
"day_degraded_minimum_count": 1,
"day_down_minimum_count": 1,
"default_status": "NONE",
"degraded_trigger": null,
"description": "",
"down_trigger": null,
"id": 15,
"image": "",
"include_degraded_in_downtime": "NO",
"monitor_type": "SSL",
"name": "Github SSL Certificate",
"status": "ACTIVE",
"tag": "GithubSSL",
"type_data": "{\"host\":\"github.com\",\"port\":443,\"degradedRemainingHours\":2,\"downRemainingHours\":1}",
"updated_at": "2025-04-23T13:38:22.965Z"
}
}
}
}
}
},
"400": {
"description": "Response400",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "id mismatch: payload: 15, url: 14"
}
}
},
"examples": {
"Response400": {
"value": {
"error": "id mismatch: payload: 15, url: 14"
}
}
}
}
}
},
"404": {
"description": "Response404",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "monitor does not exist"
}
}
},
"examples": {
"Response404": {
"value": {
"error": "monitor does not exist"
}
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"delete": {
"summary": "Delete Monitor",
"description": "API to delete a specified monitor",
"operationId": "deleteMonitor",
"tags": ["Monitors"],
"responses": {
"200": {
"description": "Response200",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
},
"examples": {
"Response200": {
"value": {}
}
}
}
}
},
"404": {
"description": "Response404",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "monitor does not exist"
}
}
},
"examples": {
"Response404": {
"value": {
"error": "monitor does not exist"
}
}
}
}
}
}
},
"security": [
{
"bearerAuth": []
}
]
},
"parameters": [
{
"name": "monitorId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
]
}
}
}
+551 -5
View File
@@ -1,17 +1,20 @@
{
"name": "kener",
"version": "3.2.5",
"version": "3.2.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kener",
"version": "3.2.5",
"version": "3.2.15",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
@@ -32,11 +35,13 @@
"express": "^4.18.2",
"figlet": "^1.8.0",
"fs-extra": "^11.1.1",
"gamedig": "^5.3.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lucide-svelte": "^0.483.0",
"marked": "^11.1.1",
"mobile-detect": "^1.4.5",
"mode-watcher": "^0.4.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
@@ -218,6 +223,36 @@
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.0"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz",
@@ -243,6 +278,21 @@
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-markdown": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.2.tgz",
"integrity": "sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz",
@@ -852,6 +902,17 @@
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/css": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.11.tgz",
"integrity": "sha512-FuAnusbLBl1SEAtfN8NdShxYJiESKw9LAFysfea1T96jD3ydBn12oYjaSG1a04BQRIUd93/0D8e5CV1cUMkmQg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
@@ -861,6 +922,17 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
"version": "1.4.21",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
@@ -892,6 +964,16 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.2.tgz",
"integrity": "sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@@ -1184,6 +1266,18 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sindresorhus/is": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
"integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sveltejs/adapter-auto": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.1.1.tgz",
@@ -1296,6 +1390,18 @@
"tslib": "^2.8.0"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
"license": "MIT",
"dependencies": {
"defer-to-connect": "^2.0.1"
},
"engines": {
"node": ">=14.16"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
@@ -1332,6 +1438,12 @@
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"license": "MIT"
},
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@@ -1740,6 +1852,39 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/barse": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz",
"integrity": "sha512-UEpvriJqAn8zuVinYICuKoPttZy3XxXEoqX/V2uYAL4zzJRuNzCK3+20nAu3YUIa2U7G53kf90wfBIp9/A+Odw==",
"license": "MIT",
"dependencies": {
"readable-stream": "~1.0.2"
}
},
"node_modules/barse/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"license": "MIT"
},
"node_modules/barse/node_modules/readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/barse/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -1984,6 +2129,45 @@
"node": ">= 0.8"
}
},
"node_modules/cacheable-lookup": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
"integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
"license": "MIT",
"engines": {
"node": ">=14.16"
}
},
"node_modules/cacheable-request": {
"version": "10.2.14",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz",
"integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==",
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "^4.0.2",
"get-stream": "^6.0.1",
"http-cache-semantics": "^4.1.1",
"keyv": "^4.5.3",
"mimic-response": "^4.0.0",
"normalize-url": "^8.0.0",
"responselike": "^3.0.0"
},
"engines": {
"node": ">=14.16"
}
},
"node_modules/cacheable-request/node_modules/mimic-response": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -2456,6 +2640,12 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
@@ -2709,6 +2899,15 @@
"node": ">=0.10.0"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -3277,6 +3476,13 @@
"node": ">= 0.6"
}
},
"node_modules/event-to-promise": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/event-to-promise/-/event-to-promise-0.7.0.tgz",
"integrity": "sha512-VOBBfyaADfe378ZzG0tgkzmsvzUyeU5arehrFzNRt5yaASUDshgctTwSrPI17ocAwR3+YftsxRClHF+GBKFByQ==",
"deprecated": "Use promise-toolbox/fromEvent instead",
"license": "MIT"
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -3378,6 +3584,24 @@
"node": ">=8.6.0"
}
},
"node_modules/fast-xml-parser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.1.tgz",
"integrity": "sha512-Kqq/ewnRACQ20e0BlQ5KqHRYWRBp7yv+jttK4Yj2yY+2ldgCoxJkrP1NHUhjypsJ+eQXlGJ/jebM3wa60s1rbQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^2.0.5"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastq": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
@@ -3536,6 +3760,15 @@
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
"integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==",
"license": "MIT",
"engines": {
"node": ">= 14.17"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -3670,6 +3903,40 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gamedig": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.0.tgz",
"integrity": "sha512-4TlnM7cWQKNOYOiyY4gUlauwYB5PInig40wZE+9qRWa78YB/G2cynT7vHb1A6vy3iygYa2Dol0cOavrjRGzTRA==",
"license": "MIT",
"dependencies": {
"fast-xml-parser": "5.2.1",
"gbxremote": "0.2.1",
"got": "13.0.0",
"iconv-lite": "0.6.3",
"long": "5.3.2",
"minimist": "1.2.8",
"seek-bzip": "2.0.0",
"varint": "6.0.0"
},
"bin": {
"gamedig": "bin/gamedig.js"
},
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/gamedig/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
@@ -3691,6 +3958,21 @@
"node": ">=10"
}
},
"node_modules/gbxremote": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.2.1.tgz",
"integrity": "sha512-SMehu6Y6ndq2Qgp9VxAb8Np3f+UUD+RWoW2SAMaxzGS96rWXyr4T1GGkecO0HHtxeH1m7pEh4FJWB8a/6aM2XQ==",
"dependencies": {
"any-promise": "^1.1.0",
"barse": "~0.4.2",
"event-to-promise": "^0.7.0",
"string-to-stream": "^1.0.1",
"xmlrpc": "^1.3.1"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -3756,6 +4038,18 @@
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-symbol-description": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
@@ -3860,6 +4154,31 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/got": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
"integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^5.2.0",
"@szmarczak/http-timer": "^5.0.1",
"cacheable-lookup": "^7.0.0",
"cacheable-request": "^10.2.8",
"decompress-response": "^6.0.0",
"form-data-encoder": "^2.1.2",
"get-stream": "^6.0.1",
"http2-wrapper": "^2.1.10",
"lowercase-keys": "^3.0.0",
"p-cancelable": "^3.0.0",
"responselike": "^3.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -4007,6 +4326,12 @@
"entities": "^4.4.0"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"license": "BSD-2-Clause"
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -4023,6 +4348,19 @@
"node": ">= 0.8"
}
},
"node_modules/http2-wrapper": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
"integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.2.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -4687,6 +5025,12 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@@ -4748,6 +5092,15 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -4943,11 +5296,23 @@
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.0.tgz",
"integrity": "sha512-5vvY5yF1zF/kXk+L94FRiTDa1Znom46UjPCH6/XbSvS8zBKMFBHTJk8KDMqJ+2J6QezQFi7k1k8v21ClJYHPaw==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/lowercase-keys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
@@ -5240,6 +5605,12 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/mobile-detect": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/mobile-detect/-/mobile-detect-1.4.5.tgz",
"integrity": "sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==",
"license": "MIT"
},
"node_modules/mode-watcher": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.4.1.tgz",
@@ -5520,6 +5891,18 @@
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz",
"integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-all": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
@@ -5827,6 +6210,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/p-cancelable": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -6538,6 +6930,12 @@
"node": ">=6"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -6614,6 +7012,18 @@
],
"license": "MIT"
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -6881,6 +7291,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"license": "MIT"
},
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -6890,6 +7306,21 @@
"node": ">=8"
}
},
"node_modules/responselike": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
"integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
"license": "MIT",
"dependencies": {
"lowercase-keys": "^3.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -7172,6 +7603,12 @@
"rimraf": "bin.js"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"license": "ISC"
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@@ -7179,6 +7616,28 @@
"license": "MIT",
"peer": true
},
"node_modules/seek-bzip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz",
"integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==",
"license": "MIT",
"dependencies": {
"commander": "^6.0.0"
},
"bin": {
"seek-bunzip": "bin/seek-bunzip",
"seek-table": "bin/seek-bzip-table"
}
},
"node_modules/seek-bzip/node_modules/commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/selderee": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
@@ -7616,6 +8075,52 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-to-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz",
"integrity": "sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.1",
"readable-stream": "^2.1.0"
}
},
"node_modules/string-to-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/string-to-stream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/string-to-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/string-to-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -7775,6 +8280,18 @@
"node": ">=0.10.0"
}
},
"node_modules/strnum": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",
"integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
@@ -8613,6 +9130,12 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
"license": "MIT"
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -8906,6 +9429,29 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xmlbuilder": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
"integrity": "sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xmlrpc": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz",
"integrity": "sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==",
"license": "MIT",
"dependencies": {
"sax": "1.2.x",
"xmlbuilder": "8.2.x"
},
"engines": {
"node": ">=0.8",
"npm": ">=1.0.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+22 -17
View File
@@ -1,26 +1,26 @@
{
"name": "kener",
"version": "3.2.5",
"version": "3.2.19",
"private": false,
"license": "MIT",
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
"description": "Kener is a modern, open-source status page application built with Node.js. It provides real-time monitoring, uptime tracking, incident management, and beautiful dashboards. Perfect for DevOps teams, SaaS providers, and businesses needing reliable service status communication with minimal setup.",
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
"keywords": [
"Node.js application",
"Open-source status page",
"Service monitoring tool",
"Real-time incident management",
"Customizable reporting",
"Service outage tracker",
"User-friendly dashboard",
"Incident communication platform",
"Scalable monitoring solution",
"Community-driven software",
"Website status tracker",
"Incident response tool",
"System status monitoring",
"Service reliability management",
"Incident alert system"
"status page",
"uptime monitoring",
"incident management",
"DevOps tools",
"service reliability",
"open source",
"Node.js",
"dashboard",
"system monitoring",
"status alerts",
"outage communication",
"API monitoring",
"SaaS status",
"performance metrics",
"status reporting"
],
"repository": {
"type": "git",
@@ -68,8 +68,11 @@
"type": "module",
"dependencies": {
"@babel/runtime": "^7.26.10",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@formkit/auto-animate": "^0.8.2",
"@number-flow/svelte": "^0.2.1",
"@scalar/express-api-reference": "^0.4.167",
@@ -90,11 +93,13 @@
"express": "^4.18.2",
"figlet": "^1.8.0",
"fs-extra": "^11.1.1",
"gamedig": "^5.3.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"lucide-svelte": "^0.483.0",
"marked": "^11.1.1",
"mobile-detect": "^1.4.5",
"mode-watcher": "^0.4.1",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
-4
View File
@@ -1,7 +1,3 @@
code:not([class^="language-"]) {
@apply rounded bg-gray-100 px-1.5 py-0.5 font-mono text-xs dark:bg-gray-800;
}
.sidebar-item.active,
.sidebar-item:hover {
color: #ed702d;
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event);
response.headers.delete("Link");
return response;
}
+124 -98
View File
@@ -1,113 +1,16 @@
/*one is the class for dotted background*/
.dots-pattern {
position: absolute;
top: 0px;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
background: linear-gradient(177deg, rgba(255, 137, 131, 0.5) 0%, rgba(35, 136, 224, 0.05) 60%);
clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%);
}
.dots-pattern::after {
content: "";
position: absolute;
background-image: radial-gradient(rgba(0, 0, 0, 0) 1.5px, var(--background-kener) 1px);
background-size: 14px 14px;
width: 100%;
height: 100vh;
top: 0;
transform: blur(3px);
left: 0;
}
.squares-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
background-repeat: no-repeat;
background-size: 100%;
height: 100svh;
/* clip-path: polygon(0 0, 100% 0, 100% 54%, 0% 100%); */
}
.squares-pattern::after {
content: "";
position: absolute;
background-image:
linear-gradient(#444cf7 1px, transparent 1px),
linear-gradient(to right, #444cf7 1px, var(--background-kener) 1px);
-webkit-mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.8) 20%,
rgba(0, 0, 0, 0.6) 40%,
rgba(0, 0, 0, 0.4) 60%,
rgba(0, 0, 0, 0.2) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.2) 0%,
rgba(0, 0, 0, 0.1) 20%,
rgba(0, 0, 0, 0.05) 40%,
rgba(0, 0, 0, 0.025) 60%,
rgba(0, 0, 0, 0.0125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-size: 32px 32px;
width: 100%;
height: 100vh;
top: 0;
left: 0;
}
.dark .squares-pattern::after {
-webkit-mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
180deg,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.25) 20%,
rgba(0, 0, 0, 0.125) 40%,
rgba(0, 0, 0, 0.0625) 60%,
rgba(0, 0, 0, 0.03125) 80%,
rgba(0, 0, 0, 0) 100%
);
background-image:
linear-gradient(#616fbf 1px, transparent 1px),
linear-gradient(to right, #616fbf 1px, var(--background-kener) 1px);
}
/*Needed to overlay content on top of dotted bg*/
section {
position: relative;
z-index: 1;
}
/*Needed overlay content on top of dotted bg*/
.blurry-bg {
/* background-color: var(--background-kener-rgba);
box-shadow: 0 0 64px 64px var(--background-kener-rgba); */
}
:root {
--up-color: #4ead94;
--down-color: #ca3038;
--degraded-color: #e6ca61;
--maintenance-color: #6679cc;
}
/*Colors for something UP*/
@@ -180,6 +83,33 @@ section {
background: linear-gradient(0deg, var(--degraded-color) 90%, var(--up-color) 1%);
}
/*Colors for something in maintenance*/
.bg-api-maintenance {
background-color: var(--maintenance-color);
}
.text-api-maintenance {
color: var(--maintenance-color);
}
.bg-api-maintenance-10 {
background: linear-gradient(0deg, var(--maintenance-color) 10%, var(--up-color) 1%);
}
.bg-api-maintenance-20 {
background: linear-gradient(0deg, var(--maintenance-color) 20%, var(--up-color) 1%);
}
.bg-api-maintenance-40 {
background: linear-gradient(0deg, var(--maintenance-color) 40%, var(--up-color) 1%);
}
.bg-api-maintenance-60 {
background: linear-gradient(0deg, var(--maintenance-color) 60%, var(--up-color) 1%);
}
.bg-api-maintenance-80 {
background: linear-gradient(0deg, var(--maintenance-color) 80%, var(--up-color) 1%);
}
.bg-api-maintenance-90 {
background: linear-gradient(0deg, var(--maintenance-color) 90%, var(--up-color) 1%);
}
/*Needed to show markdown properly*/
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::before {
content: "";
@@ -939,3 +869,99 @@ textarea::placeholder {
.dark .like-input {
background-color: rgba(0, 0, 0, 0.1) !important;
}
.ͼo .cm-gutters {
border-top-left-radius: var(--radius);
border-bottom-left-radius: var(--radius);
}
.no-gutter .ͼo .cm-gutters {
display: none;
}
.kener-md {
@apply text-sm;
}
p code:not([class^="language-"]) {
@apply rounded-sm bg-yellow-100 px-1 py-0.5 font-mono text-xs dark:bg-neutral-700 dark:text-neutral-100;
}
.kener-theme-mono .section-hero .h1,
.kener-theme-mono-hero-h1 {
@apply text-foreground;
}
.kener-theme-default .section-hero .h1,
.kener-theme-default-hero-h1 {
@apply bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-transparent;
}
.kener-theme-sunset .section-hero .h1,
.kener-theme-sunset-hero-h1 {
@apply bg-gradient-to-r from-yellow-500 via-red-500 to-pink-500 bg-clip-text text-transparent;
}
.kener-theme-forest .section-hero .h1,
.kener-theme-forest-hero-h1 {
@apply bg-gradient-to-l from-blue-500 via-teal-500 to-green-500 bg-clip-text text-transparent;
}
.kener-theme-ocean .section-hero .h1,
.kener-theme-ocean-hero-h1 {
@apply bg-gradient-to-l from-cyan-500 via-blue-500 to-indigo-500 bg-clip-text text-transparent;
}
.prose :where(hr):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1.3em;
margin-bottom: 1.3em;
}
input[type="checkbox"]:disabled + .peer,
label:has(input[type="checkbox"]:disabled) {
cursor: not-allowed;
opacity: 0.5;
}
/* Select any element whose class begins with "lucid-chevron" */
.hide-chevron [data-bits-accordion-header] .lucide-chevron-down,
.hide-chevron [data-bits-accordion-header] .lucide-chevron-up {
/* add your styles here */
display: none; /* Hides the chevron when the class 'hide-chevron' is applied */
}
.hide-chevron [data-bits-accordion-header] button:hover {
background-color: transparent;
}
.copybtn .copy-btn {
transform: scale(1);
transition: transform 0.15s;
}
.copybtn .check-btn {
transform: scale(0);
transition: transform 0.15s;
}
.copybtn:focus .copy-btn {
transform: scale(0);
}
.copybtn:focus .check-btn {
transform: scale(1);
}
.dash-after::after {
content: " ";
display: inline-block;
position: absolute;
width: 1px;
height: 24px;
border-radius: 2px;
background-color: hsl(var(--accent) / var(--tw-bg-opacity, 1));
right: -1px;
}
.dash-after:hover::after {
display: none;
}
.status-color-bar {
transition: all 0.2s ease-in-out;
}
.status-color-banner:hover .status-color-bar {
bottom: -0.75rem;
}
.status-color-banner:hover > div {
@apply bg-secondary;
}
+356
View File
@@ -0,0 +1,356 @@
[
{ "id": "sdtd", "name": "7 Days to Die", "isValve": true },
{ "id": "arma2", "name": "ARMA 2", "isValve": true },
{ "id": "a2oa", "name": "ARMA 2: Operation Arrowhead", "isValve": true },
{ "id": "arma3", "name": "ARMA 3", "isValve": true },
{ "id": "aaa", "name": "ARMA: Armed Assault" },
{ "id": "acwa", "name": "ARMA: Cold War Assault" },
{ "id": "armareforger", "name": "ARMA: Reforger", "isValve": true },
{ "id": "armaresistance", "name": "ARMA: Resistance" },
{ "id": "abioticfactor", "name": "Abiotic Factor", "isValve": true },
{ "id": "aosc", "name": "Ace of Spades Classic" },
{ "id": "ahl", "name": "Action Half-Life", "isValve": true },
{ "id": "actionsource", "name": "Action: Source", "isValve": true },
{ "id": "aoc", "name": "Age of Chivalry", "isValve": true },
{ "id": "aoe2", "name": "Age of Empires 2" },
{ "id": "alienarena", "name": "Alien Arena" },
{ "id": "alienswarm", "name": "Alien Swarm", "isValve": true },
{ "id": "avp2", "name": "Aliens versus Predator 2" },
{ "id": "avp2010", "name": "Aliens vs. Predator 2010", "isValve": true },
{ "id": "americasarmy", "name": "America's Army" },
{ "id": "americasarmy2", "name": "America's Army 2" },
{ "id": "americasarmy3", "name": "America's Army 3", "isValve": true },
{ "id": "aapg", "name": "America's Army: Proving Grounds", "isValve": true },
{ "id": "asr08", "name": "Arca Sim Racing '08" },
{ "id": "asa", "name": "Ark: Survival Ascended" },
{ "id": "ase", "name": "Ark: Survival Evolved", "isValve": true },
{ "id": "armagetronadvanced", "name": "Armagetron Advanced" },
{ "id": "assettocorsa", "name": "Assetto Corsa" },
{ "id": "atlas", "name": "Atlas", "isValve": true },
{ "id": "avorion", "name": "Avorion", "isValve": true },
{ "id": "brokeprotocol", "name": "BROKE PROTOCOL" },
{ "id": "baldursgate", "name": "Baldur's Gate" },
{ "id": "ballisticoverkill", "name": "Ballistic Overkill", "isValve": true },
{ "id": "barotrauma", "name": "Barotrauma", "isValve": true },
{ "id": "basedefense", "name": "Base Defense", "isValve": true },
{ "id": "battalion1944", "name": "Battalion 1944", "isValve": true },
{ "id": "battlefield1942", "name": "Battlefield 1942" },
{ "id": "battlefield2", "name": "Battlefield 2" },
{ "id": "battlefield2142", "name": "Battlefield 2142" },
{ "id": "battlefield3", "name": "Battlefield 3" },
{ "id": "battlefield4", "name": "Battlefield 4" },
{ "id": "battlefieldhardline", "name": "Battlefield Hardline" },
{ "id": "battlefieldvietnam", "name": "Battlefield Vietnam" },
{ "id": "bbc2", "name": "Battlefield: Bad Company 2" },
{ "id": "beammp", "name": "BeamMP (2021)" },
{ "id": "blackmesa", "name": "Black Mesa", "isValve": true },
{ "id": "bladesymphony", "name": "Blade Symphony", "isValve": true },
{ "id": "brainbread", "name": "BrainBread", "isValve": true },
{ "id": "brainbread2", "name": "BrainBread 2", "isValve": true },
{ "id": "breach", "name": "Breach", "isValve": true },
{ "id": "breed", "name": "Breed" },
{ "id": "brink", "name": "Brink", "isValve": true },
{ "id": "bas", "name": "Build and Shoot" },
{ "id": "c2d", "name": "CS2D" },
{ "id": "cod", "name": "Call of Duty" },
{ "id": "cod2", "name": "Call of Duty 2" },
{ "id": "cod3", "name": "Call of Duty 3" },
{ "id": "cod4mw", "name": "Call of Duty 4: Modern Warfare" },
{ "id": "codbo3", "name": "Call of Duty: Black Ops 3", "isValve": true },
{ "id": "codmw2", "name": "Call of Duty: Modern Warfare 2" },
{ "id": "codmw3", "name": "Call of Duty: Modern Warfare 3", "isValve": true },
{ "id": "coduo", "name": "Call of Duty: United Offensive" },
{ "id": "codwaw", "name": "Call of Duty: World at War" },
{ "id": "coj", "name": "Call of Juarez" },
{ "id": "chaser", "name": "Chaser" },
{ "id": "cmw", "name": "Chivalry: Medieval Warfare", "isValve": true },
{ "id": "chrome", "name": "Chrome" },
{ "id": "codenamecure", "name": "Codename CURE", "isValve": true },
{ "id": "codenameeagle", "name": "Codename Eagle" },
{ "id": "colonysurvival", "name": "Colony Survival", "isValve": true },
{ "id": "cacr", "name": "Command and Conquer: Renegade" },
{ "id": "c3db", "name": "Commandos 3: Destination Berlin" },
{ "id": "conanexiles", "name": "Conan Exiles", "isValve": true },
{ "id": "contagion", "name": "Contagion", "isValve": true },
{ "id": "contractjack", "name": "Contract J.A.C.K." },
{ "id": "corekeeper", "name": "Core Keeper", "isValve": true },
{ "id": "counterstrike15", "name": "Counter-Strike 1.5" },
{ "id": "counterstrike16", "name": "Counter-Strike 1.6", "isValve": true },
{ "id": "counterstrike2", "name": "Counter-Strike 2", "isValve": true },
{ "id": "cscz", "name": "Counter-Strike: Condition Zero", "isValve": true },
{ "id": "csgo", "name": "Counter-Strike: Global Offensive", "isValve": true },
{ "id": "css", "name": "Counter-Strike: Source", "isValve": true },
{ "id": "creativerse", "name": "Creativerse", "isValve": true },
{ "id": "crce", "name": "Cross Racing Championship Extreme" },
{ "id": "crysis", "name": "Crysis" },
{ "id": "crysis2", "name": "Crysis 2" },
{ "id": "crysiswars", "name": "Crysis Wars" },
{ "id": "daikatana", "name": "Daikatana" },
{ "id": "dmomam", "name": "Dark Messiah of Might and Magic", "isValve": true },
{ "id": "dal", "name": "Dark and Light", "isValve": true },
{ "id": "dhe4445", "name": "Darkest Hour: Europe '44-'45" },
{ "id": "dod", "name": "Day of Defeat", "isValve": true },
{ "id": "dods", "name": "Day of Defeat: Source", "isValve": true },
{ "id": "dayofdragons", "name": "Day of Dragons", "isValve": true },
{ "id": "doi", "name": "Day of Infamy", "isValve": true },
{ "id": "dayz", "name": "DayZ", "isValve": true },
{ "id": "dayzmod", "name": "DayZ Mod", "isValve": true },
{ "id": "dow", "name": "Days of War", "isValve": true },
{ "id": "ddpt", "name": "Deadly Dozen: Pacific Theater" },
{ "id": "deathmatchclassic", "name": "Deathmatch Classic", "isValve": true },
{ "id": "deerhunter2005", "name": "Deer Hunter 2005" },
{ "id": "descent3", "name": "Descent 3" },
{ "id": "deusex", "name": "Deus Ex" },
{ "id": "devastation", "name": "Devastation" },
{ "id": "ddd", "name": "Dino D-Day", "isValve": true },
{ "id": "dtr2", "name": "Dirt Track Racing 2" },
{ "id": "discord", "name": "Discord" },
{ "id": "dst", "name": "Don't Starve Together", "isValve": true },
{ "id": "doom3", "name": "Doom 3" },
{ "id": "dota2", "name": "Dota 2", "isValve": true },
{ "id": "dab", "name": "Double Action: Boogaloo", "isValve": true },
{ "id": "dootf", "name": "Drakan: Order of the Flame" },
{ "id": "dnf2001", "name": "Duke Nukem Forever 2001" },
{ "id": "dystopia", "name": "Dystopia", "isValve": true },
{ "id": "eco", "name": "Eco" },
{ "id": "empiresmod", "name": "Empires Mod", "isValve": true },
{ "id": "egs", "name": "Empyrion - Galactic Survival", "isValve": true },
{ "id": "etqw", "name": "Enemy Territory: Quake Wars" },
{ "id": "ets2", "name": "Euro Truck Simulator 2", "isValve": true },
{ "id": "exfil", "name": "Exfil", "isValve": true },
{ "id": "fear", "name": "F.E.A.R." },
{ "id": "f1c9902", "name": "F1 Challenge '99-'02" },
{ "id": "foundry", "name": "FOUNDRY", "isValve": true },
{ "id": "factorio", "name": "Factorio" },
{ "id": "farcry", "name": "Far Cry" },
{ "id": "farcry2", "name": "Far Cry 2" },
{ "id": "farmingsimulator19", "name": "Farming Simulator 19" },
{ "id": "farmingsimulator22", "name": "Farming Simulator 22" },
{ "id": "farmingsimulator25", "name": "Farming Simulator 25" },
{ "id": "fof", "name": "Fistful of Frags", "isValve": true },
{ "id": "formulaone2002", "name": "Formula One 2002" },
{ "id": "fortressforever", "name": "Fortress Forever", "isValve": true },
{ "id": "ffow", "name": "Frontlines: Fuel of War" },
{ "id": "garrysmod", "name": "Garry's Mod", "isValve": true },
{ "id": "geneshift", "name": "Geneshift" },
{ "id": "gck", "name": "Giants: Citizen Kabuto" },
{ "id": "globaloperations", "name": "Global Operations" },
{ "id": "goldeneyesource", "name": "GoldenEye: Source", "isValve": true },
{ "id": "gus", "name": "Gore: Ultimate Soldier" },
{ "id": "gta5f", "name": "Grand Theft Auto V - FiveM" },
{ "id": "gta5r", "name": "Grand Theft Auto V - RageMP" },
{ "id": "gta5am", "name": "Grand Theft Auto V - alt:V Multiplayer" },
{ "id": "gtasamta", "name": "Grand Theft Auto: San Andreas - Multi Theft Auto" },
{ "id": "gtasam", "name": "Grand Theft Auto: San Andreas Multiplayer" },
{ "id": "gtasao", "name": "Grand Theft Auto: San Andreas OpenMP" },
{ "id": "gtavcmta", "name": "Grand Theft Auto: Vice City - Multi Theft Auto" },
{ "id": "groundbreach", "name": "Ground Breach", "isValve": true },
{ "id": "gunmanchronicles", "name": "Gunman Chronicles", "isValve": true },
{ "id": "hl2d", "name": "Half-Life 2: Deathmatch", "isValve": true },
{ "id": "hld", "name": "Half-Life Deathmatch", "isValve": true },
{ "id": "hlds", "name": "Half-Life Deathmatch: Source", "isValve": true },
{ "id": "hlof", "name": "Half-Life: Opposing Force", "isValve": true },
{ "id": "halo", "name": "Halo" },
{ "id": "halo2", "name": "Halo 2" },
{ "id": "eldewrito", "name": "Halo Online - ElDewrito" },
{ "id": "hawakening", "name": "Hawakening" },
{ "id": "hll", "name": "Hell Let Loose", "isValve": true },
{ "id": "heretic2", "name": "Heretic II" },
{ "id": "hexen2", "name": "Hexen II" },
{ "id": "hiddendangerous2", "name": "Hidden & Dangerous 2" },
{ "id": "homefront", "name": "Homefront", "isValve": true },
{ "id": "homeworld2", "name": "Homeworld 2" },
{ "id": "hurtworld", "name": "Hurtworld", "isValve": true },
{ "id": "i2cs", "name": "IGI 2: Covert Strike" },
{ "id": "i2s", "name": "IL-2 Sturmovik" },
{ "id": "icarus", "name": "Icarus", "isValve": true },
{ "id": "insurgency", "name": "Insurgency", "isValve": true },
{ "id": "imic", "name": "Insurgency: Modern Infantry Combat", "isValve": true },
{ "id": "insurgencysandstorm", "name": "Insurgency: Sandstorm", "isValve": true },
{ "id": "ironstorm", "name": "Iron Storm" },
{ "id": "jb0n", "name": "James Bond 007: Nightfire" },
{ "id": "jc2m", "name": "Just Cause 2 - Multiplayer" },
{ "id": "jc3m", "name": "Just Cause 3 - Multiplayer", "isValve": true },
{ "id": "kspd", "name": "Kerbal Space Program - DMP" },
{ "id": "killingfloor", "name": "Killing Floor" },
{ "id": "killingfloor2", "name": "Killing Floor 2", "isValve": true },
{ "id": "kloc", "name": "Kingpin: Life of Crime" },
{ "id": "kpctnc", "name": "Kiss: Psycho Circus: The Nightmare Child" },
{ "id": "kreedzclimbing", "name": "Kreedz Climbing", "isValve": true },
{ "id": "l4d", "name": "Left 4 Dead", "isValve": true },
{ "id": "l4d2", "name": "Left 4 Dead 2", "isValve": true },
{ "id": "m2m", "name": "Mafia II - Multiplayer" },
{ "id": "m2o", "name": "Mafia II - Online" },
{ "id": "moh", "name": "Medal of Honor" },
{ "id": "moha", "name": "Medal of Honor: Airborne" },
{ "id": "mohaa", "name": "Medal of Honor: Allied Assault" },
{ "id": "mohaab", "name": "Medal of Honor: Allied Assault Breakthrough" },
{ "id": "mohaas", "name": "Medal of Honor: Allied Assault Spearhead" },
{ "id": "mohpa", "name": "Medal of Honor: Pacific Assault" },
{ "id": "mohw", "name": "Medal of Honor: Warfighter" },
{ "id": "medievalengineers", "name": "Medieval Engineers", "isValve": true },
{ "id": "minecraft", "name": "Minecraft" },
{ "id": "mbe", "name": "Minecraft: Bedrock Edition" },
{ "id": "minetest", "name": "Minetest" },
{ "id": "mnc", "name": "Monday Night Combat", "isValve": true },
{ "id": "mordhau", "name": "Mordhau", "isValve": true },
{ "id": "mumble", "name": "Mumble" },
{ "id": "mgm", "name": "Mumble - GT Murmur" },
{ "id": "mutantfactions", "name": "Mutant Factions" },
{ "id": "moe", "name": "Myth of Empires", "isValve": true },
{ "id": "nascarthunder2004", "name": "NASCAR Thunder 2004" },
{ "id": "naturalselection", "name": "Natural Selection", "isValve": true },
{ "id": "naturalselection2", "name": "Natural Selection 2", "isValve": true },
{ "id": "nfshp2", "name": "Need for Speed: Hot Pursuit 2" },
{ "id": "nab", "name": "Nerf Arena Blast" },
{ "id": "neverwinternights", "name": "Neverwinter Nights" },
{ "id": "neverwinternights2", "name": "Neverwinter Nights 2" },
{ "id": "nexuiz", "name": "Nexuiz" },
{ "id": "nitrofamily", "name": "Nitro Family" },
{ "id": "nmrih", "name": "No More Room in Hell", "isValve": true },
{ "id": "nolf2asihw", "name": "No One Lives Forever 2: A Spy in H.A.R.M.'s Way" },
{ "id": "nla", "name": "Nova-Life: Amboise", "isValve": true },
{ "id": "nucleardawn", "name": "Nuclear Dawn", "isValve": true },
{ "id": "onset", "name": "Onset", "isValve": true },
{ "id": "openarena", "name": "OpenArena" },
{ "id": "openttd", "name": "OpenTTD" },
{ "id": "ofcwc", "name": "Operation Flashpoint: Cold War Crisis" },
{ "id": "ofr", "name": "Operation Flashpoint: Resistance" },
{ "id": "ohd", "name": "Operation: Harsh Doorstop", "isValve": true },
{ "id": "painkiller", "name": "Painkiller" },
{ "id": "palworld", "name": "Palworld" },
{ "id": "pvak2", "name": "Pirates, Vikings, and Knights II", "isValve": true },
{ "id": "pixark", "name": "PixARK", "isValve": true },
{ "id": "postscriptum", "name": "Post Scriptum", "isValve": true },
{ "id": "postal2", "name": "Postal 2" },
{ "id": "prey", "name": "Prey" },
{ "id": "pce", "name": "Primal Carnage: Extinction", "isValve": true },
{ "id": "projectcars", "name": "Project Cars", "isValve": true },
{ "id": "projectcars2", "name": "Project Cars 2", "isValve": true },
{ "id": "prb2", "name": "Project Reality: Battlefield 2" },
{ "id": "projectzomboid", "name": "Project Zomboid", "isValve": true },
{ "id": "quake", "name": "Quake" },
{ "id": "quake2", "name": "Quake 2" },
{ "id": "q3a", "name": "Quake 3: Arena" },
{ "id": "quake4", "name": "Quake 4" },
{ "id": "quakelive", "name": "Quake Live", "isValve": true },
{ "id": "rdkf", "name": "Rag Doll Kung Fu", "isValve": true },
{ "id": "rainbowsix", "name": "Rainbow Six" },
{ "id": "rs2rs", "name": "Rainbow Six 2: Rogue Spear" },
{ "id": "rs3rs", "name": "Rainbow Six 3: Raven Shield" },
{ "id": "rallisportchallenge", "name": "RalliSport Challenge" },
{ "id": "rallymasters", "name": "Rally Masters" },
{ "id": "rdr2r", "name": "Red Dead Redemption 2 - RedM" },
{ "id": "redorchestra", "name": "Red Orchestra" },
{ "id": "redorchestra2", "name": "Red Orchestra 2", "isValve": true },
{ "id": "roo4145", "name": "Red Orchestra: Ostfront 41-45" },
{ "id": "redline", "name": "Redline" },
{ "id": "renegade10", "name": "Renegade X" },
{ "id": "renown", "name": "Renown" },
{ "id": "rtcw", "name": "Return to Castle Wolfenstein" },
{ "id": "ricochet", "name": "Ricochet", "isValve": true },
{ "id": "ron", "name": "Rise of Nations" },
{ "id": "rs2v", "name": "Rising Storm 2: Vietnam", "isValve": true },
{ "id": "risingworld", "name": "Rising World", "isValve": true },
{ "id": "ror2", "name": "Risk of Rain 2", "isValve": true },
{ "id": "rune", "name": "Rune" },
{ "id": "rust", "name": "Rust", "isValve": true },
{ "id": "stalker", "name": "S.T.A.L.K.E.R." },
{ "id": "swat4", "name": "SWAT 4" },
{ "id": "satisfactory", "name": "Satisfactory" },
{ "id": "s2ats", "name": "Savage 2: A Tortured Soul" },
{ "id": "serioussam", "name": "Serious Sam" },
{ "id": "serioussam2", "name": "Serious Sam 2" },
{ "id": "sstse", "name": "Serious Sam: The Second Encounter" },
{ "id": "shatteredhorizon", "name": "Shattered Horizon", "isValve": true },
{ "id": "shogo", "name": "Shogo" },
{ "id": "shootmania", "name": "Shootmania" },
{ "id": "sin", "name": "SiN" },
{ "id": "sinepisodes", "name": "SiN Episodes", "isValve": true },
{ "id": "soldat", "name": "Soldat" },
{ "id": "sof", "name": "Soldier of Fortune" },
{ "id": "sof2", "name": "Soldier of Fortune 2" },
{ "id": "sotf", "name": "Sons Of The Forest", "isValve": true },
{ "id": "soulmask", "name": "Soulmask", "isValve": true },
{ "id": "spaceengineers", "name": "Space Engineers", "isValve": true },
{ "id": "squad", "name": "Squad", "isValve": true },
{ "id": "stbc", "name": "Star Trek: Bridge Commander" },
{ "id": "stvef", "name": "Star Trek: Voyager - Elite Force" },
{ "id": "stvef2", "name": "Star Trek: Voyager - Elite Force 2" },
{ "id": "swjk2jo", "name": "Star Wars Jedi Knight II: Jedi Outcast" },
{ "id": "swjkja", "name": "Star Wars Jedi Knight: Jedi Academy" },
{ "id": "swb", "name": "Star Wars: Battlefront" },
{ "id": "swb2", "name": "Star Wars: Battlefront 2" },
{ "id": "swrc", "name": "Star Wars: Republic Commando" },
{ "id": "starmade", "name": "StarMade" },
{ "id": "starbound", "name": "Starbound", "isValve": true },
{ "id": "starsiege", "name": "Starsiege" },
{ "id": "suicidesurvival", "name": "Suicide Survival", "isValve": true },
{ "id": "stn", "name": "Survive the Nights", "isValve": true },
{ "id": "svencoop", "name": "Sven Coop", "isValve": true },
{ "id": "synergy", "name": "Synergy", "isValve": true },
{ "id": "toxikk", "name": "TOXIKK" },
{ "id": "tacticalops", "name": "Tactical Ops" },
{ "id": "toh", "name": "Take On Helicopters" },
{ "id": "teamfactor", "name": "Team Factor" },
{ "id": "teamfortress2", "name": "Team Fortress 2", "isValve": true },
{ "id": "tfc", "name": "Team Fortress Classic", "isValve": true },
{ "id": "teamspeak2", "name": "Teamspeak 2" },
{ "id": "teamspeak3", "name": "Teamspeak 3" },
{ "id": "terminus", "name": "Terminus" },
{ "id": "terrariatshock", "name": "Terraria - TShock" },
{ "id": "theforest", "name": "The Forest", "isValve": true },
{ "id": "thefront", "name": "The Front", "isValve": true },
{ "id": "thehidden", "name": "The Hidden", "isValve": true },
{ "id": "theisle", "name": "The Isle", "isValve": true },
{ "id": "tie", "name": "The Isle Evrima" },
{ "id": "tonolf", "name": "The Operative: No One Lives Forever" },
{ "id": "theship", "name": "The Ship", "isValve": true },
{ "id": "thespecialists", "name": "The Specialists", "isValve": true },
{ "id": "tcgraw", "name": "Tom Clancy's Ghost Recon Advanced Warfighter" },
{ "id": "tcgraw2", "name": "Tom Clancy's Ghost Recon Advanced Warfighter 2" },
{ "id": "thps3", "name": "Tony Hawk's Pro Skater 3" },
{ "id": "thps4", "name": "Tony Hawk's Pro Skater 4" },
{ "id": "thu2", "name": "Tony Hawk's Underground 2" },
{ "id": "towerunite", "name": "Tower Unite", "isValve": true },
{ "id": "trackmania2", "name": "Trackmania 2" },
{ "id": "trackmaniaforever", "name": "Trackmania Forever" },
{ "id": "tremulous", "name": "Tremulous" },
{ "id": "t1s", "name": "Tribes 1: Starsiege" },
{ "id": "tribesvengeance", "name": "Tribes: Vengeance" },
{ "id": "tron20", "name": "Tron 2.0" },
{ "id": "turok2", "name": "Turok 2" },
{ "id": "universalcombat", "name": "Universal Combat" },
{ "id": "unreal", "name": "Unreal" },
{ "id": "u2tax", "name": "Unreal 2: The Awakening - XMP" },
{ "id": "unrealtournament", "name": "Unreal Tournament" },
{ "id": "unrealtournament2003", "name": "Unreal Tournament 2003" },
{ "id": "unrealtournament2004", "name": "Unreal Tournament 2004" },
{ "id": "unrealtournament3", "name": "Unreal Tournament 3" },
{ "id": "urbanterror", "name": "Urban Terror" },
{ "id": "vrising", "name": "V Rising", "isValve": true },
{ "id": "v8sc", "name": "V8 Supercar Challenge" },
{ "id": "valheim", "name": "Valheim", "isValve": true },
{ "id": "vampireslayer", "name": "Vampire Slayer", "isValve": true },
{ "id": "ventrilo", "name": "Ventrilo" },
{ "id": "vcm", "name": "Vice City Multiplayer" },
{ "id": "vietcong", "name": "Vietcong" },
{ "id": "vietcong2", "name": "Vietcong 2" },
{ "id": "vintagestory", "name": "Vintage Story" },
{ "id": "warfork", "name": "Warfork" },
{ "id": "warsow", "name": "Warsow" },
{ "id": "wot", "name": "Wheel of Time" },
{ "id": "wolfenstein", "name": "Wolfenstein" },
{ "id": "wet", "name": "Wolfenstein: Enemy Territory" },
{ "id": "wop", "name": "World Of Padman" },
{ "id": "wurmunlimited", "name": "Wurm Unlimited", "isValve": true },
{ "id": "xonotic", "name": "Xonotic" },
{ "id": "xpandrally", "name": "Xpand Rally" },
{ "id": "zombiemaster", "name": "Zombie Master", "isValve": true },
{ "id": "zps", "name": "Zombie Panic: Source", "isValve": true },
{ "id": "enshrouded", "name": "enshrouded", "isValve": true },
{ "id": "netpanzer", "name": "netPanzer" },
{ "id": "rfactor", "name": "rFactor" },
{ "id": "rfactor2", "name": "rFactor 2", "isValve": true },
{ "id": "unturned", "name": "unturned", "isValve": true }
]
+10 -1
View File
@@ -66,7 +66,7 @@ export const WebhookJSONTemplate = JSON.stringify(
actions: [
{
text: "{{action_text}}",
url: "{{action_url}}",
url: "{{{action_url}}}",
},
],
},
@@ -185,6 +185,15 @@ export const DefaultTCPEval = `(async function (arrayOfPings) {
}
})`;
export const DefaultGamedigEval = `(async function (responseTime, responseRaw) {
return {
status: 'UP',
latency: responseTime,
}
})`;
export const GAMEDIG_TIMEOUT = 10 * 1000; // 10 seconds
export const GAMEDIG_SOCKET_TIMEOUT = 2 * 1000; // 2 seconds
export const ErrorSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="60" viewBox="0 0 120 60" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="30" cy="24" r="10"/>
<path d="M26 27h8"/>
+14 -10
View File
@@ -1,13 +1,17 @@
// @ts-ignore
export const analyticsEvent = (event, data) => {
// Do something with the event and data
document.dispatchEvent(
new CustomEvent("analyticsEvent", {
bubbles: true,
detail: {
event,
data
}
})
);
// Do something with the event and data
if (typeof document !== "undefined") {
event = "kener_" + event;
document.dispatchEvent(
new CustomEvent("analyticsEvent", {
bubbles: true,
detail: {
event,
data,
},
}),
);
}
};
+15
View File
@@ -258,6 +258,10 @@ function isValidRange(value, min, max) {
const num = Number(value);
return !isNaN(num) && num >= min && num <= max;
}
function IsValidPort(port) {
return isValidRange(port, 1, 65535);
}
function SortMonitor(monitorSort, resp) {
let monitors = [];
if (!!monitorSort && monitorSort.length > 0) {
@@ -277,6 +281,15 @@ function RandomString(length) {
return result;
}
/**
* Retreive game's data from its id.
* @param {string} id Id
* @return Returns game's data if found, undefined instead.
*/
function GetGameFromId(list, id) {
return list.find((game) => game.id === id);
}
export {
siteDataExtractFromDb,
storeSiteData,
@@ -285,7 +298,9 @@ export {
IsValidHost,
IsValidNameServer,
IsValidURL,
IsValidPort,
ValidateCronExpression,
SortMonitor,
RandomString,
GetGameFromId,
};
+1
View File
@@ -7,6 +7,7 @@ async function StatusColor(status) {
UP: site.colors?.UP || "#00dfa2",
DEGRADED: site.colors?.DEGRADED || "#e6ca61",
DOWN: site.colors?.DOWN || "#ca3038",
MAINTENANCE: site.colors?.MAINTENANCE || "#6679cc",
NO_DATA: "#f1f5f8"
};
}
+74 -28
View File
@@ -2,6 +2,9 @@
import { formatDistanceToNow, formatDistance } from "date-fns";
import Settings from "lucide-svelte/icons/settings";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import Copy from "lucide-svelte/icons/copy";
import Check from "lucide-svelte/icons/check";
import { analyticsEvent } from "$lib/boringOne";
import * as Accordion from "$lib/components/ui/accordion";
import { l, f, fd, fdn } from "$lib/i18n/client";
import { base } from "$app/paths";
@@ -9,9 +12,12 @@
import { Tooltip } from "bits-ui";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { marked } from "marked";
import { onMount } from "svelte";
export let incident;
export let index;
export let lang;
export let allowCollapse = true;
let startTime = new Date(incident.start_date_time * 1000);
let endTime = new Date();
let nowTime = new Date();
@@ -33,7 +39,7 @@
}
let accordionValue = "incident-0";
if ($page.data.site.incidentGroupView == "COLLAPSED") {
if ($page.data.site.incidentGroupView == "COLLAPSED" && allowCollapse) {
accordionValue = "incident-collapse";
} else if ($page.data.site.incidentGroupView == "EXPANDED") {
accordionValue = index;
@@ -73,34 +79,66 @@
maintenanceBadge = "Maintenance Completed";
maintenanceBadgeColor = "text-maintenance-completed";
}
let pathMonitorLink = "";
onMount(async () => {
let protocol = window.location.protocol;
let domain = window.location.host;
pathMonitorLink = `${protocol}//${domain}${base}/view/events/${incident.incident_type}-${incident.id}`;
});
</script>
<div class="newincident relative grid w-full grid-cols-12 gap-2 px-0 py-0 last:border-b-0">
<div class="col-span-12">
<Accordion.Root bind:value={index} class="accor">
<Accordion.Root
bind:value={index}
class="accor {allowCollapse === false ? 'hide-chevron' : ''}"
disabled={!allowCollapse}
>
<Accordion.Item value={accordionValue}>
<Accordion.Trigger class="rounded-md px-4 hover:bg-muted hover:no-underline">
<Accordion.Trigger
class="rounded-md px-4 hover:bg-muted hover:no-underline"
on:click={() => analyticsEvent("incident_open", { incident_title: incident.title })}
>
<div class="w-full text-left hover:no-underline">
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
<span class="badge-{incident.state}">
{l($page.data.lang, incident.state)}
</span>
{:else if incidentType == "MAINTENANCE"}
<span class="{maintenanceBadgeColor} ">
{l($page.data.lang, maintenanceBadge)}
</span>
{/if}
{#if $page.data.isLoggedIn}
<Button
href="{base}/manage/app/events#{incident.id}"
class=" rotate-once h-5 p-0 text-muted-foreground hover:text-primary"
variant="link"
>
<Settings class="h-4 w-4 " />
</Button>
{/if}
</p>
<div class="flex justify-start gap-x-2">
<p class="flex gap-x-2 text-xs font-semibold">
{#if incidentType == "INCIDENT"}
<span class="badge-{incident.state}">
{l($page.data.lang, incident.state)}
</span>
{:else if incidentType == "MAINTENANCE"}
<span class="{maintenanceBadgeColor} ">
{l($page.data.lang, maintenanceBadge)}
</span>
{/if}
</p>
<div class="flex justify-end gap-x-2">
{#if $page.data.isLoggedIn}
<Button
href="{base}/manage/app/events#{incident.id}"
class="rotate-once h-5 p-0 text-muted-foreground hover:text-primary"
variant="link"
>
<Settings class="h-4 w-4 " />
</Button>
{/if}
{#if !!pathMonitorLink}
<Button
size="icon"
variant="link"
class="copybtn relative h-5 p-0 text-muted-foreground hover:text-primary"
on:click={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(pathMonitorLink);
analyticsEvent("incident_copy_link", { incident_title: incident.title });
}}
>
<Check class="check-btn absolute left-0 top-0 h-4 w-4 text-green-500" />
<Copy class="copy-btn absolute left-0 top-0 h-4 w-4 " />
</Button>
{/if}
</div>
</div>
<p class="font-medium">
{incident.title}
@@ -185,7 +223,11 @@
</time>
<div class="mb-4 text-sm font-normal">
{@html comment.comment}
<div
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
{@html marked.parse(comment.comment)}
</div>
</div>
</li>
{/each}
@@ -203,9 +245,13 @@
)}
</time>
<p class="mb-2 text-sm font-normal">
{comment.comment}
</p>
<div class="mb-2 text-sm font-normal">
<div
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
{@html marked.parse(comment.comment)}
</div>
</div>
</li>
{/each}
</ol>
+353 -56
View File
@@ -8,14 +8,22 @@
import * as Select from "$lib/components/ui/select";
import locales from "$lib/locales/locales.json?raw";
import GMI from "$lib/components/gmi.svelte";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import Loader from "lucide-svelte/icons/loader";
import LaptopMinimal from "lucide-svelte/icons/laptop-minimal";
import SmartPhone from "lucide-svelte/icons/smartphone";
import X from "lucide-svelte/icons/x";
import Plus from "lucide-svelte/icons/plus";
import Info from "lucide-svelte/icons/info";
import Play from "lucide-svelte/icons/play";
import { Tooltip } from "bits-ui";
import CodeMirror from "svelte-codemirror-editor";
import { html } from "@codemirror/lang-html";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
export let data;
let formStateHero = "idle";
@@ -31,9 +39,31 @@
};
let footerHTML = "";
let tzToggle = "NO";
let showSiteStatus = "NO";
let homeIncidentCount = 10;
let homeIncidentStartTimeWithin = 30;
let incidentGroupView = "EXPAND_FIRST";
let kenerTheme = "default";
let homeDataMaxDays = {
desktop: {
maxDays: 90,
selectableDays: [1, 7, 14, 30, 60, 90]
},
mobile: {
maxDays: 90,
selectableDays: [1, 7, 14, 30, 60, 90]
}
};
let selectableDaysList = [1, 7, 14, 30, 60, 90, 120, 150, 180];
let availableThemes = [
{ name: "Default", value: "default" },
{ name: "Mono", value: "mono" },
{ name: "Sunset", value: "sunset" },
{ name: "Forest", value: "forest" },
{ name: "Ocean", value: "ocean" }
];
let nav = [];
let categories = [];
@@ -74,9 +104,19 @@
if (data.siteData.footerHTML) {
footerHTML = data.siteData.footerHTML;
}
if (data.siteData.kenerTheme) {
kenerTheme = data.siteData.kenerTheme;
}
if (data.siteData.homeDataMaxDays) {
homeDataMaxDays = data.siteData.homeDataMaxDays;
}
if (data.siteData.tzToggle) {
tzToggle = data.siteData.tzToggle;
}
if (data.siteData.showSiteStatus) {
showSiteStatus = data.siteData.showSiteStatus;
}
if (data.siteData.i18n) {
i18n = data.siteData.i18n;
//add locales that are not present in the data
@@ -98,13 +138,26 @@
let resp = await storeSiteData({
hero: JSON.stringify(hero)
});
//print data
let data = await resp.json();
formStateHero = "idle";
if (data.error) {
heroError = data.error;
return;
}
saveTheme();
}
async function saveTheme() {
let resp = await storeSiteData({
kenerTheme: kenerTheme,
showSiteStatus: showSiteStatus
});
//print data
let data = await resp.json();
if (data.error) {
alert(data.error);
return;
}
}
let navErrorMessage = "";
@@ -222,7 +275,8 @@
...categories,
{
name: "",
description: ""
description: "",
isHidden: false
}
];
}
@@ -250,6 +304,23 @@
return;
}
}
let dataErrorMessage = "";
let formStateData = "idle";
async function formSubmitData() {
dataErrorMessage = "";
formStateData = "loading";
let resp = await storeSiteData({
homeDataMaxDays: JSON.stringify(homeDataMaxDays)
});
//print data
let data = await resp.json();
formStateData = "idle";
if (data.error) {
dataErrorMessage = data.error;
return;
}
}
</script>
<Card.Root class="mt-4">
@@ -258,44 +329,16 @@
<Card.Description>Configure the hero section of your site.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitHero}>
<form class="mx-auto mt-4 flex flex-col gap-y-4" on:submit|preventDefault={formSubmitHero}>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="hero_title">
Title
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
You can set the title of the hero section here. This will be displayed on the top of the page.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Label for="hero_title">Title</Label>
<Input bind:value={hero.title} class="mt-2" type="text" id="hero_title" placeholder="Status Page for Kener" />
</div>
</div>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<Label for="hero_subtitle">
Subtitle
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
You can set the subtitle of the hero section here. This will be displayed below the title.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Label for="hero_subtitle">Subtitle</Label>
<Input
bind:value={hero.subtitle}
class="mt-2"
@@ -305,12 +348,63 @@
/>
</div>
</div>
<div class="flex flex-col gap-y-2">
<div>
<Label>Color</Label>
</div>
<div class="flex flex-row gap-x-2 rounded-md border p-2">
<div class="w-3/4 pt-2">
<span class="kener-theme-{kenerTheme}-hero-h1 font-medium">{hero.title}</span>
</div>
<div class="flex w-1/4 justify-end">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline">
{availableThemes.find((el) => el.value === kenerTheme)?.name || "Mono"}
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{#each availableThemes as theme}
<DropdownMenu.Group>
<DropdownMenu.Item>
<Button
variant="ghost"
class="h-6 w-full justify-start text-xs"
on:click={() => {
kenerTheme = theme.value;
}}
>
{theme.name}
</Button>
</DropdownMenu.Item>
</DropdownMenu.Group>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
</div>
<div>
<p class=" font-medium">
<label>
<input
on:change={(e) => {
showSiteStatus = e.target.checked === true ? "YES" : "NO";
}}
type="checkbox"
checked={showSiteStatus === "YES"}
/>
Show Site Status Banner
</label>
</p>
</div>
<div class="flex w-full justify-end gap-x-2">
{#if !!heroError}
<div class="py-2 text-sm font-medium text-destructive">{heroError}</div>
{/if}
<Button type="submit" disabled={formStateHero === "loading"}>
Save
Save Hero Section
{#if formStateHero === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -319,6 +413,173 @@
</form>
</Card.Content>
</Card.Root>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Data</Card.Title>
<Card.Description>
Configure the data shown on the homepage. This includes the time range for incidents.
</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 flex flex-col space-y-4" on:submit|preventDefault={formSubmitData}>
<div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-y-2">
<Label class="text-lg">
<LaptopMinimal class="mr-2 inline h-4 w-4" />
Desktop
</Label>
<div class="border-b pb-4">
<Label for="desktopmaxdata" class="text-sm font-medium">Select Data Range</Label>
<Select.Root
portal={null}
onSelectedChange={(e) => {
homeDataMaxDays.desktop.maxDays = e.value;
//uncheck all checkboxes greater than maxDays
homeDataMaxDays.desktop.selectableDays = homeDataMaxDays.desktop.selectableDays.filter(
(day) => day <= e.value
);
//push all days less than maxDays
selectableDaysList.forEach((day) => {
if (day <= e.value && !homeDataMaxDays.desktop.selectableDays.includes(day)) {
homeDataMaxDays.desktop.selectableDays.push(day);
}
});
}}
selected={{
value: homeDataMaxDays.desktop.maxDays,
label: homeDataMaxDays.desktop.maxDays + " Days"
}}
>
<Select.Trigger class=" mt-2 w-[200px]" id="desktopmaxdata">
<Select.Value bind:value={homeDataMaxDays.desktop.maxDays} placeholder="Select Range" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Max Days</Select.Label>
{#each selectableDaysList as day}
<Select.Item value={day} label={day + " Days"} class="text-sm font-medium">
{day} Days
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="border-b pb-4">
<Label for="desktopdropdown" class="text-sm font-medium">Select Dropdown Range</Label>
<div class="mt-2 flex flex-row flex-wrap gap-x-2">
{#each selectableDaysList as day}
<label>
<input
on:change={(e) => {
if (e.target.checked) {
homeDataMaxDays.desktop.selectableDays.push(day);
} else {
homeDataMaxDays.desktop.selectableDays = homeDataMaxDays.desktop.selectableDays.filter(
(d) => d !== day
);
}
}}
type="checkbox"
disabled={day >= homeDataMaxDays.desktop.maxDays}
checked={homeDataMaxDays.desktop.selectableDays.includes(day)}
/>
{day} Days
</label>
{/each}
</div>
</div>
</div>
<div class="flex flex-col gap-y-2">
<Label class="text-lg">
<SmartPhone class="mr-2 inline h-4 w-4" />
Mobile
</Label>
<div class="border-b pb-4">
<Label for="mobilemaxdata" class="text-sm font-medium">Select Data Range</Label>
<Select.Root
portal={null}
onSelectedChange={(e) => {
homeDataMaxDays.mobile.maxDays = e.value;
//uncheck all checkboxes greater than maxDays
homeDataMaxDays.mobile.selectableDays = homeDataMaxDays.mobile.selectableDays.filter(
(day) => day <= e.value
);
//push all days less than maxDays
selectableDaysList.forEach((day) => {
if (day <= e.value && !homeDataMaxDays.mobile.selectableDays.includes(day)) {
homeDataMaxDays.mobile.selectableDays.push(day);
}
});
}}
selected={{
value: homeDataMaxDays.mobile.maxDays,
label: homeDataMaxDays.mobile.maxDays + " Days"
}}
>
<Select.Trigger class=" mt-2 w-[200px]" id="mobilemaxdata">
<Select.Value bind:value={homeDataMaxDays.mobile.maxDays} placeholder="Select Range" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Max Days</Select.Label>
{#each selectableDaysList as day}
<Select.Item value={day} label={day + " Days"} class="text-sm font-medium">
{day} Days
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="border-b pb-4">
<Label for="mobiledropdown" class="text-sm font-medium">Select Dropdown Range</Label>
<div class="mt-2 flex flex-row flex-wrap gap-x-2">
{#each selectableDaysList as day}
<label>
<input
on:change={(e) => {
if (e.target.checked) {
homeDataMaxDays.mobile.selectableDays.push(day);
} else {
homeDataMaxDays.mobile.selectableDays = homeDataMaxDays.mobile.selectableDays.filter(
(d) => d !== day
);
}
}}
type="checkbox"
disabled={day >= homeDataMaxDays.mobile.maxDays}
checked={homeDataMaxDays.mobile.selectableDays.includes(day)}
/>
{day} Days
</label>
{/each}
</div>
</div>
</div>
</div>
<p>
<span class="text-xs font-medium text-muted-foreground">
Select the maximum number of days to show data for. This will be used to filter the data shown on the
homepage. The dropdown will show all the days that are less than or equal to the selected max days. The max
value in the dropdown will be the entire value of data range it cannot be changed
</span>
</p>
{#if !!dataErrorMessage}
<p class="text-sm font-medium text-destructive">{dataErrorMessage}</p>
{/if}
<div class="flex w-full justify-end gap-x-2">
<Button type="submit" disabled={formStateData === "loading"}>
Save Data Settings
{#if formStateData === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</form>
</Card.Content>
</Card.Root>
<Card.Root class="mt-4">
<Card.Header class="border-b">
<Card.Title>Incidents</Card.Title>
@@ -370,7 +631,7 @@
</div>
<div class="pt-8">
<Button type="submit" disabled={formStateIncident === "loading"}>
Save
Save Incidents Settings
{#if formStateIncident === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -495,7 +756,7 @@
<span class="text-sm font-medium text-destructive"> {navErrorMessage} </span>
</p>
<Button type="submit" disabled={formStateHero === "loading"}>
Save
Save Navigation
{#if formStateHero === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -574,7 +835,7 @@
<div class="py-2 text-sm font-medium text-destructive">{i18nErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStatei18n === "loading"}>
Save
Save Languages
{#if formStatei18n === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -592,8 +853,8 @@
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitCategories}>
{#each categories as cat, i}
<div class="grid grid-cols-4 gap-2">
<div class="col-span-1">
<div class="flex items-center gap-4">
<div class="flex-1">
<Label for="{i}catkey" class="block w-full">Name</Label>
<Input
bind:value={cat.name}
@@ -604,7 +865,7 @@
disabled={i === 0}
/>
</div>
<div class="relative col-span-3 pr-8">
<div class="flex-1">
<Label for="{i}catdsc" class="block w-full">Description</Label>
<Input
bind:value={cat.description}
@@ -614,9 +875,27 @@
placeholder="Category Description"
disabled={i === 0}
/>
</div>
<div class="relative flex flex-col items-center gap-y-2">
<Label for="{i}vis" class="block w-full">Visibility</Label>
<DropdownMenu.Root>
<DropdownMenu.Trigger disabled={i === 0} id="{i}vis">
<Button variant="outline" class="mr-2" disabled={cat.name === "Home"}>
{cat.isHidden ? "Private" : "Public"}
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Item on:click={() => (cat.isHidden = true)}>Private</DropdownMenu.Item>
<DropdownMenu.Item on:click={() => (cat.isHidden = false)}>Public</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<div class="flex items-center">
<Button
variant="ghost"
class="absolute right-0 top-8 ml-2 h-6 w-6 p-1 "
class="mt-4 h-6 w-6 p-1"
disabled={i === 0}
on:click={() => (categories = categories.filter((_, index) => index !== i))}
>
@@ -637,7 +916,7 @@
<div class="py-2 text-sm font-medium text-destructive">{categoriesErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStateCategories === "loading"}>
Save
Save Categories
{#if formStateCategories === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
@@ -656,24 +935,42 @@
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitFooter}>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="w-full">
<textarea
bind:value={footerHTML}
class="mt-2 h-48 w-full rounded-sm border p-2"
placeholder="eg. <p>Powered by Kener</p>"
></textarea>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={footerHTML}
lang={html()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "320px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "0.375rem"
}
}}
/>
</div>
</div>
</div>
<div class="flex w-full justify-end gap-x-2">
{#if !!footerErrorMessage}
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
{/if}
<Button type="submit" disabled={formStateFooter === "loading"}>
Save
{#if formStateFooter === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
<div class="flex w-full justify-between gap-x-2">
<div>
<Button variant="outline" class="mt-2" on:click={() => (footerHTML = data.defaultFooterHTML)}>
<span class="ml-2">Reset to default</span>
</Button>
</div>
<div class="flex flex-row justify-end gap-x-2">
{#if !!footerErrorMessage}
<div class="py-2 text-sm font-medium text-destructive">{footerErrorMessage}</div>
{/if}
</Button>
<Button type="submit" disabled={formStateFooter === "loading"}>
Save Custom Footer
{#if formStateFooter === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
</form>
</Card.Content>
+234 -10
View File
@@ -19,16 +19,20 @@
import { javascript } from "@codemirror/lang-javascript";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
import AllGamesListRaw from "$lib/all-games-list.json?raw";
import {
allRecordTypes,
ValidateIpAddress,
ValidateCronExpression,
IsValidHost,
IsValidNameServer
IsValidNameServer,
GetGameFromId,
IsValidPort
} from "$lib/clientTools.js";
import { GAMEDIG_SOCKET_TIMEOUT } from "$lib/anywhere";
const dispatch = createEventDispatcher();
let AllGamesList = JSON.parse(AllGamesListRaw);
const defaultEval = `(async function (statusCode, responseTime, responseData) {
let statusCodeShort = Math.floor(statusCode/100);
if(statusCode == 429 || (statusCodeShort >=2 && statusCodeShort <= 3)) {
@@ -188,6 +192,12 @@
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
return;
}
//validating timeout for ping
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
invalidFormMessage = "Timeout should be a number";
return;
}
hosts[i].timeout = Number(hosts[i].timeout);
//validating timeout
if (hosts[i].timeout < 1) {
invalidFormMessage = "Timeout should be greater than 0";
@@ -222,13 +232,19 @@
invalidFormMessage = `Host ${hosts[i].host} is not of type ${hosts[i].type}`;
return;
}
//validating timeout for tcp
if (!!hosts[i].timeout && isNaN(hosts[i].timeout)) {
invalidFormMessage = "Timeout should be a number";
return;
}
hosts[i].timeout = Number(hosts[i].timeout);
//validating timeout
if (hosts[i].timeout < 1) {
invalidFormMessage = "Timeout should be greater than 0";
return;
}
//validating port
if (hosts[i].port < 1 || hosts[i].port > 65535) {
if (!IsValidPort(hosts[i].port)) {
invalidFormMessage = "Port should be valid";
return;
}
@@ -302,12 +318,7 @@
return;
}
//validating port
if (
!!!newMonitor.sslConfig.port ||
isNaN(newMonitor.sslConfig.port) ||
newMonitor.sslConfig.port < 1 ||
newMonitor.sslConfig.port > 65535
) {
if (!IsValidPort(newMonitor.sslConfig.port)) {
invalidFormMessage = "Port should be valid";
return;
}
@@ -390,6 +401,54 @@
}
newMonitor.type_data = JSON.stringify(newMonitor.heartbeatConfig);
} else if (newMonitor.monitor_type === "GAMEDIG") {
// validating host
if (!newMonitor.gamedigConfig.host) {
invalidFormMessage = "Host is required";
return;
}
if (ValidateIpAddress(newMonitor.gamedigConfig.host) === "Invalid") {
invalidFormMessage = "Invalid host";
return;
}
// validating port
if (!IsValidPort(newMonitor.gamedigConfig.port)) {
invalidFormMessage = "Port should be valid";
return;
}
// validating game
const game = GetGameFromId(AllGamesList, newMonitor.gamedigConfig.gameId);
if (!game) {
invalidFormMessage = "Invalid game/service";
return;
}
if (!game.isValve && newMonitor.gamedigConfig.requestRules) {
newMonitor.gamedigConfig.requestRules = false;
}
//validating timeout
if (!!newMonitor.gamedigConfig.timeout && isNaN(newMonitor.gamedigConfig.timeout)) {
invalidFormMessage = "Timeout should be a number";
return;
}
newMonitor.gamedigConfig.timeout = Number(newMonitor.gamedigConfig.timeout);
if (newMonitor.gamedigConfig.timeout < GAMEDIG_SOCKET_TIMEOUT) {
invalidFormMessage = `Timeout should be greater than ${GAMEDIG_SOCKET_TIMEOUT}`;
return;
}
// validating eval
if (!!newMonitor.gamedigConfig.eval) {
newMonitor.gamedigConfig.eval = newMonitor.gamedigConfig.eval.trim();
if (!(await isValidEval(newMonitor.gamedigConfig.eval))) {
invalidFormMessage = invalidFormMessage + ". Invalid eval";
return;
}
}
newMonitor.type_data = JSON.stringify(newMonitor.gamedigConfig);
}
formState = "loading";
@@ -419,6 +478,35 @@
pg: "PostgreSQL",
mysql2: "MySQL"
};
let deleteMonitorConfirmText = "";
let deletingMonitor = false;
async function deleteMonitor() {
if (deleteMonitorConfirmText != `delete ${newMonitor.tag}`) {
invalidFormMessage = "Please type the correct text to delete the monitor";
return;
}
deletingMonitor = true;
try {
let data = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "deleteMonitor", data: { tag: newMonitor.tag } })
});
let resp = await data.json();
if (resp.error) {
invalidFormMessage = resp.error;
} else {
dispatch("closeModal", {});
}
} catch (error) {
invalidFormMessage = "Error while deleting monitor";
} finally {
deletingMonitor = false;
}
}
</script>
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
@@ -643,6 +731,7 @@
<Select.Item value="SSL" label="SSL" class="text-sm font-medium">SSL</Select.Item>
<Select.Item value="SQL" label="SQL" class="text-sm font-medium">SQL</Select.Item>
<Select.Item value="HEARTBEAT" label="HEARTBEAT" class="text-sm font-medium">HEARTBEAT</Select.Item>
<Select.Item value="GAMEDIG" label="GAMEDIG" class="text-sm font-medium">GAMEDIG</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
@@ -943,7 +1032,7 @@
];
}}
>
<Plus class="h-5 w-5" /> Add New
<Plus class="mr-1 h-5 w-5" /> Add New
</Button>
</div>
</div>
@@ -1263,6 +1352,141 @@
</span>
</div>
</div>
{:else if newMonitor.monitor_type == "GAMEDIG"}
<div class="mt-4 grid grid-cols-6 gap-2">
<div class="col-span-4">
<Label for="gameId">
Game <span class="text-red-500">*</span>
</Label>
<Select.Root portal={null} onSelectedChange={(e) => (newMonitor.gamedigConfig.gameId = e.value)}>
<Select.Trigger id="gameId">
<Select.Value placeholder={GetGameFromId(AllGamesList, newMonitor.gamedigConfig.gameId).name || ""} />
</Select.Trigger>
<Select.Content class="max-h-56 overflow-y-auto">
<Select.Group>
<Select.Label>Game</Select.Label>
{#each AllGamesList as game}
<Select.Item value={game.id} label={game.name} class="text-sm font-medium">{game.name}</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="col-span-2 col-start-1">
<Label for="host">
Host <span class="text-red-500">*</span>
</Label>
<Input bind:value={newMonitor.gamedigConfig.host} id="host" placeholder="172.12.14.42" />
</div>
<div class="col-span-2">
<Label for="port">
Port <span class="text-red-500">*</span>
</Label>
<Input bind:value={newMonitor.gamedigConfig.port} id="port" placeholder="port number ex 8080" />
</div>
<div class="col-span-2">
<Label for="timeout">
Timeout(ms)
<span class="text-red-500">*</span>
</Label>
<Input bind:value={newMonitor.gamedigConfig.timeout} id="timeout" />
</div>
<div class="col-span-6">
<label class="cursor-pointer">
<input
type="checkbox"
on:change={(e) => {
newMonitor.gamedigConfig.guessPort = e.target.checked;
}}
checked={newMonitor.gamedigConfig.guessPort}
/>
<span class="ml-2 text-sm">Guess port</span>
<p class="my-1 text-xs text-muted-foreground">
Used port can be different from client port, depending on queried game. Try this if you have
unsuccessful responses.
</p>
</label>
</div>
{#if newMonitor.gamedigConfig.gameId && GetGameFromId(AllGamesList, newMonitor.gamedigConfig.gameId).isValve}
<div class="col-span-6">
<label class="cursor-pointer">
<input
type="checkbox"
on:change={(e) => {
newMonitor.gamedigConfig.requestRules = e.target.checked;
}}
checked={newMonitor.gamedigConfig.requestRules}
/>
<span class="ml-2 text-sm">Request additional rules</span>
<p class="my-1 text-xs text-muted-foreground">
Valve games can provide additional 'rules' to Gamedig monitors. If checked, they will be available in
`reponseRaw.raw`, beware that it may increase query time.
</p>
</label>
</div>
{/if}
<div class="col-span-6 mt-2">
<Label for="eval">Eval</Label>
<p class="my-1 text-xs text-muted-foreground">
You can write a custom eval function to evaluate the response. The function should return a promise that
resolves to an object with status and latency. <a
target="_blank"
class="font-medium text-primary"
href="https://kener.ing/docs/monitors-gamedig#eval">Read the docs</a
> to learn
</p>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={newMonitor.gamedigConfig.eval}
lang={javascript()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "18rem",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "0.375rem"
}
}}
/>
</div>
</div>
</div>
{/if}
{#if !!newMonitor.id}
<div class="mt-4 flex">
<div class="flex w-full flex-col rounded-md border border-destructive p-2">
<div class="w-full">
<h2 class="flex justify-between text-lg">
<span class="text-destructive">Danger Zone</span>
</h2>
<p class="text-sm">Deleting a monitor is irreversible. Please be sure before deleting.</p>
</div>
<form class="flex gap-x-2" on:submit|preventDefault={deleteMonitor}>
<div class="mt-1 flex flex-col gap-y-2">
<Label for="deleteMonitor">
Type <i class="text-destructive">delete {newMonitor.tag}</i> to confirm
</Label>
<Input bind:value={deleteMonitorConfirmText} id="deleteMonitor" required />
</div>
<Button
variant="destructive"
type="submit"
class="mt-7"
disabled={deleteMonitorConfirmText != `delete ${newMonitor.tag}`}
>
Delete
{#if deletingMonitor}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</form>
</div>
</div>
{/if}
</div>
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 justify-end gap-2 border-t p-3 pr-6">
+218 -27
View File
@@ -5,6 +5,7 @@
import Settings from "lucide-svelte/icons/settings";
import Bell from "lucide-svelte/icons/bell";
import Loader from "lucide-svelte/icons/loader";
import DatabaseZap from "lucide-svelte/icons/database-zap";
import ArrowDownUp from "lucide-svelte/icons/arrow-down-up";
import Grip from "lucide-svelte/icons/grip";
import ExternalLink from "lucide-svelte/icons/external-link";
@@ -15,22 +16,32 @@
import { onMount } from "svelte";
import * as Card from "$lib/components/ui/card";
import * as Select from "$lib/components/ui/select";
import AllGamesListRaw from "$lib/all-games-list.json?raw";
import { storeSiteData, SortMonitor, RandomString } from "$lib/clientTools.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { DateInput } from "date-picker-svelte";
import { dndzone } from "svelte-dnd-action";
import { flip } from "svelte/animate";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { DefaultAPIEval, DefaultTCPEval, DefaultPingEval } from "$lib/anywhere.js";
import {
DefaultAPIEval,
DefaultTCPEval,
DefaultPingEval,
DefaultGamedigEval,
GAMEDIG_TIMEOUT
} from "$lib/anywhere.js";
export let categories = [];
export let colorDown = "#777";
export let colorDegraded = "#777";
export let monitorSort = [];
let AllGamesList = JSON.parse(AllGamesListRaw);
let monitors = [];
let status = "ACTIVE";
let showAddMonitor = false;
let formState = "idle";
let modifyMonitoringDataFormState = "idle";
let loadingData = false;
let triggers = [];
let selectedCategory = "All Categories";
@@ -119,6 +130,15 @@
degradedRemainingMinutes: 1,
downRemainingMinutes: 2,
secretString: RandomString(32)
},
gamedigConfig: {
gameId: AllGamesList[0].id,
host: "",
port: "",
timeout: GAMEDIG_TIMEOUT,
guessPort: false,
requestRules: false,
eval: DefaultGamedigEval
}
};
}
@@ -157,6 +177,8 @@
newMonitor.sqlConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "HEARTBEAT") {
newMonitor.heartbeatConfig = JSON.parse(newMonitor.type_data);
} else if (newMonitor.monitor_type == "GAMEDIG") {
newMonitor.gamedigConfig = JSON.parse(newMonitor.type_data);
}
showAddMonitor = true;
}
@@ -196,7 +218,12 @@
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "getTriggers", data: {} })
body: JSON.stringify({
action: "getTriggers",
data: {
status: "ACTIVE"
}
})
});
triggers = await apiResp.json();
} catch (error) {
@@ -228,30 +255,105 @@
};
});
let shareMenusToggle = false;
let monitorTriggers = {
down_trigger: {
failureThreshold: 1,
trigger_type: "DOWN",
successThreshold: 1,
description: "The monitor is down",
createIncident: "NO",
active: false,
triggers: [],
severity: "critical"
},
degraded_trigger: {
failureThreshold: 1,
trigger_type: "DEGRADED",
successThreshold: 1,
active: false,
description: "The monitor is degraded",
createIncident: "NO",
triggers: [],
severity: "warning"
}
let monitoringDataMenusToggle = false;
let monitorDataSelected;
let monitoringDataRequest = {
newStatus: "UP"
};
function initMonitorTriggers() {
monitorTriggers = {
down_trigger: {
failureThreshold: 1,
trigger_type: "DOWN",
successThreshold: 1,
description: "The monitor is down",
createIncident: "NO",
active: false,
triggers: [],
severity: "critical"
},
degraded_trigger: {
failureThreshold: 1,
trigger_type: "DEGRADED",
successThreshold: 1,
active: false,
description: "The monitor is degraded",
createIncident: "NO",
triggers: [],
severity: "warning"
}
};
}
let modifyMonitoringDataError = "";
async function ModifyMonitoringDataTriggers() {
modifyMonitoringDataError = "";
if (!monitoringDataRequest.start) {
modifyMonitoringDataError = "Invalid start date";
return;
}
if (!monitoringDataRequest.end) {
modifyMonitoringDataError = "Invalid end date";
return;
}
if (!monitorDataSelected.tag) {
modifyMonitoringDataError = "Invalid monitor";
return;
}
if (
!monitoringDataRequest.newStatus ||
(monitoringDataRequest.newStatus != "UP" &&
monitoringDataRequest.newStatus != "DEGRADED" &&
monitoringDataRequest.newStatus != "DOWN")
) {
modifyMonitoringDataError = "Invalid status";
return;
}
let startTimestamp = parseInt(new Date(monitoringDataRequest.start).getTime() / 1000);
let endTimestamp = parseInt(new Date(monitoringDataRequest.end).getTime() / 1000);
if (startTimestamp >= endTimestamp) {
modifyMonitoringDataError = "The start date must be before the end date";
return;
}
let data = {
monitor_tag: monitorDataSelected.tag,
start: startTimestamp,
end: endTimestamp,
newStatus: monitoringDataRequest.newStatus
};
modifyMonitoringDataFormState = "loading";
try {
let apiResp = await fetch(base + "/manage/app/api/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "updateMonitoringData", data })
});
let resp = await apiResp.json();
if (resp.error) {
modifyMonitoringDataError = resp.error;
} else {
monitoringDataMenusToggle = false;
monitoringDataRequest = {
newStatus: "UP"
};
}
} catch (error) {
modifyMonitoringDataError = "Error while modifying monitoring data";
} finally {
modifyMonitoringDataFormState = "idle";
}
}
let monitorTriggers;
initMonitorTriggers();
let saveTriggerError = "";
async function saveTriggers() {
saveTriggerError = "";
@@ -286,6 +388,7 @@
}
let currentAlertMonitor;
function openAlertMenu(m) {
initMonitorTriggers();
currentAlertMonitor = m;
if (m.down_trigger) {
monitorTriggers.down_trigger = m.down_trigger;
@@ -296,7 +399,10 @@
shareMenusToggle = true;
}
const flipDurationMs = 200;
function openMonitoringDataMenu(m) {
monitoringDataMenusToggle = true;
monitorDataSelected = m;
}
let orderErrorMessage = "";
function handleSort(e) {
dropTargetStyle = {
@@ -375,7 +481,7 @@
<div class="content px-4 py-4">
<h2 class="text-lg font-semibold">Rearrange Monitors</h2>
<div
class="mt-4"
class="mt-4 max-h-[80vh] overflow-y-auto"
use:dndzone={{ items: monitors, flipDurationMs, dropTargetStyle }}
on:consider={handleSort}
on:finalize={handleSort}
@@ -503,7 +609,7 @@
<div class="text-red-500">
{monitor.testResult.error}
</div>
{:else if !!monitor.testResult.status && !!monitor.testResult.latency}
{:else if monitor.testResult.status != undefined && monitor.testResult.latency != undefined}
<p class="text-muted-foreground">
Status: <span class="text-api-{monitor.testResult.status.toLowerCase()}"
>{monitor.testResult.status}</span
@@ -527,6 +633,9 @@
>
<Bell class="inline h-4 w-4" />
</Button>
<Button variant="secondary" class="h-8 w-8 p-2" on:click={() => openMonitoringDataMenu(monitor)}>
<DatabaseZap class="inline h-4 w-4" />
</Button>
{/if}
<Button variant="secondary" class="h-8 w-8 p-2" rel="external" href="{base}/?monitor={monitor.tag}">
<ExternalLink class="inline h-4 w-4" />
@@ -570,6 +679,88 @@
{/each}
</div>
{#if monitoringDataMenusToggle}
<div class="moldal-container fixed left-0 top-0 z-50 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm">
<div
class="absolute left-1/2 top-1/2 h-fit w-full max-w-2xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
>
<Button
variant="ghost"
on:click={() => {
monitoringDataMenusToggle = false;
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
<X class="h-4 w-4 text-muted-foreground" />
</Button>
<div class="content px-4 py-4">
<h2 class="text-lg font-semibold">Modify monitors data</h2>
<p class="text-xs text-muted-foreground">You can modify the monitor results for a given time range.</p>
<div class="grid grid-cols-3 gap-2">
<div class="col-span-1">
<Label for="start_date_time">
Start date
<span class="text-red-500">*</span>
</Label>
<DateInput
bind:value={monitoringDataRequest.start}
id="start_date_time"
timePrecision="minute"
class=" text-sm"
/>
</div>
<div class="col-span-1">
<Label for="end_date_time">
End date
<span class="text-red-500">*</span>
</Label>
<DateInput
bind:value={monitoringDataRequest.end}
id="end_date_time"
timePrecision="minute"
class="text-sm"
min={monitoringDataRequest.start}
/>
</div>
<div class="col-span-1">
<Label for="monitorDataNewStatus">New status</Label>
<Select.Root portal={null} value="UP" onSelectedChange={(e) => (monitoringDataRequest.newStatus = e.value)}>
<Select.Trigger id="monitorDataNewStatus">
<Select.Value bind:value={monitoringDataRequest.newStatus} placeholder="UP" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Item value="UP" label="UP" class="text-sm font-medium">UP</Select.Item>
<Select.Item value="DEGRADED" label="DEGRADED" class="text-sm font-medium">DEGRADED</Select.Item>
<Select.Item value="DOWN" label="DOWN" class="text-sm font-medium">DOWN</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</div>
</div>
<div class="mt-4 flex justify-end gap-x-2">
{#if !!modifyMonitoringDataError}
<div class="overflow-hidden text-ellipsis whitespace-nowrap py-2 text-sm font-medium text-destructive">
{modifyMonitoringDataError}
</div>
{/if}
<Button
class=""
on:click={ModifyMonitoringDataTriggers}
disabled={modifyMonitoringDataFormState === "loading"}
>
Save
{#if modifyMonitoringDataFormState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
</div>
</div>
{/if}
{#if shareMenusToggle}
<div class="moldal-container fixed left-0 top-0 z-50 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm">
<div
+216 -74
View File
@@ -5,9 +5,11 @@
import { Label } from "$lib/components/ui/label";
import autoAnimate from "@formkit/auto-animate";
import { siteDataExtractFromDb, storeSiteData } from "$lib/clientTools.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { base } from "$app/paths";
import Plus from "lucide-svelte/icons/plus";
import X from "lucide-svelte/icons/x";
import Check from "lucide-svelte/icons/check";
import Loader from "lucide-svelte/icons/loader";
import Info from "lucide-svelte/icons/info";
import { Tooltip } from "bits-ui";
@@ -24,26 +26,7 @@
}
];
}
let analyticsSupported = [
{
id: "",
type: "GA",
name: "Google Analytics",
script: "https://unpkg.com/@analytics/google-analytics@1.0.7/dist/@analytics/google-analytics.min.js"
},
{
id: "",
type: "AMPLITUDE",
name: "Amplitude",
script: "https://unpkg.com/@analytics/amplitude@0.1.3/dist/@analytics/amplitude.min.js"
},
{
id: "",
type: "MIXPANEL",
name: "MixPanel",
script: "https://unpkg.com/@analytics/mixpanel@0.4.0/dist/@analytics/mixpanel.min.js"
}
];
function removeRow(i) {
metaTags = metaTags.filter((_, index) => index !== i);
}
@@ -52,19 +35,6 @@
metaTags: JSON.stringify(metaTags)
}).metaTags;
let analytics = siteDataExtractFromDb(data.siteData, {
analytics: []
}).analytics;
//merge analyticsSupported with analytics
analytics = analyticsSupported.map((supported) => {
let found = analytics.find((a) => a.type === supported.type);
if (found) {
return found;
}
return supported;
});
let formState = "idle";
let formStateAnalytics = "idle";
@@ -87,17 +57,150 @@
async function formSubmitAnalytics() {
seoError = "";
formStateAnalytics = "loading";
let resp = await storeSiteData({
analytics: JSON.stringify(analytics)
let dataToSave = {};
selectedAnalytics.requirements.forEach((req) => {
dataToSave[req.label] = req.value;
});
let finalData = {};
finalData[selectedAnalytics.key] = JSON.stringify({
requirements: dataToSave,
isEnabled: selectedAnalytics.isEnabled
});
let resp = await storeSiteData(finalData);
//print data
let data = await resp.json();
formStateAnalytics = "idle";
if (data.error) {
seoError = data.error;
return;
} else {
selectedAnalytics.activeInSite = selectedAnalytics.isEnabled;
//update activeInSite in analyticsDropdownItems also
let index = analyticsDropdownItems.findIndex((item) => item.label === selectedAnalytics.label);
analyticsDropdownItems[index].activeInSite = true;
}
}
let selectedAnalytics;
let analyticsDropdownItems = [
{
label: "Google Analytics",
logo: `/ga.png`,
key: "analytics.googleTagManager",
isEnabled: false,
requirements: [
{
label: "Measurement ID",
type: "text",
placeholder: "G-S05E5E5E5E5",
required: true,
value: ""
}
]
},
{
label: "Plausible",
logo: `/plausible.png`,
key: "analytics.plausible",
isEnabled: false,
requirements: [
{
label: "Domain",
type: "text",
placeholder: "kener.ing",
required: true,
value: new URL(data.siteData.siteURL).hostname
},
{
label: "API",
type: "text",
placeholder: "https://plausible.io/api/event",
required: true,
value: "https://plausible.io/api/event"
},
{
label: "Script Source",
type: "text",
placeholder: "https://plausible.io/js/script.pageview-props.tagged-events.js",
required: true,
value: "https://plausible.io/js/script.pageview-props.tagged-events.js"
}
]
},
{
label: "MixPanel",
logo: `/mixpanel.png`,
key: "analytics.mixpanel",
isEnabled: false,
requirements: [
{
label: "Project Token",
type: "text",
placeholder: "YOUR_PROJECT_TOKEN",
required: true,
value: ""
}
]
},
{
label: "Amplitude",
logo: `/amplitude.png`,
key: "analytics.amplitude",
isEnabled: false,
requirements: [
{
label: "Amplitude API Key",
type: "text",
placeholder: "API key for your Amplitude project",
required: true,
value: ""
}
]
},
{
label: "Microsoft Clarity",
logo: `/clarity.png`,
key: "analytics.clarity",
isEnabled: false,
requirements: [
{
label: "Project ID",
type: "text",
placeholder: "Project ID for your Microsoft Clarity project",
required: true,
value: ""
}
]
},
{
label: "Umami",
logo: `/umami.png`,
key: "analytics.umami",
isEnabled: false,
requirements: [
{
label: "Website ID",
type: "text",
placeholder: "Website ID for your umami script and domain",
required: true,
value: ""
}
]
}
];
analyticsDropdownItems = analyticsDropdownItems.map((item) => {
let dbData = data.siteData[item.key];
if (!!dbData) {
item.isEnabled = dbData.isEnabled;
item.requirements.forEach((req) => {
req.value = dbData.requirements[req.label];
});
item.activeInSite = true;
}
return item;
});
selectedAnalytics = analyticsDropdownItems[0];
</script>
<Card.Root class="mt-4">
@@ -106,48 +209,87 @@
<Card.Description>Add your analytics ID/Key here. You can add multiple analytics providers.</Card.Description>
</Card.Header>
<Card.Content>
<form class="mx-auto mt-4 space-y-4" on:submit|preventDefault={formSubmitAnalytics}>
<div class="grid grid-cols-2 gap-2">
{#each analytics as analytic}
<div class="col-span-1">
<Label for={analytic.type}>
{analytic.name}
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content>
<div
class=" flex items-center justify-center rounded border bg-gray-800 p-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Add your {analytic.name} ID/Key here.
</div>
</Tooltip.Content>
</Tooltip.Root>
</Label>
<Input
bind:value={analytic.id}
class="mt-2"
type="text"
id={analytic.type}
placeholder="{analytic.type} ID/Key"
/>
</div>
<div class="mt-4 grid grid-cols-4 overflow-hidden rounded-md border">
<div class="col-span-1 flex flex-col border-r">
{#each analyticsDropdownItems as item}
<Button
variant={!!selectedAnalytics && selectedAnalytics.label === item.label ? "primary" : "ghost"}
class="flex items-center justify-between gap-x-2 rounded-none border-b text-sm last:border-none"
on:click={() => (selectedAnalytics = item)}
>
<div class="flex flex-row gap-x-3">
<img src={base + item.logo} class="w-5" alt={item.label} />
<span>{item.label}</span>
</div>
{#if item.activeInSite && item.isEnabled}
<div class="rounded-sm">
<span class="relative flex size-2">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
></span>
<span class="relative inline-flex size-2 rounded-full bg-green-500"></span>
</span>
</div>
{/if}
</Button>
{/each}
</div>
<div class="flex w-full justify-end gap-x-2">
{#if !!seoError}
<div class="py-2 text-sm font-medium text-destructive">{seoError}</div>
{/if}
<Button type="submit" disabled={formStateAnalytics === "loading"}>
Save
{#if formStateAnalytics === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
<form on:submit|preventDefault={formSubmitAnalytics} class="col-span-3 flex flex-col justify-between p-3 py-2">
{#if !!selectedAnalytics}
<div>
<div class="mb-2 text-sm font-medium text-muted-foreground">
Add details for your {selectedAnalytics.label} account
</div>
</div>
<div class="grid grid-cols-4 gap-4">
{#each selectedAnalytics.requirements as req}
<div class="col-span-2 flex flex-col gap-y-2">
<Label for={req.label}>{req.label}</Label>
<Input
bind:value={req.value}
class=""
type={req.type}
placeholder={req.placeholder}
bind:required={req.required}
/>
</div>
{/each}
</div>
<div class="mt-2 flex flex-row justify-between">
<div class="flex flex-row gap-x-2">
<label class="inline-flex cursor-pointer items-center">
<input
type="checkbox"
class="peer sr-only"
checked={selectedAnalytics.isEnabled}
on:change={() => {
selectedAnalytics.isEnabled = !selectedAnalytics.isEnabled;
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
<div class="pt-2.5 text-sm font-medium text-muted-foreground">
{selectedAnalytics.label} is
<i class="underline">{!selectedAnalytics.isEnabled ? "disabled" : "enabled"}</i> for your site
</div>
</div>
<div>
<Button type="submit" disabled={formStateAnalytics === "loading"}>
Save Changes for {selectedAnalytics.label}
{#if formStateAnalytics === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
{#if !!seoError}
<div class="py-2 text-sm font-medium text-destructive">{seoError}</div>
{/if}
</Button>
</div>
</form>
{/if}
</form>
</div>
</Card.Content>
</Card.Root>
<Card.Root class="mt-4">
@@ -202,7 +344,7 @@
<div class="py-2 text-sm font-medium text-destructive">{metaError}</div>
{/if}
<Button type="submit" disabled={formState === "loading"}>
Save
Save Meta Tags
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
+123 -39
View File
@@ -14,6 +14,11 @@
import { Tooltip } from "bits-ui";
import ColorPicker from "svelte-awesome-color-picker";
import CodeMirror from "svelte-codemirror-editor";
import { css } from "@codemirror/lang-css";
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
import { mode } from "mode-watcher";
export let data;
let formState = "idle";
@@ -26,10 +31,12 @@
barStyle: "PARTIAL",
barRoundness: "ROUNDED",
summaryStyle: "CURRENT",
kenerTheme: "default",
colorsJ: {
UP: "#4ead94",
DOWN: "#ca3038",
DEGRADED: "#e6ca61"
DEGRADED: "#e6ca61",
MAINTENANCE: "#6679cc"
},
fontJ: {
cssSrc:
@@ -44,7 +51,10 @@
themeData = siteDataExtractFromDb(data.siteData, themeData);
if (data.siteData.colors) {
themeData.colorsJ = data.siteData.colors;
if (data.siteData.colors.MAINTENANCE === undefined) {
data.siteData.colors.MAINTENANCE = "#6679cc"; // default value for MAINTENANCE
}
themeData.colorsJ = JSON.parse(JSON.stringify(data.siteData.colors));
}
if (data.siteData.font) {
themeData.fontJ = data.siteData.font;
@@ -95,16 +105,64 @@
</p>
<div class="flex">
<RadioGroup.Root bind:value={themeData.pattern} onValueChange={(e) => (themeData.pattern = e)}>
<div class="flex items-center space-x-2">
<RadioGroup.Root
class="flex flex-1 flex-wrap justify-start gap-4"
bind:value={themeData.pattern}
onValueChange={(e) => (themeData.pattern = e)}
>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="squares" id="rsq" />
<Label for="rsq">Squares</Label>
</div>
<div class="flex items-center space-x-2">
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="dots" id="rdot" />
<Label for="rdot">Dots</Label>
</div>
<div class="flex items-center space-x-2">
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="tiles" id="tiles" />
<Label for="tiles">Tiles</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="radial-blue" id="radial-blue" />
<Label for="radial-blue">Radial Blue</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="radial-mono" id="radial-mono" />
<Label for="radial-mono">Radial Mono</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="radial-midnight" id="radial-midnight" />
<Label for="radial-midnight">Radial Midnight</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="circle-mono" id="circle-mono" />
<Label for="circle-mono">Circle Mono</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="carbon-fibre" id="carbon-fibre" />
<Label for="carbon-fibre">Carbon Fibre</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="texture-sky" id="texture-sky" />
<Label for="texture-sky">Texture Sky</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="angular-mono" id="angular-mono" />
<Label for="angular-mono">Angular Mono</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="angular-spring" id="angular-spring" />
<Label for="angular-spring">Angular Spring</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="angular-bloom" id="angular-bloom" />
<Label for="angular-bloom">Angular Bloom</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="pets" id="pets" />
<Label for="pets">Pets</Label>
</div>
<div class="flex w-1/6 items-center space-x-2">
<RadioGroup.Item value="none" id="rnone" />
<Label for="rnone">None</Label>
</div>
@@ -239,58 +297,70 @@
</RadioGroup.Root>
</div>
<hr />
<p class="font-medium">
Colors
<Tooltip.Root openDelay={100}>
<Tooltip.Trigger class="">
<Info class="inline-block h-4 w-4 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content class="max-w-md">
<div
class=" flex items-center justify-center rounded border bg-gray-800 px-2 py-1.5 text-xs font-medium text-gray-300 shadow-popover outline-none"
>
Choose the colors for the monitor. You can choose between UP, DEGRADED, and DOWN.
</div>
</Tooltip.Content>
</Tooltip.Root>
</p>
<div class="flex w-full flex-row justify-evenly gap-2">
<div class="relative w-full">
<div>
<p class="font-medium">Status Colors</p>
<p class=" text-xs font-semibold text-muted-foreground">
Choose the colors for the monitor. You can choose between UP, DEGRADED, DOWN and MAINTENANCE.
</p>
</div>
<div class="flex w-full flex-row justify-start gap-x-8">
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.UP}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="UP"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.UP = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.DEGRADED}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="DEGRADED"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DEGRADED = event.detail.hex;
}}
/>
</div>
<div class="relative w-full">
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.DOWN}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="DOWN"
--cp-input-color="#777"
--cp-button-hover-color="#767676"
on:input={(event) => {
themeData.colorsJ.DOWN = event.detail.hex;
}}
/>
</div>
<div class="relative">
<ColorPicker
bind:hex={themeData.colorsJ.MAINTENANCE}
position="responsive"
isAlpha={false}
isDark={$mode == "dark"}
--input-size="16px"
isTextInput={true}
label="MAINTENANCE"
on:input={(event) => {
themeData.colorsJ.MAINTENANCE = event.detail.hex;
}}
/>
</div>
</div>
<hr />
<p class="font-medium">
@@ -338,14 +408,28 @@
<div class="flex w-full flex-row justify-start gap-2">
<div class="w-full">
<Label for="customCSS" class="text-sm text-muted-foreground">
You can add custom CSS to style your site. Do not include style tags.
You can add custom CSS to style your site. Do not include style tags. Go to <a
href="https://kener.ing/docs/custom-js-css-guide#grid-layout"
class="text-card-foreground"
target="_blank">this link</a
> to learn more about it.
</Label>
<textarea
bind:value={themeData.customCSS}
id="customCSS"
class="mt-1 h-48 w-full rounded-sm border p-2"
placeholder=".className&#123;color: red;&#125;"
></textarea>
<div class="overflow-hidden rounded-md">
<CodeMirror
bind:value={themeData.customCSS}
lang={css()}
theme={$mode == "dark" ? githubDark : githubLight}
styles={{
"&": {
width: "100%",
maxWidth: "100%",
height: "320px",
border: "1px solid hsl(var(--border) / var(--tw-border-opacity))",
borderRadius: "0.375rem"
}
}}
/>
</div>
</div>
</div>
<hr />
@@ -354,7 +438,7 @@
<div class="py-2 text-sm font-medium text-destructive">{themeError}</div>
{/if}
<Button type="submit" disabled={formState === "loading"}>
Save
Save Theme Settings
{#if formState === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
+14 -5
View File
@@ -275,10 +275,16 @@
body: JSON.stringify({ action: "testTrigger", data: { trigger_id, status } })
});
let resp = await data.json();
//check status code, if not 200 then show error
if (resp.error) {
triggers[i].testLoaders = "error";
alert(resp.error);
} else {
triggers[i].testLoaders = "success";
}
} catch (error) {
alert("Error: " + error);
} finally {
triggers[i].testLoaders = "success";
setTimeout(() => {
triggers[i].testLoaders = "idle";
}, 3000);
@@ -426,6 +432,9 @@
{#if trigger.testLoaders == "success"}
<Check class="mr-1 inline h-3 w-3 text-green-500 " />
{/if}
{#if trigger.testLoaders == "error"}
<X class="mr-1 inline h-3 w-3 text-red-500 " />
{/if}
<span class="h-4 text-xs font-medium"> Test Trigger </span>
</Button>
{#if $page.data.user.role != "member"}
@@ -493,7 +502,7 @@
newTrigger.trigger_type = "webhook";
}}
>
<img src="{base}/webhooks.svg" title="webhook" class="mr-4 w-6" />
<img src="{base}/webhooks.svg" title="webhook" class="mr-4 w-6" alt="Icon that represents webhooks" />
Webhooks
</Button>
<Button
@@ -503,7 +512,7 @@
newTrigger.trigger_type = "discord";
}}
>
<img src="{base}/discord.svg" title="discord" class="mr-4 w-6" />
<img src="{base}/discord.svg" title="discord" class="mr-4 w-6" alt="Discord logo" />
Discord
</Button>
<Button
@@ -513,7 +522,7 @@
newTrigger.trigger_type = "slack";
}}
>
<img src="{base}/slack.svg" title="slack" class="mr-4 w-6" />
<img src="{base}/slack.svg" title="slack" class="mr-4 w-6" alt="Slack logo" />
Slack
</Button>
@@ -524,7 +533,7 @@
newTrigger.trigger_type = "email";
}}
>
<img src="{base}/email.png" title="email" class="mr-4 w-6" />
<img src="{base}/email.png" title="email" class="mr-4 w-6" alt="Icon that represents emails" />
Email
</Button>
</div>
+66 -43
View File
@@ -8,7 +8,7 @@
import { sub, startOfDay, getUnixTime } from "date-fns";
import GMI from "$lib/components/gmi.svelte";
import { page } from "$app/stores";
import { analyticsEvent } from "$lib/boringOne";
import Share2 from "lucide-svelte/icons/share-2";
import ArrowRight from "lucide-svelte/icons/arrow-right";
import Settings from "lucide-svelte/icons/settings";
@@ -39,6 +39,15 @@
let uptime90Day = monitor.pageData.uptime90Day;
let incidents = {};
let dayIncidentsFull = [];
let homeDataMaxDays = monitor.pageData.homeDataMaxDays;
let dimension = {
x1: 6,
x2: 4
};
dimension.x1 = ($page.data.isMobile ? 346 : 546) / homeDataMaxDays.maxDays;
dimension.x2 = (4 / 6) * dimension.x1;
function loadIncidents() {
axios
@@ -91,52 +100,57 @@
});
}, 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 returnUptimeRollers() {
let rollers = homeDataMaxDays.selectableDays;
//sort descending
rollers.sort((a, b) => b - a);
let ret = [];
for (let i = 0; i < rollers.length; i++) {
let roller = rollers[i];
if (roller == 1) {
ret.push({
text: `${l(lang, "Today")}`,
startTs: monitor.pageData.startOfTheDay,
endTs: monitor.pageData.maxDateTodayTimestamp
});
} else {
ret.push({
text: `${roller} ${l(lang, "Days")}`,
startTs: monitor.pageData.startOfTheDay - 86400 * (roller - 1),
endTs: monitor.pageData.maxDateTodayTimestamp
});
}
//if last index
if (i == 0) {
ret[i].value = uptime90Day;
}
}
];
return ret;
}
let uptimesRollers = returnUptimeRollers();
//start of the week moment
let rolledAt = 0;
let rollerLoading = false;
async function rollSummary(r) {
let newRolledAt = r;
analyticsEvent("monitor_interval_switch", {
tag: monitor.tag,
interval: uptimesRollers[newRolledAt].text
});
if (uptimesRollers[newRolledAt].value === undefined) {
rollerLoading = true;
uptimesRollers[newRolledAt].loading = true;
let resp = await axios.post(`${base}/api/today/aggregated`, {
monitor: monitor,
monitor: {
tag: monitor.tag,
include_degraded_in_downtime: monitor.include_degraded_in_downtime
},
startTs: uptimesRollers[newRolledAt].startTs,
endTs: uptimesRollers[newRolledAt].endTs
});
@@ -179,19 +193,25 @@
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]">
<div class="monitor relative grid w-full max-w-[655px] grid-cols-12 gap-2 pb-2 pt-0">
{#if !!!embed}
<div class="col-span-12 md:w-[546px]">
<div class="pt-0">
@@ -213,7 +233,7 @@
{@html monitor.description}
{/if}
</p>
<div class="absolute right-14 top-5 flex gap-x-2">
<div class="absolute right-4 top-5 flex gap-x-2 md:right-14">
{#if $page.data.isLoggedIn}
<Button
href="{base}/manage/app/monitors#{monitor.tag}"
@@ -292,9 +312,9 @@
showDailyDataModal = false;
}}
>
<div class="daygrid90 flex min-h-[60px] justify-start overflow-x-auto overflow-y-hidden py-1">
<div class="daygrid90 flex min-h-[60px] justify-between overflow-x-auto overflow-y-hidden py-1">
{#each Object.entries(_90Day) as [ts, bar]}
<a
<button
data-ts={ts}
use:hoverAction
on:hover={(e) => {
@@ -303,24 +323,27 @@
on:click={(e) => {
dailyDataGetter(e, bar, incidents[ts]);
}}
href="#"
class="oneline h-[34px] w-[6px]
class="oneline h-[34px]
{bar.border ? 'opacity-100' : 'opacity-20'} pb-1"
style="width: {dimension.x1}px"
>
<div
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto w-[4px] rounded-{monitor.pageData.barRoundness.toUpperCase() ==
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto rounded-{monitor.pageData.barRoundness.toUpperCase() ==
'SHARP'
? 'none'
: 'sm'}"
style="width: {dimension.x2}px"
></div>
<!-- incident dot -->
{#if !!incidents[ts]}
<div
class="bg-api-{incidents[
ts
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] left-[1px] h-[4px] w-[4px] rounded-full"
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] h-[4px] w-[4px] rounded-full"
style="left: {dimension.x1 / 2 - 2}px"
></div>
{/if}
</a>
</button>
{#if bar.showDetails}
<div class="show-hover absolute text-sm">
<div class="text-{bar.textClass} text-xs font-semibold">
+57 -3
View File
@@ -8,11 +8,62 @@
import GMI from "$lib/components/gmi.svelte";
export let data;
let defaultPattern = data.site?.pattern || "squares";
let allPets = [
{
url: base + "/chicken.gif",
bottom: "-5"
},
{
url: base + "/dog.gif",
bottom: "-17"
},
{
url: base + "/cockatiel.gif",
bottom: "-10"
},
{
url: base + "/crab.gif",
bottom: "-20"
},
{
url: base + "/fox.gif",
bottom: "-9"
},
{
url: base + "/horse.gif",
bottom: "-11"
},
{
url: base + "/panda.gif",
bottom: "0"
},
{
url: base + "/totoro.gif",
bottom: "-27"
},
{
url: base + "/rabbit.gif",
bottom: "0"
},
{
url: base + "/duck.gif",
bottom: "-5"
},
{
url: base + "/snake.gif",
bottom: "0"
}
];
let randomPet = allPets[Math.floor(Math.random() * allPets.length)];
</script>
<div class="{defaultPattern}-pattern"></div>
{#if defaultPattern == "pets" && !!randomPet}
<div class="pets-pattern" style="background-image: url({randomPet.url});bottom: {randomPet.bottom}px"></div>
{:else}
<div class="{defaultPattern}-pattern"></div>
{/if}
<header class="blurry-bg sticky top-0 z-50 mx-auto md:mt-2">
<header class="sticky top-0 z-50 mx-auto md:mt-2">
<div class="container flex h-14 max-w-[820px] items-center border bg-card px-3 md:rounded-md">
<a rel="external" href={data.site.home ? data.site.home : base} class="mr-6 flex items-center space-x-2">
{#if data.site.logo}
@@ -32,7 +83,10 @@
rel="external"
href={navItem.url}
class="flex rounded-md px-3 py-2 text-card-foreground transition-all ease-linear hover:bg-background"
on:click={() => analyticsEvent("nav", navItem.name)}
on:click={() =>
analyticsEvent("navigation", {
name: navItem.name
})}
>
{#if navItem.iconURL}
<GMI src={navItem.iconURL} classList="mr-1.5 mt-0.5 inline h-4" alt={navItem.name} />
-1
View File
@@ -18,7 +18,6 @@
export let monitor;
export let lang;
export let selectedLang = "en";
let theme = "light";
let copiedLink = false;
let embedType = "js";
+366
View File
@@ -0,0 +1,366 @@
<script>
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import { page } from "$app/stores";
import { base } from "$app/paths";
import { onMount } from "svelte";
import GMI from "$lib/components/gmi.svelte";
import autoAnimate from "@formkit/auto-animate";
import Loader from "lucide-svelte/icons/loader";
import SquareChartGantt from "lucide-svelte/icons/square-chart-gantt";
import AlarmClockCheck from "lucide-svelte/icons/alarm-clock-check";
import Logout from "lucide-svelte/icons/log-out";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { l } from "$lib/i18n/client";
let apiError = "";
let view = "loading";
let localStorageKey = "user_subscription";
let login = {
userEmail: "",
verifyWithToken: "",
verificationCode: ""
};
let subscribedMonitors = [];
let subsInfo = {
allMonitors: false,
monitors: [],
hasActiveSubscription: false
};
let subscribableMonitors = $page.data.subscribableMonitors;
let callingAPI = false;
let alreadySubscribed = false;
let subscriberData = null;
onMount(async () => {
// Try to fetch stored subscription data from localStorage
await fetchStoredSubscription();
//
});
async function createNewUserSubscription() {
callingAPI = true;
apiError = "";
let action = "subscribe";
try {
const response = await callAPI(action, {
token: localStorage.getItem(localStorageKey),
monitors: subsInfo.monitors,
allMonitors: subsInfo.allMonitors
});
await fetchStoredSubscription();
} catch (e) {
apiError = e.message;
} finally {
callingAPI = false;
}
}
async function removeSubscription() {
//use confirm
if (!confirm("Are you sure you want to unsubscribe?")) {
return;
}
callingAPI = true;
apiError = "";
let action = "unsubscribe";
try {
await callAPI(action, {
token: localStorage.getItem(localStorageKey)
});
await fetchStoredSubscription();
} catch (e) {
apiError = e.message;
} finally {
callingAPI = false;
}
}
async function callAPI(action, data) {
try {
const response = await fetch(`${base}/api/subscriptions/${action}`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
if (response.ok) {
return await response.json();
} else {
let error = await response.json();
throw new Error(error.message || "An error occurred while processing your request.");
}
} catch (error) {
console.error("Error calling API:", error);
throw error;
}
}
async function doLogin() {
let action = "login";
callingAPI = true;
apiError = "";
try {
const response = await callAPI(action, {
userEmail: login.userEmail
});
if (response) {
login.verifyWithToken = response.token;
login.verificationCode = "";
view = "verify_login";
}
} catch (e) {
apiError = e.message;
} finally {
callingAPI = false;
}
}
async function verifyLogin() {
let action = "verify";
callingAPI = true;
apiError = "";
try {
const response = await callAPI(action, {
token: login.verifyWithToken,
code: login.verificationCode
});
if (response) {
localStorage.setItem(localStorageKey, response.token);
await fetchStoredSubscription();
}
} catch (e) {
apiError = e.message;
} finally {
callingAPI = false;
}
}
async function fetchStoredSubscription() {
let action = "fetch";
view = "loading";
let token = localStorage.getItem(localStorageKey);
if (!!!token) {
view = "login";
return;
}
try {
const response = await callAPI(action, {
token: token
});
if (response) {
subsInfo.email = response.email;
view = "subscribe";
subsInfo.monitors = response.monitors;
if (subsInfo.monitors && subsInfo.monitors.length > 0) {
subsInfo.hasActiveSubscription = true;
//if parsedData.monitors contains "_", set allMonitors to true
const allMonitorsIncluded = subsInfo.monitors.includes("_");
if (allMonitorsIncluded) {
subsInfo.allMonitors = true;
} else {
subsInfo.allMonitors = false;
subsInfo.monitors = subsInfo.monitors.filter((m) => m !== "_");
}
} else {
subsInfo.allMonitors = false;
subsInfo.monitors = [];
subsInfo.hasActiveSubscription = false; // Updated to set hasActiveSubscription
}
}
} catch (e) {
view = "login";
}
}
function doLogout() {
localStorage.removeItem(localStorageKey);
login.userEmail = "";
login.verificationCode = "";
login.verifyWithToken = "";
view = "login";
}
</script>
<div class="flex flex-col gap-2 pb-4" use:autoAnimate>
<div class="relative mb-2 scroll-m-20 px-4 pt-4 text-xl font-medium tracking-tight">
<p>
{l($page.data.lang, "Manage Subscription")}
</p>
</div>
{#if view === "login"}
<div class="flex flex-col gap-2 px-4">
<div>
<p class="mb-2 text-xs font-semibold text-muted-foreground">
{l(
$page.data.lang,
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe."
)}
</p>
<div class="relative">
<Input type="email" placeholder={l($page.data.lang, "Enter your email")} bind:value={login.userEmail} />
</div>
</div>
</div>
<div class="mt-2 flex flex-col gap-2 px-4">
{#if apiError}
<p class="text-xs font-semibold text-destructive">{apiError}</p>
{/if}
<Button class="w-full" disabled={callingAPI || !login.userEmail} on:click={doLogin}>
{l($page.data.lang, "Login")}
{#if callingAPI}
<Loader class="ml-2 h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
{/if}
{#if view === "loading"}
<div class="flex h-20 justify-center align-middle">
<Loader class="mt-5 h-10 w-10 animate-spin" />
</div>
{/if}
{#if view === "subscribe"}
<div class="flex flex-col gap-2 px-4">
{#if subsInfo.email}
<div
class="flex justify-between rounded-md stroke-secondary-foreground py-2 text-xs font-medium text-muted-foreground"
>
<span class="mt-1 text-xs font-semibold text-muted-foreground">
{l($page.data.lang, "You are logged in as %email", { email: subsInfo.email })}
</span>
<div>
<Button
variant="ghost"
size="icon"
class=" h-6 w-6 justify-center text-xs font-semibold text-muted-foreground"
on:click={doLogout}
>
<Logout class=" h-4 w-4" />
</Button>
</div>
</div>
{/if}
<div class="mt-2">
<label class="flex w-full cursor-pointer items-center justify-between">
<span class="text-sm font-medium">{l($page.data.lang, "Subscribe to all monitors")}</span>
<input
type="checkbox"
value=""
class="peer sr-only"
checked={subsInfo.allMonitors}
on:change={(e) => {
subsInfo.allMonitors = e.target.checked;
if (e.target.checked) {
subsInfo.monitors = [];
}
}}
/>
<div
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"
></div>
</label>
</div>
</div>
{#if subscribableMonitors.length > 0 && !subsInfo.allMonitors}
<div class="flex flex-col gap-2">
<p class="px-4">
<span class="text-xs font-semibold text-muted-foreground">
{l($page.data.lang, "Please select specific monitors to receive updates from.")}
</span>
</p>
<div class="flex max-h-96 flex-col gap-2 overflow-y-auto px-4">
{#each subscribableMonitors as monitor}
<div class="flex items-center justify-between gap-2 border-b pb-2 last:border-0">
<label for={monitor.tag} class="flex gap-x-2 text-sm font-medium text-gray-900 dark:text-gray-300">
{#if !!monitor.image}
<GMI src={monitor.image} classList="mt-1 h-4 w-4" />
{:else}
<SquareChartGantt class="mt-1 h-4 w-4" />
{/if}
{monitor.name}
</label>
<input
type="checkbox"
on:change={(e) => {
if (e.target.checked) {
subsInfo.monitors.push(monitor.tag);
} else {
subsInfo.monitors = subsInfo.monitors.filter((tag) => tag !== monitor.tag);
}
}}
id={monitor.tag}
value={monitor.tag}
bind:group={subsInfo.monitors}
class="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
/>
</div>
{/each}
</div>
</div>
{/if}
<div class="mt-2 flex flex-col gap-2 px-4">
{#if apiError}
<p class="text-xs font-semibold text-destructive">{apiError}</p>
{/if}
<Button class="w-full" disabled={callingAPI} on:click={createNewUserSubscription}>
{#if subsInfo.monitors.length > 0}
{l($page.data.lang, "Update Subscription")}
{:else}
{l($page.data.lang, "Subscribe")}
{/if}
{#if callingAPI}
<Loader class="ml-2 h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
{/if}
{#if view === "verify_login"}
<div class="flex flex-col gap-2 px-4">
<div>
<Button
variant="outline"
class="bounce-left mb-2 h-8 justify-start pl-1.5 text-xs text-muted-foreground"
on:click={() => (view = "login")}
>
<ChevronLeft class="arrow mr-1 h-5 w-5" />
{l($page.data.lang, "Change Email")}
</Button>
<p class="mb-2 text-xs text-muted-foreground">
{l($page.data.lang, "We have sent a code to your email. Please enter it below to confirm your login")}
</p>
<Input type="text" placeholder={l($page.data.lang, "Enter the code")} bind:value={login.verificationCode} />
</div>
</div>
<div class="mt-2 flex flex-col gap-2 px-4">
{#if apiError}
<p class="text-xs font-semibold text-destructive">{apiError}</p>
{/if}
<Button class="w-full" disabled={callingAPI || !login.verificationCode} on:click={verifyLogin}>
{l($page.data.lang, "Confirm Login")}
{#if callingAPI}
<Loader class="ml-2 h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
{/if}
</div>
+5 -1
View File
@@ -1,7 +1,7 @@
// @ts-nocheck
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr, nb } from "date-fns/locale";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr, nb, pl, es, it, faIR } from "date-fns/locale";
import { formatInTimeZone } from "date-fns-tz";
const locales = {
@@ -19,6 +19,10 @@ const locales = {
"pt-BR": ptBR,
tr,
"nb-NO": nb,
pl,
es,
it,
fa: faIR,
};
const f = function (date, formatStr, locale, tz) {
+28 -1
View File
@@ -1,16 +1,25 @@
{
"%status for %duration": "%status für %duration",
"120 Days": "120 Tage",
"14 Days": "14 Tage",
"150 Days": "150 Tage",
"180 Days": "180 Tage",
"30 Days": "30 Tage",
"60 Days": "60 Tage",
"7 Days": "7 Tage",
"90 Days": "90 Tage",
"Availability per Component": "Verfügbarkeit pro Komponente",
"All Systems are Degraded": "Alle Systeme sind eingeschränkt",
"All Systems are Down": "Alle Systeme sind ausgefallen",
"All Systems are Operational": "Alle Systeme sind betriebsbereit",
"All Systems are in Maintenance": "Alle Systeme sind in Wartung",
"Back": "Zurück",
"Badge Copied": "Abzeichen kopiert",
"Badge": "Abzeichen",
"Browse Events": "Ereignisse durchsuchen",
"Change Email": "E-Mail ändern",
"Code Copied": "Code kopiert",
"Confirm Login": "Login bestätigen",
"Copy Code": "Code kopieren",
"Copy Link": "Link kopieren",
"DEGRADED": "EINGESCHRÄNKT",
@@ -19,6 +28,9 @@
"Days": "Tage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Binden Sie diesen Monitor mit <script> oder <iframe> in Ihre App ein.",
"Embed": "Einbetten",
"Enter the code": "Code eingeben",
"Enter your email": "E-Mail-Adresse eingeben",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Geben Sie Ihre E-Mail-Adresse ein, um sich anzumelden. Wenn Sie nicht abonniert haben, werden Sie zum Abonnieren aufgefordert.",
"Get SVG badge for this monitor": "SVG-Abzeichen für diesen Monitor erhalten",
"Get a LIVE Status for this monitor": "LIVE-Status für diesen Monitor erhalten",
"IDENTIFIED": "IDENTIFIZIERT",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "Dauerte etwa %lastedFor",
"Light": "Hell",
"Link Copied": "Link kopiert",
"Login": "Anmelden",
"MAINTENANCE": "WARTUNG",
"MONITORING": "ÜBERWACHUNG",
"Maintenance Completed": "Wartung abgeschlossen",
"Maintenance in Progress": "Wartung läuft",
"Manage Subscription": "Abonnement verwalten",
"Mode": "Modus",
"No Data": "Keine Daten",
"No Incident in %date": "Kein Vorfall am %date",
@@ -40,11 +54,16 @@
"No Updates Yet": "Noch keine Updates",
"Ongoing Incidents": "Laufende Vorfälle",
"Pinging": "Pingen",
"Please select specific monitors to receive updates from.": "Bitte wählen Sie bestimmte Monitore aus, von denen Sie Updates erhalten möchten.",
"RESOLVED": "GELÖST",
"Recent Incidents": "Kürzliche Vorfälle",
"Recent Maintenances": "Kürzliche Wartungen",
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
"Share": "Teilen",
"Some Systems are not working as expected": "Einige Systeme funktionieren nicht wie erwartet",
"Some Systems Degraded": "Einige Systeme eingeschränkt",
"Some Systems Down": "Einige Systeme ausgefallen",
"Some Systems in Maintenance": "Einige Systeme sind in Wartung",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
"Started %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
@@ -53,13 +72,21 @@
"Starts %startedAt": "Beginnt in %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Abonnieren",
"Subscribe to Updates": "Updates abonnieren",
"Subscribe to all monitors": "Alle Monitore abonnieren",
"Switch to your timezone": "Wechseln Sie zu Ihrer Zeitzone",
"Theme": "Thema",
"There are no upcoming maintenances": "Es gibt keine bevorstehenden Wartungen",
"Timezone set to %tz": "Zeitzone auf %tz gesetzt",
"Today": "Heute",
"UP": "VERFÜGBAR",
"Update Subscription": "Abonnement aktualisieren",
"Upcoming Maintenance": "Bevorstehende Wartung",
"Upcoming Maintenances": "Bevorstehende Wartungen",
"Updates": "Updates",
"Uptime": "Verfügbarkeit",
"View in detail": "Details anzeigen"
"View in detail": "Details anzeigen",
"We have sent a code to your email. Please enter it below to confirm your login": "Wir haben einen Code an Ihre E-Mail-Adresse gesendet. Bitte geben Sie ihn unten ein, um Ihren Login zu bestätigen",
"You are logged in as %email": "Sie sind angemeldet als %email"
}
+28 -1
View File
@@ -5,12 +5,21 @@
"60 Days": "60 Dage",
"7 Days": "7 Dage",
"90 Days": "90 Dage",
"120 Days": "120 Dage",
"150 Days": "150 Dage",
"180 Days": "180 Dage",
"Availability per Component": "Tilgængelighed pr. Komponent",
"All Systems are Degraded": "Alle Systemer er Forringet",
"All Systems are Down": "Alle Systemer er Nede",
"All Systems are Operational": "Alle Systemer er Operationelle",
"All Systems are in Maintenance": "Alle systemer er i vedligeholdelse",
"Back": "Tilbage",
"Badge Copied": "Mærke Kopieret",
"Badge": "Mærke",
"Browse Events": "Gennemse begivenheder",
"Change Email": "Skift Email",
"Code Copied": "Kode Kopieret",
"Confirm Login": "Bekræft Login",
"Copy Code": "Kopier Kode",
"Copy Link": "Kopier Link",
"DEGRADED": "FORRINGET",
@@ -19,6 +28,9 @@
"Days": "Dage",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Integrer denne monitor ved at bruge <script> eller <iframe> i din app.",
"Embed": "Integrer",
"Enter the code": "Indtast koden",
"Enter your email": "Indtast din email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Indtast din email for at logge ind. Hvis du ikke er abonnent, vil du blive bedt om at abonnere.",
"Get SVG badge for this monitor": "Hent SVG-mærke til denne monitor",
"Get a LIVE Status for this monitor": "Hent en LIVE-status for denne monitor",
"IDENTIFIED": "IDENTIFICERET",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "Varede i cirka %lastedFor",
"Light": "Lys",
"Link Copied": "Link Kopieret",
"Login": "Log ind",
"MAINTENANCE": "VEDLIGEHOLDELSE",
"MONITORING": "OVERVÅGNING",
"Maintenance Completed": "Vedligeholdelse afsluttet",
"Maintenance in Progress": "Vedligeholdelse i gang",
"Manage Subscription": "Administrer Abonnement",
"Mode": "Tilstand",
"No Data": "Ingen Data",
"No Incident in %date": "Ingen Hændelse Den %date",
@@ -40,11 +54,16 @@
"No Updates Yet": "Ingen Opdateringer Endnu",
"Ongoing Incidents": "Igangværende Hændelser",
"Pinging": "Pinger",
"Please select specific monitors to receive updates from.": "Vælg venligst specifikke monitorer for at modtage opdateringer fra.",
"RESOLVED": "LØST",
"Recent Incidents": "Seneste Hændelser",
"Recent Maintenances": "Seneste Vedligeholdelser",
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
"Share": "Del",
"Some Systems are not working as expected": "Nogle Systemer fungerer ikke som forventet",
"Some Systems Degraded": "Nogle Systemer er Forringet",
"Some Systems Down": "Nogle Systemer er Nede",
"Some Systems in Maintenance": "Nogle Systemer er i Vedligeholdelse",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
"Started %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
@@ -53,13 +72,21 @@
"Starts %startedAt": "Starter om %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Abonner",
"Subscribe to Updates": "Abonner på opdateringer",
"Subscribe to all monitors": "Abonner på alle monitorer",
"Switch to your timezone": "Skift til din tidszone",
"Theme": "Tema",
"There are no upcoming maintenances": "Der er ingen kommende vedligeholdelser",
"Timezone set to %tz": "Tidszone sat til %tz",
"Today": "I dag",
"UP": "OPPE",
"Update Subscription": "Opdater Abonnement",
"Upcoming Maintenance": "Kommende vedligeholdelse",
"Upcoming Maintenances": "Kommende vedligeholdelser",
"Updates": "Opdateringer",
"Uptime": "Oppetid",
"View in detail": "Vis i detaljer"
"View in detail": "Vis i detaljer",
"We have sent a code to your email. Please enter it below to confirm your login": "Vi har sendt en kode til din email. Indtast den venligst nedenfor for at bekræfte dit login",
"You are logged in as %email": "Du er logget ind som %email"
}
+28 -1
View File
@@ -1,16 +1,25 @@
{
"%status for %duration": "%status for %duration",
"120 Days": "120 Days",
"14 Days": "14 Days",
"150 Days": "150 Days",
"180 Days": "180 Days",
"30 Days": "30 Days",
"60 Days": "60 Days",
"7 Days": "7 Days",
"90 Days": "90 Days",
"Availability per Component": "Availability per Component",
"All Systems are Degraded": "All Systems are Degraded",
"All Systems are Down": "All Systems are Down",
"All Systems are Operational": "All Systems are Operational",
"All Systems are in Maintenance": "All Systems are in Maintenance",
"Back": "Back",
"Badge Copied": "Badge Copied",
"Badge": "Badge",
"Browse Events": "Browse Events",
"Change Email": "Change Email",
"Code Copied": "Code Copied",
"Confirm Login": "Confirm Login",
"Copy Code": "Copy Code",
"Copy Link": "Copy Link",
"DEGRADED": "DEGRADED",
@@ -19,6 +28,9 @@
"Days": "Days",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Embed this monitor using <script> or <iframe> in your app.",
"Embed": "Embed",
"Enter the code": "Enter the code",
"Enter your email": "Enter your email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.",
"Get SVG badge for this monitor": "Get SVG badge for this monitor",
"Get a LIVE Status for this monitor": "Get a LIVE Status for this monitor",
"IDENTIFIED": "IDENTIFIED",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "Lasted for about %lastedFor",
"Light": "Light",
"Link Copied": "Link Copied",
"Login": "Login",
"MAINTENANCE": "MAINTENANCE",
"MONITORING": "MONITORING",
"Maintenance Completed": "Maintenance Completed",
"Maintenance in Progress": "Maintenance in Progress",
"Manage Subscription": "Manage Subscription",
"Mode": "Mode",
"No Data": "No Data",
"No Incident in %date": "No Incident in %date",
@@ -40,11 +54,16 @@
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"Please select specific monitors to receive updates from.": "Please select specific monitors to receive updates from.",
"RESOLVED": "RESOLVED",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"Share this monitor using a link with others": "Share this monitor using a link with others",
"Share": "Share",
"Some Systems are not working as expected": "Some Systems are not working as expected",
"Some Systems Degraded": "Some Systems Degraded",
"Some Systems Down": "Some Systems Down",
"Some Systems in Maintenance": "Some Systems in Maintenance",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Started %startedAt, lasted for %lastedFor",
"Started %startedAt, still ongoing": "Started %startedAt, still ongoing",
@@ -53,13 +72,21 @@
"Starts %startedAt": "Starts %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Subscribe",
"Subscribe to Updates": "Subscribe to Updates",
"Subscribe to all monitors": "Subscribe to all monitors",
"Switch to your timezone": "Switch to your timezone",
"Theme": "Theme",
"There are no upcoming maintenances": "There are no upcoming maintenances",
"Timezone set to %tz": "Timezone set to %tz",
"Today": "Today",
"UP": "UP",
"Update Subscription": "Update Subscription",
"Upcoming Maintenance": "Upcoming Maintenance",
"Upcoming Maintenances": "Upcoming Maintenances",
"Updates": "Updates",
"Uptime": "Uptime",
"View in detail": "View in detail"
"View in detail": "View in detail",
"We have sent a code to your email. Please enter it below to confirm your login": "We have sent a code to your email. Please enter it below to confirm your login",
"You are logged in as %email": "You are logged in as %email"
}
+90
View File
@@ -0,0 +1,90 @@
{
"%status for %duration": "%status durante %duration",
"120 Days": "120 Días",
"14 Days": "14 Días",
"150 Days": "150 Días",
"180 Days": "180 Días",
"30 Days": "30 Días",
"60 Days": "60 Días",
"7 Days": "7 Días",
"90 Days": "90 Días",
"Availability per Component": "Disponibilidad por componente",
"All Systems are Degraded": "Todos los sistemas están degradados",
"All Systems are Down": "Todos los sistemas están caídos",
"All Systems are Operational": "Todos los sistemas están operativos",
"Back": "Volver",
"Badge Copied": "Distintivo copiado",
"Badge": "Distintivo",
"Browse Events": "Ver eventos",
"Change Email": "Cambiar correo electrónico",
"Code Copied": "Código copiado",
"Confirm Login": "Confirmar inicio de sesión",
"Copy Code": "Copiar código",
"Copy Link": "Copiar enlace",
"DEGRADED": "DEGRADADO",
"DOWN": "CAÍDO",
"Dark": "Oscuro",
"Days": "Días",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Incrusta este monitor usando <script> o <iframe> en tu aplicación.",
"Embed": "Incrustar",
"Enter the code": "Ingresa el código",
"Enter your email": "Ingresa tu correo electrónico",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Ingresa tu correo electrónico para iniciar sesión. Si no estás suscrito, se te pedirá que te suscribas.",
"Get SVG badge for this monitor": "Obtener distintivo SVG para este monitor",
"Get a LIVE Status for this monitor": "Obtener estado EN VIVO para este monitor",
"IDENTIFIED": "IDENTIFICADO",
"INVESTIGATING": "INVESTIGANDO",
"Incident Updates": "Actualizaciones de incidentes",
"LIVE Status": "Estado EN VIVO",
"Lasted for about %lastedFor": "Duró aproximadamente %lastedFor",
"Light": "Claro",
"Link Copied": "Enlace copiado",
"Login": "Iniciar sesión",
"MAINTENANCE": "MANTENIMIENTO",
"MONITORING": "MONITOREANDO",
"Maintenance Completed": "Mantenimiento completado",
"Maintenance in Progress": "Mantenimiento en progreso",
"Manage Subscription": "Gestionar suscripción",
"Mode": "Modo",
"No Data": "Sin datos",
"No Incident in %date": "Sin incidentes en %date",
"No Incidents": "Sin incidentes",
"No Monitor Found": "No se encontró monitor",
"No Updates Yet": "Aún no hay actualizaciones",
"Ongoing Incidents": "Incidentes en curso",
"Pinging": "Haciendo ping",
"Please select specific monitors to receive updates from.": "Por favor selecciona monitores específicos para recibir actualizaciones.",
"RESOLVED": "RESUELTO",
"Recent Incidents": "Incidentes recientes",
"Recent Maintenances": "Mantenimientos recientes",
"Share this monitor using a link with others": "Comparte este monitor mediante un enlace con otros",
"Share": "Compartir",
"Some Systems are not working as expected": "Algunos sistemas no funcionan como se espera",
"Some Systems Degraded": "Algunos sistemas degradados",
"Some Systems Down": "Algunos sistemas caídos",
"Standard": "Estándar",
"Started %startedAt, lasted for %lastedFor": "Comenzó el %startedAt, duró %lastedFor",
"Started %startedAt, still ongoing": "Comenzó el %startedAt, aún en curso",
"Started %startedAt, will last for %lastedFor more": "Comenzó el %startedAt, durará %lastedFor más",
"Starts %startedAt, will last for %lastedFor": "Empieza el %startedAt, durará %lastedFor",
"Starts %startedAt": "Empieza el %startedAt",
"Status OK": "Estado OK",
"Status": "Estado",
"Subscribe": "Suscribirse",
"Subscribe to Updates": "Suscribirse a actualizaciones",
"Subscribe to all monitors": "Suscribirse a todos los monitores",
"Switch to your timezone": "Cambiar a tu zona horaria",
"Theme": "Tema",
"There are no upcoming maintenances": "No hay mantenimientos próximos",
"Timezone set to %tz": "Zona horaria configurada a %tz",
"Today": "Hoy",
"UP": "ACTIVO",
"Update Subscription": "Actualizar suscripción",
"Upcoming Maintenance": "Mantenimiento próximo",
"Upcoming Maintenances": "Mantenimientos próximos",
"Updates": "Actualizaciones",
"Uptime": "Tiempo en línea",
"View in detail": "Ver en detalle",
"We have sent a code to your email. Please enter it below to confirm your login": "Hemos enviado un código a tu correo. Por favor, ingrésalo abajo para confirmar tu inicio de sesión",
"You are logged in as %email": "Has iniciado sesión como %email"
}
+92
View File
@@ -0,0 +1,92 @@
{
"%status for %duration": "%status به مدت %duration",
"120 Days": "۱۲۰ روزه",
"14 Days": "۱۴ روزه",
"150 Days": "۱۵۰ روزه",
"180 Days": "۱۸۰ روزه",
"30 Days": "۳۰ روزه",
"60 Days": "۶۰ روزه",
"7 Days": "۷ روزه",
"90 Days": "۹۰ روزه",
"Availability per Component": "دسترس‌پذیری هر کامپوننت",
"All Systems are Degraded": "همه سامانه‌ها دچار اختلال‌اند",
"All Systems are Down": "همه سامانه‌ها از کار افتاده‌اند",
"All Systems are Operational": "همه سامانه‌ها در حال کار هستند",
"All Systems are in Maintenance": "همه سامانه‌ها در حال تعمیر هستند",
"Back": "بازگشت",
"Badge Copied": "نشان کپی شد",
"Badge": "نشان",
"Browse Events": "مشاهده رویدادها",
"Change Email": "تغییر ایمیل",
"Code Copied": "کد کپی شد",
"Confirm Login": "تأیید ورود",
"Copy Code": "کپی کد",
"Copy Link": "کپی لینک",
"DEGRADED": "اختلال",
"DOWN": "از کار افتاده",
"Dark": "تاریک",
"Days": "روزها",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "این مانیتور را با استفاده از <script> یا <iframe> در برنامه‌تان قرار دهید.",
"Embed": "درج",
"Enter the code": "کد را وارد کنید",
"Enter your email": "ایمیل خود را وارد کنید",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "برای ورود، ایمیل خود را وارد کنید. اگر عضو نیستید، از شما خواسته می‌شود عضو شوید.",
"Get SVG badge for this monitor": "دریافت نشان SVG برای این مانیتور",
"Get a LIVE Status for this monitor": "دریافت وضعیت زنده این مانیتور",
"IDENTIFIED": "شناسایی شده",
"INVESTIGATING": "در حال بررسی",
"Incident Updates": "به‌روزرسانی‌های حادثه",
"LIVE Status": "وضعیت زنده",
"Lasted for about %lastedFor": "حدوداً به مدت %lastedFor ادامه داشت",
"Light": "روشن",
"Link Copied": "لینک کپی شد",
"Login": "ورود",
"MAINTENANCE": "تعمیر و نگهداری",
"MONITORING": "در حال پایش",
"Maintenance Completed": "تعمیرات به پایان رسید",
"Maintenance in Progress": "تعمیرات در حال انجام است",
"Manage Subscription": "مدیریت اشتراک",
"Mode": "حالت",
"No Data": "بدون داده",
"No Incident in %date": "هیچ حادثه‌ای در %date وجود ندارد",
"No Incidents": "هیچ حادثه‌ای وجود ندارد",
"No Monitor Found": "هیچ مانیتوری یافت نشد",
"No Updates Yet": "هنوز به‌روزرسانی‌ای وجود ندارد",
"Ongoing Incidents": "حوادث در حال وقوع",
"Pinging": "در حال پینگ گرفتن",
"Please select specific monitors to receive updates from.": "لطفاً مانیتورهای خاصی را برای دریافت به‌روزرسانی انتخاب کنید.",
"RESOLVED": "حل شده",
"Recent Incidents": "حوادث اخیر",
"Recent Maintenances": "تعمیرات اخیر",
"Share this monitor using a link with others": "این مانیتور را با لینک با دیگران به اشتراک بگذارید",
"Share": "اشتراک‌گذاری",
"Some Systems are not working as expected": "برخی سامانه‌ها به‌درستی کار نمی‌کنند",
"Some Systems Degraded": "برخی سامانه‌ها دچار اختلال‌اند",
"Some Systems Down": "برخی سامانه‌ها از کار افتاده‌اند",
"Some Systems in Maintenance": "برخی سامانه‌ها در حال تعمیر هستند",
"Standard": "معمولی",
"Started %startedAt, lasted for %lastedFor": "شروع در %startedAt، به مدت %lastedFor ادامه داشت",
"Started %startedAt, still ongoing": "شروع در %startedAt، هنوز ادامه دارد",
"Started %startedAt, will last for %lastedFor more": "شروع در %startedAt، تا %lastedFor دیگر ادامه خواهد داشت",
"Starts %startedAt, will last for %lastedFor": "شروع در %startedAt، به مدت %lastedFor ادامه دارد",
"Starts %startedAt": "شروع در %startedAt",
"Status OK": "وضعیت خوب",
"Status": "وضعیت",
"Subscribe": "اشتراک",
"Subscribe to Updates": "مشترک شدن در به‌روزرسانی‌ها",
"Subscribe to all monitors": "اشتراک همه مانیتورها",
"Switch to your timezone": "تغییر به منطقه زمانی شما",
"Theme": "تم",
"There are no upcoming maintenances": "هیچ تعمیر آینده‌ای وجود ندارد",
"Timezone set to %tz": "منطقه زمانی روی %tz تنظیم شد",
"Today": "امروز",
"UP": "فعال",
"Update Subscription": "به‌روزرسانی اشتراک",
"Upcoming Maintenance": "تعمیر آینده",
"Upcoming Maintenances": "تعمیرات آینده",
"Updates": "به‌روزرسانی‌ها",
"Uptime": "زمان کارکرد",
"View in detail": "مشاهده جزئیات",
"We have sent a code to your email. Please enter it below to confirm your login": "کدی به ایمیل شما ارسال کردیم. لطفاً برای تأیید ورود آن را وارد کنید.",
"You are logged in as %email": "شما با ایمیل %email وارد شده‌اید"
}
+28 -1
View File
@@ -5,12 +5,21 @@
"60 Days": "60 jours",
"7 Days": "7 jours",
"90 Days": "90 jours",
"120 Days": "120 jours",
"150 Days": "150 jours",
"180 Days": "180 jours",
"Availability per Component": "Disponibilité par composant",
"All Systems are Degraded": "Tous les systèmes sont dégradés",
"All Systems are Down": "Tous les systèmes sont hors ligne",
"All Systems are Operational": "Tous les systèmes sont opérationnels",
"All Systems are in Maintenance": "Tous les systèmes sont en maintenance",
"Back": "Retour",
"Badge Copied": "Badge copié",
"Badge": "Badge",
"Browse Events": "Parcourir les événements",
"Change Email": "Changer l'e-mail",
"Code Copied": "Code copié",
"Confirm Login": "Confirmer la connexion",
"Copy Code": "Copier le code",
"Copy Link": "Copier le lien",
"DEGRADED": "Dégradé",
@@ -19,6 +28,9 @@
"Days": "Jours",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Intégrez ce moniteur dans votre application en utilisant <script> ou <iframe>.",
"Embed": "Intégrer",
"Enter the code": "Entrez le code",
"Enter your email": "Entrez votre e-mail",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Entrez votre e-mail pour vous connecter. Si vous n'êtes pas abonné, vous serez invité à vous abonner.",
"Get SVG badge for this monitor": "Obtenez un badge SVG pour ce moniteur",
"Get a LIVE Status for this monitor": "Obtenez un statut LIVE pour ce moniteur",
"IDENTIFIED": "Identifié",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "A duré environ %lastedFor",
"Light": "Clair",
"Link Copied": "Lien copié",
"Login": "Connexion",
"MAINTENANCE": "Maintenance",
"MONITORING": "En surveillance",
"Maintenance Completed": "Maintenance terminée",
"Maintenance in Progress": "Maintenance en cours",
"Manage Subscription": "Gérer l'abonnement",
"Mode": "Mode",
"No Data": "Pas de données",
"No Incident in %date": "Pas d'incident le %date",
@@ -40,11 +54,16 @@
"No Updates Yet": "Pas encore de mises à jour",
"Ongoing Incidents": "Incidents en cours",
"Pinging": "Ping en cours",
"Please select specific monitors to receive updates from.": "Veuillez sélectionner les moniteurs spécifiques dont vous souhaitez recevoir des mises à jour.",
"RESOLVED": "Résolu",
"Recent Incidents": "Incidents récents",
"Recent Maintenances": "Maintenances récentes",
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
"Share": "Partager",
"Some Systems are not working as expected": "Certains systèmes ne fonctionnent pas comme prévu",
"Some Systems Degraded": "Certains systèmes sont dégradés",
"Some Systems Down": "Certains systèmes sont hors ligne",
"Some Systems in Maintenance": "Certains systèmes sont en maintenance",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
"Started %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
@@ -53,13 +72,21 @@
"Starts %startedAt": "Commence dans %startedAt",
"Status OK": "Statut OK",
"Status": "Statut",
"Subscribe": "S'abonner",
"Subscribe to Updates": "S'abonner aux mises à jour",
"Subscribe to all monitors": "S'abonner à tous les moniteurs",
"Switch to your timezone": "Passer à votre fuseau horaire",
"Theme": "Thème",
"There are no upcoming maintenances": "Il n'y a pas de maintenances à venir",
"Timezone set to %tz": "Fuseau horaire défini sur %tz",
"Today": "Aujourd'hui",
"UP": "En ligne",
"Update Subscription": "Mettre à jour l'abonnement",
"Upcoming Maintenance": "Maintenance à venir",
"Upcoming Maintenances": "Maintenances à venir",
"Updates": "Mises à jour",
"Uptime": "Temps de fonctionnement",
"View in detail": "Voir en détail"
"View in detail": "Voir en détail",
"We have sent a code to your email. Please enter it below to confirm your login": "Nous avons envoyé un code à votre e-mail. Veuillez l'entrer ci-dessous pour confirmer votre connexion",
"You are logged in as %email": "Vous êtes connecté en tant que %email"
}
+28 -1
View File
@@ -5,12 +5,21 @@
"60 Days": "60 दिन",
"7 Days": "7 दिन",
"90 Days": "90 दिन",
"120 Days": "120 दिन",
"150 Days": "150 दिन",
"180 Days": "180 दिन",
"Availability per Component": "प्रत्येक घटक की उपलब्धता",
"All Systems are Degraded": "सभी सिस्टम क्षीण हैं",
"All Systems are Down": "सभी सिस्टम डाउन हैं",
"All Systems are Operational": "सभी सिस्टम कार्यरत हैं",
"All Systems are in Maintenance": "सभी सिस्टम रखरखाव में हैं",
"Back": "वापस",
"Badge Copied": "बैज कॉपी किया गया",
"Badge": "बैज",
"Browse Events": "ईवेंट ब्राउज़ करें",
"Change Email": "ईमेल बदलें",
"Code Copied": "कोड कॉपी किया गया",
"Confirm Login": "लॉगिन पुष्टि करें",
"Copy Code": "कोड कॉपी करें",
"Copy Link": "लिंक कॉपी करें",
"DEGRADED": "क्षीण",
@@ -19,6 +28,9 @@
"Days": "दिन",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "इस मॉनिटर को अपनी ऐप में <script> या <iframe> का उपयोग करके एम्बेड करें।",
"Embed": "एम्बेड",
"Enter the code": "कोड दर्ज करें",
"Enter your email": "अपना ईमेल दर्ज करें",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "लॉगिन करने के लिए अपना ईमेल दर्ज करें। यदि आप सब्सक्राइब नहीं हैं, तो आपको सब्सक्राइब करने के लिए कहा जाएगा।",
"Get SVG badge for this monitor": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
"Get a LIVE Status for this monitor": "इस मॉनिटर के लिए लाइव स्टेटस प्राप्त करें",
"IDENTIFIED": "पहचाना गया",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "%lastedFor तक चला",
"Light": "लाइट",
"Link Copied": "लिंक कॉपी किया गया",
"Login": "लॉगिन",
"MAINTENANCE": "मेंटेनेंस",
"MONITORING": "निगरानी",
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
"Maintenance in Progress": "मेंटेनेंस जारी है",
"Manage Subscription": "सब्सक्रिप्शन प्रबंधित करें",
"Mode": "मोड",
"No Data": "कोई डेटा नहीं",
"No Incident in %date": "%date को कोई घटना नहीं",
@@ -40,11 +54,16 @@
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
"Ongoing Incidents": "चल रही घटनाएँ",
"Pinging": "पिंगिंग",
"Please select specific monitors to receive updates from.": "अपडेट प्राप्त करने के लिए विशिष्ट मॉनिटर चुनें।",
"RESOLVED": "सुलझाया गया",
"Recent Incidents": "हाल की घटनाएँ",
"Recent Maintenances": "हाल की मेंटेनेंस",
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
"Share": "साझा करें",
"Some Systems are not working as expected": "कुछ सिस्टम अपेक्षित रूप से काम नहीं कर रहे हैं",
"Some Systems Degraded": "कुछ सिस्टम क्षीण हैं",
"Some Systems Down": "कुछ सिस्टम डाउन हैं",
"Some Systems in Maintenance": "कुछ सिस्टम रखरखाव में हैं",
"Standard": "मानक",
"Started %startedAt, lasted for %lastedFor": "%startedAt शुरू हुआ, %lastedFor तक चला",
"Started %startedAt, still ongoing": "%startedAt शुरू हुआ, अभी भी जारी है",
@@ -53,13 +72,21 @@
"Starts %startedAt": "%startedAt में शुरू होगा",
"Status OK": "स्थिति ठीक है",
"Status": "स्थिति",
"Subscribe": "सब्सक्राइब करें",
"Subscribe to Updates": "अपडेट्स के लिए सब्सक्राइब करें",
"Subscribe to all monitors": "सभी मॉनिटर को सब्सक्राइब करें",
"Switch to your timezone": "अपने समय क्षेत्र में स्विच करें",
"Theme": "थीम",
"There are no upcoming maintenances": "कोई आगामी मेंटेनेंस नहीं है",
"Timezone set to %tz": "समय क्षेत्र सेट किया गया %tz",
"Today": "आज",
"UP": "अप",
"Update Subscription": "सब्सक्रिप्शन अपडेट करें",
"Upcoming Maintenance": "आगामी मेंटेनेंस",
"Upcoming Maintenances": "आगामी मेंटेनेंस सूची",
"Updates": "अपडेट्स",
"Uptime": "अपटाइम",
"View in detail": "विस्तार में देखें"
"View in detail": "विस्तार में देखें",
"We have sent a code to your email. Please enter it below to confirm your login": "हमने आपके ईमेल पर एक कोड भेजा है। लॉगिन की पुष्टि करने के लिए इसे नीचे दर्ज करें",
"You are logged in as %email": "आप %email के रूप में लॉग इन हैं"
}
+93
View File
@@ -0,0 +1,93 @@
{
"%status for %duration": "%status per %duration",
"120 Days": "120 Giorni",
"14 Days": "14 Giorni",
"150 Days": "150 Giorni",
"180 Days": "180 Giorni",
"30 Days": "30 Giorni",
"60 Days": "60 Giorni",
"7 Days": "7 Giorni",
"90 Days": "90 Giorni",
"Availability per Component": "Disponibilità per componente",
"All Systems are Degraded": "Tutti i sistemi sono degradati",
"All Systems are Down": "Tutti i sistemi sono inattivi",
"All Systems are Operational": "Tutti i sistemi sono operativi",
"All Systems are in Maintenance": "Tutti i sistemi sono in manutenzione",
"Back": "Indietro",
"Badge Copied": "Badge copiato",
"Badge": "Badge",
"Browse Events": "Sfoglia eventi",
"Change Email": "Cambia email",
"Code Copied": "Codice copiato",
"Confirm Login": "Conferma accesso",
"Copy Code": "Copia codice",
"Copy Link": "Copia link",
"DEGRADED": "DEGRADATO",
"DOWN": "NON DISPONIBILE",
"Dark": "Scuro",
"Days": "Giorni",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Incorpora questo monitor usando <script> o <iframe> nella tua app.",
"Embed": "Incorpora",
"Enter the code": "Inserisci il codice",
"Enter your email": "Inserisci la tua email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Inserisci la tua email per accedere. Se non sei iscritto, ti verrà chiesto di iscriverti.",
"Get SVG badge for this monitor": "Ottieni badge SVG per questo monitor",
"Get a LIVE Status for this monitor": "Ottieni lo stato LIVE per questo monitor",
"IDENTIFIED": "IDENTIFICATO",
"INVESTIGATING": "IN ANALISI",
"Incident Updates": "Aggiornamenti incidenti",
"LIVE Status": "Stato LIVE",
"Lasted for about %lastedFor": "Durato circa %lastedFor",
"Light": "Chiaro",
"Link Copied": "Link copiato",
"Login": "Accedi",
"MAINTENANCE": "MANUTENZIONE",
"MONITORING": "MONITORAGGIO",
"Maintenance Completed": "Manutenzione completata",
"Maintenance in Progress": "Manutenzione in corso",
"Manage Subscription": "Gestisci iscrizione",
"Mode": "Modalità",
"No Data": "Nessun dato",
"No Incident in %date": "Nessun incidente in %date",
"No Incidents": "Nessun incidente",
"No Monitor Found": "Nessun monitor trovato",
"No Updates Yet": "Nessun aggiornamento ancora",
"Ongoing Incidents": "Incidenti in corso",
"Pinging": "Ping in corso",
"Please select specific monitors to receive updates from.": "Seleziona i monitor specifici da cui ricevere aggiornamenti.",
"RESOLVED": "RISOLTO",
"Recent Incidents": "Incidenti recenti",
"Recent Maintenances": "Manutenzioni recenti",
"Share this monitor using a link with others": "Condividi questo monitor tramite un link con altri",
"Share": "Condividi",
"Some Systems are not working as expected": "Alcuni sistemi non funzionano come previsto",
"Some Systems Degraded": "Alcuni sistemi degradati",
"Some Systems Down": "Alcuni sistemi inattivi",
"Some Systems in Maintenance": "Alcuni sistemi in manutenzione",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Iniziato il %startedAt, durato %lastedFor",
"Started %startedAt, still ongoing": "Iniziato il %startedAt, ancora in corso",
"Started %startedAt, will last for %lastedFor more": "Iniziato il %startedAt, durerà ancora %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Inizia il %startedAt, durerà %lastedFor",
"Starts %startedAt": "Inizia il %startedAt",
"Status OK": "Stato OK",
"Status": "Stato",
"Subscribe": "Iscriviti",
"Subscribe to Updates": "Iscriviti agli aggiornamenti",
"Subscribe to all monitors": "Iscriviti a tutti i monitor",
"Switch to your timezone": "Passa al tuo fuso orario",
"Theme": "Tema",
"There are no upcoming maintenances": "Non ci sono manutenzioni imminenti",
"Timezone set to %tz": "Fuso orario impostato su %tz",
"Today": "Oggi",
"UP": "ATTIVO",
"Update Subscription": "Aggiorna iscrizione",
"Upcoming Maintenance": "Manutenzione imminente",
"Upcoming Maintenances": "Manutenzioni imminenti",
"Updates": "Aggiornamenti",
"Uptime": "Tempo di attività",
"View in detail": "Visualizza nel dettaglio",
"We have sent a code to your email. Please enter it below to confirm your login": "Abbiamo inviato un codice alla tua email. Inseriscilo qui sotto per confermare l'accesso",
"You are logged in as %email": "Sei connesso come %email"
}
+28 -1
View File
@@ -1,16 +1,25 @@
{
"%status for %duration": "%duration の間 %status",
"120 Days": "120日間",
"14 Days": "14日間",
"150 Days": "150日間",
"180 Days": "180日間",
"30 Days": "30日間",
"60 Days": "60日間",
"7 Days": "7日間",
"90 Days": "90日間",
"Availability per Component": "コンポーネントごとの可用性",
"All Systems are Degraded": "すべてのシステムが劣化しています",
"All Systems are Down": "すべてのシステムがダウンしています",
"All Systems are Operational": "すべてのシステムが正常に稼働しています",
"All Systems are in Maintenance": "すべてのシステムがメンテナンス中です",
"Back": "戻る",
"Badge Copied": "バッジがコピーされました",
"Badge": "バッジ",
"Browse Events": "イベントを閲覧",
"Change Email": "メールを変更",
"Code Copied": "コードがコピーされました",
"Confirm Login": "ログインを確認",
"Copy Code": "コードをコピー",
"Copy Link": "リンクをコピー",
"DEGRADED": "劣化",
@@ -19,6 +28,9 @@
"Days": "日間",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "このモニターをアプリに <script> または <iframe> を使用して埋め込む。",
"Embed": "埋め込み",
"Enter the code": "コードを入力してください",
"Enter your email": "メールアドレスを入力してください",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "ログインするにはメールアドレスを入力してください。登録されていない場合は、購読を促されます。",
"Get SVG badge for this monitor": "このモニターのSVGバッジを取得",
"Get a LIVE Status for this monitor": "このモニターのライブステータスを取得",
"IDENTIFIED": "識別済み",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "約 %lastedFor 続きました",
"Light": "ライト",
"Link Copied": "リンクがコピーされました",
"Login": "ログイン",
"MAINTENANCE": "メンテナンス",
"MONITORING": "モニタリング中",
"Maintenance Completed": "メンテナンス完了",
"Maintenance in Progress": "メンテナンス中",
"Manage Subscription": "購読を管理",
"Mode": "モード",
"No Data": "データなし",
"No Incident in %date": "%date にインシデントなし",
@@ -40,11 +54,16 @@
"No Updates Yet": "まだ更新はありません",
"Ongoing Incidents": "進行中のインシデント",
"Pinging": "ピング中",
"Please select specific monitors to receive updates from.": "更新を受け取る特定のモニターを選択してください。",
"RESOLVED": "解決済み",
"Recent Incidents": "最近のインシデント",
"Recent Maintenances": "最近のメンテナンス",
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
"Share": "共有",
"Some Systems are not working as expected": "一部のシステムが期待通りに動作していません",
"Some Systems Degraded": "一部のシステムが劣化しています",
"Some Systems Down": "一部のシステムがダウンしています",
"Some Systems in Maintenance": "いくつかのシステムがメンテナンス中です",
"Standard": "標準",
"Started %startedAt, lasted for %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
"Started %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
@@ -53,13 +72,21 @@
"Starts %startedAt": "%startedAt 後に開始",
"Status OK": "ステータス正常",
"Status": "ステータス",
"Subscribe": "購読する",
"Subscribe to Updates": "更新を購読する",
"Subscribe to all monitors": "すべてのモニターを購読する",
"Switch to your timezone": "あなたのタイムゾーンに切り替え",
"Theme": "テーマ",
"There are no upcoming maintenances": "予定されているメンテナンスはありません",
"Timezone set to %tz": "タイムゾーンを %tz に設定",
"Today": "今日",
"UP": "稼働中",
"Update Subscription": "購読を更新する",
"Upcoming Maintenance": "今後のメンテナンス",
"Upcoming Maintenances": "今後のメンテナンス一覧",
"Updates": "更新",
"Uptime": "稼働時間",
"View in detail": "詳細を表示"
"View in detail": "詳細を表示",
"We have sent a code to your email. Please enter it below to confirm your login": "メールにコードを送信しました。ログインを確認するには、以下に入力してください",
"You are logged in as %email": "%email としてログインしています"
}
+57 -30
View File
@@ -1,17 +1,26 @@
{
"%status for %duration": "%duration 동안 %status 상태였음",
"14 Days": "14 일",
"30 Days": "30 ",
"60 Days": "60 일",
"7 Days": "7 일",
"90 Days": "90 일",
"Availability per Component": "항목당 가용성",
"%status for %duration": "%duration 동안 %status 상태",
"120 Days": "120일",
"14 Days": "14일",
"150 Days": "150일",
"180 Days": "180일",
"30 Days": "30일",
"60 Days": "60일",
"7 Days": "7일",
"90 Days": "90일",
"Availability per Component": "항목 당 가용성",
"All Systems are Degraded": "모든 시스템이 성능 저하 상태입니다",
"All Systems are Down": "모든 시스템이 중단되었습니다",
"All Systems are Operational": "모든 시스템이 정상 작동 중입니다",
"All Systems are in Maintenance": "모든 시스템이 유지 보수 중입니다",
"Back": "뒤로",
"Badge Copied": "배지 복사됨",
"Badge Copied": "배지 복사 됨",
"Badge": "배지",
"Browse Events": "이벤트 찾아보기",
"Code Copied": "소스코드 복사됨",
"Copy Code": "소스코드 복사",
"Change Email": "이메일 변경",
"Code Copied": "소스 코드 복사",
"Confirm Login": "로그인 확인",
"Copy Code": "소스 코드 복사",
"Copy Link": "링크 복사",
"DEGRADED": "성능 저하됨",
"DOWN": "중단",
@@ -19,47 +28,65 @@
"Days": "일",
"Embed this monitor using &#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": "확인됨",
"Enter the code": "코드를 입력하세요",
"Enter your email": "이메일을 입력하세요",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "로그인하려면 이메일을 입력하세요. 구독하지 않은 경우 구독 요청이 표시됩니다.",
"Get SVG badge for this monitor": "이 모니터링 항목에 대한 SVG 배지를 얻어보세요.",
"Get a LIVE Status for this monitor": "이 모니터링 항목에 대한 실시간 상태를 얻어보세요.",
"IDENTIFIED": "확인 됨",
"INVESTIGATING": "조사 중",
"Incident Updates": "사건 보고서",
"LIVE Status": "실시간 상태",
"Lasted for about %lastedFor": "%lastedFor 동안 지속",
"Light": "라이트",
"Link Copied": "링크 복사됨",
"Light": "밝게",
"Link Copied": "링크 복사 됨",
"Login": "로그인",
"MAINTENANCE": "유지보수",
"MONITORING": "감시 중",
"Maintenance Completed": "유지보수 완료",
"Maintenance in Progress": "유지보수 진행 중",
"Maintenance Completed": "유지 보수 완료",
"Maintenance in Progress": "유지 보수 진행 중",
"Manage Subscription": "구독 관리",
"Mode": "모드",
"No Data": "데이터 없음",
"No Incident in %date": "%date 에는 아무 사건도 없었음",
"No Incident in %date": "%date에는 아무 사건도 없었음",
"No Incidents": "사건 없음",
"No Monitor Found": "모니터링 항목을 찾을 수 없음",
"No Updates Yet": "아직 아무 업데이트도 없음",
"Ongoing Incidents": "진행중 사건",
"Ongoing Incidents": "진행 중 사건",
"Pinging": "핑 요청",
"RESOLVED": "해결됨",
"Please select specific monitors to receive updates from.": "업데이트를 받을 특정 모니터를 선택하세요.",
"RESOLVED": "해결 됨",
"Recent Incidents": "최근 사건",
"Recent Maintenances": "최근 유지보수",
"Recent Maintenances": "최근 유지 보수",
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
"Share": "공유",
"Some Systems are not working as expected": "일부 시스템이 예상대로 작동하지 않습니다",
"Some Systems Degraded": "일부 시스템이 성능 저하 상태입니다",
"Some Systems Down": "일부 시스템이 중단되었습니다",
"Some Systems in Maintenance": "일부 시스템이 유지 보수 중입니다",
"Standard": "기본",
"Started %startedAt, lasted for %lastedFor": "%startedAt에 시작되었으며, %lastedFor 동안 지속되습니다",
"Started %startedAt, still ongoing": "%startedAt에 시작되었으며, 아직 진행중입니다",
"Started %startedAt, will last for %lastedFor more": "%startedAt에 시작되었으며, %lastedFor 더 지속될 예정입니다",
"Starts %startedAt, will last for %lastedFor": "%startedAt에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
"Starts %startedAt": "%startedAt 에 시작되었습니다",
"Started %startedAt, lasted for %lastedFor": "%startedAt에 시작되었으며, %lastedFor 동안 지속되고 있습니다",
"Started %startedAt, still ongoing": "%startedAt에 시작되었으며, 아직 진행 중입니다",
"Started %startedAt, will last for %lastedFor more": "%startedAt에 시작되었으며, %lastedFor 더 지속될 예정입니다",
"Starts %startedAt, will last for %lastedFor": "%startedAt에 시작되며, %lastedFor 지속될 예정입니다",
"Starts %startedAt": "%startedAt에 시작되었습니다",
"Status OK": "양호",
"Status": "상태",
"Switch to your timezone": "귀하의 시간대로 변경",
"Subscribe": "구독",
"Subscribe to Updates": "업데이트 구독하기",
"Subscribe to all monitors": "모든 모니터 구독",
"Switch to your timezone": "내 시간대로 변경",
"Theme": "테마",
"Timezone set to %tz": "시간대 설정 %tz",
"There are no upcoming maintenances": "예정된 유지 보수가 없습니다",
"Timezone set to %tz": "%tz(으)로 시간대 설정",
"Today": "오늘",
"UP": "작동 중",
"Upcoming Maintenance": "예정된 유지보수",
"Update Subscription": "구독 업데이트",
"Upcoming Maintenance": "예정된 유지 보수",
"Upcoming Maintenances": "예정된 유지 보수 목록",
"Updates": "업데이트",
"Uptime": "업타임",
"View in detail": "자세히 보기"
"View in detail": "자세히 보기",
"We have sent a code to your email. Please enter it below to confirm your login": "이메일로 코드를 보냈습니다. 로그인을 확인하려면 아래에 입력하세요",
"You are logged in as %email": "%email로 로그인했습니다"
}
+16
View File
@@ -54,5 +54,21 @@
{
"code": "nb-NO",
"name": "Norsk Bokmål"
},
{
"code": "pl",
"name": "Polski"
},
{
"code": "es",
"name": "Español"
},
{
"code": "it",
"name": "Italiano"
},
{
"code": "fa",
"name": "Persian (فارسی)"
}
]
+28 -1
View File
@@ -5,12 +5,21 @@
"60 Days": "60 dager",
"7 Days": "7 dager",
"90 Days": "90 dager",
"120 Days": "120 dager",
"150 Days": "150 dager",
"180 Days": "180 dager",
"Availability per Component": "Tilgjengelighet per komponent",
"All Systems are Degraded": "Alle systemer har nedsatt ytelse",
"All Systems are Down": "Alle systemer er nede",
"All Systems are Operational": "Alle systemer er operative",
"All Systems are in Maintenance": "Alle systemer er til vedlikehold",
"Back": "Tilbake",
"Badge Copied": "Merke kopiert",
"Badge": "Merke",
"Browse Events": "Bla gjennom hendelser",
"Change Email": "Endre e-post",
"Code Copied": "Kode kopiert",
"Confirm Login": "Bekreft pålogging",
"Copy Code": "Kopier kode",
"Copy Link": "Kopier lenke",
"DEGRADED": "NEDSATT",
@@ -19,6 +28,9 @@
"Days": "Dager",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Bygg inn denne monitoren ved å bruke <script> eller <iframe> i appen din.",
"Embed": "Bygg inn",
"Enter the code": "Skriv inn koden",
"Enter your email": "Skriv inn din e-post",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Skriv inn din e-post for å logge inn. Hvis du ikke er abonnent, vil du bli bedt om å abonnere.",
"Get SVG badge for this monitor": "Hent SVG-merke for denne monitoren",
"Get a LIVE Status for this monitor": "Få en LIVE-status for denne monitoren",
"IDENTIFIED": "IDENTIFISERT",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "Varte i omtrent %lastedFor",
"Light": "Lys",
"Link Copied": "Lenke kopiert",
"Login": "Logg inn",
"MAINTENANCE": "VEDLIKEHOLD",
"MONITORING": "OVERVÅKER",
"Maintenance Completed": "Vedlikehold fullført",
"Maintenance in Progress": "Vedlikehold pågår",
"Manage Subscription": "Administrer abonnement",
"Mode": "Modus",
"No Data": "Ingen data",
"No Incident in %date": "Ingen hendelser på %date",
@@ -40,11 +54,16 @@
"No Updates Yet": "Ingen oppdateringer ennå",
"Ongoing Incidents": "Pågående hendelser",
"Pinging": "Pinger",
"Please select specific monitors to receive updates from.": "Vennligst velg spesifikke monitorer å motta oppdateringer fra.",
"RESOLVED": "LØST",
"Recent Incidents": "Nylige hendelser",
"Recent Maintenances": "Nylige vedlikehold",
"Share this monitor using a link with others": "Del denne monitoren med andre via lenke",
"Share": "Del",
"Some Systems are not working as expected": "Noen systemer fungerer ikke som forventet",
"Some Systems Degraded": "Noen systemer har nedsatt ytelse",
"Some Systems Down": "Noen systemer er nede",
"Some Systems in Maintenance": "Noen systemer er til vedlikehold",
"Standard": "Standard",
"Started %startedAt, lasted for %lastedFor": "Startet %startedAt, varte i %lastedFor",
"Started %startedAt, still ongoing": "Startet %startedAt, fortsatt pågår",
@@ -53,13 +72,21 @@
"Starts %startedAt": "Starter %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Abonner",
"Subscribe to Updates": "Abonner på oppdateringer",
"Subscribe to all monitors": "Abonner på alle monitorer",
"Switch to your timezone": "Bytt til din tidssone",
"Theme": "Tema",
"There are no upcoming maintenances": "Det er ingen kommende vedlikeholdsperioder",
"Timezone set to %tz": "Tidssone satt til %tz",
"Today": "I dag",
"UP": "OPPE",
"Update Subscription": "Oppdater abonnement",
"Upcoming Maintenance": "Kommende vedlikehold",
"Upcoming Maintenances": "Kommende vedlikeholdsperioder",
"Updates": "Oppdateringer",
"Uptime": "Oppetid",
"View in detail": "Vis detaljer"
"View in detail": "Vis detaljer",
"We have sent a code to your email. Please enter it below to confirm your login": "Vi har sendt en kode til din e-post. Vennligst skriv den inn nedenfor for å bekrefte påloggingen",
"You are logged in as %email": "Du er logget inn som %email"
}
+28 -1
View File
@@ -1,16 +1,25 @@
{
"%status for %duration": "%status voor %duration",
"120 Days": "120 dagen",
"14 Days": "14 dagen",
"150 Days": "150 dagen",
"180 Days": "180 dagen",
"30 Days": "30 dagen",
"60 Days": "60 dagen",
"7 Days": "7 dagen",
"90 Days": "90 dagen",
"Availability per Component": "Beschikbaarheid per component",
"All Systems are Degraded": "Alle systemen werken met beperkte functionaliteit",
"All Systems are Down": "Alle systemen zijn niet bereikbaar",
"All Systems are Operational": "Alle systemen zijn operationeel",
"All Systems are in Maintenance": "Alle systemen zijn in onderhoud",
"Back": "Terug",
"Badge Copied": "Badge gekopieerd",
"Badge": "Badge",
"Browse Events": "Evenementen bekijken",
"Change Email": "E-mail wijzigen",
"Code Copied": "Code gekopieerd",
"Confirm Login": "Inloggen bevestigen",
"Copy Code": "Kopieer code",
"Copy Link": "Kopieer link",
"DEGRADED": "BEPERKT",
@@ -19,6 +28,9 @@
"Days": "Dagen",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Sluit deze monitor in met behulp van <script> of <iframe> in je app.",
"Embed": "Insluiten",
"Enter the code": "Voer de code in",
"Enter your email": "Voer je e-mailadres in",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Voer je e-mailadres in om in te loggen. Als je nog niet geabonneerd bent, word je gevraagd om je te abonneren.",
"Get SVG badge for this monitor": "SVG-badge ophalen voor deze monitor",
"Get a LIVE Status for this monitor": "LIVE-status ophalen voor deze monitor",
"IDENTIFIED": "GEIDENTIFICEERD",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "Duurde ongeveer %lastedFor",
"Light": "Licht",
"Link Copied": "Link gekopieerd",
"Login": "Inloggen",
"MAINTENANCE": "ONDERHOUD",
"MONITORING": "MONITOREN",
"Maintenance Completed": "Onderhoud voltooid",
"Maintenance in Progress": "Onderhoud bezig",
"Manage Subscription": "Abonnement beheren",
"Mode": "Modus",
"No Data": "Geen gegevens",
"No Incident in %date": "Geen incident op %date",
@@ -40,11 +54,16 @@
"No Updates Yet": "Nog geen updates",
"Ongoing Incidents": "Lopende incidenten",
"Pinging": "Pingen",
"Please select specific monitors to receive updates from.": "Selecteer specifieke monitoren waarvan je updates wilt ontvangen.",
"RESOLVED": "OPGELOST",
"Recent Incidents": "Recente incidenten",
"Recent Maintenances": "Recente onderhoudsbeurten",
"Share this monitor using a link with others": "Deel deze monitor met anderen via een link",
"Share": "Delen",
"Some Systems are not working as expected": "Sommige systemen werken niet zoals verwacht",
"Some Systems Degraded": "Sommige systemen werken met beperkte functionaliteit",
"Some Systems Down": "Sommige systemen zijn niet bereikbaar",
"Some Systems in Maintenance": "Sommige systemen zijn in onderhoud",
"Standard": "Standaard",
"Started %startedAt, lasted for %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
"Started %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
@@ -53,13 +72,21 @@
"Starts %startedAt": "Begint over %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Abonneren",
"Subscribe to Updates": "Abonneren op updates",
"Subscribe to all monitors": "Abonneren op alle monitoren",
"Switch to your timezone": "Schakel over naar je tijdzone",
"Theme": "Thema",
"There are no upcoming maintenances": "Er zijn geen geplande onderhoudsperiodes",
"Timezone set to %tz": "Tijdzone ingesteld op %tz",
"Today": "Vandaag",
"UP": "BEREIKBAAR",
"Update Subscription": "Abonnement bijwerken",
"Upcoming Maintenance": "Aankomend onderhoud",
"Upcoming Maintenances": "Aankomende onderhoudsperiodes",
"Updates": "Updates",
"Uptime": "Uptime",
"View in detail": "Bekijk in detail"
"View in detail": "Bekijk in detail",
"We have sent a code to your email. Please enter it below to confirm your login": "We hebben een code naar je e-mailadres gestuurd. Voer deze hieronder in om je login te bevestigen",
"You are logged in as %email": "Je bent ingelogd als %email"
}
+92
View File
@@ -0,0 +1,92 @@
{
"%status for %duration": "%status przez %duration",
"120 Days": "120 dni",
"14 Days": "14 dni",
"150 Days": "150 dni",
"180 Days": "180 dni",
"30 Days": "30 dni",
"60 Days": "60 dni",
"7 Days": "7 dni",
"90 Days": "90 dni",
"Availability per Component": "Dostępność według komponentu",
"All Systems are Degraded": "Wszystkie systemy działają z ograniczeniami",
"All Systems are Down": "Wszystkie systemy są niedostępne",
"All Systems are Operational": "Wszystkie systemy działają poprawnie",
"All Systems are in Maintenance": "Wszystkie systemy są w konserwacji",
"Back": "Wstecz",
"Badge Copied": "Odznaka skopiowana",
"Badge": "Odznaka",
"Browse Events": "Przeglądaj zdarzenia",
"Change Email": "Zmień email",
"Code Copied": "Kod skopiowany",
"Confirm Login": "Potwierdź logowanie",
"Copy Code": "Skopiuj kod",
"Copy Link": "Skopiuj link",
"DEGRADED": "OGRANICZONA",
"DOWN": "NIEDOSTĘPNY",
"Dark": "Ciemny",
"Days": "Dni",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Osadź ten monitor za pomocą <script> lub <iframe> w swojej aplikacji.",
"Embed": "Osadź",
"Enter the code": "Wprowadź kod",
"Enter your email": "Wprowadź swój email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Wprowadź swój email, aby się zalogować. Jeśli nie masz subskrypcji, zostaniesz poproszony o jej utworzenie.",
"Get SVG badge for this monitor": "Pobierz odznakę SVG dla tego monitora",
"Get a LIVE Status for this monitor": "Uzyskaj STATUS NA ŻYWO tego monitora",
"IDENTIFIED": "ZIDENTYFIKOWANE",
"INVESTIGATING": "BADANE",
"Incident Updates": "Aktualizacje incydentu",
"LIVE Status": "Status NA ŻYWO",
"Lasted for about %lastedFor": "Trwało około %lastedFor",
"Light": "Jasny",
"Link Copied": "Link skopiowany",
"Login": "Zaloguj się",
"MAINTENANCE": "KONSERWACJA",
"MONITORING": "MONITOROWANE",
"Maintenance Completed": "Konserwacja zakończona",
"Maintenance in Progress": "Trwa konserwacja",
"Manage Subscription": "Zarządzaj subskrypcją",
"Mode": "Tryb",
"No Data": "Brak danych",
"No Incident in %date": "Brak incydentów w dniu %date",
"No Incidents": "Brak incydentów",
"No Monitor Found": "Nie znaleziono monitora",
"No Updates Yet": "Brak aktualizacji",
"Ongoing Incidents": "Trwające incydenty",
"Pinging": "Pingowanie",
"Please select specific monitors to receive updates from.": "Wybierz konkretne monitory, z których chcesz otrzymywać aktualizacje.",
"RESOLVED": "ROZWIĄZANE",
"Recent Incidents": "Ostatnie incydenty",
"Recent Maintenances": "Ostatnie konserwacje",
"Share this monitor using a link with others": "Udostępnij ten monitor za pomocą linku innym osobom",
"Share": "Udostępnij",
"Some Systems are not working as expected": "Niektóre systemy nie działają zgodnie z oczekiwaniami",
"Some Systems Degraded": "Niektóre systemy działają z ograniczeniami",
"Some Systems Down": "Niektóre systemy są niedostępne",
"Some Systems in Maintenance": "Niektóre systemy są w konserwacji",
"Standard": "Standardowy",
"Started %startedAt, lasted for %lastedFor": "Rozpoczęto %startedAt, trwało %lastedFor",
"Started %startedAt, still ongoing": "Rozpoczęto %startedAt, nadal trwa",
"Started %startedAt, will last for %lastedFor more": "Rozpoczęto %startedAt, potrwa jeszcze %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Rozpocznie się %startedAt, potrwa %lastedFor",
"Starts %startedAt": "Rozpocznie się %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Subskrybuj",
"Subscribe to Updates": "Subskrybuj aktualizacje",
"Subscribe to all monitors": "Subskrybuj wszystkie monitory",
"Switch to your timezone": "Przełącz na swoją strefę czasową",
"Theme": "Motyw",
"There are no upcoming maintenances": "Nie ma nadchodzących konserwacji",
"Timezone set to %tz": "Strefa czasowa ustawiona na %tz",
"Today": "Dzisiaj",
"UP": "DOSTĘPNY",
"Update Subscription": "Aktualizuj subskrypcję",
"Upcoming Maintenance": "Nadchodząca konserwacja",
"Upcoming Maintenances": "Nadchodzące konserwacje",
"Updates": "Aktualizacje",
"Uptime": "Czas działania",
"View in detail": "Zobacz szczegóły",
"We have sent a code to your email. Please enter it below to confirm your login": "Wysłaliśmy kod na Twój adres email. Wprowadź go poniżej, aby potwierdzić logowanie",
"You are logged in as %email": "Jesteś zalogowany jako %email"
}
+28 -1
View File
@@ -1,16 +1,25 @@
{
"%status for %duration": "%status por %duration",
"120 Days": "120 Dias",
"14 Days": "14 Dias",
"150 Days": "150 Dias",
"180 Days": "180 Dias",
"30 Days": "30 Dias",
"60 Days": "60 Dias",
"7 Days": "7 Dias",
"90 Days": "90 Dias",
"Availability per Component": "Disponibilidade por Componente",
"All Systems are Degraded": "Todos os Sistemas estão com Falhas",
"All Systems are Down": "Todos os Sistemas estão Indisponíveis",
"All Systems are Operational": "Todos os Sistemas estão Operacionais",
"All Systems are in Maintenance": "Todos os Sistemas estão em Manutenção",
"Back": "Voltar",
"Badge Copied": "Emblema Copiado",
"Badge": "Emblema",
"Browse Events": "Navegar Eventos",
"Change Email": "Alterar E-mail",
"Code Copied": "Código Copiado",
"Confirm Login": "Confirmar Login",
"Copy Code": "Copiar Código",
"Copy Link": "Copiar Link",
"DEGRADED": "FALHA",
@@ -19,6 +28,9 @@
"Days": "Dias",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
"Embed": "Incorporar",
"Enter the code": "Digite o código",
"Enter your email": "Digite seu e-mail",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Digite seu e-mail para fazer login. Se você não estiver inscrito, você será solicitado a se inscrever.",
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
"IDENTIFIED": "IDENTIFICADO",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
"Light": "Claro",
"Link Copied": "Link Copiado",
"Login": "Entrar",
"MAINTENANCE": "MANUTENÇÃO",
"MONITORING": "MONITORANDO",
"Maintenance Completed": "Manutenção Concluída",
"Maintenance in Progress": "Manutenção em Progresso",
"Manage Subscription": "Gerenciar Inscrição",
"Mode": "Modo",
"No Data": "Sem dados",
"No Incident in %date": "Nenhum incidente em %date",
@@ -40,11 +54,16 @@
"No Updates Yet": "Nenhuma atualização ainda",
"Ongoing Incidents": "Incidentes em Andamento",
"Pinging": "Animado",
"Please select specific monitors to receive updates from.": "Por favor, selecione monitores específicos para receber atualizações.",
"RESOLVED": "RESOLVIDO",
"Recent Incidents": "Incidentes Recentes",
"Recent Maintenances": "Manutenções Recentes",
"Share this monitor using a link with others": "Compartilhar este monitor através do link",
"Share": "Compartilhar",
"Some Systems are not working as expected": "Alguns Sistemas não estão funcionando como esperado",
"Some Systems Degraded": "Alguns Sistemas estão com Falhas",
"Some Systems Down": "Alguns Sistemas estão Indisponíveis",
"Some Systems in Maintenance": "Alguns Sistemas estão em Manutenção",
"Standard": "Padrão",
"Started %startedAt, lasted for %lastedFor": "Ocorreu faz %startedAt e durou %lastedFor",
"Started %startedAt, still ongoing": "Iniciou faz %startedAt, ainda em andamento",
@@ -53,13 +72,21 @@
"Starts %startedAt": "Inicia em %startedAt",
"Status OK": "Status OK",
"Status": "Status",
"Subscribe": "Inscrever-se",
"Subscribe to Updates": "Inscrever-se para atualizações",
"Subscribe to all monitors": "Inscrever-se em todos os monitores",
"Switch to your timezone": "Mudar para o seu fuso horário",
"Theme": "Tema",
"There are no upcoming maintenances": "Não há manutenções agendadas",
"Timezone set to %tz": "Fuso horário definido para %tz",
"Today": "Hoje",
"UP": "OPERACIONAL",
"Update Subscription": "Atualizar Inscrição",
"Upcoming Maintenance": "Manutenção Agendada",
"Upcoming Maintenances": "Manutenções Agendadas",
"Updates": "Atualizações",
"Uptime": "Tempo de Atividade",
"View in detail": "Ver em detalhes"
"View in detail": "Ver em detalhes",
"We have sent a code to your email. Please enter it below to confirm your login": "Enviamos um código para seu e-mail. Por favor, digite-o abaixo para confirmar seu login",
"You are logged in as %email": "Você está logado como %email"
}
+60 -33
View File
@@ -1,65 +1,92 @@
{
"%status for %duration": "%status в течение %duration",
"14 Days": "14 Дней",
"30 Days": "30 Дней",
"60 Days": "60 Дней",
"7 Days": "7 Дней",
"90 Days": "90 Дней",
"120 Days": "120 дней",
"14 Days": "14 дней",
"150 Days": "150 дней",
"180 Days": "180 дней",
"30 Days": "30 дней",
"60 Days": "60 дней",
"7 Days": "7 дней",
"90 Days": "90 дней",
"Availability per Component": "Доступность по компонентам",
"All Systems are Degraded": "Системы работают нестабильно",
"All Systems are Down": "Системы не работают",
"All Systems are Operational": "Системы работают нормально",
"All Systems are in Maintenance": "Системы на обслуживании",
"Back": "Назад",
"Badge Copied": "Значок скопирован",
"Badge": "Значок",
"Browse Events": "Просмотр событий",
"Change Email": "Изменить электронную почту",
"Code Copied": "Код скопирован",
"Copy Code": "Копировать код",
"Copy Link": "Копировать ссылку",
"DEGRADED": "УХУДШЕНО",
"DOWN": "НЕДОСТУПНО",
"Dark": "Темная",
"Days": "Дней",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Встройте этот монитор используя <script> или <iframe> в ваше приложение.",
"Embed": "Встроить",
"Confirm Login": "Подтвердите вход",
"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": "Вставить",
"Enter the code": "Введите код",
"Enter your email": "Введите ваш email",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Введите ваш email для входа. Если вы не подписаны, вам будет предложено подписаться.",
"Get SVG badge for this monitor": "Получить SVG значок для этого монитора",
"Get a LIVE Status for this monitor": "Получить ЖИВОЙ статус для этого монитора",
"Get a LIVE Status for this monitor": "Получить текущий статус для этого монитора",
"IDENTIFIED": "ВЫЯВЛЕНО",
"INVESTIGATING": ССЛЕДУЕТСЯ",
"Incident Updates": "Обновления инцидента",
"LIVE Status": "ЖИВОЙ статус",
"Lasted for about %lastedFor": "Длилось около %lastedFor",
"INVESTIGATING": ЗУЧАЕТСЯ",
"Incident Updates": "Обновления инцидентов",
"LIVE Status": "Текущий статус",
"Lasted for about %lastedFor": "Продолжалось около %lastedFor",
"Light": "Светлая",
"Link Copied": "Ссылка скопирована",
"Login": "Войти",
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
"MONITORING": "МОНИТОРИНГ",
"Maintenance Completed": "Техническое обслуживание завершено",
"Maintenance in Progress": "Техническое обслуживание",
"Maintenance Completed": "Обслуживание завершено",
"Maintenance in Progress": "Обслуживание в процессе",
"Manage Subscription": "Управление подпиской",
"Mode": "Режим",
"No Data": "Нет данных",
"No Incident in %date": "Нет инцидентов на %date",
"No Incidents": "Нет инцидентов",
"No Incident in %date": "Нет инцидентов за %date",
"No Incidents": "Инцидентов нет",
"No Monitor Found": "Монитор не найден",
"No Updates Yet": "Пока нет обновлений",
"No Updates Yet": "Обновлений пока нет",
"Ongoing Incidents": "Текущие инциденты",
"Pinging": ингуется",
"Pinging": роверка доступности",
"Please select specific monitors to receive updates from.": "Пожалуйста, выберите мониторы для получения обновлений.",
"RESOLVED": "РЕШЕНО",
"Recent Incidents": "Недавние инциденты",
"Recent Maintenances": "Последние обслуживания",
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
"Share": "Поделиться",
"Some Systems are not working as expected": "Некоторые системы работают нестабильно",
"Some Systems Degraded": "Некоторые системы работают нестабильно",
"Some Systems Down": "Некоторые системы недоступны",
"Some Systems in Maintenance": "Некоторые системы на обслуживании",
"Standard": "Стандартная",
"Started %startedAt, lasted for %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
"Started %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
"Started %startedAt, will last for %lastedFor more": "Начнется около %startedAt, продлится еще около %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
"Starts %startedAt": "Начнется через %startedAt",
"Started %startedAt, lasted for %lastedFor": "Началось %startedAt, продолжалось %lastedFor",
"Started %startedAt, still ongoing": "Началось %startedAt, всё ещё продолжается",
"Started %startedAt, will last for %lastedFor more": "Началось %startedAt, продлится ещё около %lastedFor",
"Starts %startedAt, will last for %lastedFor": "Начнётся %startedAt, продлится %lastedFor",
"Starts %startedAt": "Начнётся %startedAt",
"Status OK": "Статус ОК",
"Status": "Статус",
"Subscribe": "Подписаться",
"Subscribe to Updates": "Подписаться на обновления",
"Subscribe to all monitors": "Подписаться на все мониторы",
"Switch to your timezone": "Переключиться на ваш часовой пояс",
"Theme": "Тема",
"Timezone set to %tz": "Часовой пояс установлен на %tz",
"There are no upcoming maintenances": "Предстоящих обслуживаний нет",
"Timezone set to %tz": "Часовой пояс установлен: %tz",
"Today": "Сегодня",
"UP": "РАБОТАЕТ",
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
"Update Subscription": "Обновить подписку",
"Upcoming Maintenance": "Предстоящее обслуживание",
"Upcoming Maintenances": "Предстоящие обслуживания",
"Updates": "Обновления",
"Uptime": "Время работы",
"View in detail": "Подробный просмотр"
"Uptime": "Время безотказной работы",
"View in detail": "Подробнее",
"We have sent a code to your email. Please enter it below to confirm your login": "Мы отправили код на вашу почту. Введите его ниже для подтверждения входа",
"You are logged in as %email": "Вы вошли как %email"
}
+28 -1
View File
@@ -5,12 +5,21 @@
"60 Days": "60 Gün",
"7 Days": "7 Gün",
"90 Days": "90 Gün",
"120 Days": "120 Gün",
"150 Days": "150 Gün",
"180 Days": "180 Gün",
"Availability per Component": "Bileşen Kullanılabilirliği",
"All Systems are Degraded": "Tüm Sistemler Bozuk Durumda",
"All Systems are Down": "Tüm Sistemler Çalışmıyor",
"All Systems are Operational": "Tüm Sistemler Çalışıyor",
"All Systems are in Maintenance": "Tüm Sistemler Bakımda",
"Back": "Geri",
"Badge Copied": "Rozet Kopyalandı",
"Badge": "Rozet",
"Browse Events": "Etkinliklere Göz At",
"Change Email": "E-postayı Değiştir",
"Code Copied": "Kod Kopyalandı",
"Confirm Login": "Girişi Onayla",
"Copy Code": "Kodu Kopyala",
"Copy Link": "Bağlantıyı Kopyala",
"DEGRADED": "BOZUK",
@@ -19,6 +28,9 @@
"Days": "Günler",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Uygulamanıza <script> veya <iframe> kullanarak bu hizmeti yerleştirin.",
"Embed": "Yerleştir",
"Enter the code": "Kodu girin",
"Enter your email": "E-posta adresinizi girin",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Giriş yapmak için e-posta adresinizi girin. Abone değilseniz, abone olmanız istenecektir.",
"Get SVG badge for this monitor": "Bu servis için SVG rozet alın",
"Get a LIVE Status for this monitor": "Bu servis için CANLI Durum alın",
"IDENTIFIED": "TANIMLANDI",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "%lastedFor sürdü",
"Light": "Açık",
"Link Copied": "Bağlantı Kopyalandı",
"Login": "Giriş Yap",
"MAINTENANCE": "BAKIM",
"MONITORING": "İZLEME",
"Maintenance Completed": "Bakım Tamamlandı",
"Maintenance in Progress": "Bakım Devam Ediyor",
"Manage Subscription": "Aboneliği Yönet",
"Mode": "Mod",
"No Data": "Veri Yok",
"No Incident in %date": "%date Tarihinde Olay Yok",
@@ -40,11 +54,16 @@
"No Updates Yet": "Henüz Güncelleme Yok",
"Ongoing Incidents": "Devam Eden Olaylar",
"Pinging": "Pinging",
"Please select specific monitors to receive updates from.": "Lütfen güncellemeleri almak istediğiniz belirli servisleri seçin.",
"RESOLVED": "ÇÖZÜLDÜ",
"Recent Incidents": "Son Olaylar",
"Recent Maintenances": "Son Bakımlar",
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
"Share": "Paylaş",
"Some Systems are not working as expected": "Bazı Sistemler Beklendiği Gibi Çalışmıyor",
"Some Systems Degraded": "Bazı Sistemler Bozuk Durumda",
"Some Systems Down": "Bazı Sistemler Çalışmıyor",
"Some Systems in Maintenance": "Bazı Sistemler Bakımda",
"Standard": "Standart",
"Started %startedAt, lasted for %lastedFor": "%startedAt başladı, %lastedFor sürdü",
"Started %startedAt, still ongoing": "%startedAt başladı, devam ediyor",
@@ -53,13 +72,21 @@
"Starts %startedAt": "%startedAt başlıyor",
"Status OK": "Durum UYGUN",
"Status": "Durum",
"Subscribe": "Abone Ol",
"Subscribe to Updates": "Güncellemelere Abone Ol",
"Subscribe to all monitors": "Tüm servislere abone ol",
"Switch to your timezone": "Saat diliminize geçin",
"Theme": "Tema",
"There are no upcoming maintenances": "Yaklaşan bakım bulunmamaktadır",
"Timezone set to %tz": "Saat dilimi %tz olarak ayarlandı",
"Today": "Bugün",
"UP": "ÇALIŞIYOR",
"Update Subscription": "Aboneliği Güncelle",
"Upcoming Maintenance": "Yaklaşan Bakım",
"Upcoming Maintenances": "Yaklaşan Bakımlar",
"Updates": "Güncellemeler",
"Uptime": "Çalışma Süresi",
"View in detail": "Detaylı Görüntüle"
"View in detail": "Detaylı Görüntüle",
"We have sent a code to your email. Please enter it below to confirm your login": "E-posta adresinize bir kod gönderdik. Girişinizi onaylamak için lütfen aşağıya girin",
"You are logged in as %email": "%email olarak giriş yaptınız"
}
+28 -1
View File
@@ -5,12 +5,21 @@
"60 Days": "60 ngày",
"7 Days": "7 ngày",
"90 Days": "90 ngày",
"120 Days": "120 ngày",
"150 Days": "150 ngày",
"180 Days": "180 ngày",
"Availability per Component": "Tính khả dụng theo thành phần",
"All Systems are Degraded": "Tất cả hệ thống đang suy giảm hiệu suất",
"All Systems are Down": "Tất cả hệ thống đang ngừng hoạt động",
"All Systems are Operational": "Tất cả hệ thống đang hoạt động bình thường",
"All Systems are in Maintenance": "Tất cả hệ thống đang bảo trì",
"Back": "Quay lại",
"Badge Copied": "Huy hiệu đã được sao chép",
"Badge": "Huy hiệu",
"Browse Events": "Duyệt sự kiện",
"Change Email": "Thay đổi email",
"Code Copied": "Mã đã được sao chép",
"Confirm Login": "Xác nhận đăng nhập",
"Copy Code": "Sao chép mã",
"Copy Link": "Sao chép liên kết",
"DEGRADED": "Suy giảm",
@@ -19,6 +28,9 @@
"Days": "Ngày",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "Nhúng trình theo dõi này vào ứng dụng của bạn bằng <script> hoặc <iframe>.",
"Embed": "Nhúng",
"Enter the code": "Nhập mã",
"Enter your email": "Nhập email của bạn",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "Nhập email của bạn để đăng nhập. Nếu bạn chưa đăng ký, bạn sẽ được nhắc đăng ký.",
"Get SVG badge for this monitor": "Lấy huy hiệu SVG cho trình theo dõi này",
"Get a LIVE Status for this monitor": "Lấy trạng thái TRỰC TIẾP cho trình theo dõi này",
"IDENTIFIED": "Đã xác định",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "Kéo dài khoảng %lastedFor",
"Light": "Sáng",
"Link Copied": "Liên kết đã được sao chép",
"Login": "Đăng nhập",
"MAINTENANCE": "Bảo trì",
"MONITORING": "Đang giám sát",
"Maintenance Completed": "Bảo trì đã hoàn thành",
"Maintenance in Progress": "Bảo trì đang diễn ra",
"Manage Subscription": "Quản lý đăng ký",
"Mode": "Chế độ",
"No Data": "Không có dữ liệu",
"No Incident in %date": "Không có sự cố vào %date",
@@ -40,11 +54,16 @@
"No Updates Yet": "Chưa có cập nhật",
"Ongoing Incidents": "Sự cố đang diễn ra",
"Pinging": "Đang kiểm tra",
"Please select specific monitors to receive updates from.": "Vui lòng chọn các trình theo dõi cụ thể để nhận cập nhật.",
"RESOLVED": "Đã giải quyết",
"Recent Incidents": "Sự cố gần đây",
"Recent Maintenances": "Bảo trì gần đây",
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
"Share": "Chia sẻ",
"Some Systems are not working as expected": "Một số hệ thống không hoạt động như mong đợi",
"Some Systems Degraded": "Một số hệ thống đang suy giảm hiệu suất",
"Some Systems Down": "Một số hệ thống đang ngừng hoạt động",
"Some Systems in Maintenance": "Một số hệ thống đang bảo trì",
"Standard": "Tiêu chuẩn",
"Started %startedAt, lasted for %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
"Started %startedAt, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
@@ -53,13 +72,21 @@
"Starts %startedAt": "Bắt đầu trong %startedAt",
"Status OK": "Trạng thái OK",
"Status": "Trạng thái",
"Subscribe": "Đăng ký",
"Subscribe to Updates": "Đăng ký nhận cập nhật",
"Subscribe to all monitors": "Đăng ký tất cả trình theo dõi",
"Switch to your timezone": "Chuyển sang múi giờ của bạn",
"Theme": "Giao diện",
"There are no upcoming maintenances": "Không có bảo trì sắp tới nào",
"Timezone set to %tz": "Đã đặt múi giờ %tz",
"Today": "Hôm nay",
"UP": "Hoạt động",
"Update Subscription": "Cập nhật đăng ký",
"Upcoming Maintenance": "Bảo trì sắp tới",
"Upcoming Maintenances": "Các bảo trì sắp tới",
"Updates": "Cập nhật",
"Uptime": "Thời gian hoạt động",
"View in detail": "Xem chi tiết"
"View in detail": "Xem chi tiết",
"We have sent a code to your email. Please enter it below to confirm your login": "Chúng tôi đã gửi một mã đến email của bạn. Vui lòng nhập mã đó dưới đây để xác nhận đăng nhập",
"You are logged in as %email": "Bạn đã đăng nhập với tư cách %email"
}
+28 -1
View File
@@ -1,16 +1,25 @@
{
"%status for %duration": "%duration 内的 %status",
"120 Days": "120天",
"14 Days": "14天",
"150 Days": "150天",
"180 Days": "180天",
"30 Days": "30天",
"60 Days": "60天",
"7 Days": "7天",
"90 Days": "90天",
"Availability per Component": "每个组件的可用性",
"All Systems are Degraded": "所有系统性能下降",
"All Systems are Down": "所有系统不可用",
"All Systems are Operational": "所有系统运行正常",
"All Systems are in Maintenance": "所有系统正在维护",
"Back": "返回",
"Badge Copied": "徽章已复制",
"Badge": "徽章",
"Browse Events": "浏览事件",
"Change Email": "更改邮箱",
"Code Copied": "代码已复制",
"Confirm Login": "确认登录",
"Copy Code": "复制代码",
"Copy Link": "复制链接",
"DEGRADED": "性能下降",
@@ -19,6 +28,9 @@
"Days": "天",
"Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.": "使用 <script> 或 <iframe> 将此监控嵌入您的应用。",
"Embed": "嵌入",
"Enter the code": "输入验证码",
"Enter your email": "输入您的邮箱",
"Enter your email to log in. If you are not subscribed, you will be prompted to subscribe.": "输入您的邮箱进行登录。如果您尚未订阅,将会提示您进行订阅。",
"Get SVG badge for this monitor": "获取此监控的SVG徽章",
"Get a LIVE Status for this monitor": "获取此监控的实时状态",
"IDENTIFIED": "已识别",
@@ -28,10 +40,12 @@
"Lasted for about %lastedFor": "持续了约 %lastedFor",
"Light": "浅色",
"Link Copied": "链接已复制",
"Login": "登录",
"MAINTENANCE": "维护",
"MONITORING": "正在监控",
"Maintenance Completed": "维护已完成",
"Maintenance in Progress": "维护进行中",
"Manage Subscription": "管理订阅",
"Mode": "模式",
"No Data": "无数据",
"No Incident in %date": "%date 无事件",
@@ -40,11 +54,16 @@
"No Updates Yet": "尚无更新",
"Ongoing Incidents": "正在处理的事件",
"Pinging": "正在测试",
"Please select specific monitors to receive updates from.": "请选择您希望接收更新的特定监控。",
"RESOLVED": "已解决",
"Recent Incidents": "最近的事件",
"Recent Maintenances": "最近的维护",
"Share this monitor using a link with others": "通过链接与他人分享此监控",
"Share": "分享",
"Some Systems are not working as expected": "部分系统运行异常",
"Some Systems Degraded": "部分系统性能下降",
"Some Systems Down": "部分系统不可用",
"Some Systems in Maintenance": "部分系统正在维护",
"Standard": "标准",
"Started %startedAt, lasted for %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
"Started %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
@@ -53,13 +72,21 @@
"Starts %startedAt": "%startedAt 后开始",
"Status OK": "状态正常",
"Status": "状态",
"Subscribe": "订阅",
"Subscribe to Updates": "订阅更新",
"Subscribe to all monitors": "订阅所有监控",
"Switch to your timezone": "切换到您的时区",
"Theme": "主题",
"There are no upcoming maintenances": "没有即将进行的维护",
"Timezone set to %tz": "时区设置为 %tz",
"Today": "今天",
"UP": "可用",
"Update Subscription": "更新订阅",
"Upcoming Maintenance": "即将进行的维护",
"Upcoming Maintenances": "即将进行的维护列表",
"Updates": "更新",
"Uptime": "正常运行时间",
"View in detail": "详细查看"
"View in detail": "详细查看",
"We have sent a code to your email. Please enter it below to confirm your login": "我们已向您的邮箱发送了验证码。请在下方输入以确认您的登录",
"You are logged in as %email": "您已作为 %email 登录"
}
+29 -13
View File
@@ -8,6 +8,7 @@ import {
CreateIncident,
AddIncidentComment,
AddIncidentMonitor,
GetIncidentByIDDashboard,
InsertNewAlert,
} from "./controllers/controller.js";
@@ -15,6 +16,7 @@ import db from "./db/db.js";
const TRIGGERED = "TRIGGERED";
const RESOLVED = "RESOLVED";
const CLOSED = "CLOSED";
async function createJSONCommonAlert(monitor, config, alert, severity) {
let siteData = await GetAllSiteData();
@@ -58,17 +60,22 @@ async function createNewIncident(monitor, alert, commonData) {
};
let update = commonData.description;
update =
update +
`<div style="line-height: 1.5;">
<p><strong>Monitor Name:</strong> ${monitor.name}</p>
<p><strong>Incident Status:</strong> ${commonData.status}</p>
<p><strong>Severity:</strong> ${commonData.severity}</p>
<p><strong>Monitor Status:</strong> ${alert.monitor_status}</p>
<p><strong>Monitor Health Checks:</strong> ${alert.health_checks}</p>
<p><strong>Monitor Failure Threshold:</strong> ${commonData.details.threshold}</p>
<p><strong>Visit:</strong> <a href="${commonData.actions[0].url}">${commonData.actions[0].url}</a></p>
</div>`;
update = `#### ${update}
---
**Monitor Name:** ${monitor.name}
**Incident Status:** ${commonData.status}
**Severity:** ${commonData.severity}
**Monitor Status:** ${alert.monitor_status}
**Monitor Health Checks:** ${alert.health_checks}
**Monitor Failure Threshold:** ${commonData.details.threshold}
**Visit:** [${commonData.actions[0].url}](${commonData.actions[0].url})
`;
let newIncident = await CreateIncident(payload);
@@ -82,13 +89,22 @@ async function createNewIncident(monitor, alert, commonData) {
}
async function closeIncident(alert, comment) {
//check if incident is already resolved
let incident = await GetIncidentByIDDashboard({
incident_id: alert.incident_number,
});
if (incident.status === CLOSED || incident.state === RESOLVED) {
console.log("Incident is already resolved");
return;
}
let incident_id = alert.incident_number;
return await AddIncidentComment(incident_id, comment, "RESOLVED", moment(alert.updated_at).unix());
return await AddIncidentComment(incident_id, comment, RESOLVED, moment(alert.updated_at).unix());
}
function createClosureComment(alert, commonJSON) {
let comment = "The incident has been auto resolved";
let downtimeDuration = moment(alert.updated_at).diff(moment(alert.created_at), "minutes");
let downtimeDuration = Math.round(moment(alert.updated_at).diff(moment(alert.created_at), "seconds") / 60);
comment = comment + `, Total downtime: ` + downtimeDuration + ` minutes`;
return comment;
}
+1
View File
@@ -8,6 +8,7 @@ const SITE = "./config/site.yaml";
const UP = "UP";
const DOWN = "DOWN";
const DEGRADED = "DEGRADED";
const MAINTENANCE = "MAINTENANCE";
const NO_DATA = "NO_DATA";
const API_TIMEOUT = 10 * 1000; // 10 seconds
const AnalyticsProviders = {
+591 -162
View File
@@ -1,175 +1,115 @@
// @ts-nocheck
// @ts-nocheck
import {
IsValidURL,
IsValidGHObject,
IsValidObject,
IsValidNav,
IsValidHero,
IsValidI18n,
IsValidAnalytics,
IsValidColors,
IsValidJSONString,
IsValidHero,
IsValidI18n,
IsValidJSONArray,
IsValidJSONString,
IsValidNav,
IsValidURL,
} from "./validators.js";
import { siteDataKeys } from "./siteDataKeys.js";
import db from "../db/db.js";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { Resend } from "resend";
import Queue from "queue";
import crypto from "crypto";
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
import { UP, DOWN, DEGRADED, NO_DATA, REALTIME, SIGNAL } from "../constants.js";
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC, ReplaceAllOccurrences } from "../tool.js";
import { addMonths, format, startOfMonth, subMonths } from "date-fns";
import { DEGRADED, DOWN, NO_DATA, SIGNAL, UP, REALTIME } from "../constants.js";
import {
GetMinuteStartNowTimestampUTC,
GetMinuteStartTimestampUTC,
GetNowTimestampUTC,
ReplaceAllOccurrences,
ValidateEmail,
} from "../tool.js";
import getSMTPTransport from "../notification/smtps.js";
import fs from "fs-extra";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const saltRounds = 10;
const DUMMY_SECRET = "DUMMY_SECRET";
const siteDataKeys = [
{
key: "title",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "siteName",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "siteURL",
isValid: IsValidURL,
data_type: "string",
},
{
key: "home",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "favicon",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "logo",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "metaTags",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "nav",
isValid: IsValidNav,
data_type: "object",
},
{
key: "hero",
isValid: IsValidHero,
data_type: "object",
},
{
key: "footerHTML",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "customCSS",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "i18n",
isValid: IsValidI18n,
data_type: "object",
},
{
key: "pattern",
//string dots or sqaures or circ
isValid: (value) => typeof value === "string" && ["dots", "squares", "none"].includes(value),
data_type: "string",
},
{
key: "analytics",
isValid: IsValidAnalytics,
data_type: "object",
},
{
key: "theme",
//light dark system none
isValid: (value) => typeof value === "string" && ["light", "dark", "system", "none"].includes(value),
data_type: "string",
},
{
key: "themeToggle",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "tzToggle",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "barStyle",
//PARTIAL or FULL
isValid: (value) => typeof value === "string" && ["PARTIAL", "FULL"].includes(value),
data_type: "string",
},
{
key: "barRoundness",
//SHARP or ROUNDED
isValid: (value) => typeof value === "string" && ["SHARP", "ROUNDED"].includes(value),
data_type: "string",
},
{
key: "summaryStyle",
//CURRENT or DAY
isValid: (value) => typeof value === "string" && ["CURRENT", "DAY"].includes(value),
data_type: "string",
},
{
key: "colors",
isValid: IsValidColors,
data_type: "object",
},
{
key: "font",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "monitorSort",
isValid: IsValidJSONArray,
data_type: "object",
},
{
key: "categories",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "homeIncidentCount",
isValid: (value) => parseInt(value) >= 0,
data_type: "string",
},
{
key: "homeIncidentStartTimeWithin",
isValid: (value) => parseInt(value) >= 1,
data_type: "string",
},
{
key: "incidentGroupView",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
];
const eventQueue = new Queue({
concurrency: 10, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true, // Automatically start the queue (optional)
});
const insertStatusQueue = new Queue({
concurrency: 1, // Number of tasks that can run concurrently
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
autostart: true, // Automatically start the queue (optional)
});
export const PushDataToQueue = async (eventID, eventName, eventData) => {
//fetch subscription trigger config from db of email type
let subscription = await db.getSubscriptionTriggerByType("email");
if (!subscription) {
return;
}
let config;
try {
config = JSON.parse(subscription.config);
} catch (e) {
return;
}
if (!config[eventName]) {
return;
}
//get incident data from db using incident id
let tags = ["_"];
let monitors = await db.getIncidentMonitorsByIncidentID(eventID); //get email template
if (monitors) {
for (let i = 0; i < monitors.length; i++) {
const monitor = monitors[i];
tags.push(monitor.monitor_tag);
}
}
//get all the eligible emails that are there in subscription table
//get email template
const emailTemplate = fs.readFileSync(path.join(__dirname, "../templates/event_update.html"), "utf8");
let siteData = await GetAllSiteData();
let base = !!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "";
let emailData = {
brand_name: siteData.siteName,
logo_url: await GetSiteLogoURL(siteData.siteURL, siteData.logo, base),
incident_url: await GetSiteLogoURL(
siteData.siteURL,
`/view/events/${eventData.incident_type.toLowerCase()}-${eventID}`,
base,
),
update_message: eventData.message,
title: `[${eventData.incident_type}] ` + eventData.title,
};
let eligibleEmails = await db.getSubscriberEmails(tags);
if (eligibleEmails) {
for (let i = 0; i < eligibleEmails.length; i++) {
let email = eligibleEmails[i];
eventQueue.push(async (cb) => {
await SendEmailWithTemplate(
emailTemplate,
emailData,
email.subscriber_send,
`[${eventData.incident_type}] ${eventData.title}`,
eventData.message,
);
cb(eventData);
});
}
}
};
export function InsertKeyValue(key, value) {
let f = siteDataKeys.find((k) => k.key === key);
@@ -203,6 +143,20 @@ export async function GetAllSiteData() {
return transformedData;
}
//get all analytics data as json
export async function GetAllAnalyticsData() {
let data = await db.getAllSiteDataAnalytics();
//return all data as key value pairs, transform using data_type
let transformedData = [];
for (const d of data) {
transformedData.push({
key: d.key,
value: JSON.parse(d.value),
});
}
return transformedData;
}
export const CreateUpdateMonitor = async (monitor) => {
let monitorData = { ...monitor };
if (monitorData.id) {
@@ -212,9 +166,123 @@ export const CreateUpdateMonitor = async (monitor) => {
}
};
export const UpdateMonitoringData = async (data) => {
let queryData = { ...data };
return await db.updateMonitoringData(
queryData.monitor_tag,
GetMinuteStartTimestampUTC(queryData.start),
GetMinuteStartTimestampUTC(queryData.end),
queryData.newStatus,
queryData.type,
);
};
export const CreateMonitor = async (monitor) => {
let monitorData = { ...monitor };
if (monitorData.id) {
throw new Error("monitor id must be empty or 0");
}
return await db.insertMonitor(monitorData);
};
export const UpdateMonitor = async (monitor) => {
let monitorData = { ...monitor };
if (!!!monitorData.id || monitorData.id === 0) {
throw new Error("monitor id cannot be empty or 0");
}
return await db.updateMonitor(monitorData);
};
export const GetMonitors = async (data) => {
return await db.getMonitors(data);
};
export const SystemDataMessage = async () => {
//get all active monitors
let monitors = await db.getMonitors({ status: "ACTIVE" });
//get last status using getLatestMonitoringData(monitor_tag)
let monitorTags = monitors.map((monitor) => monitor.tag);
let upsCount = 0;
let degradedCount = 0;
let downCount = 0;
let maintenanceCount = 0;
for (let i = 0; i < monitors.length; i++) {
let status = await db.getLatestMonitoringData(monitors[i].tag);
if (status) {
if (status.status === "UP") {
upsCount++;
} else if (status.status === "DEGRADED") {
degradedCount++;
} else if (status.status === "DOWN") {
downCount++;
} else if (status.status === "MAINTENANCE") {
maintenanceCount++;
}
}
}
const total = upsCount + degradedCount + downCount + maintenanceCount;
if (total === 0) {
return {
text: "No Systems",
upsPercentage: 0,
degradedPercentage: 0,
downsPercentage: 0,
maintenancePercentage: 0,
};
}
let upsPercentage = Math.round((upsCount / total) * 100);
let degradedPercentage = Math.round((degradedCount / total) * 100);
let downsPercentage = Math.round((downCount / total) * 100);
let maintenancePercentage = Math.round((maintenanceCount / total) * 100);
let message = "";
// Determine message based on the combination of system states
if (upsCount > 0 && degradedCount === 0 && downCount === 0 && maintenanceCount == 0) {
// UP=1|DOWN=0|DEGRADED=0|MAINTENANCE=0
message = "All Systems are Operational";
} else if (upsCount === 0 && degradedCount > 0 && downCount === 0 && maintenanceCount == 0) {
// UP=0|DOWN=0|DEGRADED=1|MAINTENANCE=0
message = "All Systems are Degraded";
} else if (upsCount === 0 && degradedCount === 0 && downCount > 0 && maintenanceCount == 0) {
// UP=0|DOWN=1|DEGRADED=0|MAINTENANCE=0
message = "All Systems are Down";
} else if (upsCount === 0 && degradedCount === 0 && downCount === 0 && maintenanceCount > 0) {
message = "All Systems are in Maintenance";
} else if (
(upsCount > 0 && degradedCount > 0 && downCount > 0 && maintenanceCount > 0) ||
(upsCount === 0 && degradedCount > 0 && downCount > 0 && maintenanceCount > 0)
) {
// UP=1|DOWN=1|DEGRADED=1|MAINTENANCE=1 or UP=0|DOWN=1|DEGRADED=1|MAINTENANCE=1
message = "Some Systems are not working as expected";
} else if (upsCount > 0 && degradedCount === 0 && downCount > 0 && maintenanceCount == 0) {
// UP=1|DOWN=1|DEGRADED=0|MAINTENANCE=0
message = "Some Systems Down";
} else if (upsCount > 0 && degradedCount > 0 && downCount === 0 && maintenanceCount == 0) {
// UP=1|DOWN=0|DEGRADED=1|MAINTENANCE=0
message = "Some Systems Degraded";
} else if (upsCount > 0 && degradedCount === 0 && downCount === 0 && maintenanceCount > 0) {
// UP=1|DOWN=0|DEGRADED=0|MAINTENANCE=1
message = "Some Systems in Maintenance";
}
//if percentage is not 100 sum, then add remaining to up
if (upsPercentage + degradedPercentage + downsPercentage + maintenancePercentage < 100) {
upsPercentage = 100 - (degradedPercentage + downsPercentage + maintenancePercentage);
}
return {
text: message,
upsPercentage,
degradedPercentage,
downsPercentage,
maintenancePercentage
};
};
export const GetMonitorsParsed = async (query) => {
// Retrieve monitors from the database based on the provided query
const rawMonitors = await db.getMonitors(query);
@@ -242,6 +310,16 @@ export const GetMonitorsParsed = async (query) => {
export const CreateUpdateTrigger = async (alert) => {
let alertData = { ...alert };
let alertMetaJSON = JSON.parse(alertData.trigger_meta);
if (alertData.trigger_type === "email") {
let emailsArray = alertMetaJSON.to.split(",").map((email) => email.trim());
for (let i = 0; i < emailsArray.length; i++) {
if (!ValidateEmail(emailsArray[i])) {
throw new Error(`Invalid email: ${emailsArray[i]}`);
}
}
}
if (alertData.id) {
return await db.updateTrigger(alertData);
} else {
@@ -326,6 +404,100 @@ export const GetLastHeartbeat = async (monitor_tag) => {
return await db.getLastHeartbeat(monitor_tag);
};
export const ProcessGroupUpdate = async (data) => {
//find all active monitor that are of type group
let groupActiveMonitors = await db.getMonitors({ status: "ACTIVE", monitor_type: "GROUP" });
let validMonitorTags = [];
for (let i = 0; i < groupActiveMonitors.length; i++) {
let groupActiveMonitor = groupActiveMonitors[i];
let typeData = JSON.parse(groupActiveMonitor.type_data);
let monitorsInGroup = typeData.monitors;
let selectedMonitorTags = monitorsInGroup
.filter((monitor) => {
if (!!monitor.selected) {
return monitor.tag;
}
})
.map((monitor) => monitor.tag);
validMonitorTags.push({
groupTag: groupActiveMonitor.tag,
selectedMonitorTags: selectedMonitorTags,
});
}
for (let i = 0; i < validMonitorTags.length; i++) {
let groupActiveMonitor = validMonitorTags[i];
if (groupActiveMonitor.selectedMonitorTags.indexOf(data.monitor_tag) !== -1) {
//do db insert
//get last status by tag for the group tag
let updateData = {};
let lastStatus = await db.getMonitoringDataAt(groupActiveMonitor.groupTag, data.timestamp);
if (!!lastStatus) {
let status = lastStatus.status;
let timestamp = lastStatus.timestamp;
let receivedStatus = data.status;
let receivedTimestamp = data.timestamp;
if (receivedStatus === DOWN) {
updateData = {
monitor_tag: groupActiveMonitor.groupTag,
timestamp: receivedTimestamp,
status: DOWN,
type: REALTIME,
latency: data.latency,
};
} else if (receivedStatus === DEGRADED && status !== DOWN) {
updateData = {
monitor_tag: groupActiveMonitor.groupTag,
timestamp: receivedTimestamp,
status: DEGRADED,
type: REALTIME,
latency: data.latency,
};
} else if (receivedStatus === UP && status !== DOWN && status !== DEGRADED) {
updateData = {
monitor_tag: groupActiveMonitor.groupTag,
timestamp: receivedTimestamp,
status: UP,
type: REALTIME,
latency: data.latency,
};
}
} else {
//if no last status then insert the new status
updateData = {
monitor_tag: groupActiveMonitor.groupTag,
timestamp: data.timestamp,
status: data.status,
type: REALTIME,
latency: data.latency,
};
}
if (!!updateData.status) {
await db.insertMonitoringData(updateData);
}
}
}
};
export const InsertMonitoringData = async (data) => {
//do validation if present all fields below
if (!data.monitor_tag || !data.timestamp || !data.status || !data.type) {
throw new Error("Invalid data");
}
insertStatusQueue.push(async (cb) => {
await ProcessGroupUpdate(data);
cb();
});
return await db.insertMonitoringData({
monitor_tag: data.monitor_tag,
timestamp: data.timestamp,
status: data.status,
latency: data.latency || 0,
type: data.type,
});
};
export const RegisterHeartbeat = async (tag, secret) => {
let monitor = await db.getMonitorByTag(tag);
if (!monitor) {
@@ -339,7 +511,7 @@ export const RegisterHeartbeat = async (tag, secret) => {
let heartbeatConfig = JSON.parse(typeData);
let heartbeatSecret = heartbeatConfig.secretString;
if (heartbeatSecret === secret) {
return await db.insertMonitoringData({
return InsertMonitoringData({
monitor_tag: monitor.tag,
timestamp: GetMinuteStartNowTimestampUTC(GetNowTimestampUTC()),
status: UP,
@@ -365,6 +537,19 @@ export const GenerateToken = async (data) => {
}
};
//generate token with expiry
export const GenerateTokenWithExpiry = async (data, expiry) => {
try {
const token = jwt.sign(data, process.env.KENER_SECRET_KEY || DUMMY_SECRET, {
expiresIn: expiry,
});
return token;
} catch (err) {
console.error("Error generating token with expiry:", err);
throw err;
}
};
export const ForgotPasswordJWT = async (data) => {
try {
const token = jwt.sign(data, process.env.KENER_SECRET_KEY || DUMMY_SECRET, {
@@ -382,7 +567,6 @@ export const VerifyToken = async (token) => {
const decoded = jwt.verify(token, process.env.KENER_SECRET_KEY || DUMMY_SECRET);
return decoded; // Returns the decoded payload if the token is valid
} catch (err) {
console.error("Error verifying token:", err);
return undefined; // Returns null if the token is invalid
}
};
@@ -393,11 +577,13 @@ export const GetAllAlertsPaginated = async (data) => {
total: await db.getMonitorAlertsCount(),
};
};
function generateApiKey() {
const prefix = "kener_";
const randomKey = crypto.randomBytes(32).toString("hex"); // 64-character hexadecimal string
return prefix + randomKey;
}
function createHash(apiKey) {
return crypto
.createHmac("sha256", process.env.KENER_SECRET_KEY || DUMMY_SECRET)
@@ -547,6 +733,7 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
UP: 0,
DOWN: 0,
DEGRADED: 0,
MAINTENANCE: 0,
NO_DATA: 0,
};
}
@@ -565,6 +752,7 @@ export const GetDataGroupByDayAlternative = async (monitor_tag, start, end, time
UP: group.UP,
DOWN: group.DOWN,
DEGRADED: group.DEGRADED,
MAINTENANCE: group.MAINTENANCE,
NO_DATA: group.NO_DATA,
}));
};
@@ -596,6 +784,10 @@ export const CreateIncident = async (data) => {
}
let newIncident = await db.createIncident(incident);
PushDataToQueue(newIncident.id, "createIncident", {
message: `${incident.incident_type} Created`,
...incident,
});
return {
incident_id: newIncident.id,
};
@@ -621,6 +813,26 @@ export const UpdateIncident = async (incident_id, data) => {
state: data.state || incidentExists.state,
end_date_time: data.end_date_time || incidentExists.end_date_time,
};
//check if updateObject same as incidentExists
if (
JSON.stringify(updateObject) ===
JSON.stringify({
id: incidentExists.id,
title: incidentExists.title,
start_date_time: incidentExists.start_date_time,
status: incidentExists.status,
state: incidentExists.state,
end_date_time: incidentExists.end_date_time,
})
) {
PushDataToQueue(incident_id, "updateIncident", {
message: `${incidentExists.incident_type} has been updated to ${updateObject.state}`,
...incidentExists,
...updateObject,
});
}
return await db.updateIncident(updateObject);
};
@@ -664,9 +876,9 @@ export const RemoveIncidentMonitor = async (incident_id, monitor_tag) => {
};
export const AddIncidentMonitor = async (incident_id, monitor_tag, monitor_impact) => {
//monitor_impact must be DOWN or DEGRADED or NONE
if (!["DOWN", "DEGRADED"].includes(monitor_impact)) {
throw new Error("Monitor impact must be either DOWN, DEGRADED ");
//monitor_impact must be DOWN or DEGRADED or MAINTENANCE or NONE
if (!["DOWN", "DEGRADED", "MAINTENANCE"].includes(monitor_impact)) {
throw new Error("Monitor impact must be either DOWN, DEGRADED, MAINTENANCE");
}
//check if monitor exists
@@ -681,6 +893,11 @@ export const AddIncidentMonitor = async (incident_id, monitor_tag, monitor_impac
throw new Error(`Incident with id ${incident_id} does not exist`);
}
PushDataToQueue(incident_id, "insertIncidentMonitor", {
title: incidentExists.title,
message: `Monitor ${monitor_tag} added to ${incidentExists.incident_type}. Impact is ${monitor_impact}`,
...incidentExists,
});
return await db.insertIncidentMonitor(incident_id, monitor_tag, monitor_impact);
};
@@ -708,6 +925,11 @@ export const UpdateCommentByID = async (incident_id, comment_id, comment, state,
if (!commentExists) {
throw new Error(`Comment with id ${comment_id} does not exist`);
}
PushDataToQueue(incident_id, "updateIncidentComment", {
title: incidentExists.title,
message: `${comment}`,
...incidentExists,
});
let c = await db.updateIncidentCommentByID(comment_id, comment, state, commented_at);
if (c) {
let incidentUpdate = {
@@ -733,7 +955,11 @@ export const AddIncidentComment = async (incident_id, comment, state, commented_
if (!!!state) {
state = incidentExists.state;
}
PushDataToQueue(incident_id, "insertIncidentComment", {
title: incidentExists.title,
message: `${comment}`,
...incidentExists,
});
let c = await db.insertIncidentComment(incident_id, comment, state, commented_at);
let incidentType = incidentExists.incident_type;
//update incident state
@@ -787,6 +1013,45 @@ export const GetIncidentByIDDashboard = async (data) => {
return incident;
};
export const GetIncidentsPaginated = async (page, limit, filter, direction) => {
let incidents = await db.getIncidentsPaginated(page, limit, filter, direction);
let allMonitors = {};
for (let i = 0; i < incidents.length; i++) {
let incidentMonitors = await GetIncidentMonitors(incidents[i].id);
incidents[i].monitors = incidentMonitors;
}
//for each monitor tag, in monitorsTagAndImpact for every incident, call get monitor by tag
for (let i = 0; i < incidents.length; i++) {
for (let j = 0; j < incidents[i].monitors.length; j++) {
let monitorTag = incidents[i].monitors[j].monitor_tag;
let monitorImpact = incidents[i].monitors[j].monitor_impact;
if (!allMonitors[monitorTag]) {
let monitor = await db.getMonitorByTag(monitorTag);
if (monitor) {
allMonitors[monitorTag] = {
id: monitor.id,
tag: monitor.tag,
name: monitor.name,
image: monitor.image,
impact_type: monitorImpact,
};
}
}
incidents[i].monitors[j] = allMonitors[monitorTag];
}
}
//get comments
for (let i = 0; i < incidents.length; i++) {
incidents[i].comments = await GetIncidentActiveComments(incidents[i].id);
}
return incidents;
};
export const GetIncidentsOpenHome = async (homeIncidentCount, start, end) => {
homeIncidentCount = parseInt(homeIncidentCount);
@@ -928,7 +1193,7 @@ export const GetSMTPFromENV = () => {
smtp_user: process.env.SMTP_USER,
smtp_from_email: process.env.SMTP_FROM_EMAIL,
smtp_pass: process.env.SMTP_PASS,
smtp_secure: !!process.env.SMTP_SECURE,
smtp_secure: !!Number(process.env.SMTP_SECURE),
};
};
@@ -1176,6 +1441,81 @@ export const CreateNewUser = async (currentUser, data) => {
return await db.insertUser(user);
};
export const DeleteMonitorCompletelyUsingTag = async (tag) => {
await db.deleteMonitorDataByTag(tag);
await db.deleteIncidentMonitorsByTag(tag);
await db.deleteMonitorAlertsByTag(tag);
return await db.deleteMonitorsByTag(tag);
};
export const CreateNewSubscriber = async (data) => {
await db.insertSubscriber(data);
return await GetSubscriberByEmailAndType(data.subscriber_send, data.subscriber_type);
};
export const GetSubscriberByEmailAndType = async (email, type) => {
return await db.getSubscriberByDetails(email, type);
};
//get subscriber by id
export const GetSubscriberByID = async (id) => {
return await db.getSubscriberById(id);
};
//remove all subscriptions for a subscriber
export const RemoveAllSubscriptions = async (subscriber_id) => {
return await db.removeAllDataFromSubscriptions(subscriber_id);
};
export const CreateNewSubscription = async (subscriber_id, monitors) => {
if (!monitors || monitors.length === 0) {
throw new Error("No monitors found");
}
await db.removeAllDataFromSubscriptions(subscriber_id);
for (let i = 0; i < monitors.length; i++) {
let tag = monitors[i];
let subscription = {
subscriber_id: subscriber_id,
subscriptions_status: "ACTIVE",
subscriptions_monitors: tag,
subscriptions_meta: "",
};
await db.insertSubscription(subscription);
}
return {
message: "Subscriptions created successfully",
count: monitors.length,
};
};
//updateSubscriberMeta given id
export const UpdateSubscriberMeta = async (id, meta) => {
return await db.updateSubscriberMeta(id, meta);
};
//updateSubscriberStatus
export const UpdateSubscriberStatus = async (id, status) => {
return await db.updateSubscriberStatus(id, status);
};
//delete subscriber by id
export const DeleteSubscriberByID = async (id) => {
return await db.deleteSubscriberById(id);
};
//get subscriptions by subscriber id
export const GetSubscriptionsBySubscriberID = async (subscriber_id) => {
return await db.getSubscriptionsBySubscriberId(subscriber_id);
};
//getMonitorsByTag
export const GetMonitorsByTag = async (tag) => {
return await db.getMonitorsByTag(tag);
};
export const GetSiteMap = async (cookies) => {
let siteMapData = [];
let siteURLData = await GetSiteDataByKey("siteURL");
@@ -1265,3 +1605,92 @@ export const GetSiteMap = async (cookies) => {
.join("")}
</urlset>`;
};
// fetch the single subscription_trigger (only type=email supported)
export const GetSubscriptionTriggerByEmail = async () => {
return await db.getSubscriptionTriggerByType("email");
};
// create a subscription_trigger record for email type
export const CreateSubscriptionTrigger = async (data) => {
// only email supported
if (data.subscription_trigger_type !== "email") {
throw new Error("Only email trigger type is supported");
}
//update subscription_trigger_status and subscription_trigger_id given subscription_trigger_type, if not present insert otherwise update
let subscriptionTrigger = await db.getSubscriptionTriggerByType(data.subscription_trigger_type);
if (!subscriptionTrigger) {
await db.insertSubscriptionTrigger({
subscription_trigger_type: data.subscription_trigger_type,
subscription_trigger_status: "ACTIVE",
config: data.config,
});
} else {
await db.updateSubscriptionTrigger({
id: subscriptionTrigger.id,
subscription_trigger_status: data.subscription_trigger_status || "ACTIVE",
subscription_trigger_type: subscriptionTrigger.subscription_trigger_type,
config: data.config,
});
}
return {
subscription_trigger_type: data.subscription_trigger_type,
subscription_trigger_status: data.subscription_trigger_status,
config: data.config,
};
};
//updateSubscriptionTriggerStatus
export const UpdateSubscriptionTriggerStatus = async (id, status) => {
return await db.updateSubscriptionTriggerStatus(id, status);
};
// Get subscribers paginated
export const GetSubscribersPaginated = async (data) => {
const page = parseInt(data.page) || 1;
const limit = parseInt(data.limit) || 10;
const subscriptions = await db.getSubscriptionsPaginated(page, limit);
const total = await db.getTotalSubscriptionCount();
//all monitor tags
let allTags = subscriptions.map((subscription) => subscription.subscriptions_monitors);
//get all monitors by tags
let monitors = await db.getMonitorsByTags(allTags);
let tagMonitor = {};
//convert monitors to map in tagMonitor
for (let i = 0; i < monitors.length; i++) {
let m = monitors[i];
tagMonitor[monitors[i].tag] = {
name: m.name,
tag: m.tag,
image: m.image,
};
}
let subscriberIDObj = {};
//for each subscription get subscriber details
for (let i = 0; i < subscriptions.length; i++) {
let subsID = subscriptions[i].subscriber_id;
if (!subscriberIDObj[subsID]) {
const subscriber = await db.getSubscriberById(subscriptions[i].subscriber_id);
subscriberIDObj[subsID] = {
id: subscriber.id,
email: subscriber.subscriber_send,
status: subscriber.subscriber_status,
};
}
subscriptions[i].subscriber = subscriberIDObj[subsID];
subscriptions[i].monitor = tagMonitor[subscriptions[i].subscriptions_monitors];
}
return {
subscriptions: subscriptions,
total: total,
};
};
//updateSubscriptionStatus
export const UpdateSubscriptionStatus = async (subscription_id, status) => {
return await db.updateSubscriptionStatus(subscription_id, status);
};
+219
View File
@@ -0,0 +1,219 @@
// @ts-nocheck
import {
IsValidAnalytics,
IsValidColors,
IsValidHero,
IsValidI18n,
IsValidJSONArray,
IsValidJSONString,
IsValidNav,
IsValidURL,
} from "./validators.js";
export const siteDataKeys = [
{
key: "title",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "siteName",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "siteURL",
isValid: IsValidURL,
data_type: "string",
},
{
key: "homeDataMaxDays",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "home",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "favicon",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "logo",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "metaTags",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "nav",
isValid: IsValidNav,
data_type: "object",
},
{
key: "hero",
isValid: IsValidHero,
data_type: "object",
},
{
key: "footerHTML",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "kenerTheme",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "customCSS",
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "i18n",
isValid: IsValidI18n,
data_type: "object",
},
{
key: "pattern",
//string dots or squares or circle
isValid: (value) =>
typeof value === "string" &&
[
"dots",
"squares",
"tiles",
"none",
"radial-blue",
"radial-mono",
"radial-midnight",
"circle-mono",
"carbon-fibre",
"texture-sky",
"angular-mono",
"angular-spring",
"angular-bloom",
"pets",
].includes(value),
data_type: "string",
},
{
key: "analytics",
isValid: IsValidAnalytics,
data_type: "object",
},
{
key: "theme",
//light dark system none
isValid: (value) => typeof value === "string" && ["light", "dark", "system", "none"].includes(value),
data_type: "string",
},
{
key: "themeToggle",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "tzToggle",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "showSiteStatus",
//boolean
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "barStyle",
//PARTIAL or FULL
isValid: (value) => typeof value === "string" && ["PARTIAL", "FULL"].includes(value),
data_type: "string",
},
{
key: "barRoundness",
//SHARP or ROUNDED
isValid: (value) => typeof value === "string" && ["SHARP", "ROUNDED"].includes(value),
data_type: "string",
},
{
key: "summaryStyle",
//CURRENT or DAY
isValid: (value) => typeof value === "string" && ["CURRENT", "DAY"].includes(value),
data_type: "string",
},
{
key: "colors",
isValid: IsValidColors,
data_type: "object",
},
{
key: "font",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "monitorSort",
isValid: IsValidJSONArray,
data_type: "object",
},
{
key: "categories",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "homeIncidentCount",
isValid: (value) => parseInt(value) >= 0,
data_type: "string",
},
{
key: "homeIncidentStartTimeWithin",
isValid: (value) => parseInt(value) >= 1,
data_type: "string",
},
{
key: "incidentGroupView",
isValid: (value) => typeof value === "string" && value.trim().length > 0,
data_type: "string",
},
{
key: "analytics.googleTagManager",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.plausible",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.mixpanel",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.amplitude",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.clarity",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "analytics.umami",
isValid: IsValidJSONString,
data_type: "object",
},
];
+1 -1
View File
@@ -109,7 +109,7 @@ export function IsValidColors(colors) {
return false;
}
if (typeof colors !== "object") return false;
const validColorKeys = ["UP", "DOWN", "DEGRADED"];
const validColorKeys = ["UP", "DOWN", "DEGRADED", "MAINTENANCE"];
for (const key of validColorKeys) {
if (typeof colors[key] !== "string" || !/^#[0-9A-Fa-f]{6}$/.test(colors[key])) return false;
}
+11 -3
View File
@@ -12,6 +12,7 @@ import path from "path";
import db from "./db/db.js";
import notification from "./notification/notif.js";
import DNSResolver from "./dns.js";
import { InsertMonitoringData } from "./controllers/controller.js";
dotenv.config();
@@ -41,6 +42,10 @@ async function manualIncident(monitor) {
for (let i = 0; i < impactArr.length; i++) {
const element = impactArr[i];
if (element.monitor_impact === "MAINTENANCE") {
impact = "MAINTENANCE";
break;
}
if (element.monitor_impact === "DOWN") {
impact = "DOWN";
break;
@@ -83,7 +88,7 @@ const Minuter = async (monitor) => {
await Wait(500); //wait for 500ms
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
serviceClient.execute().then(async (data) => {
await db.insertMonitoringData({
await InsertMonitoringData({
monitor_tag: monitor.tag,
timestamp: startOfMinute,
status: data.status,
@@ -101,13 +106,16 @@ const Minuter = async (monitor) => {
} else if (monitor.monitor_type === "DNS") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "GROUP") {
realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
// realTimeData[startOfMinute] = await serviceClient.execute(startOfMinute);
realTimeData[startOfMinute] = null;
} else if (monitor.monitor_type === "SSL") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "SQL") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "HEARTBEAT") {
realTimeData[startOfMinute] = await serviceClient.execute();
} else if (monitor.monitor_type === "GAMEDIG") {
realTimeData[startOfMinute] = await serviceClient.execute();
}
manualData = await manualIncident(monitor);
@@ -139,7 +147,7 @@ const Minuter = async (monitor) => {
for (const timestamp in mergedData) {
const element = mergedData[timestamp];
db.insertMonitoringData({
await InsertMonitoringData({
monitor_tag: monitor.tag,
timestamp: parseInt(timestamp),
status: element.status,
+365 -4
View File
@@ -48,6 +48,16 @@ class DbImpl {
.first();
}
//get data at time stamp
async getMonitoringDataAt(monitor_tag, timestamp) {
return await this.knex("monitoring_data")
.where("monitor_tag", monitor_tag)
.where("timestamp", timestamp)
.orderBy("timestamp", "desc")
.limit(1)
.first();
}
//get latest data for all active monitors
async getLatestMonitoringDataAllActive(monitor_tags) {
// Find the latest timestamp for each provided monitor tag
@@ -138,9 +148,9 @@ class DbImpl {
return data;
}
async getLastStatusBeforeCombined(monitor_tags_arr, timestamp) {
async getLastStatusBeforeCombined(monitor_tags_arr, timestamp, minTimestamp) {
//monitor_tag_arr is an array of string
return await this.knex("monitoring_data")
let query = this.knex("monitoring_data")
.select(
"timestamp",
this.knex.raw("COUNT(*) as total_entries"),
@@ -153,8 +163,15 @@ class DbImpl {
END as status
`),
)
.whereIn("monitor_tag", monitor_tags_arr)
.where("timestamp", "=", timestamp)
.whereIn("monitor_tag", monitor_tags_arr);
if (!!minTimestamp) {
query = query.whereBetween("timestamp", [minTimestamp, timestamp]);
} else {
query = query.where("timestamp", "=", timestamp);
}
return await query
.groupBy("timestamp")
.havingRaw("COUNT(*) = ?", [monitor_tags_arr.length])
.orderBy("timestamp", "desc")
@@ -271,6 +288,61 @@ class DbImpl {
return await this.knex("monitor_alerts").count("* as count").first();
}
//getMonitorsByTags array of string
async getMonitorsByTags(tags) {
return await this.knex("monitors").whereIn("tag", tags);
}
async getMonitorsByTag(tag) {
return await this.knex("monitors").where("tag", tag).first();
}
//get incidents paginated and direction, given a start timestamp, also might have filter on incident_type, incident_source
async getIncidentsPaginated(page, limit, filter, direction = "after") {
let query = this.knex("incidents").select("*").whereRaw("1=1");
if (filter && filter.status) {
query = query.andWhere("status", filter.status);
}
if (filter && filter.start && direction === "after") {
query = query.andWhere("start_date_time", ">=", filter.start);
}
if (filter && filter.start && direction === "before") {
query = query.andWhere("start_date_time", "<=", filter.start);
}
if (filter && filter.end && direction === "after") {
query = query.andWhere("start_date_time", ">=", filter.end);
}
if (filter && filter.end && direction === "before") {
query = query.andWhere("start_date_time", "<=", filter.end);
}
if (filter && filter.state) {
query = query.andWhere("state", filter.state);
}
if (filter && filter.id) {
query = query.andWhere("id", filter.id);
}
if (filter && filter.incident_type) {
query = query.andWhere("incident_type", filter.incident_type);
}
if (filter && filter.incident_source) {
query = query.andWhere("incident_source", filter.incident_source);
}
if (direction === "after") {
query = query
.orderBy("start_date_time", "asc")
.limit(limit)
.offset((page - 1) * limit);
} else {
query = query
.orderBy("start_date_time", "desc")
.limit(limit)
.offset((page - 1) * limit);
}
return await query;
}
//update alert to inactive given monitor_tag, monitor_status, given id
async updateAlertStatus(id, alert_status) {
return await this.knex("monitor_alerts").where({ id }).update({
@@ -317,6 +389,11 @@ class DbImpl {
return await this.knex("site_data").where("key", key).first();
}
//get site data that starts with analytics.
async getAllSiteDataAnalytics() {
return await this.knex("site_data").where("key", "like", "analytics.%");
}
//insert into monitors
async insertMonitor(data) {
return await this.knex("monitors").insert({
@@ -358,6 +435,39 @@ class DbImpl {
});
}
//given monitor_tag, start and end timestamp and a status, update all monitoring data with this status
async updateMonitoringData(monitor_tag, start, end, newStatus, type) {
const count = Math.floor((end - start) / 60) + 1;
const timestamps = Array.from({ length: count }, (_, i) => start + i * 60);
const records = timestamps.map((ts) => ({
monitor_tag,
timestamp: ts,
status: newStatus,
type,
latency: 0,
}));
// Use transaction for better performance with batches
const batchSize = 500;
return await this.knex.transaction(async (trx) => {
const results = [];
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);
const result = await trx("monitoring_data")
.insert(batch)
.onConflict(["monitor_tag", "timestamp"])
.merge({ status: newStatus });
results.push(result);
}
return results;
});
}
async updateMonitorTrigger(data) {
return await this.knex("monitors").where({ id: data.id }).update({
down_trigger: data.down_trigger,
@@ -378,6 +488,10 @@ class DbImpl {
if (!!data.id) {
query = query.andWhere("id", data.id);
}
//monitor_type
if (!!data.monitor_type) {
query = query.andWhere("monitor_type", data.monitor_type);
}
if (!!data.tag) {
query = query.andWhere("tag", data.tag);
}
@@ -591,6 +705,10 @@ class DbImpl {
query = query.andWhere("state", filter.state);
}
if (filter && filter.id) {
query = query.andWhere("id", filter.id);
}
query = query
.orderBy("id", "desc")
.limit(limit)
@@ -911,6 +1029,249 @@ class DbImpl {
.andWhere("invitation_expiry", ">", this.knex.fn.now())
.first();
}
//delete from monitor_data using tag
async deleteMonitorDataByTag(tag) {
return await this.knex("monitoring_data").where("monitor_tag", tag).del();
}
//delete from incident monitors using tag
async deleteIncidentMonitorsByTag(tag) {
return await this.knex("incident_monitors").where("monitor_tag", tag).del();
}
//delete from monitor alerts using tag
async deleteMonitorAlertsByTag(tag) {
return await this.knex("monitor_alerts").where("monitor_tag", tag).del();
}
//delete from monitors using tag
async deleteMonitorsByTag(tag) {
return await this.knex("monitors").where("tag", tag).del();
}
//insert into subscribers
async insertSubscriber(data) {
return await this.knex("subscribers").insert({
subscriber_send: data.subscriber_send,
subscriber_meta: data.subscriber_meta,
subscriber_type: data.subscriber_type,
subscriber_status: data.subscriber_status,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
});
}
//update subscriber_meta given id
async updateSubscriberMeta(id, subscriber_meta) {
return await this.knex("subscribers").where({ id }).update({
subscriber_meta,
updated_at: this.knex.fn.now(),
});
}
//update subscriber_status given id
async updateSubscriberStatus(id, subscriber_status) {
return await this.knex("subscribers").where({ id }).update({
subscriber_status,
updated_at: this.knex.fn.now(),
});
}
//delete subscriber by id
async deleteSubscriberById(id) {
return await this.knex("subscribers").where({ id }).del();
}
//insert into subscriptions table
async insertSubscription(data) {
return await this.knex("subscriptions").insert({
subscriber_id: data.subscriber_id,
subscriptions_status: data.subscriptions_status,
subscriptions_monitors: data.subscriptions_monitors,
subscriptions_meta: data.subscriptions_meta,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
});
}
//given subscriber_id remove all data from subscriptions
async removeAllDataFromSubscriptions(subscriber_id) {
return await this.knex("subscriptions").where({ subscriber_id }).del();
}
//get subscriptions by subscriber_id
async getSubscriptionsBySubscriberId(subscriber_id) {
return await this.knex("subscriptions").where("subscriber_id", subscriber_id).orderBy("id", "desc");
}
//update subscription status
async updateSubscriptionStatus(id, subscriptions_status) {
return await this.knex("subscriptions").where({ id }).update({
subscriptions_status,
updated_at: this.knex.fn.now(),
});
}
//get all subscribers with active status
async getAllActiveSubscribers() {
return await this.knex("subscribers").where("subscriber_status", "ACTIVE");
}
//get subscriptions for a monitor tag
async getSubscriptionsForMonitor(monitor_tag) {
return await this.knex("subscriptions as s")
.join("subscribers as sub", "s.subscriber_id", "sub.id")
.where("s.subscriptions_status", "ACTIVE")
.where("sub.subscriber_status", "ACTIVE")
.whereRaw("s.subscriptions_monitors = ? OR s.subscriptions_monitors = 'ALL'", [monitor_tag])
.select("sub.subscriber_send", "sub.subscriber_type", "sub.subscriber_meta", "s.subscriptions_meta");
}
//get subscriber by subscriber_send and subscriber_type
async getSubscriberByDetails(subscriber_send, subscriber_type) {
return await this.knex("subscribers")
.where({
subscriber_send,
subscriber_type,
})
.first();
}
//get all subscribers by type
async getSubscribersByType(subscriber_type) {
return await this.knex("subscribers").where("subscriber_type", subscriber_type).orderBy("id", "desc");
}
//get subscriptions paginated
async getSubscriptionsPaginated(page, limit) {
return await this.knex("subscriptions")
.select(
"id",
"subscriber_id",
"subscriptions_status",
"subscriptions_monitors",
"subscriptions_meta",
"created_at",
)
.orderBy("id", "desc")
.limit(limit)
.offset((page - 1) * limit);
}
//get total subscription count
async getTotalSubscriptionCount() {
const result = await this.knex("subscriptions").count("* as count").first();
return result.count || 0;
}
//get all subscriber emails from subscriptions table that are of given tag or = _
async getSubscriberEmails(monitor_tags) {
return await this.knex("subscriptions")
.join("subscribers", "subscribers.id", "subscriptions.subscriber_id")
.distinct("subscribers.subscriber_send as subscriber_send")
.where("subscriptions.subscriptions_status", "ACTIVE")
.whereIn("subscriptions.subscriptions_monitors", monitor_tags)
.orderBy("subscriber_send");
}
//getSubscriberById
async getSubscriberById(id) {
return await this.knex("subscribers")
.select("id", "subscriber_send", "subscriber_meta", "subscriber_type", "subscriber_status", "created_at")
.where("id", id)
.first();
}
// Get subscribers paginated
async getSubscribersPaginated(page, limit) {
// Use a subquery for pagination to apply limit/offset before joining
const subquery = this.knex("subscribers")
.select("id")
.orderBy("created_at", "desc")
.limit(limit)
.offset((page - 1) * limit)
.as("paginated_subscribers");
const dbType = GetDbType(); // Get the database type
const aggregationFunction = dbType === "postgresql" ? "STRING_AGG" : "GROUP_CONCAT";
const results = await this.knex("subscribers as s")
.select(
"s.id",
"s.subscriber_send",
"s.subscriber_status",
"s.created_at",
// Aggregate monitor tags based on DB type
this.knex.raw(`${aggregationFunction}(sub.subscriptions_monitors, ',') as monitors_agg`),
)
.innerJoin(subquery, "s.id", "paginated_subscribers.id") // Join with paginated IDs
.leftJoin("subscriptions as sub", "s.id", "sub.subscriber_id") // Left join to include subscribers with no subscriptions
.groupBy("s.id", "s.subscriber_send", "s.subscriber_status", "s.created_at") // Group by subscriber details
.orderBy("s.created_at", "desc"); // Ensure final order
// Process results to split the aggregated string into an array
return results.map((row) => ({
...row,
subscriptions_monitors: row.monitors_agg ? row.monitors_agg.split(",") : [], // Split string into array, handle null
monitors_agg: undefined, // Remove the temporary aggregation field
}));
}
// Get total count of subscribers
async getSubscribersCount() {
const result = await this.knex("subscribers").count("* as count").first();
return result.count || 0; // Return count or 0 if no subscribers
}
// CRUD operations for subscription_triggers
async insertSubscriptionTrigger(data) {
return await this.knex("subscription_triggers").insert({
subscription_trigger_type: data.subscription_trigger_type,
subscription_trigger_status: data.subscription_trigger_status,
config: data.config,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
});
}
async getSubscriptionTriggerById(id) {
return await this.knex("subscription_triggers").where({ id }).first();
}
async getAllSubscriptionTriggers() {
return await this.knex("subscription_triggers").orderBy("id", "desc");
}
// get a single subscription_trigger by type
async getSubscriptionTriggerByType(subscription_trigger_type) {
return await this.knex("subscription_triggers")
.where("subscription_trigger_type", subscription_trigger_type)
.first();
}
async updateSubscriptionTrigger(data) {
return await this.knex("subscription_triggers").where({ id: data.id }).update({
subscription_trigger_type: data.subscription_trigger_type,
subscription_trigger_status: data.subscription_trigger_status,
config: data.config,
updated_at: this.knex.fn.now(),
});
}
async updateSubscriptionTriggerStatus(id, subscription_trigger_status) {
return await this.knex("subscription_triggers").where({ id }).update({
subscription_trigger_status,
updated_at: this.knex.fn.now(),
});
}
//deleteSubscriptionTriggerByType
async deleteSubscriptionTriggerByType(subscription_trigger_type) {
return await this.knex("subscription_triggers").where({ subscription_trigger_type }).del();
}
async deleteSubscriptionTriggerById(id) {
return await this.knex("subscription_triggers").where({ id }).del();
}
}
export default DbImpl;
+28 -3
View File
@@ -43,8 +43,20 @@ const seedSiteData = {
title: "Build stunning status pages in minutes",
subtitle: "Let your users know what's going on.",
},
footerHTML:
'<p class="text-center">\nMade using \n<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">\n Kener\n</a>\nan open source status page system built with Svelte and TailwindCSS.<br/>\nCreated with ❤️ by <a href="https://rajnandan.com" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.\n</p>\n',
footerHTML: `<div
class="container relative mt-4 max-w-[655px]"
>
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
<p class="text-center text-sm leading-loose text-muted-foreground ">
Made using
<a href="https://github.com/rajnandan1/kener" target="_blank" class="font-medium underline underline-offset-4">
Kener
</a>
an open source status page system built with Svelte and TailwindCSS.<br/>
Created with by <a href="https://rajnandan.com" target="_blank" class="font-medium underline underline-offset-4">Raj Nandan Sharma</a>.
</p>
</div>
</div>`,
i18n: {
defaultLocale: "en",
locales: [{ code: "en", name: "English", selected: true, disabled: false }],
@@ -80,14 +92,27 @@ const seedSiteData = {
UP: "#67ab95",
DOWN: "#ca3038",
DEGRADED: "#e6ca61",
MAINTENANCE: "#6679cc",
},
font: {
cssSrc: "",
family: "",
},
categories: [{ name: "Home", description: "Monitors for Home Page" }],
categories: [{ name: "Home", description: "Monitors for Home Page", isHidden: false }],
homeIncidentCount: 5,
homeIncidentStartTimeWithin: 30,
homeDataMaxDays: {
desktop: {
maxDays: 90,
selectableDays: [1, 7, 14, 30, 60, 90],
},
mobile: {
maxDays: 90,
selectableDays: [1, 7, 14, 30, 60, 90],
},
},
kenerTheme: "default",
showSiteStatus: "NO",
};
export default seedSiteData;
+1 -1
View File
@@ -64,7 +64,7 @@ class Discord {
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
return { error: error.message };
}
}
}
+1 -1
View File
@@ -221,7 +221,7 @@ class Email {
return await transporter.sendMail(mailOptions);
} catch (error) {
console.error("Error sending email via SMTP", error);
return error;
return { error: error.message };
}
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ class Slack {
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
return { error: error.message };
}
}
}
+2 -2
View File
@@ -93,12 +93,12 @@ class Webhook {
body: this.transformData(data),
});
if (!response.ok) {
throw new Error(`Error from webhook`);
throw new Error(`Error from webhook: ${response.status} ${response.statusText}`);
}
return response;
} catch (error) {
console.error("Error sending webhook", error);
return error;
return { error: error.message };
}
}
}
+18 -3
View File
@@ -78,13 +78,16 @@ function getCountOfSimilarStatuesEnd(arr, statusType) {
return count;
}
const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
const FetchData = async function (site, monitor, localTz, selectedLang, lang, isMobile) {
const secondsInDay = 24 * 60 * 60;
//get offset from utc in minutes
const nowUTC = GetMinuteStartNowTimestampUTC();
const midnightUTC = GetDayStartTimestampUTC(nowUTC);
let deviceType = isMobile ? "mobile" : "desktop";
let homeDataMaxDays = site.homeDataMaxDays[deviceType];
const midnightTz = BeginningOfDay({ timeZone: localTz });
const midnight90DaysAgoTz = midnightTz - 90 * 24 * 60 * 60;
const midnight90DaysAgoTz = midnightTz - (homeDataMaxDays.maxDays - 1) * 24 * 60 * 60;
const NO_DATA = "No Data";
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(nowUTC) - midnightTz) / 60);
const maxDateTodayTimestampTz = BeginningOfMinute({ timeZone: localTz });
@@ -122,6 +125,11 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
summaryDuration = getSummaryDuration(dayData.DOWN, selectedLang);
summaryStatus = "DOWN";
}
if (dayData.MAINTENANCE > 0) {
cssClass = returnStatusClass(dayData.MAINTENANCE, dayData.total, StatusObj.MAINTENANCE, site.barStyle);
summaryDuration = getSummaryDuration(dayData.MAINTENANCE, selectedLang);
summaryStatus = "MAINTENANCE";
}
if (dayData.NO_DATA === dayData.total) {
cssClass = StatusObj.NO_DATA;
summaryStatus = NO_DATA;
@@ -175,6 +183,11 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
summaryStatus = "DOWN";
summaryColorClass = "api-down";
}
if (!!lastRow && lastRow.status == "MAINTENANCE") {
summaryDuration = getSummaryDuration(getCountOfSimilarStatuesEnd(todayDataDb, "MAINTENANCE"), selectedLang);
summaryStatus = "MAINTENANCE";
summaryColorClass = "api-maintenance";
}
if (lastRow.status === "NO_DATA") {
summaryStatus = NO_DATA;
summaryColorClass = "api-nodata";
@@ -197,6 +210,8 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
barRoundness: site.barRoundness,
midnight90DaysAgo: midnight90DaysAgoTz,
maxDateTodayTimestamp: maxDateTodayTimestampTz,
startOfTheDay: midnightTz,
homeDataMaxDays,
};
};
export { FetchData };
+64 -63
View File
@@ -3,85 +3,86 @@ import net from "net"; // Use import instead of require
import ping from "ping";
const TCP = function (type, host, port, timeout) {
port = parseInt(port, 10);
return new Promise((resolve) => {
const socket = new net.Socket();
const start = process.hrtime.bigint(); // High-precision timestamp
let resolved = false;
timeout = Number(timeout);
port = parseInt(port, 10);
return new Promise((resolve) => {
const socket = new net.Socket();
const start = process.hrtime.bigint(); // High-precision timestamp
let resolved = false;
const onFinish = (status) => {
if (!resolved) {
resolved = true;
const end = process.hrtime.bigint();
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
socket.destroy();
resolve({ status, latency, host, port, type });
}
};
const onFinish = (status) => {
if (!resolved) {
resolved = true;
const end = process.hrtime.bigint();
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
socket.destroy();
resolve({ status, latency, host, port, type });
}
};
socket.setTimeout(timeout);
socket.once("connect", () => onFinish("open"));
socket.once("timeout", () => onFinish("timeout"));
socket.once("error", () => onFinish("error"));
socket.setTimeout(timeout);
socket.once("connect", () => onFinish("open"));
socket.once("timeout", () => onFinish("timeout"));
socket.once("error", () => onFinish("error"));
// Check if it's an IPv6 address (contains ':')
const options = {
host,
port,
family: type === "IP6" ? 6 : 4
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
socket.connect(options);
});
// Check if it's an IPv6 address (contains ':')
const options = {
host,
port,
family: type === "IP6" ? 6 : 4,
}; //host.includes(":") ? { host, port, family: 6 } : { host, port };
socket.connect(options);
});
};
const Ping = async function (type, host, timeout, count) {
let output = {
alive: false,
min: null,
max: null,
avg: null,
latencies: [],
latency: null,
host: host,
type: type
};
let output = {
alive: false,
min: null,
max: null,
avg: null,
latencies: [],
latency: null,
host: host,
type: type,
};
try {
let res = await ping.promise.probe(host, {
v6: type === "IP6",
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
min_reply: count
});
output.alive = res.alive;
output.min = res.min;
output.max = res.max;
output.avg = res.avg;
output.latencies = res.times;
output.latency = res.time;
} catch (error) {
console.log(`Error in pingCall IP4 for ${host}`, error);
}
return output;
try {
let res = await ping.promise.probe(host, {
v6: type === "IP6",
timeout: type === "IP6" ? false : Math.floor(timeout / 1000),
min_reply: count,
});
output.alive = res.alive;
output.min = res.min;
output.max = res.max;
output.avg = res.avg;
output.latencies = res.times;
output.latency = res.time;
} catch (error) {
console.log(`Error in pingCall IP4 for ${host}`, error);
}
return output;
};
/**
* @param {string} input
*/
function ExtractIPv6HostAndPort(input) {
const parts = input.split(":"); // Split by colons
const parts = input.split(":"); // Split by colons
// If there's a valid port at the end, extract it
const lastPart = parts[parts.length - 1];
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
// If there's a valid port at the end, extract it
const lastPart = parts[parts.length - 1];
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
// Reconstruct the IPv6 address
const host = parts.join(":");
// Reconstruct the IPv6 address
const host = parts.join(":");
// Ensure it's a valid IPv6 format
if (host.includes(":")) {
return { host, port }; // Port may be null if not present
}
return null; // Return null if the format is incorrect
// Ensure it's a valid IPv6 format
if (host.includes(":")) {
return { host, port }; // Port may be null if not present
}
return null; // Return null if the format is incorrect
}
export { TCP, ExtractIPv6HostAndPort, Ping };
+67
View File
@@ -0,0 +1,67 @@
// @ts-nocheck
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import { GameDig } from 'gamedig';
import { DefaultGamedigEval, GAMEDIG_SOCKET_TIMEOUT, GAMEDIG_TIMEOUT } from "../../anywhere.js";
class GamedigCall {
monitor;
constructor(monitor) {
this.monitor = monitor;
}
async execute() {
const tag = this.monitor.tag;
const host = this.monitor.type_data.host;
const port = this.monitor.type_data.port;
const timeout = !!this.monitor.type_data.timeout ? this.monitor.type_data.timeout : GAMEDIG_TIMEOUT;
const gamedigEval = !!this.monitor.type_data.eval ? this.monitor.type_data.eval : DefaultGamedigEval;
// Query
let responseTime, responseRaw;
try {
const response = await GameDig.query({
type: this.monitor.type_data.gameId,
host: host,
port: port,
socketTimeout: GAMEDIG_SOCKET_TIMEOUT,
attemptTimeout: timeout,
givenPortOnly: !this.monitor.type_data.guessPort,
requestRules: this.monitor.type_data.requestRules,
});
responseTime = response.ping;
responseRaw = response.raw;
} catch (error) {
console.log(`Error while requesting game's information for ${tag}: `, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
// Eval
let evalResp = undefined;
const evalFunction = new Function("responseTime", "responseRaw", `return (${gamedigEval})(responseTime, responseRaw);`);
try {
evalResp = await evalFunction(responseTime, responseRaw);
} catch (error) {
console.log(`Error in gamedigEval for ${tag}: `, error.message);
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
// Result
return {
status: evalResp.status,
latency: evalResp.latency,
type: REALTIME,
};
}
}
export default GamedigCall;
+8 -13
View File
@@ -4,21 +4,16 @@ import { GetRequiredSecrets, ReplaceAllOccurrences, Wait, GetMinuteStartNowTimes
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import db from "../db/db.js";
async function waitForDataAndReturn(arr, ts, d, maxTime) {
await Wait(d);
let data = await db.getLastStatusBeforeCombined(arr, ts);
if (data) {
data.type = REALTIME;
return data;
} else if (d > maxTime) {
async function waitForDataAndReturn(tag) {
let res = await db.getLatestMonitoringData(tag);
if (!!res) {
return {
status: DOWN,
latency: 0,
type: TIMEOUT,
status: res.status,
latency: res.latency,
type: REALTIME,
};
} else {
return await waitForDataAndReturn(arr, ts, d * 2, maxTime);
}
return null;
}
class GroupCall {
@@ -33,7 +28,7 @@ class GroupCall {
startOfMinute = GetMinuteStartNowTimestampUTC();
}
let tagArr = this.monitor.type_data.monitors.map((m) => m.tag);
return await waitForDataAndReturn(tagArr, startOfMinute, 500, this.monitor.type_data.timeout);
return await waitForDataAndReturn(this.monitor.tag);
}
}
+30 -2
View File
@@ -2,6 +2,7 @@
import { UP, DOWN, DEGRADED, REALTIME, TIMEOUT, ERROR, MANUAL } from "../constants.js";
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
import { GetLastHeartbeat } from "../controllers/controller.js";
import { Cron } from "croner";
class HeartbeatCall {
monitor;
@@ -11,12 +12,37 @@ class HeartbeatCall {
}
async execute() {
let nowMinute = GetMinuteStartNowTimestampUTC();
let nowMinute = GetMinuteStartNowTimestampUTC(); // current time in seconds (minute start)
let latestData = await GetLastHeartbeat(this.monitor.tag);
if (!latestData) {
return {};
}
let latency = nowMinute - latestData.timestamp;
// Use croner to calculate expected heartbeat time
let expectedTime;
try {
const cronJob = Cron(this.monitor.cron);
const prevDate = cronJob.previousRun();
expectedTime = Math.floor(prevDate.getTime() / 1000); // seconds
} catch (err) {
return {
status: DOWN,
latency: 0,
type: ERROR,
};
}
// If heartbeat was received after or at expected time, it's UP
if (latestData.timestamp >= expectedTime) {
return {
status: UP,
latency: nowMinute - latestData.timestamp,
type: REALTIME,
};
}
// Calculate how late the expected heartbeat is
let latency = nowMinute - expectedTime;
let downRemainingMinutes = Number(this.monitor.type_data.downRemainingMinutes);
if (latency > downRemainingMinutes * 60) {
return {
@@ -25,6 +51,7 @@ class HeartbeatCall {
type: REALTIME,
};
}
let degradedRemainingMinutes = Number(this.monitor.type_data.degradedRemainingMinutes);
if (latency > degradedRemainingMinutes * 60) {
return {
@@ -33,6 +60,7 @@ class HeartbeatCall {
type: REALTIME,
};
}
return {
status: UP,
latency: latency,
+3
View File
@@ -7,6 +7,7 @@ import GroupCall from "./groupCall.js";
import SSLCall from "./sslCall.js";
import SqlCall from "./sqlCall.js";
import HeartbeatCall from "./heartbeatCall.js";
import GamedigCall from "./gamedigCall.js";
class Service {
service;
@@ -30,6 +31,8 @@ class Service {
this.service = new SqlCall(monitor);
} else if (monitor.monitor_type === "HEARTBEAT") {
this.service = new HeartbeatCall(monitor);
} else if (monitor.monitor_type === "GAMEDIG") {
this.service = new GamedigCall(monitor);
} else {
console.log("Invalid monitor.monitor_type ", monitor.monitor_type);
process.exit(1);
+178
View File
@@ -0,0 +1,178 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<link rel="preload" as="image" href="{{logo_url}}" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<body
style="
background-color: rgb(243, 244, 246);
font-family:
ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;,
&quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;
padding-top: 40px;
padding-bottom: 40px;
"
>
<!--$-->
<div
style="
display: none;
overflow: hidden;
line-height: 1px;
opacity: 0;
max-height: 0;
max-width: 0;
"
>
Your verification code: {{email_code}}
</div>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
background-color: rgb(255, 255, 255);
border-radius: 8px;
margin-left: auto;
margin-right: auto;
margin-top: 0px;
margin-bottom: 0px;
padding: 24px;
max-width: 600px;
"
>
<tbody>
<tr style="width: 100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-top: 8px; margin-bottom: 32px; text-align: center"
>
<tbody>
<tr>
<td>
<img
alt="{{brand_name}}"
height="40"
src="{{logo_url}}"
style="
margin-left: auto;
margin-right: auto;
display: block;
outline: none;
border: none;
text-decoration: none;
"
width="120"
/>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tbody>
<tr>
<td>
<h1
style="
font-size: 24px;
font-weight: 700;
color: rgb(31, 41, 55);
margin-bottom: 16px;
text-align: center;
"
>
Verification Code
</h1>
<p
style="
font-size: 16px;
color: rgb(75, 85, 99);
margin-bottom: 24px;
line-height: 24px;
margin-top: 16px;
"
>
To complete your {{action}} for {{brand_name}} updates, please use the
verification code below:
</p>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
background-color: rgb(249, 250, 251);
border-width: 1px;
border-color: rgb(229, 231, 235);
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
text-align: center;
"
>
<tbody>
<tr>
<td>
<p
style="
font-size: 32px;
font-weight: 700;
letter-spacing: 4px;
color: rgb(31, 41, 55);
margin: 0px;
line-height: 24px;
margin-bottom: 0px;
margin-top: 0px;
margin-left: 0px;
margin-right: 0px;
"
>
{{email_code}}
</p>
</td>
</tr>
</tbody>
</table>
<p
style="
font-size: 16px;
color: rgb(75, 85, 99);
margin-bottom: 24px;
line-height: 24px;
margin-top: 16px;
"
>
This code will expire in 5 minutes. If you didn&#x27;t request this code, you
can safely ignore this email.
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--7--><!--/$-->
</body>
</html>
+248
View File
@@ -0,0 +1,248 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<link rel="preload" as="image" href="{{logo_url}}" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<body
style="
background-color: rgb(243, 244, 246);
font-family:
ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;,
&quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;
padding-top: 40px;
padding-bottom: 40px;
"
>
<!--$-->
<div
style="
display: none;
overflow: hidden;
line-height: 1px;
opacity: 0;
max-height: 0;
max-width: 0;
"
>
{{title}}
</div>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
max-width: 600px;
margin-left: auto;
margin-right: auto;
background-color: rgb(255, 255, 255);
border-radius: 8px;
overflow: hidden;
"
>
<tbody>
<tr style="width: 100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
background-color: #e4e5ec;
padding-left: 24px;
padding-right: 24px;
padding-top: 24px;
padding-bottom: 24px;
"
>
<tbody>
<tr>
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tbody style="width: 100%">
<tr style="width: 100%">
<td data-id="__react-email-column" style="text-align: center">
<img
alt="{{brand_name}}"
src="{{logo_url}}"
style="
height: auto;
display: block;
outline: none;
border: none;
text-decoration: none;
margin-left: auto;
margin-right: auto;
"
width="80"
/>
<p
style="
font-size: 20px;
font-weight: bold;
color: #191919;
margin: 8px 0 0 0;
text-align: center;
"
>
{{brand_name}}
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
padding-left: 24px;
padding-right: 24px;
padding-top: 32px;
padding-bottom: 32px;
"
>
<tbody>
<tr>
<td>
<h1
style="
font-size: 24px;
font-weight: 700;
color: rgb(31, 41, 55);
margin-bottom: 16px;
"
>
{{title}}
</h1>
<p
style="
font-size: 16px;
color: rgb(55, 65, 81);
margin-bottom: 16px;
line-height: 24px;
margin-top: 16px;
"
>
Dear Valued Customer,
</p>
<p
style="
font-size: 16px;
color: rgb(55, 65, 81);
margin-bottom: 16px;
line-height: 24px;
margin-top: 16px;
"
>
We would like to provide you with an update regarding the current system
status.
</p>
<p
style="
font-size: 16px;
color: rgb(55, 65, 81);
margin-bottom: 16px;
line-height: 24px;
margin-top: 16px;
"
>
<strong>Update:</strong>
{{update_message}}
</p>
<a
href="{{incident_url}}"
style="
background-color: rgb(22, 163, 74);
color: rgb(255, 255, 255);
font-weight: 700;
padding-top: 12px;
padding-bottom: 12px;
padding-left: 24px;
padding-right: 24px;
border-radius: 4px;
font-size: 16px;
text-decoration-line: none;
text-align: center;
display: block;
box-sizing: border-box;
line-height: 100%;
text-decoration: none;
max-width: 100%;
mso-padding-alt: 0px;
padding: 12px 24px 12px 24px;
"
target="_blank"
><span
><!--[if mso
]><i style="mso-font-width: 400%; mso-text-raise: 18" hidden
>&#8202;&#8202;&#8202;</i
><!
[endif]--></span
><span
style="
max-width: 100%;
display: inline-block;
line-height: 120%;
mso-padding-alt: 0px;
mso-text-raise: 9px;
"
>View Incident Details</span
><span
><!--[if mso
]><i style="mso-font-width: 400%" hidden
>&#8202;&#8202;&#8202;&#8203;</i
><!
[endif]--></span
></a
>
</td>
</tr>
</tbody>
</table>
<hr
style="
border-top-width: 1px;
border-color: rgb(209, 213, 219);
margin-top: 8px;
margin-bottom: 8px;
width: 100%;
border: none;
border-top: 1px solid #eaeaea;
"
/>
<footer>
<p style="text-align: center; color: #6b7280; font-size: 16px; margin: 24px 0 0 0">
Thank you,<br />The {{brand_name}} Team
</p>
</footer>
<div style="height: 32px"></div>
</td>
</tr>
</tbody>
</table>
<!--7--><!--/$-->
</body>
</html>
+20
View File
@@ -149,6 +149,7 @@ const StatusObj = {
UP: "api-up",
DEGRADED: "api-degraded",
DOWN: "api-down",
MAINTENANCE: "api-maintenance",
NO_DATA: "api-nodata",
};
// @ts-ignore
@@ -280,6 +281,9 @@ function Wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function MaskString(str) {
if (str.length <= 4) {
return "*".repeat(str.length);
}
return "*".repeat(str.length - 4) + str.slice(-4);
}
@@ -293,6 +297,20 @@ function HashString(str) {
return hash.digest("hex");
}
//function to validate email address
function ValidateEmail(email) {
const regex =
/^(?:(?:[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+\.)*[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]+)@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\[(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?:\.(?!$)|$)){4}\]))$/;
return regex.test(email);
}
//function to generate 6 digit random number, number of digits as argument
function GenerateRandomNumber(digits) {
const min = Math.pow(10, digits - 1);
const max = Math.pow(10, digits) - 1;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export {
IsValidURL,
IsValidHTTPMethod,
@@ -322,4 +340,6 @@ export {
MaskString,
GetDbType,
HashString,
ValidateEmail,
GenerateRandomNumber,
};
+2 -2
View File
@@ -9,7 +9,7 @@ import {
import db from "./db/db.js";
import { WEBHOOK } from "./constants.js";
import { GetMonitors, VerifyAPIKey } from "./controllers/controller.js";
import { GetMonitors, VerifyAPIKey, InsertMonitoringData } from "./controllers/controller.js";
const GetAllTags = async function () {
let tags = [];
@@ -94,7 +94,7 @@ const store = async function (data) {
//get the monitor object matching the tag
await db.insertMonitoringData({
await InsertMonitoringData({
monitor_tag: tag,
timestamp: data.timestampInSeconds,
status: resp.status,
+42
View File
@@ -0,0 +1,42 @@
/**
* ***************************************************
* * *
* * Amplitude Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.amplitude.add(window.sessionReplay.plugin({ sampleRate: 1 }));
window.amplitude.init("{{api_key}}", { fetchRemoteConfig: true, autocapture: true });
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.amplitude.track(eventName, eventData);
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://cdn.amplitude.com/script/{{api_key}}.js", initJS, document.getElementsByTagName("head")[0]);
});
})();
+20
View File
@@ -0,0 +1,20 @@
/**
* ***************************************************
* * *
* * Clarity Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function (c, l, a, r, i, t, y) {
c[a] =
c[a] ||
function () {
(c[a].q = c[a].q || []).push(arguments);
};
t = l.createElement(r);
t.async = 1;
t.src = "https://www.clarity.ms/tag/" + i;
y = l.getElementsByTagName(r)[0];
y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "{{project_id}}");
@@ -0,0 +1,48 @@
/**
* ***************************************************
* * *
* * Google Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "{{id}}");
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
// Send the event to Google Tag Manager
if (window.dataLayer && eventName) {
gtag("event", eventName, eventData);
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://www.googletagmanager.com/gtag/js?id={{id}}", initJS, document.getElementsByTagName("head")[0]);
});
})();
+78
View File
@@ -0,0 +1,78 @@
/**
* ***************************************************
* * *
* * Mixpanel Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function (f, b) {
if (!b.__SV) {
var e, g, i, h;
window.mixpanel = b;
b._i = [];
b.init = function (e, f, c) {
function g(a, d) {
var b = d.split(".");
2 == b.length && ((a = a[b[0]]), (d = b[1]));
a[d] = function () {
a.push([d].concat(Array.prototype.slice.call(arguments, 0)));
};
}
var a = b;
"undefined" !== typeof c ? (a = b[c] = []) : (c = "mixpanel");
a.people = a.people || [];
a.toString = function (a) {
var d = "mixpanel";
"mixpanel" !== c && (d += "." + c);
a || (d += " (stub)");
return d;
};
a.people.toString = function () {
return a.toString(1) + ".people (stub)";
};
i =
"disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(
" ",
);
for (h = 0; h < i.length; h++) g(a, i[h]);
var j = "set set_once union unset remove delete".split(" ");
a.get_group = function () {
function b(c) {
d[c] = function () {
call2_args = arguments;
call2 = [c].concat(Array.prototype.slice.call(call2_args, 0));
a.push([e, call2]);
};
}
for (var d = {}, e = ["get_group"].concat(Array.prototype.slice.call(arguments, 0)), c = 0; c < j.length; c++)
b(j[c]);
return d;
};
b._i.push([e, f, c]);
};
b.__SV = 1.2;
e = f.createElement("script");
e.type = "text/javascript";
e.async = !0;
e.src =
"undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL
? MIXPANEL_CUSTOM_LIB_URL
: "file:" === f.location.protocol && "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)
? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"
: "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";
g = f.getElementsByTagName("script")[0];
g.parentNode.insertBefore(e, g);
}
})(document, window.mixpanel || []);
window.mixpanel.init("{{YOUR_PROJECT_TOKEN}}", { track_pageview: true, autocapture: true });
window.addEventListener("analyticsEvent", function (e) {
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.mixpanel.track(eventName, eventData);
}
});
+47
View File
@@ -0,0 +1,47 @@
/**
* ***************************************************
* * *
* * Plausible Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.defer = true;
scriptTag.dataset.domain = "{{domain}}";
scriptTag.dataset.api = "{{api}}";
scriptTag.onload = implementationCode;
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.plausible =
window.plausible ||
function () {
(window.plausible.q = window.plausible.q || []).push(arguments);
};
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
if (eventName) {
window.plausible(eventName, { props: eventData });
}
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("{{script_src}}", initJS, document.getElementsByTagName("head")[0]);
});
})();
+38
View File
@@ -0,0 +1,38 @@
/**
* ***************************************************
* * *
* * Umami Analytics Capture Snippet *
* * *
* ***************************************************
**/
// @ts-nocheck
(function () {
let loadJS = function (url, implementationCode, location) {
//url is URL of external file, implementationCode is the code
//to be called from the file, location is the location to
//insert the <script> element
let scriptTag = document.createElement("script");
scriptTag.src = url;
scriptTag.onload = implementationCode;
scriptTag.dataset.websiteId = "{{website_id}}";
scriptTag.onreadystatechange = implementationCode;
location.appendChild(scriptTag);
};
let initJS = function () {
window.addEventListener("analyticsEvent", function (e) {
// Extract event name and data from the custom event
const eventName = e.detail.event;
const eventData = e.detail.data || {};
window.umami.track(eventName, eventData);
});
};
//on dom ready
document.addEventListener("DOMContentLoaded", function () {
loadJS("https://cloud.umami.is/script.js", initJS, document.getElementsByTagName("head")[0]);
});
})();
+1 -1
View File
@@ -1 +1 @@
<app-embed-monitor monitor="http://localhost:3000/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"></app-embed-monitor>
<app-embed-monitor monitor="{siteURL}{base}/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"></app-embed-monitor>
+1 -1
View File
@@ -1,5 +1,5 @@
<iframe
src="http://localhost:3000/embed/monitor-{selectedMonitor.value}?bgc={embedBgc.substr(1)}&locale={embedLocale.value}&theme={embedTheme}"
src="{siteURL}{base}/embed/monitor-{selectedMonitor.value}?bgc={embedBgc.substr(1)}&locale={embedLocale.value}&theme={embedTheme}"
width="100%"
height="200"
allowfullscreen="allowfullscreen"
+1 -1
View File
@@ -1 +1 @@
<script async src="http://localhost:3000/embed/monitor-{selectedMonitor.value}/js?bgc={embedBgc.substr(1)}&locale={embedLocale.value}&theme={embedTheme}&monitor=http://localhost:3000/embed/monitor-{selectedMonitor.value}"></script>
<script async src="{siteURL}{base}/embed/monitor-{selectedMonitor.value}/js?bgc={embedBgc.substr(1)}&locale={embedLocale.value}&theme={embedTheme}&monitor={siteURL}{base}/embed/monitor-{selectedMonitor.value}"></script>
+1 -1
View File
@@ -4,7 +4,7 @@ function App() {
return (
<div>
<h2>React Embed</h2>
<EmbedMonitor monitor="http://localhost:3000/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
<EmbedMonitor monitor="{siteURL}{base}/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
</div>
);
}
+1 -1
View File
@@ -2,4 +2,4 @@
import EmbedMonitor from "./EmbedMonitor.svelte";
</script>
<EmbedMonitor monitor="http://localhost:3000/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
<EmbedMonitor monitor="{siteURL}{base}/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div>
<h2>Vue Embed</h2>
<EmbedMonitor monitor="http://localhost:3000/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
<EmbedMonitor monitor="{siteURL}{base}/embed/monitor-{selectedMonitor.value}" theme="{embedTheme}" bgc="{embedBgc.substr(1)}" locale="{embedLocale.value}"/>
</div>
</template>
-14
View File
@@ -1,20 +1,6 @@
.tabs [data-state="active"] {
border-color: orange;
}
.copybtn .copy-btn {
transform: scale(1);
transition: transform 0.15s;
}
.copybtn .check-btn {
transform: scale(0);
transition: transform 0.15s;
}
.copybtn:focus .copy-btn {
transform: scale(0);
}
.copybtn:focus .check-btn {
transform: scale(1);
}
input,
[data-bits-select-trigger],
+1 -1
View File
@@ -217,7 +217,7 @@
</div>
</div>
<div
class="prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
class="kener-md prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:font-normal prose-pre:bg-opacity-0 dark:prose-pre:bg-neutral-900"
>
<slot />
</div>

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