mirror of
https://github.com/rajnandan1/kener.git
synced 2026-01-06 01:20:15 -06:00
Merge pull request #78 from rajnandan1/pretty
refactor: added prettier config
This commit is contained in:
22
.prettierignore
Normal file
22
.prettierignore
Normal file
@@ -0,0 +1,22 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
static/kener
|
||||
build
|
||||
config/monitors.yaml
|
||||
config/site.yaml
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.okgit/
|
||||
config/static/*
|
||||
!config/static/.kener
|
||||
**/*.yaml
|
||||
**/*.yml
|
||||
.github/
|
||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
77
README.md
77
README.md
@@ -1,8 +1,6 @@
|
||||
|
||||
<p align="center">
|
||||
<img src="https://kener.ing/ss.png" width="100%" height="auto" alt="kener example illustration">
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/rajnandan1/kener?label=Star%20Repo&style=social">
|
||||
@@ -12,62 +10,66 @@
|
||||
|
||||
#### 👉 Visit a live server [here](https://kener.ing)
|
||||
|
||||
#### 👉 Read the documentation [here](https://kener.ing/docs)
|
||||
#### 👉 Read the documentation [here](https://kener.ing/docs)
|
||||
|
||||
# Kener - Status Page System
|
||||
|
||||
Kener: Open-source Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment.
|
||||
|
||||
It uses files to store the data. Other adapters are coming soon
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
**Monitoring and Tracking:**
|
||||
- Real-time monitoring
|
||||
- Polls HTTP endpoint or Push data to monitor using Rest APIs
|
||||
- Handles Timezones for visitors
|
||||
- Categorize Monitors into different Sections
|
||||
- Cron-based scheduling for monitors. Minimum per minute
|
||||
- Flexible monitor configuration using YAML. Define your own parsing for monitor being UP/DOWN/DEGRADED
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors. Example defaultStatus=DOWN if you dont hit API per minute with Status UP
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
|
||||
- Real-time monitoring
|
||||
- Polls HTTP endpoint or Push data to monitor using Rest APIs
|
||||
- Handles Timezones for visitors
|
||||
- Categorize Monitors into different Sections
|
||||
- Cron-based scheduling for monitors. Minimum per minute
|
||||
- Flexible monitor configuration using YAML. Define your own parsing for monitor being UP/DOWN/DEGRADED
|
||||
- Construct complex API Polls - Chain, Secrets etc
|
||||
- Supports a Default Status for Monitors. Example defaultStatus=DOWN if you dont hit API per minute with Status UP
|
||||
- Supports base path for hosting in k8s
|
||||
- Pre-built docker image for easy deployment
|
||||
|
||||
**Customization and Branding:**
|
||||
- Customizable status page using yaml or code
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
|
||||
- Customizable status page using yaml or code
|
||||
- Badge generation for status and uptime of Monitors
|
||||
- Support for custom domains
|
||||
- Embed Monitor as an iframe or widget
|
||||
- Light + Dark Theme
|
||||
- Internationalization support
|
||||
|
||||
**Incident Management:**
|
||||
- Create Incidents using Github Issues - Rich Text
|
||||
- Or use APIs to create Incidents
|
||||
|
||||
- Create Incidents using Github Issues - Rich Text
|
||||
- Or use APIs to create Incidents
|
||||
|
||||
**User Experience and Design:**
|
||||
- 100% Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
|
||||
|
||||
- 100% Accessibility Score
|
||||
- Easy installation and setup
|
||||
- User-friendly interface
|
||||
- Responsive design for various devices
|
||||
- Auto SEO and Social Media ready
|
||||
|
||||
## Technologies used
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Inspired from
|
||||
- [Upptime](https://upptime.js.org/)
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [shadcn-svelte](https://www.shadcn-svelte.com/)
|
||||
|
||||
## Inspired from
|
||||
|
||||
- [Upptime](https://upptime.js.org/)
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] Add api to create incident
|
||||
- [x] Add docker file
|
||||
- [ ] Add notification
|
||||
- [ ] Add Mysql adapter
|
||||
- [x] Add api to create incident
|
||||
- [x] Add docker file
|
||||
- [ ] Add notification
|
||||
- [ ] Add Mysql adapter
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -81,7 +83,6 @@ It uses files to store the data. Other adapters are coming soon
|
||||

|
||||

|
||||
|
||||
|
||||
## Support
|
||||
|
||||
<a href="https://stackexchange.com/users/3713933"><img src="https://stackexchange.com/users/flair/3713933.png" width="108" height="28" alt="profile for Raj Nandan Sharma on Stack Exchange, a network of free, community-driven Q&A sites" title="profile for Raj Nandan Sharma on Stack Exchange, a network of free, community-driven Q&A sites"></a>
|
||||
@@ -89,5 +90,3 @@ It uses files to store the data. Other adapters are coming soon
|
||||
<a href="https://www.buymeacoffee.com/rajnandan1"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rajnandan1&button_colour=5F7FFF&font_colour=ffffff&font_family=Poppins&outline_colour=000000&coffee_colour=FFDD00" /></a>
|
||||
|
||||
<a href="https://www.paypal.com/paypalme/rajnandan1"><img style="height:90px;margin-left:-15px" src="static/paypal.png" /></a>
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.postcss",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils"
|
||||
}
|
||||
}
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.postcss",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
tag: "google-search"
|
||||
image: "/google.png"
|
||||
api:
|
||||
method: GET
|
||||
url: https://www.google.com/webhp
|
||||
method: GET
|
||||
url: https://www.google.com/webhp
|
||||
- name: Svelte Website
|
||||
description: Cybernetically enhanced web apps
|
||||
tag: "svelte-website"
|
||||
api:
|
||||
method: GET
|
||||
url: https://svelte.dev/
|
||||
method: GET
|
||||
url: https://svelte.dev/
|
||||
image: "/svelte.svg"
|
||||
- name: Earth
|
||||
description: Our blue planet
|
||||
@@ -22,5 +22,5 @@
|
||||
tag: "frogment"
|
||||
image: "/frogment.png"
|
||||
api:
|
||||
method: GET
|
||||
url: https://www.frogment.com
|
||||
method: GET
|
||||
url: https://www.frogment.com
|
||||
|
||||
@@ -2,42 +2,41 @@ title: "Kener"
|
||||
home: "/"
|
||||
logo: "/logo.png"
|
||||
github:
|
||||
owner: "rajnandan1"
|
||||
repo: "kener"
|
||||
incidentSince: 48
|
||||
owner: "rajnandan1"
|
||||
repo: "kener"
|
||||
incidentSince: 48
|
||||
metaTags:
|
||||
description: "Kener: Open-source modern looking Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment."
|
||||
keywords: "Node.js status page, Incident management tool, Service monitoring, Service outage tracking, Real-time status updates, GitHub integration for incidents, Open-source status page, Node.js monitoring application, Service reliability, User-friendly incident management, Collaborative incident resolution, Seamless outage communication, Service disruption tracker, Real-time incident alerts, Node.js status reporting"
|
||||
og:description: "Kener: Open-source Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment."
|
||||
og:image: "https://kener.ing/ss.png"
|
||||
og:title: "Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
og:type: "website"
|
||||
og:site_name: "Kener"
|
||||
twitter:card: "summary_large_image"
|
||||
twitter:site: "@_rajnandan_"
|
||||
twitter:creator: "@_rajnandan_"
|
||||
twitter:image: "https://kener.ing/ss.png"
|
||||
twitter:title: "Kener: Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
twitter:description: "Kener: Open-source Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment."
|
||||
description: "Kener: Open-source modern looking Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment."
|
||||
keywords: "Node.js status page, Incident management tool, Service monitoring, Service outage tracking, Real-time status updates, GitHub integration for incidents, Open-source status page, Node.js monitoring application, Service reliability, User-friendly incident management, Collaborative incident resolution, Seamless outage communication, Service disruption tracker, Real-time incident alerts, Node.js status reporting"
|
||||
og:description: "Kener: Open-source Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment."
|
||||
og:image: "https://kener.ing/ss.png"
|
||||
og:title: "Kener - Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
og:type: "website"
|
||||
og:site_name: "Kener"
|
||||
twitter:card: "summary_large_image"
|
||||
twitter:site: "@_rajnandan_"
|
||||
twitter:creator: "@_rajnandan_"
|
||||
twitter:image: "https://kener.ing/ss.png"
|
||||
twitter:title: "Kener: Open-Source and Modern looking Node.js Status Page for Effortless Incident Management"
|
||||
twitter:description: "Kener: Open-source Node.js status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents. And the best part? Kener integrates seamlessly with GitHub, making incident management a team effort—making it easier for us to track and fix issues together in a collaborative and friendly environment."
|
||||
nav:
|
||||
- name: "Documentation"
|
||||
url: "/docs"
|
||||
- name: "Github"
|
||||
url: "https://github.com/rajnandan1/kener"
|
||||
- name: "Documentation"
|
||||
url: "/docs"
|
||||
- name: "Github"
|
||||
url: "https://github.com/rajnandan1/kener"
|
||||
hero:
|
||||
title: Kener is a Open-Source Status Page System
|
||||
subtitle: Let your users know what's going on.
|
||||
title: Kener is a Open-Source Status Page System
|
||||
subtitle: Let your users know what's going on.
|
||||
footerHTML: |
|
||||
Made using
|
||||
<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">
|
||||
Kener
|
||||
</a>
|
||||
an open source status page system built with Svelte and TailwindCSS.
|
||||
Made using
|
||||
<a href="https://github.com/rajnandan1/kener" target="_blank" rel="noreferrer" class="font-medium underline underline-offset-4">
|
||||
Kener
|
||||
</a>
|
||||
an open source status page system built with Svelte and TailwindCSS.
|
||||
i18n:
|
||||
defaultLocale: "en"
|
||||
locales:
|
||||
en: "English"
|
||||
hi: "हिन्दी"
|
||||
zh-CN: "中文"
|
||||
ja: "日本語"
|
||||
|
||||
defaultLocale: "en"
|
||||
locales:
|
||||
en: "English"
|
||||
hi: "हिन्दी"
|
||||
zh-CN: "中文"
|
||||
ja: "日本語"
|
||||
|
||||
110
locales/en.json
110
locales/en.json
@@ -1,57 +1,57 @@
|
||||
{
|
||||
"root": {
|
||||
"ongoing_incidents": "Ongoing Incidents",
|
||||
"availability_per_component": "Availability per Component",
|
||||
"other_monitors": "Other Monitors",
|
||||
"no_monitors": "No monitors found",
|
||||
"read_doc_monitor": "Read the documentation to add your first monitor",
|
||||
"here": "here",
|
||||
"category": "Category",
|
||||
"incident": "Incident",
|
||||
"incidents": "Incidents",
|
||||
"no_recent_incident": "No recent incident",
|
||||
"recent_incidents": "Recent Incidents",
|
||||
"active_incidents": "Active Incidents",
|
||||
"no_active_incident": "No Active Incident",
|
||||
"last_x_hours": "Last %hours hours"
|
||||
},
|
||||
"statuses": {
|
||||
"UP": "UP",
|
||||
"DOWN": "DOWN",
|
||||
"DEGRADED": "DEGRADED"
|
||||
},
|
||||
"incident": {
|
||||
"identified": "Identified",
|
||||
"resolved": "Resolved",
|
||||
"maintenance": "Maintenance"
|
||||
},
|
||||
"monitor": {
|
||||
"share": "Share",
|
||||
"badge": "Badge",
|
||||
"embed": "Embed",
|
||||
"mode": "Mode",
|
||||
"status": "Status",
|
||||
"copied": "Copied",
|
||||
"uptime": "Uptime",
|
||||
"theme": "Theme",
|
||||
"theme_light": "Light",
|
||||
"theme_dark": "Dark",
|
||||
"today": "Today",
|
||||
"90_day": "90 Day",
|
||||
"share_desc": "Share this monitor using a link with others",
|
||||
"badge_desc": "Get SVG badge for this monitor",
|
||||
"embed_desc": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"cp_link": "Copy Link",
|
||||
"cpd_link": "Link Copied",
|
||||
"cp_code": "Copy Code",
|
||||
"cpd_code": "Code Copied",
|
||||
"status_x_minute": "%status for %minute minute",
|
||||
"status_x_minutes": "%status for %minutes minutes",
|
||||
"status_x_hour_y_minute": "%status for %hours h and %minutes m",
|
||||
"status_no_data": "No Data",
|
||||
"status_ok": "Status OK",
|
||||
"am": "am",
|
||||
"pm": "pm"
|
||||
},
|
||||
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
"root": {
|
||||
"ongoing_incidents": "Ongoing Incidents",
|
||||
"availability_per_component": "Availability per Component",
|
||||
"other_monitors": "Other Monitors",
|
||||
"no_monitors": "No monitors found",
|
||||
"read_doc_monitor": "Read the documentation to add your first monitor",
|
||||
"here": "here",
|
||||
"category": "Category",
|
||||
"incident": "Incident",
|
||||
"incidents": "Incidents",
|
||||
"no_recent_incident": "No recent incident",
|
||||
"recent_incidents": "Recent Incidents",
|
||||
"active_incidents": "Active Incidents",
|
||||
"no_active_incident": "No Active Incident",
|
||||
"last_x_hours": "Last %hours hours"
|
||||
},
|
||||
"statuses": {
|
||||
"UP": "UP",
|
||||
"DOWN": "DOWN",
|
||||
"DEGRADED": "DEGRADED"
|
||||
},
|
||||
"incident": {
|
||||
"identified": "Identified",
|
||||
"resolved": "Resolved",
|
||||
"maintenance": "Maintenance"
|
||||
},
|
||||
"monitor": {
|
||||
"share": "Share",
|
||||
"badge": "Badge",
|
||||
"embed": "Embed",
|
||||
"mode": "Mode",
|
||||
"status": "Status",
|
||||
"copied": "Copied",
|
||||
"uptime": "Uptime",
|
||||
"theme": "Theme",
|
||||
"theme_light": "Light",
|
||||
"theme_dark": "Dark",
|
||||
"today": "Today",
|
||||
"90_day": "90 Day",
|
||||
"share_desc": "Share this monitor using a link with others",
|
||||
"badge_desc": "Get SVG badge for this monitor",
|
||||
"embed_desc": "Embed this monitor using <script> or <iframe> in your app.",
|
||||
"cp_link": "Copy Link",
|
||||
"cpd_link": "Link Copied",
|
||||
"cp_code": "Copy Code",
|
||||
"cpd_code": "Code Copied",
|
||||
"status_x_minute": "%status for %minute minute",
|
||||
"status_x_minutes": "%status for %minutes minutes",
|
||||
"status_x_hour_y_minute": "%status for %hours h and %minutes m",
|
||||
"status_no_data": "No Data",
|
||||
"status_ok": "Status OK",
|
||||
"am": "am",
|
||||
"pm": "pm"
|
||||
},
|
||||
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
}
|
||||
|
||||
110
locales/hi.json
110
locales/hi.json
@@ -1,57 +1,57 @@
|
||||
{
|
||||
"incident": {
|
||||
"identified": "डेंटिफ़िएड",
|
||||
"maintenance": "मेंटेनेंस",
|
||||
"resolved": "रेसोल्वेड"
|
||||
},
|
||||
"monitor": {
|
||||
"90_day": "९० दिन",
|
||||
"am": "ऍम",
|
||||
"badge": "बैज",
|
||||
"badge_desc": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
|
||||
"copied": "कोपीएड",
|
||||
"cp_code": "कॉपी कोड",
|
||||
"cp_link": "लिंक कॉपी करें",
|
||||
"cpd_code": "कोड कॉपी किया गया",
|
||||
"cpd_link": "लिंक कॉपी किया गया",
|
||||
"embed": "एम्बेड",
|
||||
"embed_desc": "इस मॉनीटर को एम्बेड करें<script> या<iframe> अपने ऐप में.",
|
||||
"mode": "मोड",
|
||||
"pm": "पं",
|
||||
"share": "शेयर",
|
||||
"share_desc": "लिंक का उपयोग करके इस मॉनिटर को अन्य लोगों के साथ शेयर करें",
|
||||
"status": "स्टेटस",
|
||||
"status_no_data": "कोई डेटा नहीं",
|
||||
"status_ok": "स्थिति ठीक है",
|
||||
"status_x_hour_y_minute": "%hours घंटा %minutes मिनट के लिए %status",
|
||||
"status_x_minute": "%minute मिनट के लिए %status",
|
||||
"status_x_minutes": "%minutes मिनट के लिए %status",
|
||||
"theme": "थीम",
|
||||
"theme_dark": "डार्क",
|
||||
"theme_light": "लाइट",
|
||||
"today": "आज",
|
||||
"uptime": "अपटाइम"
|
||||
},
|
||||
"numbers": ["०", "१", "२", "३", "४", "५", "६", "७", "८", "९"],
|
||||
"root": {
|
||||
"active_incidents": "सक्रिय घटनाएं",
|
||||
"availability_per_component": "प्रति कॉम्पोनेन्ट उपलब्धता",
|
||||
"category": "श्रेणी",
|
||||
"here": "यहाँ",
|
||||
"incident": "हादसा",
|
||||
"incidents": "घटनाएं",
|
||||
"last_x_hours": "अंतिम %hours घंटे",
|
||||
"no_active_incident": "कोई सक्रिय घटना नहीं",
|
||||
"no_monitors": "कोई मॉनिटर नहीं मिला",
|
||||
"no_recent_incident": "कोई हालिया घटना नहीं",
|
||||
"ongoing_incidents": "चल रही घटनाएँ",
|
||||
"other_monitors": "अन्य मॉनिटर",
|
||||
"read_doc_monitor": "अपना पहला मॉनिटर जोड़ने के लिए डॉक्यूमेंटेशन पढ़ें",
|
||||
"recent_incidents": "हाल की घटनाएँ"
|
||||
},
|
||||
"statuses": {
|
||||
"DEGRADED": "डेग्रेडेड",
|
||||
"DOWN": "डाउन",
|
||||
"UP": "उप"
|
||||
}
|
||||
"incident": {
|
||||
"identified": "डेंटिफ़िएड",
|
||||
"maintenance": "मेंटेनेंस",
|
||||
"resolved": "रेसोल्वेड"
|
||||
},
|
||||
"monitor": {
|
||||
"90_day": "९० दिन",
|
||||
"am": "ऍम",
|
||||
"badge": "बैज",
|
||||
"badge_desc": "इस मॉनिटर के लिए SVG बैज प्राप्त करें",
|
||||
"copied": "कोपीएड",
|
||||
"cp_code": "कॉपी कोड",
|
||||
"cp_link": "लिंक कॉपी करें",
|
||||
"cpd_code": "कोड कॉपी किया गया",
|
||||
"cpd_link": "लिंक कॉपी किया गया",
|
||||
"embed": "एम्बेड",
|
||||
"embed_desc": "इस मॉनीटर को एम्बेड करें<script> या<iframe> अपने ऐप में.",
|
||||
"mode": "मोड",
|
||||
"pm": "पं",
|
||||
"share": "शेयर",
|
||||
"share_desc": "लिंक का उपयोग करके इस मॉनिटर को अन्य लोगों के साथ शेयर करें",
|
||||
"status": "स्टेटस",
|
||||
"status_no_data": "कोई डेटा नहीं",
|
||||
"status_ok": "स्थिति ठीक है",
|
||||
"status_x_hour_y_minute": "%hours घंटा %minutes मिनट के लिए %status",
|
||||
"status_x_minute": "%minute मिनट के लिए %status",
|
||||
"status_x_minutes": "%minutes मिनट के लिए %status",
|
||||
"theme": "थीम",
|
||||
"theme_dark": "डार्क",
|
||||
"theme_light": "लाइट",
|
||||
"today": "आज",
|
||||
"uptime": "अपटाइम"
|
||||
},
|
||||
"numbers": ["०", "१", "२", "३", "४", "५", "६", "७", "८", "९"],
|
||||
"root": {
|
||||
"active_incidents": "सक्रिय घटनाएं",
|
||||
"availability_per_component": "प्रति कॉम्पोनेन्ट उपलब्धता",
|
||||
"category": "श्रेणी",
|
||||
"here": "यहाँ",
|
||||
"incident": "हादसा",
|
||||
"incidents": "घटनाएं",
|
||||
"last_x_hours": "अंतिम %hours घंटे",
|
||||
"no_active_incident": "कोई सक्रिय घटना नहीं",
|
||||
"no_monitors": "कोई मॉनिटर नहीं मिला",
|
||||
"no_recent_incident": "कोई हालिया घटना नहीं",
|
||||
"ongoing_incidents": "चल रही घटनाएँ",
|
||||
"other_monitors": "अन्य मॉनिटर",
|
||||
"read_doc_monitor": "अपना पहला मॉनिटर जोड़ने के लिए डॉक्यूमेंटेशन पढ़ें",
|
||||
"recent_incidents": "हाल की घटनाएँ"
|
||||
},
|
||||
"statuses": {
|
||||
"DEGRADED": "डेग्रेडेड",
|
||||
"DOWN": "डाउन",
|
||||
"UP": "उप"
|
||||
}
|
||||
}
|
||||
|
||||
110
locales/ja.json
110
locales/ja.json
@@ -1,57 +1,57 @@
|
||||
{
|
||||
"root": {
|
||||
"ongoing_incidents": "進行中のインシデント",
|
||||
"availability_per_component": "コンポーネントごとの可用性",
|
||||
"other_monitors": "その他のモニター",
|
||||
"no_monitors": "モニターはありません",
|
||||
"read_doc_monitor": "最初のモニターを追加するには、ドキュメントをお読みください",
|
||||
"here": "ここ",
|
||||
"category": "カテゴリー",
|
||||
"incident": "インシデント",
|
||||
"incidents": "インシデント",
|
||||
"no_recent_incident": "最近のインシデントはありません",
|
||||
"recent_incidents": "最近のインシデント",
|
||||
"active_incidents": "現在のインシデント",
|
||||
"no_active_incident": "現在のインシデントはありません",
|
||||
"last_x_hours": "最近%hours時間"
|
||||
},
|
||||
"statuses": {
|
||||
"UP": "正常",
|
||||
"DOWN": "ダウン",
|
||||
"DEGRADED": "縮退"
|
||||
},
|
||||
"incident": {
|
||||
"identified": "確認済み",
|
||||
"resolved": "解決済み",
|
||||
"maintenance": "メンテナンス"
|
||||
},
|
||||
"monitor": {
|
||||
"share": "シェア",
|
||||
"badge": "バッジ",
|
||||
"embed": "埋め込み",
|
||||
"mode": "モード",
|
||||
"status": "ステータス",
|
||||
"copied": "コピーしました",
|
||||
"uptime": "稼働時間",
|
||||
"theme": "テーマ",
|
||||
"theme_light": "ライト",
|
||||
"theme_dark": "ダーク",
|
||||
"today": "今日",
|
||||
"90_day": "90日間",
|
||||
"share_desc": "このモニターをリンクで他の人にシェア",
|
||||
"badge_desc": "このモニターのSVGバッジを取得",
|
||||
"embed_desc": "このモニターを <script> や <iframe> で埋め込む",
|
||||
"cp_link": "リンクをコピー",
|
||||
"cpd_link": "リンクをコピーしました",
|
||||
"cp_code": "コードをコピー",
|
||||
"cpd_code": "コードをコピーしました",
|
||||
"status_x_minute": "%minute分間の%status",
|
||||
"status_x_minutes": "%minutes分間の%status",
|
||||
"status_x_hour_y_minute": "%hours時間%minutes分間の%status",
|
||||
"status_no_data": "データはありません",
|
||||
"status_ok": "正常",
|
||||
"am": "午前",
|
||||
"pm": "午後"
|
||||
},
|
||||
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
"root": {
|
||||
"ongoing_incidents": "進行中のインシデント",
|
||||
"availability_per_component": "コンポーネントごとの可用性",
|
||||
"other_monitors": "その他のモニター",
|
||||
"no_monitors": "モニターはありません",
|
||||
"read_doc_monitor": "最初のモニターを追加するには、ドキュメントをお読みください",
|
||||
"here": "ここ",
|
||||
"category": "カテゴリー",
|
||||
"incident": "インシデント",
|
||||
"incidents": "インシデント",
|
||||
"no_recent_incident": "最近のインシデントはありません",
|
||||
"recent_incidents": "最近のインシデント",
|
||||
"active_incidents": "現在のインシデント",
|
||||
"no_active_incident": "現在のインシデントはありません",
|
||||
"last_x_hours": "最近%hours時間"
|
||||
},
|
||||
"statuses": {
|
||||
"UP": "正常",
|
||||
"DOWN": "ダウン",
|
||||
"DEGRADED": "縮退"
|
||||
},
|
||||
"incident": {
|
||||
"identified": "確認済み",
|
||||
"resolved": "解決済み",
|
||||
"maintenance": "メンテナンス"
|
||||
},
|
||||
"monitor": {
|
||||
"share": "シェア",
|
||||
"badge": "バッジ",
|
||||
"embed": "埋め込み",
|
||||
"mode": "モード",
|
||||
"status": "ステータス",
|
||||
"copied": "コピーしました",
|
||||
"uptime": "稼働時間",
|
||||
"theme": "テーマ",
|
||||
"theme_light": "ライト",
|
||||
"theme_dark": "ダーク",
|
||||
"today": "今日",
|
||||
"90_day": "90日間",
|
||||
"share_desc": "このモニターをリンクで他の人にシェア",
|
||||
"badge_desc": "このモニターのSVGバッジを取得",
|
||||
"embed_desc": "このモニターを <script> や <iframe> で埋め込む",
|
||||
"cp_link": "リンクをコピー",
|
||||
"cpd_link": "リンクをコピーしました",
|
||||
"cp_code": "コードをコピー",
|
||||
"cpd_code": "コードをコピーしました",
|
||||
"status_x_minute": "%minute分間の%status",
|
||||
"status_x_minutes": "%minutes分間の%status",
|
||||
"status_x_hour_y_minute": "%hours時間%minutes分間の%status",
|
||||
"status_no_data": "データはありません",
|
||||
"status_ok": "正常",
|
||||
"am": "午前",
|
||||
"pm": "午後"
|
||||
},
|
||||
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
{
|
||||
"root": {
|
||||
"ongoing_incidents": "正在进行的事件",
|
||||
"availability_per_component": "每个服务的可用性",
|
||||
"other_monitors": "其他显示器",
|
||||
"no_monitors": "未找到显示器",
|
||||
"read_doc_monitor": "阅读文档以添加您的第一个显示器",
|
||||
"here": "这里",
|
||||
"category": "类别",
|
||||
"incident": "事件",
|
||||
"incidents": "事件",
|
||||
"no_recent_incident": "最近没有发生事件",
|
||||
"recent_incidents": "最近发生的事件",
|
||||
"active_incidents": "活跃事件",
|
||||
"no_active_incident": "没有活跃事件",
|
||||
"last_x_hours": "最近%hours个小时"
|
||||
},
|
||||
"statuses": {
|
||||
"UP": "正常",
|
||||
"DOWN": "故障",
|
||||
"DEGRADED": "异常"
|
||||
},
|
||||
"incident": {
|
||||
"identified": "确认",
|
||||
"resolved": "解决",
|
||||
"maintenance": "维护"
|
||||
},
|
||||
"monitor": {
|
||||
"share": "分享",
|
||||
"badge": "徽章",
|
||||
"embed": "嵌入",
|
||||
"mode": "模式",
|
||||
"status": "状态",
|
||||
"copied": "已复制",
|
||||
"uptime": "正常运行时间",
|
||||
"theme": "主题",
|
||||
"theme_light": "Light",
|
||||
"theme_dark": "Dark",
|
||||
"today": "今天",
|
||||
"90_day": "90 天",
|
||||
"share_desc": "使用链接与其他人共享此显示器",
|
||||
"badge_desc": "获取此显示器的 SVG 徽章",
|
||||
"embed_desc": "在您的应用程序中使用 <script> 或 <iframe> 嵌入此监视器。",
|
||||
"cp_link": "复制链接",
|
||||
"cpd_link": "链接已复制",
|
||||
"cp_code": "复制代码",
|
||||
"cpd_code": "代码已复制",
|
||||
"status_x_minute": "%minute分钟的%status",
|
||||
"status_x_minutes": "%minutes分钟的%status",
|
||||
"status_x_hour_y_minute": "%hours小时%minutes分钟的%status",
|
||||
"status_no_data": "没有数据",
|
||||
"status_ok": "状态正常",
|
||||
"am": "上午",
|
||||
"pm": "下午"
|
||||
},
|
||||
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
"root": {
|
||||
"ongoing_incidents": "正在进行的事件",
|
||||
"availability_per_component": "每个服务的可用性",
|
||||
"other_monitors": "其他显示器",
|
||||
"no_monitors": "未找到显示器",
|
||||
"read_doc_monitor": "阅读文档以添加您的第一个显示器",
|
||||
"here": "这里",
|
||||
"category": "类别",
|
||||
"incident": "事件",
|
||||
"incidents": "事件",
|
||||
"no_recent_incident": "最近没有发生事件",
|
||||
"recent_incidents": "最近发生的事件",
|
||||
"active_incidents": "活跃事件",
|
||||
"no_active_incident": "没有活跃事件",
|
||||
"last_x_hours": "最近%hours个小时"
|
||||
},
|
||||
"statuses": {
|
||||
"UP": "正常",
|
||||
"DOWN": "故障",
|
||||
"DEGRADED": "异常"
|
||||
},
|
||||
"incident": {
|
||||
"identified": "确认",
|
||||
"resolved": "解决",
|
||||
"maintenance": "维护"
|
||||
},
|
||||
"monitor": {
|
||||
"share": "分享",
|
||||
"badge": "徽章",
|
||||
"embed": "嵌入",
|
||||
"mode": "模式",
|
||||
"status": "状态",
|
||||
"copied": "已复制",
|
||||
"uptime": "正常运行时间",
|
||||
"theme": "主题",
|
||||
"theme_light": "Light",
|
||||
"theme_dark": "Dark",
|
||||
"today": "今天",
|
||||
"90_day": "90 天",
|
||||
"share_desc": "使用链接与其他人共享此显示器",
|
||||
"badge_desc": "获取此显示器的 SVG 徽章",
|
||||
"embed_desc": "在您的应用程序中使用 <script> 或 <iframe> 嵌入此监视器。",
|
||||
"cp_link": "复制链接",
|
||||
"cpd_link": "链接已复制",
|
||||
"cp_code": "复制代码",
|
||||
"cpd_code": "代码已复制",
|
||||
"status_x_minute": "%minute分钟的%status",
|
||||
"status_x_minutes": "%minutes分钟的%status",
|
||||
"status_x_hour_y_minute": "%hours小时%minutes分钟的%status",
|
||||
"status_no_data": "没有数据",
|
||||
"status_ok": "状态正常",
|
||||
"am": "上午",
|
||||
"pm": "下午"
|
||||
},
|
||||
"numbers": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
}
|
||||
|
||||
8324
package-lock.json
generated
8324
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
146
package.json
146
package.json
@@ -1,73 +1,77 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "0.0.14",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
|
||||
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
|
||||
"keywords": [
|
||||
"Node.js application",
|
||||
"Open-source status page",
|
||||
"Service monitoring tool",
|
||||
"Real-time incident management",
|
||||
"Customizable reporting",
|
||||
"Service outage tracker",
|
||||
"User-friendly dashboard",
|
||||
"Incident communication platform",
|
||||
"Scalable monitoring solution",
|
||||
"Community-driven software",
|
||||
"Website status tracker",
|
||||
"Incident response tool",
|
||||
"System status monitoring",
|
||||
"Service reliability management",
|
||||
"Incident alert system"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rajnandan1/kener.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/check.js && vite build",
|
||||
"serve": "node prod.js",
|
||||
"kener:dev": "cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener node scripts/check.js && concurrently \"cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener node dev.js\" \"cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener vite dev\"",
|
||||
"kener:dev-monitor": "cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener node dev.js",
|
||||
"kener:build": "cross-env NODE_ENV=production node scripts/check.js && cross-env NODE_ENV=production vite build",
|
||||
"kener": "cross-env NODE_ENV=production node prod.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.27.4",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
"bits-ui": "^0.9.9",
|
||||
"clsx": "^2.0.0",
|
||||
"croner": "^7.0.5",
|
||||
"express": "^4.18.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lucide-svelte": "^0.292.0",
|
||||
"marked": "^11.1.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"node-cache": "^5.1.2",
|
||||
"queue": "^7.0.0",
|
||||
"randomstring": "^1.3.0",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-variants": "^0.1.18"
|
||||
}
|
||||
"name": "kener",
|
||||
"version": "0.0.14",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"description": "Kener: An open-source Node.js status page application for real-time service monitoring, incident management, and customizable reporting. Simplify service outage tracking, enhance incident communication, and ensure a seamless user experience.",
|
||||
"author": "Raj Nandan Sharma <rajnandan1@gmail.com>",
|
||||
"keywords": [
|
||||
"Node.js application",
|
||||
"Open-source status page",
|
||||
"Service monitoring tool",
|
||||
"Real-time incident management",
|
||||
"Customizable reporting",
|
||||
"Service outage tracker",
|
||||
"User-friendly dashboard",
|
||||
"Incident communication platform",
|
||||
"Scalable monitoring solution",
|
||||
"Community-driven software",
|
||||
"Website status tracker",
|
||||
"Incident response tool",
|
||||
"System status monitoring",
|
||||
"Service reliability management",
|
||||
"Incident alert system"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rajnandan1/kener.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/check.js && vite build",
|
||||
"serve": "node prod.js",
|
||||
"kener:dev": "cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener node scripts/check.js && concurrently \"cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener node dev.js\" \"cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener vite dev\"",
|
||||
"kener:dev-monitor": "cross-env NODE_ENV=development PUBLIC_KENER_FOLDER=./static/kener node dev.js",
|
||||
"kener:build": "cross-env NODE_ENV=production node scripts/check.js && cross-env NODE_ENV=production vite build",
|
||||
"kener": "cross-env NODE_ENV=production node prod.js",
|
||||
"prettify": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.27.4",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"badge-maker": "^3.3.1",
|
||||
"bits-ui": "^0.9.9",
|
||||
"clsx": "^2.0.0",
|
||||
"croner": "^7.0.5",
|
||||
"express": "^4.18.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lucide-svelte": "^0.292.0",
|
||||
"marked": "^11.1.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"node-cache": "^5.1.2",
|
||||
"queue": "^7.0.0",
|
||||
"randomstring": "^1.3.0",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-variants": "^0.1.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ const tailwindcss = require("tailwindcss");
|
||||
const autoprefixer = require("autoprefixer");
|
||||
|
||||
const config = {
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer,
|
||||
],
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
12
prod.js
12
prod.js
@@ -9,13 +9,13 @@ Startup();
|
||||
|
||||
const app = express();
|
||||
app.use((req, res, next) => {
|
||||
if (req.path.startsWith("/embed")) {
|
||||
res.setHeader("X-Frame-Options", "None");
|
||||
}
|
||||
next();
|
||||
if (req.path.startsWith("/embed")) {
|
||||
res.setHeader("X-Frame-Options", "None");
|
||||
}
|
||||
next();
|
||||
});
|
||||
app.get("/healthcheck", (req, res) => {
|
||||
res.end("ok");
|
||||
res.end("ok");
|
||||
});
|
||||
|
||||
app.get("/sitemap.xml", (req, res) => {
|
||||
@@ -26,5 +26,5 @@ app.get("/sitemap.xml", (req, res) => {
|
||||
app.use(handler);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log("Kener is running on port " + PORT + "!");
|
||||
console.log("Kener is running on port " + PORT + "!");
|
||||
});
|
||||
|
||||
@@ -4,30 +4,28 @@ import { IsStringURLSafe } from "./tool.js";
|
||||
import fs from "fs-extra";
|
||||
let STATUS_OK = false;
|
||||
if (!!process.env.PUBLIC_KENER_FOLDER) {
|
||||
console.log(`✅ PUBLIC_KENER_FOLDER is ${process.env.PUBLIC_KENER_FOLDER}`);
|
||||
console.log(`✅ PUBLIC_KENER_FOLDER is ${process.env.PUBLIC_KENER_FOLDER}`);
|
||||
} else {
|
||||
console.log(`❌ process.env.PUBLIC_KENER_FOLDER is not set
|
||||
Set PUBLIC_KENER_FOLDER as an environment variable. Value should be the path to a directory where kener will store its data.
|
||||
Example:
|
||||
export PUBLIC_KENER_FOLDER=${process.cwd()}/static/kener`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(FOLDER)) {
|
||||
console.log(`❌ Directory does not exist\n\nRun:\nmkdir -p ${FOLDER}`);
|
||||
export PUBLIC_KENER_FOLDER=${process.cwd()}/static/kener`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(FOLDER)) {
|
||||
console.log(`❌ Directory does not exist\n\nRun:\nmkdir -p ${FOLDER}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(FOLDER_SITE)) {
|
||||
fs.writeFileSync(FOLDER_SITE, JSON.stringify({}));
|
||||
console.log("✅ site.json file created successfully!");
|
||||
fs.writeFileSync(FOLDER_SITE, JSON.stringify({}));
|
||||
console.log("✅ site.json file created successfully!");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(FOLDER_MONITOR)) {
|
||||
fs.writeFileSync(FOLDER_MONITOR, JSON.stringify([]));
|
||||
console.log("✅ monitors.json file created successfully!");
|
||||
fs.writeFileSync(FOLDER_MONITOR, JSON.stringify([]));
|
||||
console.log("✅ monitors.json file created successfully!");
|
||||
}
|
||||
|
||||
if (ENV === undefined) {
|
||||
@@ -35,53 +33,62 @@ if (ENV === undefined) {
|
||||
} else {
|
||||
console.log(`✅ process.env.NODE_ENV is set. Value is ${ENV}`);
|
||||
}
|
||||
if(process.env.GH_TOKEN === undefined) {
|
||||
console.log(`❗ GH_TOKEN is not set. Go to https://kener.ing/docs#h2github-setup to learn how to set it up`);
|
||||
if (process.env.GH_TOKEN === undefined) {
|
||||
console.log(
|
||||
`❗ GH_TOKEN is not set. Go to https://kener.ing/docs#h2github-setup to learn how to set it up`
|
||||
);
|
||||
} else {
|
||||
console.log(`✅ GH_TOKEN is set`);
|
||||
}
|
||||
|
||||
if(process.env.API_TOKEN === undefined) {
|
||||
console.log(`❗ API_TOKEN is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up`);
|
||||
if (process.env.API_TOKEN === undefined) {
|
||||
console.log(
|
||||
`❗ API_TOKEN is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up`
|
||||
);
|
||||
} else {
|
||||
console.log(`✅ API_TOKEN is set`);
|
||||
}
|
||||
if (process.env.API_IP === undefined) {
|
||||
console.log(`❗ API_IP is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up`);
|
||||
console.log(
|
||||
`❗ API_IP is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up`
|
||||
);
|
||||
} else {
|
||||
console.log(`✅ API_IP is set`);
|
||||
console.log(`✅ API_IP is set`);
|
||||
}
|
||||
if (process.env.MONITOR_YAML_PATH === undefined) {
|
||||
console.log(`❗ MONITOR_YAML_PATH is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up. Defaulting to config/monitors.yaml`);
|
||||
console.log(
|
||||
`❗ MONITOR_YAML_PATH is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up. Defaulting to config/monitors.yaml`
|
||||
);
|
||||
} else {
|
||||
console.log(`✅ MONITOR_YAML_PATH is set`);
|
||||
}
|
||||
if (process.env.SITE_YAML_PATH === undefined) {
|
||||
console.log(`❗ SITE_YAML_PATH is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up. Defaulting to config/site.yaml`);
|
||||
console.log(
|
||||
`❗ SITE_YAML_PATH is not set. Go to https://kener.ing/docs#h2environment-variable to learn how to set it up. Defaulting to config/site.yaml`
|
||||
);
|
||||
} else {
|
||||
console.log(`✅ SITE_YAML_PATH is set`);
|
||||
console.log(`✅ SITE_YAML_PATH is set`);
|
||||
}
|
||||
if (process.env.PORT === undefined) {
|
||||
console.log(`❗ PORT is not set. Defaulting to 3000`);
|
||||
console.log(`❗ PORT is not set. Defaulting to 3000`);
|
||||
} else {
|
||||
console.log(`✅ PORT is set. Value is ${process.env.PORT}`);
|
||||
console.log(`✅ PORT is set. Value is ${process.env.PORT}`);
|
||||
}
|
||||
|
||||
if (process.env.KENER_BASE_PATH !== undefined) {
|
||||
if (process.env.KENER_BASE_PATH[0] !== "/") {
|
||||
console.log("❌ KENER_BASE_PATH should start with /");
|
||||
process.exit(1);
|
||||
}
|
||||
if (process.env.KENER_BASE_PATH[process.env.KENER_BASE_PATH.length - 1] === "/") {
|
||||
console.log("❌ KENER_BASE_PATH should not end with /");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!IsStringURLSafe(process.env.KENER_BASE_PATH.substr(1))) {
|
||||
console.log("❌ KENER_BASE_PATH is not url safe");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("❌ KENER_BASE_PATH should start with /");
|
||||
process.exit(1);
|
||||
}
|
||||
if (process.env.KENER_BASE_PATH[process.env.KENER_BASE_PATH.length - 1] === "/") {
|
||||
console.log("❌ KENER_BASE_PATH should not end with /");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!IsStringURLSafe(process.env.KENER_BASE_PATH.substr(1))) {
|
||||
console.log("❌ KENER_BASE_PATH is not url safe");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
STATUS_OK = true;
|
||||
export { STATUS_OK };
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
// Define your constants
|
||||
const FOLDER = process.env.PUBLIC_KENER_FOLDER;
|
||||
const ENV = process.env.NODE_ENV;
|
||||
|
||||
@@ -1,308 +1,336 @@
|
||||
import axios from "axios";
|
||||
import fs from "fs-extra";
|
||||
import { UP, DOWN, DEGRADED } from "./constants.js";
|
||||
import { GetNowTimestampUTC, GetMinuteStartNowTimestampUTC, GetMinuteStartTimestampUTC } from "./tool.js";
|
||||
import {
|
||||
GetNowTimestampUTC,
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
GetMinuteStartTimestampUTC
|
||||
} from "./tool.js";
|
||||
import { GetIncidents, GetEndTimeFromBody, GetStartTimeFromBody, CloseIssue } from "./github.js";
|
||||
import Randomstring from "randomstring";
|
||||
import Queue from "queue";
|
||||
|
||||
const Kener_folder = process.env.PUBLIC_KENER_FOLDER;
|
||||
const apiQueue = new Queue({
|
||||
concurrency: 10, // Number of tasks that can run concurrently
|
||||
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
|
||||
autostart: true, // Automatically start the queue (optional)
|
||||
concurrency: 10, // Number of tasks that can run concurrently
|
||||
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
|
||||
autostart: true // Automatically start the queue (optional)
|
||||
});
|
||||
|
||||
async function manualIncident(monitor, githubConfig){
|
||||
let incidentsResp = await GetIncidents(monitor.tag, githubConfig, "open");
|
||||
|
||||
async function manualIncident(monitor, githubConfig) {
|
||||
let incidentsResp = await GetIncidents(monitor.tag, githubConfig, "open");
|
||||
|
||||
let manualData = {};
|
||||
if (incidentsResp.length == 0) {
|
||||
return manualData;
|
||||
}
|
||||
let timeDownStart = +Infinity;
|
||||
let timeDownEnd = 0;
|
||||
let timeDegradedStart = +Infinity;
|
||||
let timeDegradedEnd = 0;
|
||||
for (let i = 0; i < incidentsResp.length; i++) {
|
||||
const incident = incidentsResp[i];
|
||||
if (incidentsResp.length == 0) {
|
||||
return manualData;
|
||||
}
|
||||
let timeDownStart = +Infinity;
|
||||
let timeDownEnd = 0;
|
||||
let timeDegradedStart = +Infinity;
|
||||
let timeDegradedEnd = 0;
|
||||
for (let i = 0; i < incidentsResp.length; i++) {
|
||||
const incident = incidentsResp[i];
|
||||
const incidentNumber = incident.number;
|
||||
let start_time = GetStartTimeFromBody(incident.body);
|
||||
let start_time = GetStartTimeFromBody(incident.body);
|
||||
let allLabels = incident.labels.map((label) => label.name);
|
||||
if (allLabels.indexOf("incident-degraded") == -1 && allLabels.indexOf("incident-down") == -1) {
|
||||
if (
|
||||
allLabels.indexOf("incident-degraded") == -1 &&
|
||||
allLabels.indexOf("incident-down") == -1
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (start_time === null) {
|
||||
}
|
||||
|
||||
if (start_time === null) {
|
||||
continue;
|
||||
}
|
||||
let newIncident = {
|
||||
start_time: start_time,
|
||||
};
|
||||
let end_time = GetEndTimeFromBody(incident.body);
|
||||
|
||||
if (end_time !== null) {
|
||||
newIncident.end_time = end_time;
|
||||
if(end_time <= GetNowTimestampUTC() && incident.state === "open"){
|
||||
}
|
||||
let newIncident = {
|
||||
start_time: start_time
|
||||
};
|
||||
let end_time = GetEndTimeFromBody(incident.body);
|
||||
|
||||
if (end_time !== null) {
|
||||
newIncident.end_time = end_time;
|
||||
if (end_time <= GetNowTimestampUTC() && incident.state === "open") {
|
||||
//close the issue after 30 secs
|
||||
setTimeout(async () => {
|
||||
await CloseIssue(githubConfig, incidentNumber)
|
||||
}, 30000)
|
||||
await CloseIssue(githubConfig, incidentNumber);
|
||||
}, 30000);
|
||||
}
|
||||
} else {
|
||||
newIncident.end_time = GetNowTimestampUTC();
|
||||
}
|
||||
} else {
|
||||
newIncident.end_time = GetNowTimestampUTC();
|
||||
}
|
||||
|
||||
|
||||
//check if labels has incident-degraded
|
||||
|
||||
//check if labels has incident-degraded
|
||||
|
||||
if (allLabels.indexOf("incident-degraded") !== -1) {
|
||||
timeDegradedStart = Math.min(timeDegradedStart, newIncident.start_time);
|
||||
timeDegradedEnd = Math.max(timeDegradedEnd, newIncident.end_time);
|
||||
}
|
||||
if (allLabels.indexOf("incident-down") !== -1) {
|
||||
timeDownStart = Math.min(timeDownStart, newIncident.start_time);
|
||||
timeDownEnd = Math.max(timeDownEnd, newIncident.end_time);
|
||||
}
|
||||
if (allLabels.indexOf("incident-degraded") !== -1) {
|
||||
timeDegradedStart = Math.min(timeDegradedStart, newIncident.start_time);
|
||||
timeDegradedEnd = Math.max(timeDegradedEnd, newIncident.end_time);
|
||||
}
|
||||
if (allLabels.indexOf("incident-down") !== -1) {
|
||||
timeDownStart = Math.min(timeDownStart, newIncident.start_time);
|
||||
timeDownEnd = Math.max(timeDownEnd, newIncident.end_time);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//start from start of minute if unix timeDownStart to timeDownEnd, step each minute
|
||||
let start = GetMinuteStartTimestampUTC(timeDegradedStart);
|
||||
let end = GetMinuteStartTimestampUTC(timeDegradedEnd);
|
||||
|
||||
//start from start of minute if unix timeDownStart to timeDownEnd, step each minute
|
||||
let start = GetMinuteStartTimestampUTC(timeDegradedStart);
|
||||
let end = GetMinuteStartTimestampUTC(timeDegradedEnd);
|
||||
|
||||
for (let i = start; i <= end; i += 60) {
|
||||
manualData[i] = {
|
||||
status: DEGRADED,
|
||||
latency: 0,
|
||||
type: "manual",
|
||||
};
|
||||
}
|
||||
|
||||
start = GetMinuteStartTimestampUTC(timeDownStart);
|
||||
end = GetMinuteStartTimestampUTC(timeDownEnd);
|
||||
for (let i = start; i <= end; i += 60) {
|
||||
manualData[i] = {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
for (let i = start; i <= end; i += 60) {
|
||||
manualData[i] = {
|
||||
status: DEGRADED,
|
||||
latency: 0,
|
||||
type: "manual"
|
||||
};
|
||||
}
|
||||
return manualData;
|
||||
};
|
||||
}
|
||||
|
||||
start = GetMinuteStartTimestampUTC(timeDownStart);
|
||||
end = GetMinuteStartTimestampUTC(timeDownEnd);
|
||||
for (let i = start; i <= end; i += 60) {
|
||||
manualData[i] = {
|
||||
status: DOWN,
|
||||
latency: 0,
|
||||
type: "manual"
|
||||
};
|
||||
}
|
||||
return manualData;
|
||||
}
|
||||
|
||||
function replaceAllOccurrences(originalString, searchString, replacement) {
|
||||
const regex = new RegExp(`\\${searchString}`, "g");
|
||||
const replacedString = originalString.replace(regex, replacement);
|
||||
return replacedString;
|
||||
const regex = new RegExp(`\\${searchString}`, "g");
|
||||
const replacedString = originalString.replace(regex, replacement);
|
||||
return replacedString;
|
||||
}
|
||||
|
||||
|
||||
const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorEval) => {
|
||||
let axiosHeaders = {};
|
||||
axiosHeaders["User-Agent"] = "Kener/0.0.1";
|
||||
axiosHeaders["Accept"] = "*/*";
|
||||
const start = Date.now();
|
||||
//replace all secrets
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
if (!!body) {
|
||||
body = replaceAllOccurrences(body, secret.find, secret.replace);
|
||||
}
|
||||
if (!!url) {
|
||||
url = replaceAllOccurrences(url, secret.find, secret.replace);
|
||||
}
|
||||
if (!!headers) {
|
||||
headers = replaceAllOccurrences(headers, secret.find, secret.replace);
|
||||
}
|
||||
}
|
||||
if (!!headers) {
|
||||
headers = JSON.parse(headers);
|
||||
axiosHeaders = { ...axiosHeaders, ...headers };
|
||||
}
|
||||
let axiosHeaders = {};
|
||||
axiosHeaders["User-Agent"] = "Kener/0.0.1";
|
||||
axiosHeaders["Accept"] = "*/*";
|
||||
const start = Date.now();
|
||||
//replace all secrets
|
||||
for (let i = 0; i < envSecrets.length; i++) {
|
||||
const secret = envSecrets[i];
|
||||
if (!!body) {
|
||||
body = replaceAllOccurrences(body, secret.find, secret.replace);
|
||||
}
|
||||
if (!!url) {
|
||||
url = replaceAllOccurrences(url, secret.find, secret.replace);
|
||||
}
|
||||
if (!!headers) {
|
||||
headers = replaceAllOccurrences(headers, secret.find, secret.replace);
|
||||
}
|
||||
}
|
||||
if (!!headers) {
|
||||
headers = JSON.parse(headers);
|
||||
axiosHeaders = { ...axiosHeaders, ...headers };
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: method,
|
||||
headers: headers,
|
||||
timeout: timeout,
|
||||
transformResponse: (r) => r,
|
||||
};
|
||||
if (!!headers) {
|
||||
options.headers = headers;
|
||||
}
|
||||
if (!!body) {
|
||||
options.data = body;
|
||||
}
|
||||
let statusCode = 500;
|
||||
let latency = 0;
|
||||
let resp = "";
|
||||
let timeoutError = false;
|
||||
try {
|
||||
let data = await axios(url, options);
|
||||
statusCode = data.status;
|
||||
resp = data.data;
|
||||
} catch (err) {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: headers,
|
||||
timeout: timeout,
|
||||
transformResponse: (r) => r
|
||||
};
|
||||
if (!!headers) {
|
||||
options.headers = headers;
|
||||
}
|
||||
if (!!body) {
|
||||
options.data = body;
|
||||
}
|
||||
let statusCode = 500;
|
||||
let latency = 0;
|
||||
let resp = "";
|
||||
let timeoutError = false;
|
||||
try {
|
||||
let data = await axios(url, options);
|
||||
statusCode = data.status;
|
||||
resp = data.data;
|
||||
} catch (err) {
|
||||
if (err.message.startsWith("timeout of") && err.message.endsWith("exceeded")) {
|
||||
timeoutError = true;
|
||||
}
|
||||
timeoutError = true;
|
||||
}
|
||||
|
||||
if (err.response !== undefined && err.response.status !== undefined) {
|
||||
statusCode = err.response.status;
|
||||
}
|
||||
if (err.response !== undefined && err.response.data !== undefined) {
|
||||
resp = err.response.data;
|
||||
}
|
||||
} finally {
|
||||
const end = Date.now();
|
||||
latency = end - start;
|
||||
}
|
||||
resp = Buffer.from(resp).toString("base64");
|
||||
let evalResp = eval(monitorEval + `(${statusCode}, ${latency}, "${resp}")`);
|
||||
if (evalResp === undefined || evalResp === null) {
|
||||
evalResp = {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: "error",
|
||||
};
|
||||
} else if (evalResp.status === undefined || evalResp.status === null || [UP, DOWN, DEGRADED].indexOf(evalResp.status) === -1) {
|
||||
evalResp = {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: "error",
|
||||
};
|
||||
} else {
|
||||
evalResp.type = "realtime";
|
||||
}
|
||||
if (err.response !== undefined && err.response.status !== undefined) {
|
||||
statusCode = err.response.status;
|
||||
}
|
||||
if (err.response !== undefined && err.response.data !== undefined) {
|
||||
resp = err.response.data;
|
||||
}
|
||||
} finally {
|
||||
const end = Date.now();
|
||||
latency = end - start;
|
||||
}
|
||||
resp = Buffer.from(resp).toString("base64");
|
||||
let evalResp = eval(monitorEval + `(${statusCode}, ${latency}, "${resp}")`);
|
||||
if (evalResp === undefined || evalResp === null) {
|
||||
evalResp = {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: "error"
|
||||
};
|
||||
} else if (
|
||||
evalResp.status === undefined ||
|
||||
evalResp.status === null ||
|
||||
[UP, DOWN, DEGRADED].indexOf(evalResp.status) === -1
|
||||
) {
|
||||
evalResp = {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: "error"
|
||||
};
|
||||
} else {
|
||||
evalResp.type = "realtime";
|
||||
}
|
||||
|
||||
let toWrite = {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: "error",
|
||||
};
|
||||
if (evalResp.status !== undefined && evalResp.status !== null) {
|
||||
toWrite.status = evalResp.status;
|
||||
}
|
||||
if (evalResp.latency !== undefined && evalResp.latency !== null) {
|
||||
toWrite.latency = evalResp.latency;
|
||||
}
|
||||
if (evalResp.type !== undefined && evalResp.type !== null) {
|
||||
toWrite.type = evalResp.type;
|
||||
}
|
||||
if (timeoutError) {
|
||||
toWrite.type = "timeout";
|
||||
}
|
||||
|
||||
return toWrite;
|
||||
let toWrite = {
|
||||
status: DOWN,
|
||||
latency: latency,
|
||||
type: "error"
|
||||
};
|
||||
if (evalResp.status !== undefined && evalResp.status !== null) {
|
||||
toWrite.status = evalResp.status;
|
||||
}
|
||||
if (evalResp.latency !== undefined && evalResp.latency !== null) {
|
||||
toWrite.latency = evalResp.latency;
|
||||
}
|
||||
if (evalResp.type !== undefined && evalResp.type !== null) {
|
||||
toWrite.type = evalResp.type;
|
||||
}
|
||||
if (timeoutError) {
|
||||
toWrite.type = "timeout";
|
||||
}
|
||||
|
||||
return toWrite;
|
||||
};
|
||||
const getWebhookData = async (monitor) => {
|
||||
let originalData = {};
|
||||
let originalData = {};
|
||||
|
||||
let files = fs.readdirSync(Kener_folder);
|
||||
files = files.filter((file) => file.startsWith(monitor.folderName + ".webhook"));
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
let webhookData = {};
|
||||
try {
|
||||
let fd = fs.readFileSync(Kener_folder + "/" + file, "utf8");
|
||||
webhookData = JSON.parse(fd);
|
||||
for (const timestamp in webhookData) {
|
||||
originalData[timestamp] = webhookData[timestamp];
|
||||
}
|
||||
//delete the file
|
||||
fs.unlinkSync(Kener_folder + "/" + file);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return originalData;
|
||||
let files = fs.readdirSync(Kener_folder);
|
||||
files = files.filter((file) => file.startsWith(monitor.folderName + ".webhook"));
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
let webhookData = {};
|
||||
try {
|
||||
let fd = fs.readFileSync(Kener_folder + "/" + file, "utf8");
|
||||
webhookData = JSON.parse(fd);
|
||||
for (const timestamp in webhookData) {
|
||||
originalData[timestamp] = webhookData[timestamp];
|
||||
}
|
||||
//delete the file
|
||||
fs.unlinkSync(Kener_folder + "/" + file);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return originalData;
|
||||
};
|
||||
const updateDayData = async (mergedData, startOfMinute, monitor) => {
|
||||
|
||||
let dayData = JSON.parse(fs.readFileSync(monitor.path0Day, "utf8"));
|
||||
|
||||
for (const timestamp in mergedData) {
|
||||
dayData[timestamp] = mergedData[timestamp];
|
||||
}
|
||||
|
||||
let since = 24*91;
|
||||
let mxBackDate = startOfMinute - since * 3600;
|
||||
let _0Day = {};
|
||||
for (const ts in dayData) {
|
||||
const element = dayData[ts];
|
||||
if (ts >= mxBackDate) {
|
||||
_0Day[ts] = element;
|
||||
}
|
||||
}
|
||||
dayData[timestamp] = mergedData[timestamp];
|
||||
}
|
||||
|
||||
//sort the keys
|
||||
let keys = Object.keys(_0Day);
|
||||
keys.sort();
|
||||
let sortedDay0 = {};
|
||||
keys.reverse().forEach((key) => {
|
||||
sortedDay0[key] = _0Day[key];
|
||||
});
|
||||
try {
|
||||
fs.writeFileSync(monitor.path0Day, JSON.stringify(sortedDay0, null, 2));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
let since = 24 * 91;
|
||||
let mxBackDate = startOfMinute - since * 3600;
|
||||
let _0Day = {};
|
||||
for (const ts in dayData) {
|
||||
const element = dayData[ts];
|
||||
if (ts >= mxBackDate) {
|
||||
_0Day[ts] = element;
|
||||
}
|
||||
}
|
||||
|
||||
//sort the keys
|
||||
let keys = Object.keys(_0Day);
|
||||
keys.sort();
|
||||
let sortedDay0 = {};
|
||||
keys.reverse().forEach((key) => {
|
||||
sortedDay0[key] = _0Day[key];
|
||||
});
|
||||
try {
|
||||
fs.writeFileSync(monitor.path0Day, JSON.stringify(sortedDay0, null, 2));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const Minuter = async (envSecrets, monitor, githubConfig) => {
|
||||
if (apiQueue.length > 0) console.log("Queue length is " + apiQueue.length);
|
||||
let apiData = {};
|
||||
let webhookData = {};
|
||||
let manualData = {};
|
||||
const startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
if (apiQueue.length > 0) console.log("Queue length is " + apiQueue.length);
|
||||
let apiData = {};
|
||||
let webhookData = {};
|
||||
let manualData = {};
|
||||
const startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
|
||||
if (monitor.hasAPI) {
|
||||
let apiResponse = await apiCall(envSecrets, monitor.api.url, monitor.api.method, JSON.stringify(monitor.api.headers), monitor.api.body, monitor.api.timeout, monitor.api.eval);
|
||||
apiData[startOfMinute] = apiResponse;
|
||||
if (apiResponse.type === "timeout") {
|
||||
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
|
||||
//retry
|
||||
apiQueue.push(async (cb) => {
|
||||
apiCall(envSecrets, monitor.api.url, monitor.api.method, JSON.stringify(monitor.api.headers), monitor.api.body, monitor.api.timeout, monitor.api.eval).then(async (data) => {
|
||||
let day0 = {};
|
||||
day0[startOfMinute] = data;
|
||||
fs.writeFileSync(Kener_folder + `/${monitor.folderName}.webhook.${Randomstring.generate()}.json`, JSON.stringify(day0, null, 2));
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
webhookData = await getWebhookData(monitor);
|
||||
if (monitor.hasAPI) {
|
||||
let apiResponse = await apiCall(
|
||||
envSecrets,
|
||||
monitor.api.url,
|
||||
monitor.api.method,
|
||||
JSON.stringify(monitor.api.headers),
|
||||
monitor.api.body,
|
||||
monitor.api.timeout,
|
||||
monitor.api.eval
|
||||
);
|
||||
apiData[startOfMinute] = apiResponse;
|
||||
if (apiResponse.type === "timeout") {
|
||||
console.log(
|
||||
"Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout"
|
||||
);
|
||||
//retry
|
||||
apiQueue.push(async (cb) => {
|
||||
apiCall(
|
||||
envSecrets,
|
||||
monitor.api.url,
|
||||
monitor.api.method,
|
||||
JSON.stringify(monitor.api.headers),
|
||||
monitor.api.body,
|
||||
monitor.api.timeout,
|
||||
monitor.api.eval
|
||||
).then(async (data) => {
|
||||
let day0 = {};
|
||||
day0[startOfMinute] = data;
|
||||
fs.writeFileSync(
|
||||
Kener_folder +
|
||||
`/${monitor.folderName}.webhook.${Randomstring.generate()}.json`,
|
||||
JSON.stringify(day0, null, 2)
|
||||
);
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
webhookData = await getWebhookData(monitor);
|
||||
manualData = await manualIncident(monitor, githubConfig);
|
||||
//merge noData, apiData, webhookData, dayData
|
||||
let mergedData = {};
|
||||
//merge noData, apiData, webhookData, dayData
|
||||
let mergedData = {};
|
||||
if (monitor.defaultStatus !== undefined && monitor.defaultStatus !== null) {
|
||||
if ([UP, DOWN, DEGRADED].indexOf(monitor.defaultStatus) !== -1) {
|
||||
mergedData[startOfMinute] = {
|
||||
status: monitor.defaultStatus,
|
||||
latency: 0,
|
||||
type: "defaultStatus",
|
||||
};
|
||||
}
|
||||
}
|
||||
for (const timestamp in apiData) {
|
||||
mergedData[timestamp] = apiData[timestamp];
|
||||
}
|
||||
for (const timestamp in webhookData) {
|
||||
mergedData[timestamp] = webhookData[timestamp];
|
||||
}
|
||||
for (const timestamp in manualData) {
|
||||
mergedData[timestamp] = manualData[timestamp];
|
||||
}
|
||||
if ([UP, DOWN, DEGRADED].indexOf(monitor.defaultStatus) !== -1) {
|
||||
mergedData[startOfMinute] = {
|
||||
status: monitor.defaultStatus,
|
||||
latency: 0,
|
||||
type: "defaultStatus"
|
||||
};
|
||||
}
|
||||
}
|
||||
for (const timestamp in apiData) {
|
||||
mergedData[timestamp] = apiData[timestamp];
|
||||
}
|
||||
for (const timestamp in webhookData) {
|
||||
mergedData[timestamp] = webhookData[timestamp];
|
||||
}
|
||||
for (const timestamp in manualData) {
|
||||
mergedData[timestamp] = manualData[timestamp];
|
||||
}
|
||||
|
||||
//update day data
|
||||
await updateDayData(mergedData, startOfMinute, monitor);
|
||||
//update day data
|
||||
await updateDayData(mergedData, startOfMinute, monitor);
|
||||
};
|
||||
apiQueue.start((err) => {
|
||||
if (err) {
|
||||
console.error("Error occurred:", err);
|
||||
} else {
|
||||
console.log("All tasks completed");
|
||||
}
|
||||
if (err) {
|
||||
console.error("Error occurred:", err);
|
||||
} else {
|
||||
console.log("All tasks completed");
|
||||
}
|
||||
});
|
||||
export { Minuter };
|
||||
|
||||
@@ -3,326 +3,371 @@ import axios from "axios";
|
||||
import { GetMinuteStartNowTimestampUTC } from "./tool.js";
|
||||
import { marked } from "marked";
|
||||
const GH_TOKEN = process.env.GH_TOKEN;
|
||||
const GhnotconfireguredMsg = "owner or repo or GH_TOKEN is undefined. Read the docs to configure github: https://kener.ing/docs#h2github-setup";
|
||||
const GhnotconfireguredMsg =
|
||||
"owner or repo or GH_TOKEN is undefined. Read the docs to configure github: https://kener.ing/docs#h2github-setup";
|
||||
/**
|
||||
* @param {any} url
|
||||
*/
|
||||
function getAxiosOptions(url) {
|
||||
const options = {
|
||||
url: url,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: "Bearer " + GH_TOKEN,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
};
|
||||
return options;
|
||||
const options = {
|
||||
url: url,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: "Bearer " + GH_TOKEN,
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
};
|
||||
return options;
|
||||
}
|
||||
function postAxiosOptions(url, data) {
|
||||
const options = {
|
||||
url: url,
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: "Bearer " + GH_TOKEN,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
data: data,
|
||||
};
|
||||
return options;
|
||||
const options = {
|
||||
url: url,
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: "Bearer " + GH_TOKEN,
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
},
|
||||
data: data
|
||||
};
|
||||
return options;
|
||||
}
|
||||
function patchAxiosOptions(url, data) {
|
||||
const options = {
|
||||
url: url,
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: "Bearer " + GH_TOKEN,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
data: data,
|
||||
};
|
||||
return options;
|
||||
const options = {
|
||||
url: url,
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: "Bearer " + GH_TOKEN,
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
},
|
||||
data: data
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
const GetAllGHLabels = async function (owner, repo) {
|
||||
if (owner === undefined || repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return [];
|
||||
}
|
||||
const options = getAxiosOptions(`https://api.github.com/repos/${owner}/${repo}/labels?per_page=1000`);
|
||||
if (owner === undefined || repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return [];
|
||||
}
|
||||
const options = getAxiosOptions(
|
||||
`https://api.github.com/repos/${owner}/${repo}/labels?per_page=1000`
|
||||
);
|
||||
|
||||
let labels = [];
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
labels = response.data.map((label) => label.name);
|
||||
} catch (error) {
|
||||
console.log(error.response?.data);
|
||||
return [];
|
||||
}
|
||||
return labels;
|
||||
let labels = [];
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
labels = response.data.map((label) => label.name);
|
||||
} catch (error) {
|
||||
console.log(error.response?.data);
|
||||
return [];
|
||||
}
|
||||
return labels;
|
||||
};
|
||||
function generateRandomColor() {
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
return randomColor;
|
||||
//random color will be freshly served
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
return randomColor;
|
||||
//random color will be freshly served
|
||||
}
|
||||
const CreateGHLabel = async function (owner, repo, label, description, color) {
|
||||
if (owner === undefined || repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
if (color === undefined) {
|
||||
color = generateRandomColor();
|
||||
}
|
||||
if (owner === undefined || repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
if (color === undefined) {
|
||||
color = generateRandomColor();
|
||||
}
|
||||
|
||||
const options = postAxiosOptions(`https://api.github.com/repos/${owner}/${repo}/labels`, {
|
||||
name: label,
|
||||
color: color,
|
||||
description: description,
|
||||
});
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
const options = postAxiosOptions(`https://api.github.com/repos/${owner}/${repo}/labels`, {
|
||||
name: label,
|
||||
color: color,
|
||||
description: description
|
||||
});
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const GetStartTimeFromBody = function (text) {
|
||||
const pattern = /\[start_datetime:(\d+)\]/;
|
||||
const pattern = /\[start_datetime:(\d+)\]/;
|
||||
|
||||
const matches = pattern.exec(text);
|
||||
const matches = pattern.exec(text);
|
||||
|
||||
if (matches) {
|
||||
const timestamp = matches[1];
|
||||
return parseInt(timestamp);
|
||||
}
|
||||
return null;
|
||||
if (matches) {
|
||||
const timestamp = matches[1];
|
||||
return parseInt(timestamp);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const GetEndTimeFromBody = function (text) {
|
||||
const pattern = /\[end_datetime:(\d+)\]/;
|
||||
const pattern = /\[end_datetime:(\d+)\]/;
|
||||
|
||||
const matches = pattern.exec(text);
|
||||
const matches = pattern.exec(text);
|
||||
|
||||
if (matches) {
|
||||
const timestamp = matches[1];
|
||||
return parseInt(timestamp);
|
||||
}
|
||||
return null;
|
||||
if (matches) {
|
||||
const timestamp = matches[1];
|
||||
return parseInt(timestamp);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const GetIncidentByNumber = async function (githubConfig, incidentNumber) {
|
||||
if (githubConfig.owner === undefined || githubConfig.repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${incidentNumber}`;
|
||||
const options = getAxiosOptions(url);
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.message, options, url);
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
githubConfig.owner === undefined ||
|
||||
githubConfig.repo === undefined ||
|
||||
GH_TOKEN === undefined
|
||||
) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${incidentNumber}`;
|
||||
const options = getAxiosOptions(url);
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.message, options, url);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const GetIncidents = async function (tagName, githubConfig, state = "all") {
|
||||
if (githubConfig.owner === undefined || githubConfig.repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return [];
|
||||
}
|
||||
if (tagName === undefined) {
|
||||
return [];
|
||||
}
|
||||
const since = GetMinuteStartNowTimestampUTC() - githubConfig.incidentSince * 60 * 60;
|
||||
const sinceISO = new Date(since * 1000).toISOString();
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?state=${state}&labels=${tagName},incident&sort=created&direction=desc&since=${sinceISO}`;
|
||||
const options = getAxiosOptions(url);
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
let issues = response.data;
|
||||
//issues.createAt should be after sinceISO
|
||||
issues = issues.filter((issue) => {
|
||||
return new Date(issue.created_at) >= new Date(sinceISO);
|
||||
});
|
||||
return issues;
|
||||
} catch (error) {
|
||||
console.log(error.response?.data);
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
githubConfig.owner === undefined ||
|
||||
githubConfig.repo === undefined ||
|
||||
GH_TOKEN === undefined
|
||||
) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return [];
|
||||
}
|
||||
if (tagName === undefined) {
|
||||
return [];
|
||||
}
|
||||
const since = GetMinuteStartNowTimestampUTC() - githubConfig.incidentSince * 60 * 60;
|
||||
const sinceISO = new Date(since * 1000).toISOString();
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?state=${state}&labels=${tagName},incident&sort=created&direction=desc&since=${sinceISO}`;
|
||||
const options = getAxiosOptions(url);
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
let issues = response.data;
|
||||
//issues.createAt should be after sinceISO
|
||||
issues = issues.filter((issue) => {
|
||||
return new Date(issue.created_at) >= new Date(sinceISO);
|
||||
});
|
||||
return issues;
|
||||
} catch (error) {
|
||||
console.log(error.response?.data);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const GetOpenIncidents = async function (githubConfig) {
|
||||
if (githubConfig.owner === undefined || githubConfig.repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return [];
|
||||
}
|
||||
|
||||
const since = GetMinuteStartNowTimestampUTC() - githubConfig.incidentSince * 60 * 60;
|
||||
const sinceISO = new Date(since * 1000).toISOString();
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?state=open&labels=incident&sort=created&direction=desc&since=${sinceISO}`;
|
||||
const options = getAxiosOptions(url);
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
let issues = response.data;
|
||||
//issues.createAt should be after sinceISO
|
||||
issues = issues.filter((issue) => {
|
||||
return new Date(issue.created_at) >= new Date(sinceISO);
|
||||
});
|
||||
return issues;
|
||||
} catch (error) {
|
||||
console.log(error.response?.data);
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
githubConfig.owner === undefined ||
|
||||
githubConfig.repo === undefined ||
|
||||
GH_TOKEN === undefined
|
||||
) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return [];
|
||||
}
|
||||
|
||||
const since = GetMinuteStartNowTimestampUTC() - githubConfig.incidentSince * 60 * 60;
|
||||
const sinceISO = new Date(since * 1000).toISOString();
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?state=open&labels=incident&sort=created&direction=desc&since=${sinceISO}`;
|
||||
const options = getAxiosOptions(url);
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
let issues = response.data;
|
||||
//issues.createAt should be after sinceISO
|
||||
issues = issues.filter((issue) => {
|
||||
return new Date(issue.created_at) >= new Date(sinceISO);
|
||||
});
|
||||
return issues;
|
||||
} catch (error) {
|
||||
console.log(error.response?.data);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
function FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive) {
|
||||
let openIncidentExploded = [];
|
||||
for (let i = 0; i < openIncidentsReduced.length; i++) {
|
||||
for (let j = 0; j < monitorsActive.length; j++) {
|
||||
if (openIncidentsReduced[i].labels.includes(monitorsActive[j].tag)) {
|
||||
let incident = JSON.parse(JSON.stringify(openIncidentsReduced[i]));
|
||||
incident.monitor = {
|
||||
name: monitorsActive[j].name,
|
||||
tag: monitorsActive[j].tag,
|
||||
image: monitorsActive[j].image,
|
||||
description: monitorsActive[j].description,
|
||||
};
|
||||
openIncidentExploded.push(incident);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let j = 0; j < monitorsActive.length; j++) {
|
||||
if (openIncidentsReduced[i].labels.includes(monitorsActive[j].tag)) {
|
||||
let incident = JSON.parse(JSON.stringify(openIncidentsReduced[i]));
|
||||
incident.monitor = {
|
||||
name: monitorsActive[j].name,
|
||||
tag: monitorsActive[j].tag,
|
||||
image: monitorsActive[j].image,
|
||||
description: monitorsActive[j].description
|
||||
};
|
||||
openIncidentExploded.push(incident);
|
||||
}
|
||||
}
|
||||
}
|
||||
return openIncidentExploded;
|
||||
}
|
||||
function Mapper(issue) {
|
||||
const html = marked.parse(issue.body);
|
||||
const html = marked.parse(issue.body);
|
||||
|
||||
//convert issue.created_at from iso to timestamp UTC minutes
|
||||
const issueCreatedAt = new Date(issue.created_at);
|
||||
const issueCreatedAtTimestamp = issueCreatedAt.getTime() / 1000;
|
||||
//convert issue.created_at from iso to timestamp UTC minutes
|
||||
const issueCreatedAt = new Date(issue.created_at);
|
||||
const issueCreatedAtTimestamp = issueCreatedAt.getTime() / 1000;
|
||||
|
||||
//convert issue.closed_at from iso to timestamp UTC minutes
|
||||
let issueClosedAtTimestamp = null;
|
||||
if (issue.closed_at !== null) {
|
||||
const issueClosedAt = new Date(issue.closed_at);
|
||||
issueClosedAtTimestamp = issueClosedAt.getTime() / 1000;
|
||||
}
|
||||
//convert issue.closed_at from iso to timestamp UTC minutes
|
||||
let issueClosedAtTimestamp = null;
|
||||
if (issue.closed_at !== null) {
|
||||
const issueClosedAt = new Date(issue.closed_at);
|
||||
issueClosedAtTimestamp = issueClosedAt.getTime() / 1000;
|
||||
}
|
||||
|
||||
|
||||
let labels = issue.labels.map(function (label) {
|
||||
return label.name;
|
||||
});
|
||||
return label.name;
|
||||
});
|
||||
//find and add monitors tag in labels
|
||||
|
||||
let res = {
|
||||
title: issue.title,
|
||||
incident_start_time: GetStartTimeFromBody(issue.body) || issueCreatedAtTimestamp,
|
||||
incident_end_time: GetEndTimeFromBody(issue.body) || issueClosedAtTimestamp,
|
||||
number: issue.number,
|
||||
body: html,
|
||||
created_at: issue.created_at,
|
||||
updated_at: issue.updated_at,
|
||||
collapsed: true,
|
||||
// @ts-ignore
|
||||
state: issue.state,
|
||||
closed_at: issue.closed_at,
|
||||
// @ts-ignore
|
||||
labels: labels,
|
||||
html_url: issue.html_url,
|
||||
comments: [],
|
||||
};
|
||||
|
||||
return res;
|
||||
let res = {
|
||||
title: issue.title,
|
||||
incident_start_time: GetStartTimeFromBody(issue.body) || issueCreatedAtTimestamp,
|
||||
incident_end_time: GetEndTimeFromBody(issue.body) || issueClosedAtTimestamp,
|
||||
number: issue.number,
|
||||
body: html,
|
||||
created_at: issue.created_at,
|
||||
updated_at: issue.updated_at,
|
||||
collapsed: true,
|
||||
// @ts-ignore
|
||||
state: issue.state,
|
||||
closed_at: issue.closed_at,
|
||||
// @ts-ignore
|
||||
labels: labels,
|
||||
html_url: issue.html_url,
|
||||
comments: []
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
async function GetCommentsForIssue(issueID, githubConfig) {
|
||||
if (githubConfig.owner === undefined || githubConfig.repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return [];
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${issueID}/comments`;
|
||||
try {
|
||||
const response = await axios.request(getAxiosOptions(url));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
githubConfig.owner === undefined ||
|
||||
githubConfig.repo === undefined ||
|
||||
GH_TOKEN === undefined
|
||||
) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return [];
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${issueID}/comments`;
|
||||
try {
|
||||
const response = await axios.request(getAxiosOptions(url));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async function CreateIssue(githubConfig, issueTitle, issueBody, issueLabels) {
|
||||
if (githubConfig.owner === undefined || githubConfig.repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues`;
|
||||
try {
|
||||
const payload = {
|
||||
title: issueTitle,
|
||||
body: issueBody,
|
||||
labels: issueLabels,
|
||||
};
|
||||
const response = await axios.request(postAxiosOptions(url, payload));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
githubConfig.owner === undefined ||
|
||||
githubConfig.repo === undefined ||
|
||||
GH_TOKEN === undefined
|
||||
) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues`;
|
||||
try {
|
||||
const payload = {
|
||||
title: issueTitle,
|
||||
body: issueBody,
|
||||
labels: issueLabels
|
||||
};
|
||||
const response = await axios.request(postAxiosOptions(url, payload));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function UpdateIssue(githubConfig, incidentNumber, issueTitle, issueBody, issueLabels, state = "open") {
|
||||
if (githubConfig.owner === undefined || githubConfig.repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${incidentNumber}`;
|
||||
try {
|
||||
const payload = {
|
||||
title: issueTitle,
|
||||
body: issueBody,
|
||||
labels: issueLabels,
|
||||
state: state,
|
||||
};
|
||||
const response = await axios.request(patchAxiosOptions(url, payload));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
async function UpdateIssue(
|
||||
githubConfig,
|
||||
incidentNumber,
|
||||
issueTitle,
|
||||
issueBody,
|
||||
issueLabels,
|
||||
state = "open"
|
||||
) {
|
||||
if (
|
||||
githubConfig.owner === undefined ||
|
||||
githubConfig.repo === undefined ||
|
||||
GH_TOKEN === undefined
|
||||
) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${incidentNumber}`;
|
||||
try {
|
||||
const payload = {
|
||||
title: issueTitle,
|
||||
body: issueBody,
|
||||
labels: issueLabels,
|
||||
state: state
|
||||
};
|
||||
const response = await axios.request(patchAxiosOptions(url, payload));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function CloseIssue(githubConfig, incidentNumber) {
|
||||
if (githubConfig.owner === undefined || githubConfig.repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${incidentNumber}`;
|
||||
try {
|
||||
const payload = {
|
||||
state: "closed"
|
||||
};
|
||||
const response = await axios.request(patchAxiosOptions(url, payload));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
githubConfig.owner === undefined ||
|
||||
githubConfig.repo === undefined ||
|
||||
GH_TOKEN === undefined
|
||||
) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${incidentNumber}`;
|
||||
try {
|
||||
const payload = {
|
||||
state: "closed"
|
||||
};
|
||||
const response = await axios.request(patchAxiosOptions(url, payload));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function AddComment(githubConfig, incidentNumber, commentBody) {
|
||||
if (githubConfig.owner === undefined || githubConfig.repo === undefined || GH_TOKEN === undefined) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${incidentNumber}/comments`;
|
||||
try {
|
||||
const payload = {
|
||||
body: commentBody,
|
||||
};
|
||||
const response = await axios.request(postAxiosOptions(url, payload));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
githubConfig.owner === undefined ||
|
||||
githubConfig.repo === undefined ||
|
||||
GH_TOKEN === undefined
|
||||
) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues/${incidentNumber}/comments`;
|
||||
try {
|
||||
const payload = {
|
||||
body: commentBody
|
||||
};
|
||||
const response = await axios.request(postAxiosOptions(url, payload));
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
//update issue labels
|
||||
async function UpdateIssueLabels(githubConfig, incidentNumber, issueLabels, body, state = "open") {
|
||||
if (githubConfig.owner === undefined || githubConfig.repo === undefined || GH_TOKEN === undefined) {
|
||||
if (
|
||||
githubConfig.owner === undefined ||
|
||||
githubConfig.repo === undefined ||
|
||||
GH_TOKEN === undefined
|
||||
) {
|
||||
console.log(GhnotconfireguredMsg);
|
||||
return null;
|
||||
}
|
||||
@@ -331,7 +376,7 @@ async function UpdateIssueLabels(githubConfig, incidentNumber, issueLabels, body
|
||||
const payload = {
|
||||
labels: issueLabels,
|
||||
body: body,
|
||||
state: state,
|
||||
state: state
|
||||
};
|
||||
const response = await axios.request(patchAxiosOptions(url, payload));
|
||||
return response.data;
|
||||
@@ -348,26 +393,25 @@ async function SearchIssue(query, page, per_page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const searchQuery =
|
||||
query
|
||||
.filter(function (q) {
|
||||
if (q == "" || q === undefined || q === null) {
|
||||
return false;
|
||||
}
|
||||
const qs = q.split(":");
|
||||
if (qs.length < 2) {
|
||||
return false;
|
||||
}
|
||||
if (qs[1] === "" || qs[1] === undefined || qs[1] === null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.join(" ")
|
||||
|
||||
const searchQuery = query
|
||||
.filter(function (q) {
|
||||
if (q == "" || q === undefined || q === null) {
|
||||
return false;
|
||||
}
|
||||
const qs = q.split(":");
|
||||
if (qs.length < 2) {
|
||||
return false;
|
||||
}
|
||||
if (qs[1] === "" || qs[1] === undefined || qs[1] === null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
const url = `https://api.github.com/search/issues?q=${encodeURIComponent(
|
||||
searchQuery
|
||||
)}&per_page=${per_page}&page=${page}`;
|
||||
searchQuery
|
||||
)}&per_page=${per_page}&page=${page}`;
|
||||
|
||||
try {
|
||||
const response = await axios.request(getAxiosOptions(url));
|
||||
@@ -378,22 +422,21 @@ async function SearchIssue(query, page, per_page) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
GetAllGHLabels,
|
||||
CreateGHLabel,
|
||||
GetIncidents,
|
||||
GetStartTimeFromBody,
|
||||
GetEndTimeFromBody,
|
||||
GetCommentsForIssue,
|
||||
Mapper,
|
||||
CreateIssue,
|
||||
AddComment,
|
||||
GetIncidentByNumber,
|
||||
UpdateIssueLabels,
|
||||
UpdateIssue,
|
||||
CloseIssue,
|
||||
GetOpenIncidents,
|
||||
FilterAndInsertMonitorInIncident,
|
||||
SearchIssue,
|
||||
GetAllGHLabels,
|
||||
CreateGHLabel,
|
||||
GetIncidents,
|
||||
GetStartTimeFromBody,
|
||||
GetEndTimeFromBody,
|
||||
GetCommentsForIssue,
|
||||
Mapper,
|
||||
CreateIssue,
|
||||
AddComment,
|
||||
GetIncidentByNumber,
|
||||
UpdateIssueLabels,
|
||||
UpdateIssue,
|
||||
CloseIssue,
|
||||
GetOpenIncidents,
|
||||
FilterAndInsertMonitorInIncident,
|
||||
SearchIssue
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ import fs from "fs-extra";
|
||||
import { GetMinuteStartNowTimestampUTC, BeginningOfDay } from "./tool.js";
|
||||
import { StatusObj, ParseUptime } from "../src/lib/helpers.js";
|
||||
|
||||
function getDayMessage(type, numOfMinute){
|
||||
if(numOfMinute > 59){
|
||||
function getDayMessage(type, numOfMinute) {
|
||||
if (numOfMinute > 59) {
|
||||
let hour = Math.floor(numOfMinute / 60);
|
||||
let minute = numOfMinute % 60;
|
||||
return `${type} for ${hour}h:${minute}m`;
|
||||
@@ -13,148 +13,143 @@ function getDayMessage(type, numOfMinute){
|
||||
}
|
||||
const NO_DATA = "No Data";
|
||||
|
||||
function getDayData(
|
||||
day0,
|
||||
startTime,
|
||||
endTime,
|
||||
dayDownMinimumCount,
|
||||
dayDegradedMinimumCount
|
||||
) {
|
||||
let dayData = {
|
||||
UP: 0,
|
||||
DEGRADED: 0,
|
||||
DOWN: 0,
|
||||
timestamp: startTime,
|
||||
cssClass: StatusObj.NO_DATA,
|
||||
message: NO_DATA,
|
||||
};
|
||||
//loop through the ts range
|
||||
for (let i = startTime; i <= endTime; i += 60) {
|
||||
//if the ts is in the day0 then add up, down degraded data, if not initialize it
|
||||
if (day0[i] === undefined) {
|
||||
continue;
|
||||
}
|
||||
function getDayData(day0, startTime, endTime, dayDownMinimumCount, dayDegradedMinimumCount) {
|
||||
let dayData = {
|
||||
UP: 0,
|
||||
DEGRADED: 0,
|
||||
DOWN: 0,
|
||||
timestamp: startTime,
|
||||
cssClass: StatusObj.NO_DATA,
|
||||
message: NO_DATA
|
||||
};
|
||||
//loop through the ts range
|
||||
for (let i = startTime; i <= endTime; i += 60) {
|
||||
//if the ts is in the day0 then add up, down degraded data, if not initialize it
|
||||
if (day0[i] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (day0[i].status == "UP") {
|
||||
dayData.UP++;
|
||||
} else if (day0[i].status == "DEGRADED") {
|
||||
dayData.DEGRADED++;
|
||||
} else if (day0[i].status == "DOWN") {
|
||||
dayData.DOWN++;
|
||||
}
|
||||
}
|
||||
if (day0[i].status == "UP") {
|
||||
dayData.UP++;
|
||||
} else if (day0[i].status == "DEGRADED") {
|
||||
dayData.DEGRADED++;
|
||||
} else if (day0[i].status == "DOWN") {
|
||||
dayData.DOWN++;
|
||||
}
|
||||
}
|
||||
|
||||
let cssClass = StatusObj.UP;
|
||||
let message = "Status OK";
|
||||
let cssClass = StatusObj.UP;
|
||||
let message = "Status OK";
|
||||
|
||||
if (dayData.DEGRADED >= dayDegradedMinimumCount) {
|
||||
cssClass = StatusObj.DEGRADED;
|
||||
message = getDayMessage("DEGRADED", dayData.DEGRADED);
|
||||
}
|
||||
if (dayData.DOWN >= dayDownMinimumCount) {
|
||||
cssClass = StatusObj.DOWN;
|
||||
message = getDayMessage("DOWN", dayData.DOWN);
|
||||
}
|
||||
if (dayData.DEGRADED + dayData.DOWN + dayData.UP >= Math.min(dayDownMinimumCount, dayDegradedMinimumCount)) {
|
||||
dayData.message = message;
|
||||
dayData.cssClass = cssClass;
|
||||
}
|
||||
if (dayData.DEGRADED >= dayDegradedMinimumCount) {
|
||||
cssClass = StatusObj.DEGRADED;
|
||||
message = getDayMessage("DEGRADED", dayData.DEGRADED);
|
||||
}
|
||||
if (dayData.DOWN >= dayDownMinimumCount) {
|
||||
cssClass = StatusObj.DOWN;
|
||||
message = getDayMessage("DOWN", dayData.DOWN);
|
||||
}
|
||||
if (
|
||||
dayData.DEGRADED + dayData.DOWN + dayData.UP >=
|
||||
Math.min(dayDownMinimumCount, dayDegradedMinimumCount)
|
||||
) {
|
||||
dayData.message = message;
|
||||
dayData.cssClass = cssClass;
|
||||
}
|
||||
|
||||
return dayData;
|
||||
return dayData;
|
||||
}
|
||||
|
||||
const Ninety = async (monitor) => {
|
||||
let _0Day = {};
|
||||
let _90Day = {};
|
||||
let uptime0Day = "0";
|
||||
let dailyUps = 0;
|
||||
let dailyDown = 0;
|
||||
let dailyDegraded = 0;
|
||||
let completeUps = 0;
|
||||
let completeDown = 0;
|
||||
let completeDegraded = 0;
|
||||
|
||||
let _90Day = {};
|
||||
let uptime0Day = "0";
|
||||
let dailyUps = 0;
|
||||
let dailyDown = 0;
|
||||
let dailyDegraded = 0;
|
||||
let completeUps = 0;
|
||||
let completeDown = 0;
|
||||
let completeDegraded = 0;
|
||||
|
||||
const secondsInDay = 24 * 60 * 60;
|
||||
const now = GetMinuteStartNowTimestampUTC();
|
||||
const now = GetMinuteStartNowTimestampUTC();
|
||||
const midnight = BeginningOfDay({ timeZone: "GMT" });
|
||||
const midnight90DaysAgo = midnight - 90 * 24 * 60 * 60;
|
||||
const midnightTomorrow = midnight + secondsInDay;
|
||||
const midnight90DaysAgo = midnight - 90 * 24 * 60 * 60;
|
||||
const midnightTomorrow = midnight + secondsInDay;
|
||||
|
||||
for (let i = midnight; i <= now; i += 60) {
|
||||
_0Day[i] = {
|
||||
timestamp: i,
|
||||
status: "NO_DATA",
|
||||
cssClass: StatusObj.NO_DATA,
|
||||
index: (i - midnight) / 60,
|
||||
};
|
||||
}
|
||||
_0Day[i] = {
|
||||
timestamp: i,
|
||||
status: "NO_DATA",
|
||||
cssClass: StatusObj.NO_DATA,
|
||||
index: (i - midnight) / 60
|
||||
};
|
||||
}
|
||||
|
||||
let day0 = JSON.parse(fs.readFileSync(monitor.path0Day, "utf8"));
|
||||
|
||||
for (const timestamp in day0) {
|
||||
const element = day0[timestamp];
|
||||
let status = element.status;
|
||||
if (status == "UP") {
|
||||
completeUps++;
|
||||
} else if (status == "DEGRADED") {
|
||||
completeDegraded++;
|
||||
} else if (status == "DOWN") {
|
||||
completeDown++;
|
||||
}
|
||||
//0 Day data
|
||||
if (_0Day[timestamp] !== undefined) {
|
||||
_0Day[timestamp].status = status;
|
||||
_0Day[timestamp].cssClass = StatusObj[status];
|
||||
const element = day0[timestamp];
|
||||
let status = element.status;
|
||||
if (status == "UP") {
|
||||
completeUps++;
|
||||
} else if (status == "DEGRADED") {
|
||||
completeDegraded++;
|
||||
} else if (status == "DOWN") {
|
||||
completeDown++;
|
||||
}
|
||||
//0 Day data
|
||||
if (_0Day[timestamp] !== undefined) {
|
||||
_0Day[timestamp].status = status;
|
||||
_0Day[timestamp].cssClass = StatusObj[status];
|
||||
|
||||
dailyUps = status == "UP" ? dailyUps + 1 : dailyUps;
|
||||
dailyDown = status == "DOWN" ? dailyDown + 1 : dailyDown;
|
||||
dailyDegraded = status == "DEGRADED" ? dailyDegraded + 1 : dailyDegraded;
|
||||
}
|
||||
}
|
||||
dailyUps = status == "UP" ? dailyUps + 1 : dailyUps;
|
||||
dailyDown = status == "DOWN" ? dailyDown + 1 : dailyDown;
|
||||
dailyDegraded = status == "DEGRADED" ? dailyDegraded + 1 : dailyDegraded;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = midnight90DaysAgo; i < midnightTomorrow; i += secondsInDay) {
|
||||
_90Day[i] = getDayData(
|
||||
day0,
|
||||
i,
|
||||
i + secondsInDay - 1,
|
||||
monitor.dayDownMinimumCount,
|
||||
monitor.dayDegradedMinimumCount
|
||||
);
|
||||
}
|
||||
_90Day[i] = getDayData(
|
||||
day0,
|
||||
i,
|
||||
i + secondsInDay - 1,
|
||||
monitor.dayDownMinimumCount,
|
||||
monitor.dayDegradedMinimumCount
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in _90Day) {
|
||||
const element = _90Day[key];
|
||||
delete _90Day[key].UP;
|
||||
delete _90Day[key].DEGRADED;
|
||||
delete _90Day[key].DOWN;
|
||||
if (element.message == NO_DATA) continue;
|
||||
}
|
||||
for (const key in _90Day) {
|
||||
const element = _90Day[key];
|
||||
delete _90Day[key].UP;
|
||||
delete _90Day[key].DEGRADED;
|
||||
delete _90Day[key].DOWN;
|
||||
if (element.message == NO_DATA) continue;
|
||||
}
|
||||
|
||||
let uptime0DayNumerator = dailyUps + dailyDegraded;
|
||||
let uptime0DayDenominator = dailyUps + dailyDown + dailyDegraded;
|
||||
let uptime90DayNumerator = completeUps + completeDegraded;
|
||||
let uptime90DayDenominator = completeUps + completeDown + completeDegraded;
|
||||
|
||||
if(monitor.includeDegradedInDowntime === true) {
|
||||
if (monitor.includeDegradedInDowntime === true) {
|
||||
uptime0DayNumerator = dailyUps;
|
||||
uptime90DayNumerator = completeUps;
|
||||
}
|
||||
uptime0Day = ParseUptime(uptime0DayNumerator, uptime0DayDenominator);
|
||||
uptime0Day = ParseUptime(uptime0DayNumerator, uptime0DayDenominator);
|
||||
|
||||
const dataToWrite = {
|
||||
_90Day: _90Day,
|
||||
uptime0Day,
|
||||
uptime90Day: ParseUptime(uptime90DayNumerator, uptime90DayDenominator),
|
||||
dailyUps,
|
||||
dailyDown,
|
||||
dailyDegraded,
|
||||
};
|
||||
_90Day: _90Day,
|
||||
uptime0Day,
|
||||
uptime90Day: ParseUptime(uptime90DayNumerator, uptime90DayDenominator),
|
||||
dailyUps,
|
||||
dailyDown,
|
||||
dailyDegraded
|
||||
};
|
||||
|
||||
await fs.writeJson(monitor.path90Day, dataToWrite);
|
||||
|
||||
return true;
|
||||
|
||||
};
|
||||
|
||||
export { Ninety };
|
||||
export { Ninety };
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
//read from process.env.PUBLIC_KENER_FOLDER / monitors.json
|
||||
// create sitemap.xml
|
||||
import fs from "fs-extra";
|
||||
let siteMap = ""
|
||||
let siteMap = "";
|
||||
const site = JSON.parse(fs.readFileSync(process.env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
const monitors = JSON.parse(fs.readFileSync(process.env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
if(site.siteURL !== undefined && site.siteURL !== null && site.siteURL !== ""){
|
||||
if(monitors.length > 0){
|
||||
const monitors = JSON.parse(
|
||||
fs.readFileSync(process.env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8")
|
||||
);
|
||||
if (site.siteURL !== undefined && site.siteURL !== null && site.siteURL !== "") {
|
||||
if (monitors.length > 0) {
|
||||
siteMap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
@@ -14,29 +16,28 @@ if(site.siteURL !== undefined && site.siteURL !== null && site.siteURL !== ""){
|
||||
xsi:schemaLocation="https://www.sitemaps.org/schemas/sitemap/0.9
|
||||
https://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
${monitors
|
||||
.map((monitor) => {
|
||||
return `<url>
|
||||
.map((monitor) => {
|
||||
return `<url>
|
||||
<loc>${site.siteURL}/incident/${monitor.folderName}</loc>
|
||||
<lastmod>${new Date().toISOString()}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>`;
|
||||
})
|
||||
.join("\n")}
|
||||
})
|
||||
.join("\n")}
|
||||
${monitors
|
||||
.map((monitor) => {
|
||||
return `<url>
|
||||
.map((monitor) => {
|
||||
return `<url>
|
||||
<loc>${site.siteURL}/monitor-${encodeURIComponent(monitor.tag)}</loc>
|
||||
<lastmod>${new Date().toISOString()}</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>`;
|
||||
})
|
||||
.join("\n")}
|
||||
})
|
||||
.join("\n")}
|
||||
</urlset>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//export default siteMap
|
||||
export default siteMap;
|
||||
export default siteMap;
|
||||
|
||||
@@ -32,289 +32,328 @@ const defaultEval = `(function (statusCode, responseTime, responseData) {
|
||||
})`;
|
||||
|
||||
function checkIfDuplicateExists(arr) {
|
||||
return new Set(arr).size !== arr.length;
|
||||
return new Set(arr).size !== arr.length;
|
||||
}
|
||||
function getWordsStartingWithDollar(text) {
|
||||
const regex = /\$\w+/g;
|
||||
const wordsArray = text.match(regex);
|
||||
return wordsArray || [];
|
||||
const regex = /\$\w+/g;
|
||||
const wordsArray = text.match(regex);
|
||||
return wordsArray || [];
|
||||
}
|
||||
if (!fs.existsSync(FOLDER)) {
|
||||
fs.mkdirSync(FOLDER);
|
||||
console.log(".kener folder created successfully!");
|
||||
fs.mkdirSync(FOLDER);
|
||||
console.log(".kener folder created successfully!");
|
||||
}
|
||||
|
||||
const Startup = async () => {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(LoadMonitorsPath(), "utf8");
|
||||
site = yaml.load(fs.readFileSync(LoadSitePath(), "utf8"));
|
||||
monitors = yaml.load(fileContent);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
const fileContent = fs.readFileSync(LoadMonitorsPath(), "utf8");
|
||||
site = yaml.load(fs.readFileSync(LoadSitePath(), "utf8"));
|
||||
monitors = yaml.load(fileContent);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Use the 'monitors' array of JSON objects as needed
|
||||
//check if each object has name, url, method
|
||||
//if not, exit with error
|
||||
//if yes, check if name is unique
|
||||
// Use the 'monitors' array of JSON objects as needed
|
||||
//check if each object has name, url, method
|
||||
//if not, exit with error
|
||||
//if yes, check if name is unique
|
||||
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
let name = monitor.name;
|
||||
let tag = monitor.tag;
|
||||
let hasAPI = monitor.api !== undefined && monitor.api !== null;
|
||||
let folderName = name.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
monitors[i].folderName = folderName;
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
let name = monitor.name;
|
||||
let tag = monitor.tag;
|
||||
let hasAPI = monitor.api !== undefined && monitor.api !== null;
|
||||
let folderName = name.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
monitors[i].folderName = folderName;
|
||||
|
||||
if (!name || !tag) {
|
||||
console.log("name, tag are required");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!name || !tag) {
|
||||
console.log("name, tag are required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if(monitor.dayDegradedMinimumCount && (isNaN(monitor.dayDegradedMinimumCount) || monitor.dayDegradedMinimumCount < 1)){
|
||||
if (
|
||||
monitor.dayDegradedMinimumCount &&
|
||||
(isNaN(monitor.dayDegradedMinimumCount) || monitor.dayDegradedMinimumCount < 1)
|
||||
) {
|
||||
console.log("dayDegradedMinimumCount is not a number or it is less than 1");
|
||||
process.exit(1);
|
||||
} else if(monitor.dayDegradedMinimumCount === undefined) {
|
||||
} else if (monitor.dayDegradedMinimumCount === undefined) {
|
||||
monitors[i].dayDegradedMinimumCount = 1;
|
||||
}
|
||||
|
||||
if(monitor.dayDownMinimumCount && (isNaN(monitor.dayDownMinimumCount) || monitor.dayDownMinimumCount < 1)){
|
||||
if (
|
||||
monitor.dayDownMinimumCount &&
|
||||
(isNaN(monitor.dayDownMinimumCount) || monitor.dayDownMinimumCount < 1)
|
||||
) {
|
||||
console.log("dayDownMinimumCount is not a number or it is less than 1");
|
||||
process.exit(1);
|
||||
} else if(monitor.dayDownMinimumCount === undefined) {
|
||||
} else if (monitor.dayDownMinimumCount === undefined) {
|
||||
monitors[i].dayDownMinimumCount = 1;
|
||||
}
|
||||
|
||||
if (monitor.includeDegradedInDowntime === undefined || monitor.includeDegradedInDowntime !== true) {
|
||||
if (
|
||||
monitor.includeDegradedInDowntime === undefined ||
|
||||
monitor.includeDegradedInDowntime !== true
|
||||
) {
|
||||
monitors[i].includeDegradedInDowntime = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(hasAPI) {
|
||||
let url = monitor.api.url;
|
||||
let method = monitor.api.method;
|
||||
let headers = monitor.api.headers;
|
||||
let evaluator = monitor.api.eval;
|
||||
let body = monitor.api.body;
|
||||
let timeout = monitor.api.timeout;
|
||||
//url
|
||||
if (!!url) {
|
||||
if (!IsValidURL(url)) {
|
||||
console.log("url is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (!!method) {
|
||||
if (!IsValidHTTPMethod(method)) {
|
||||
console.log("method is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
method = method.toUpperCase();
|
||||
} else {
|
||||
method = "GET";
|
||||
}
|
||||
monitors[i].api.method = method;
|
||||
//headers
|
||||
if (headers === undefined || headers === null) {
|
||||
monitors[i].api.headers = undefined;
|
||||
} else {
|
||||
//check if headers is a valid json
|
||||
try {
|
||||
JSON.parse(JSON.stringify(headers));
|
||||
} catch (error) {
|
||||
console.log("headers are not valid. Quiting");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
//eval
|
||||
if (evaluator === undefined || evaluator === null) {
|
||||
monitors[i].api.eval = defaultEval;
|
||||
} else {
|
||||
let evalResp = eval(evaluator + `(200, 1000, "e30=")`);
|
||||
|
||||
if (evalResp === undefined || evalResp === null || evalResp.status === undefined || evalResp.status === null || evalResp.latency === undefined || evalResp.latency === null) {
|
||||
console.log("eval is not valid ");
|
||||
process.exit(1);
|
||||
}
|
||||
if (hasAPI) {
|
||||
let url = monitor.api.url;
|
||||
let method = monitor.api.method;
|
||||
let headers = monitor.api.headers;
|
||||
let evaluator = monitor.api.eval;
|
||||
let body = monitor.api.body;
|
||||
let timeout = monitor.api.timeout;
|
||||
//url
|
||||
if (!!url) {
|
||||
if (!IsValidURL(url)) {
|
||||
console.log("url is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (!!method) {
|
||||
if (!IsValidHTTPMethod(method)) {
|
||||
console.log("method is not valid");
|
||||
process.exit(1);
|
||||
}
|
||||
method = method.toUpperCase();
|
||||
} else {
|
||||
method = "GET";
|
||||
}
|
||||
monitors[i].api.method = method;
|
||||
//headers
|
||||
if (headers === undefined || headers === null) {
|
||||
monitors[i].api.headers = undefined;
|
||||
} else {
|
||||
//check if headers is a valid json
|
||||
try {
|
||||
JSON.parse(JSON.stringify(headers));
|
||||
} catch (error) {
|
||||
console.log("headers are not valid. Quiting");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
//eval
|
||||
if (evaluator === undefined || evaluator === null) {
|
||||
monitors[i].api.eval = defaultEval;
|
||||
} else {
|
||||
let evalResp = eval(evaluator + `(200, 1000, "e30=")`);
|
||||
|
||||
if (
|
||||
evalResp === undefined ||
|
||||
evalResp === null ||
|
||||
evalResp.status === undefined ||
|
||||
evalResp.status === null ||
|
||||
evalResp.latency === undefined ||
|
||||
evalResp.latency === null
|
||||
) {
|
||||
console.log("eval is not valid ");
|
||||
process.exit(1);
|
||||
}
|
||||
monitors[i].api.eval = evaluator;
|
||||
}
|
||||
//body
|
||||
if (body === undefined || body === null) {
|
||||
monitors[i].api.body = undefined;
|
||||
} else {
|
||||
//check if body is a valid string
|
||||
if (typeof body !== "string") {
|
||||
console.log("body is not valid should be a string");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
//timeout
|
||||
if (timeout === undefined || timeout === null) {
|
||||
monitors[i].api.timeout = API_TIMEOUT;
|
||||
} else {
|
||||
//check if timeout is a valid number
|
||||
if (isNaN(timeout) || timeout < 0) {
|
||||
console.log("timeout is not valid ");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
//body
|
||||
if (body === undefined || body === null) {
|
||||
monitors[i].api.body = undefined;
|
||||
} else {
|
||||
//check if body is a valid string
|
||||
if (typeof body !== "string") {
|
||||
console.log("body is not valid should be a string");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
//timeout
|
||||
if (timeout === undefined || timeout === null) {
|
||||
monitors[i].api.timeout = API_TIMEOUT;
|
||||
} else {
|
||||
//check if timeout is a valid number
|
||||
if (isNaN(timeout) || timeout < 0) {
|
||||
console.log("timeout is not valid ");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
//add a description to the monitor if it is website using api.url and method = GET and headers == undefined
|
||||
//call the it to see if recevied content-type is text/html
|
||||
//if yes, append to description
|
||||
if ((headers === undefined || headers === null) && url !== undefined && method === "GET") {
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method: "GET",
|
||||
url: url,
|
||||
timeout: API_TIMEOUT,
|
||||
});
|
||||
if (response.headers["content-type"].includes("text/html")) {
|
||||
//add a description to the monitor if it is website using api.url and method = GET and headers == undefined
|
||||
//call the it to see if recevied content-type is text/html
|
||||
//if yes, append to description
|
||||
if (
|
||||
(headers === undefined || headers === null) &&
|
||||
url !== undefined &&
|
||||
method === "GET"
|
||||
) {
|
||||
try {
|
||||
const response = await axios({
|
||||
method: "GET",
|
||||
url: url,
|
||||
timeout: API_TIMEOUT
|
||||
});
|
||||
if (response.headers["content-type"].includes("text/html")) {
|
||||
let link = `<a href="${url}" class="font-medium underline underline-offset-4" target="_blank">${url}</a>`;
|
||||
if(monitors[i].description === undefined) {
|
||||
monitors[i].description = link;
|
||||
if (monitors[i].description === undefined) {
|
||||
monitors[i].description = link;
|
||||
} else {
|
||||
monitors[i].description = monitors[i].description?.trim() + " " + link;
|
||||
monitors[i].description = monitors[i].description?.trim() + " " + link;
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
monitors[i].path0Day = `${FOLDER}/${folderName}.0day.utc.json`;
|
||||
monitors[i].path90Day = `${FOLDER}/${folderName}.90day.utc.json`;
|
||||
monitors[i].hasAPI = hasAPI;
|
||||
monitors[i].path0Day = `${FOLDER}/${folderName}.0day.utc.json`;
|
||||
monitors[i].path90Day = `${FOLDER}/${folderName}.90day.utc.json`;
|
||||
monitors[i].hasAPI = hasAPI;
|
||||
|
||||
//secrets can be in url/body/headers
|
||||
//match in monitor.url if a words starts with $, get the word
|
||||
const requiredSecrets = getWordsStartingWithDollar(`${monitor.url} ${monitor.body} ${JSON.stringify(monitor.headers)}`).map((x) => x.substr(1));
|
||||
//secrets can be in url/body/headers
|
||||
//match in monitor.url if a words starts with $, get the word
|
||||
const requiredSecrets = getWordsStartingWithDollar(
|
||||
`${monitor.url} ${monitor.body} ${JSON.stringify(monitor.headers)}`
|
||||
).map((x) => x.substr(1));
|
||||
|
||||
//iterate over process.env
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (requiredSecrets.indexOf(key) !== -1) {
|
||||
envSecrets.push({
|
||||
find: `$${key}`,
|
||||
replace: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
//iterate over process.env
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (requiredSecrets.indexOf(key) !== -1) {
|
||||
envSecrets.push({
|
||||
find: `$${key}`,
|
||||
replace: value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
site.github === undefined ||
|
||||
site.github.owner === undefined ||
|
||||
site.github.repo === undefined
|
||||
) {
|
||||
console.log("github owner and repo are required");
|
||||
process.exit(1);
|
||||
}
|
||||
if (site.github.incidentSince === undefined || site.github.incidentSince === null) {
|
||||
site.github.incidentSince = 48;
|
||||
}
|
||||
if (checkIfDuplicateExists(monitors.map((monitor) => monitor.folderName)) === true) {
|
||||
console.log("duplicate monitor detected");
|
||||
process.exit(1);
|
||||
}
|
||||
if (checkIfDuplicateExists(monitors.map((monitor) => monitor.tag)) === true) {
|
||||
console.log("duplicate tag detected");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if (site.github === undefined || site.github.owner === undefined || site.github.repo === undefined) {
|
||||
console.log("github owner and repo are required");
|
||||
process.exit(1);
|
||||
}
|
||||
if (site.github.incidentSince === undefined || site.github.incidentSince === null) {
|
||||
site.github.incidentSince = 48;
|
||||
}
|
||||
if (checkIfDuplicateExists(monitors.map((monitor) => monitor.folderName)) === true) {
|
||||
console.log("duplicate monitor detected");
|
||||
process.exit(1);
|
||||
}
|
||||
if (checkIfDuplicateExists(monitors.map((monitor) => monitor.tag)) === true) {
|
||||
console.log("duplicate tag detected");
|
||||
process.exit(1);
|
||||
}
|
||||
fs.ensureFileSync(FOLDER_MONITOR);
|
||||
fs.ensureFileSync(FOLDER_SITE);
|
||||
|
||||
fs.ensureFileSync(FOLDER_MONITOR);
|
||||
fs.ensureFileSync(FOLDER_SITE);
|
||||
try {
|
||||
fs.writeFileSync(FOLDER_MONITOR, JSON.stringify(monitors, null, 4));
|
||||
fs.writeFileSync(FOLDER_SITE, JSON.stringify(site, null, 4));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(FOLDER_MONITOR, JSON.stringify(monitors, null, 4));
|
||||
fs.writeFileSync(FOLDER_SITE, JSON.stringify(site, null, 4));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!!site.github && !!site.github.owner && !!site.github.repo) {
|
||||
const ghowner = site.github.owner;
|
||||
const ghrepo = site.github.repo;
|
||||
const ghlabels = await GetAllGHLabels(ghowner, ghrepo);
|
||||
const tagsAndDescription = monitors.map((monitor) => {
|
||||
return { tag: monitor.tag, description: monitor.name };
|
||||
});
|
||||
//add incident label if does not exist
|
||||
|
||||
if (!!site.github && !!site.github.owner && !!site.github.repo) {
|
||||
const ghowner = site.github.owner;
|
||||
const ghrepo = site.github.repo;
|
||||
const ghlabels = await GetAllGHLabels(ghowner, ghrepo);
|
||||
const tagsAndDescription = monitors.map((monitor) => {
|
||||
return { tag: monitor.tag, description: monitor.name };
|
||||
});
|
||||
//add incident label if does not exist
|
||||
if (ghlabels.indexOf("incident") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "incident", "Status of the site");
|
||||
}
|
||||
if (ghlabels.indexOf("resolved") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "resolved", "Incident is resolved", "65dba6");
|
||||
}
|
||||
if (ghlabels.indexOf("identified") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "identified", "Incident is Identified", "EBE3D5");
|
||||
}
|
||||
if (ghlabels.indexOf("investigating") === -1) {
|
||||
await CreateGHLabel(
|
||||
ghowner,
|
||||
ghrepo,
|
||||
"investigating",
|
||||
"Incident is investigated",
|
||||
"D4E2D4"
|
||||
);
|
||||
}
|
||||
if (ghlabels.indexOf("incident-degraded") === -1) {
|
||||
await CreateGHLabel(
|
||||
ghowner,
|
||||
ghrepo,
|
||||
"incident-degraded",
|
||||
"Status is degraded of the site",
|
||||
"f5ba60"
|
||||
);
|
||||
}
|
||||
if (ghlabels.indexOf("incident-down") === -1) {
|
||||
await CreateGHLabel(
|
||||
ghowner,
|
||||
ghrepo,
|
||||
"incident-down",
|
||||
"Status is down of the site",
|
||||
"ea3462"
|
||||
);
|
||||
}
|
||||
//add tags if does not exist
|
||||
for (let i = 0; i < tagsAndDescription.length; i++) {
|
||||
const tag = tagsAndDescription[i].tag;
|
||||
const description = tagsAndDescription[i].description;
|
||||
if (ghlabels.indexOf(tag) === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, tag, description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ghlabels.indexOf("incident") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "incident", "Status of the site");
|
||||
}
|
||||
if (ghlabels.indexOf("resolved") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "resolved", "Incident is resolved", "65dba6");
|
||||
}
|
||||
if (ghlabels.indexOf("identified") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "identified", "Incident is Identified", "EBE3D5");
|
||||
}
|
||||
if (ghlabels.indexOf("investigating") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "investigating", "Incident is investigated", "D4E2D4");
|
||||
}
|
||||
if (ghlabels.indexOf("incident-degraded") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "incident-degraded", "Status is degraded of the site", "f5ba60");
|
||||
}
|
||||
if (ghlabels.indexOf("incident-down") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "incident-down", "Status is down of the site", "ea3462");
|
||||
}
|
||||
//add tags if does not exist
|
||||
for (let i = 0; i < tagsAndDescription.length; i++) {
|
||||
const tag = tagsAndDescription[i].tag;
|
||||
const description = tagsAndDescription[i].description;
|
||||
if (ghlabels.indexOf(tag) === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, tag, description);
|
||||
}
|
||||
}
|
||||
}
|
||||
// init monitors
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
if (!fs.existsSync(monitor.path0Day)) {
|
||||
fs.ensureFileSync(monitor.path0Day);
|
||||
fs.writeFileSync(monitor.path0Day, JSON.stringify({}));
|
||||
}
|
||||
if (!fs.existsSync(monitor.path90Day)) {
|
||||
fs.ensureFileSync(monitor.path90Day);
|
||||
fs.writeFileSync(monitor.path90Day, JSON.stringify({}));
|
||||
}
|
||||
|
||||
// init monitors
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
if (!fs.existsSync(monitor.path0Day)) {
|
||||
fs.ensureFileSync(monitor.path0Day);
|
||||
fs.writeFileSync(monitor.path0Day, JSON.stringify({}));
|
||||
}
|
||||
if (!fs.existsSync(monitor.path90Day)) {
|
||||
fs.ensureFileSync(monitor.path90Day);
|
||||
fs.writeFileSync(monitor.path90Day, JSON.stringify({}));
|
||||
}
|
||||
|
||||
console.log("Initial Fetch for ", monitor.name);
|
||||
await Minuter(envSecrets, monitor, site.github);
|
||||
console.log("Initial Fetch for ", monitor.name);
|
||||
await Minuter(envSecrets, monitor, site.github);
|
||||
await Ninety(monitor);
|
||||
}
|
||||
}
|
||||
|
||||
//trigger minute cron
|
||||
//trigger minute cron
|
||||
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
|
||||
let cronExpession = "* * * * *";
|
||||
if (monitor.cron !== undefined && monitor.cron !== null) {
|
||||
cronExpession = monitor.cron;
|
||||
}
|
||||
console.log("Staring " + cronExpession + " Cron for ", monitor.name);
|
||||
Cron(cronExpession, async () => {
|
||||
await Minuter(envSecrets, monitor, site.github);
|
||||
});
|
||||
}
|
||||
let cronExpession = "* * * * *";
|
||||
if (monitor.cron !== undefined && monitor.cron !== null) {
|
||||
cronExpession = monitor.cron;
|
||||
}
|
||||
console.log("Staring " + cronExpession + " Cron for ", monitor.name);
|
||||
Cron(cronExpession, async () => {
|
||||
await Minuter(envSecrets, monitor, site.github);
|
||||
});
|
||||
}
|
||||
|
||||
//pre compute 90 day data at 1 minute interval
|
||||
Cron(
|
||||
"* * * * *",
|
||||
async () => {
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
Ninety(monitor);
|
||||
}
|
||||
},
|
||||
{
|
||||
protect: true,
|
||||
}
|
||||
);
|
||||
|
||||
"* * * * *",
|
||||
async () => {
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
const monitor = monitors[i];
|
||||
Ninety(monitor);
|
||||
}
|
||||
},
|
||||
{
|
||||
protect: true
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export { Startup };
|
||||
|
||||
@@ -4,9 +4,9 @@ let ts = GetMinuteStartNowTimestampUTC();
|
||||
console.log("GetMinuteStartNowTimestampUTC India 12AM: " + ts);
|
||||
let tm = GetDayStartWithOffset(GetMinuteStartNowTimestampUTC(), tzOffset);
|
||||
console.log("GetMinuteStartTimestampUTC India 12AM: should be 18:30PM " + tm);
|
||||
console.log(`getUTCTimestampAtStartOfDayForOffset(${GetMinuteStartNowTimestampUTC()}, ${tzOffset})`);
|
||||
|
||||
|
||||
console.log(
|
||||
`getUTCTimestampAtStartOfDayForOffset(${GetMinuteStartNowTimestampUTC()}, ${tzOffset})`
|
||||
);
|
||||
|
||||
console.log(BeginningOfDay({ timeZone: "GMT" }));
|
||||
console.log(BeginningOfDay({ timeZone: "Asia/Kolkata", date: new Date(1703223388000) }));
|
||||
console.log(BeginningOfDay({ timeZone: "Asia/Kolkata", date: new Date(1703223388000) }));
|
||||
|
||||
202
scripts/tool.js
202
scripts/tool.js
@@ -2,137 +2,157 @@
|
||||
import { MONITOR, SITE } from "./constants.js";
|
||||
|
||||
const IsValidURL = function (url) {
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
};
|
||||
const IsStringURLSafe = function (str) {
|
||||
const regex = /^[A-Za-z0-9\-_.~]+$/;
|
||||
return regex.test(str);
|
||||
const regex = /^[A-Za-z0-9\-_.~]+$/;
|
||||
return regex.test(str);
|
||||
};
|
||||
const IsValidHTTPMethod = function (method) {
|
||||
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
|
||||
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
|
||||
};
|
||||
function generateRandomColor() {
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
return randomColor;
|
||||
//random color will be freshly served
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
return randomColor;
|
||||
//random color will be freshly served
|
||||
}
|
||||
const LoadMonitorsPath = function () {
|
||||
const argv = process.argv;
|
||||
const argv = process.argv;
|
||||
|
||||
if (!!process.env.MONITOR_YAML_PATH) {
|
||||
return process.env.MONITOR_YAML_PATH;
|
||||
}
|
||||
if (!!process.env.MONITOR_YAML_PATH) {
|
||||
return process.env.MONITOR_YAML_PATH;
|
||||
}
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--monitors") {
|
||||
return argv[i + 1];
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--monitors") {
|
||||
return argv[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return MONITOR;
|
||||
return MONITOR;
|
||||
};
|
||||
const LoadSitePath = function () {
|
||||
const argv = process.argv;
|
||||
const argv = process.argv;
|
||||
|
||||
if (!!process.env.SITE_YAML_PATH) {
|
||||
return process.env.SITE_YAML_PATH;
|
||||
}
|
||||
if (!!process.env.SITE_YAML_PATH) {
|
||||
return process.env.SITE_YAML_PATH;
|
||||
}
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--site") {
|
||||
return argv[i + 1];
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--site") {
|
||||
return argv[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return SITE;
|
||||
return SITE;
|
||||
};
|
||||
//return given timestamp in UTC
|
||||
const GetNowTimestampUTC = function () {
|
||||
//use js date instead of moment
|
||||
const now = new Date();
|
||||
const timestamp = now.getTime();
|
||||
return Math.floor(timestamp / 1000);
|
||||
//use js date instead of moment
|
||||
const now = new Date();
|
||||
const timestamp = now.getTime();
|
||||
return Math.floor(timestamp / 1000);
|
||||
};
|
||||
//return given timestamp minute start timestamp in UTC
|
||||
const GetMinuteStartTimestampUTC = function (timestamp) {
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const minuteStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), 0, 0);
|
||||
const minuteStartTimestamp = minuteStart.getTime();
|
||||
return Math.floor(minuteStartTimestamp / 1000);
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const minuteStart = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
now.getHours(),
|
||||
now.getMinutes(),
|
||||
0,
|
||||
0
|
||||
);
|
||||
const minuteStartTimestamp = minuteStart.getTime();
|
||||
return Math.floor(minuteStartTimestamp / 1000);
|
||||
};
|
||||
//return current timestamp minute start timestamp in UTC
|
||||
const GetMinuteStartNowTimestampUTC = function () {
|
||||
//use js date instead of moment
|
||||
const now = new Date();
|
||||
const minuteStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), 0, 0);
|
||||
const minuteStartTimestamp = minuteStart.getTime();
|
||||
return Math.floor(minuteStartTimestamp / 1000);
|
||||
//use js date instead of moment
|
||||
const now = new Date();
|
||||
const minuteStart = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
now.getHours(),
|
||||
now.getMinutes(),
|
||||
0,
|
||||
0
|
||||
);
|
||||
const minuteStartTimestamp = minuteStart.getTime();
|
||||
return Math.floor(minuteStartTimestamp / 1000);
|
||||
};
|
||||
//return given timestamp day start timestamp in UTC
|
||||
const GetDayStartTimestampUTC = function (timestamp) {
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const dayStart = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0));
|
||||
const dayStartTimestamp = dayStart.getTime();
|
||||
return Math.floor(dayStartTimestamp / 1000);
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const dayStart = new Date(
|
||||
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0)
|
||||
);
|
||||
const dayStartTimestamp = dayStart.getTime();
|
||||
return Math.floor(dayStartTimestamp / 1000);
|
||||
};
|
||||
const GetDayEndTimestampUTC = function (timestamp) {
|
||||
//use js date instead of moment
|
||||
const now = new Date(timestamp * 1000);
|
||||
const dayEnd = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999));
|
||||
const dayEnd = new Date(
|
||||
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999)
|
||||
);
|
||||
const dayEndTimestamp = dayEnd.getTime();
|
||||
return Math.floor(dayEndTimestamp / 1000) + 60;
|
||||
}
|
||||
};
|
||||
const DurationInMinutes = function (start, end) {
|
||||
return Math.floor((end - start) / 60);
|
||||
}
|
||||
};
|
||||
const GetDayStartWithOffset = function (timeStampInSeconds, offsetInMinutes) {
|
||||
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
|
||||
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
|
||||
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
|
||||
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
|
||||
//have to figure out when to add a day
|
||||
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
|
||||
|
||||
//if xtm - 330 > 1 day , add a day to xtm - 330
|
||||
|
||||
if (offsetInMinutes < 0) {
|
||||
//add one day to dayStartThen
|
||||
dayStartThen = dayStartThen + 24 * 60 * 60;
|
||||
}
|
||||
return dayStartThen + offsetInMinutes * 60;
|
||||
const then = new Date(GetMinuteStartTimestampUTC(timeStampInSeconds) * 1000);
|
||||
let dayStartThen = GetDayStartTimestampUTC(then.getTime() / 1000);
|
||||
let dayStartTomorrow = dayStartThen + 24 * 60 * 60;
|
||||
let dayStartYesterday = dayStartThen - 24 * 60 * 60;
|
||||
//have to figure out when to add a day
|
||||
//20-12AM [21-12AM] =21:630 xtm [22-12AM] xtd =22:630 23-12AM
|
||||
|
||||
|
||||
}
|
||||
//if xtm - 330 > 1 day , add a day to xtm - 330
|
||||
|
||||
if (offsetInMinutes < 0) {
|
||||
//add one day to dayStartThen
|
||||
dayStartThen = dayStartThen + 24 * 60 * 60;
|
||||
}
|
||||
return dayStartThen + offsetInMinutes * 60;
|
||||
};
|
||||
const BeginningOfDay = (options = {}) => {
|
||||
const { date = new Date(), timeZone } = options;
|
||||
const parts = Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
hourCycle: "h23",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
}).formatToParts(date);
|
||||
const hour = parseInt(parts.find((i) => i.type === "hour").value);
|
||||
const minute = parseInt(parts.find((i) => i.type === "minute").value);
|
||||
const second = parseInt(parts.find((i) => i.type === "second").value);
|
||||
const dt = new Date(1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000));
|
||||
const { date = new Date(), timeZone } = options;
|
||||
const parts = Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
hourCycle: "h23",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric"
|
||||
}).formatToParts(date);
|
||||
const hour = parseInt(parts.find((i) => i.type === "hour").value);
|
||||
const minute = parseInt(parts.find((i) => i.type === "minute").value);
|
||||
const second = parseInt(parts.find((i) => i.type === "second").value);
|
||||
const dt = new Date(
|
||||
1000 * Math.floor((date - hour * 3600000 - minute * 60000 - second * 1000) / 1000)
|
||||
);
|
||||
return dt.getTime() / 1000;
|
||||
};
|
||||
export {
|
||||
IsValidURL,
|
||||
IsValidHTTPMethod,
|
||||
LoadMonitorsPath,
|
||||
LoadSitePath,
|
||||
GetMinuteStartTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
GetDayStartTimestampUTC,
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
DurationInMinutes,
|
||||
GetDayStartWithOffset,
|
||||
BeginningOfDay,
|
||||
IsStringURLSafe,
|
||||
IsValidURL,
|
||||
IsValidHTTPMethod,
|
||||
LoadMonitorsPath,
|
||||
LoadSitePath,
|
||||
GetMinuteStartTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
GetDayStartTimestampUTC,
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
DurationInMinutes,
|
||||
GetDayStartWithOffset,
|
||||
BeginningOfDay,
|
||||
IsStringURLSafe
|
||||
};
|
||||
|
||||
38
src/app.html
38
src/app.html
@@ -1,26 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark dark:bg-background">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q3MLRXCBFT"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q3MLRXCBFT"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
|
||||
gtag("config", "G-Q3MLRXCBFT");
|
||||
</script>
|
||||
</body>
|
||||
gtag("config", "G-Q3MLRXCBFT");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
104
src/app.postcss
104
src/app.postcss
@@ -3,80 +3,80 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background-kener: hsl(0, 0%, 100%);
|
||||
--background-kener-rgba: rgba(255,255,255,.5);
|
||||
--foreground: 240 10% 4%;
|
||||
--background-kener-rgba: rgba(255, 255, 255, 0.5);
|
||||
--foreground: 240 10% 4%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 4%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 4%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 4%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 4%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 72.2% 50.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 240 10% 4%;
|
||||
--ring: 240 10% 4%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 4%;
|
||||
--background-kener: hsl(240, 10%, 4%);
|
||||
--background-kener-rgba: rgba(9, 9, 11, .35);
|
||||
--foreground: 210 40% 98%;
|
||||
.dark {
|
||||
--background: 240 10% 4%;
|
||||
--background-kener: hsl(240, 10%, 4%);
|
||||
--background-kener-rgba: rgba(9, 9, 11, 0.35);
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--muted: 240 4% 16%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--popover: 240 10% 4%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--popover: 240 10% 4%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--card: 240 10% 4%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--card: 240 10% 4%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--border: 240 4% 16%;
|
||||
--input: 240 4% 16%;
|
||||
--border: 240 4% 16%;
|
||||
--input: 240 4% 16%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 240 4% 16%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--secondary: 240 4% 16%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--accent: 240 4% 16%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--accent: 240 4% 16%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: hsl(212.7, 26.8%, 83.9);
|
||||
}
|
||||
--ring: hsl(212.7, 26.8%, 83.9);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,81 @@
|
||||
/*one is the class for dotted background*/
|
||||
.one {
|
||||
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%);
|
||||
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%);
|
||||
}
|
||||
|
||||
.one::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;
|
||||
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;
|
||||
}
|
||||
|
||||
/*Needed to overlay content on top of dotted bg*/
|
||||
section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
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);
|
||||
background-color: var(--background-kener-rgba);
|
||||
box-shadow: 0 0 64px 64px var(--background-kener-rgba);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*Colors for something UP*/
|
||||
.bg-api-up {
|
||||
background-color: #00dfa2;
|
||||
background-color: #00dfa2;
|
||||
}
|
||||
.text-api-up {
|
||||
color: #0aca97;
|
||||
color: #0aca97;
|
||||
}
|
||||
/*Colors for something DOWN*/
|
||||
.bg-api-down {
|
||||
background-color: #ff0060;
|
||||
background-color: #ff0060;
|
||||
}
|
||||
.text-api-down {
|
||||
color: #ff0060;
|
||||
color: #ff0060;
|
||||
}
|
||||
/*Colors for something Not there*/
|
||||
.bg-api-nodata {
|
||||
background-color:#f1f5f8;
|
||||
background-color: #f1f5f8;
|
||||
}
|
||||
.text-api-nodata {
|
||||
color: #b8bcbe;
|
||||
color: #b8bcbe;
|
||||
}
|
||||
.dark .bg-api-nodata {
|
||||
background-color: rgba(100, 100, 100, .4);
|
||||
background-color: rgba(100, 100, 100, 0.4);
|
||||
}
|
||||
|
||||
/*Colors for something degraded*/
|
||||
.bg-api-degraded {
|
||||
background-color: #ffb84c;
|
||||
background-color: #ffb84c;
|
||||
}
|
||||
.text-api-degraded {
|
||||
color: #ffb84c;
|
||||
color: #ffb84c;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*Needed to show markdown properly*/
|
||||
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::before{
|
||||
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::before {
|
||||
content: "";
|
||||
}
|
||||
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::after{
|
||||
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
|
||||
/*Needed to show monitor stacked properly*/
|
||||
.monitors-card .monitor {
|
||||
padding: 1.3em 1em;
|
||||
@@ -92,15 +87,15 @@ section {
|
||||
}
|
||||
|
||||
/*Tag Color*/
|
||||
.tag-maintenance{
|
||||
background-color: #A076F9;
|
||||
.tag-maintenance {
|
||||
background-color: #a076f9;
|
||||
color: #09090b;
|
||||
}
|
||||
.tag-resolved{
|
||||
background-color: #2CD3E1;
|
||||
.tag-resolved {
|
||||
background-color: #2cd3e1;
|
||||
color: #09090b;
|
||||
}
|
||||
.tag-indetified{
|
||||
background-color: #FEFFAC;
|
||||
.tag-indetified {
|
||||
background-color: #feffac;
|
||||
color: #09090b;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +1,208 @@
|
||||
<script>
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { StatusObj } from "$lib/helpers.js";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { ChevronDown } from "lucide-svelte";
|
||||
import * as Collapsible from "$lib/components/ui/collapsible";
|
||||
import { l } from '$lib/i18n/client';
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { StatusObj } from "$lib/helpers.js";
|
||||
import moment from "moment";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { ChevronDown } from "lucide-svelte";
|
||||
import * as Collapsible from "$lib/components/ui/collapsible";
|
||||
import { l } from "$lib/i18n/client";
|
||||
import axios from "axios";
|
||||
import { Skeleton } from "$lib/components/ui/skeleton";
|
||||
import { base } from '$app/paths';
|
||||
import { base } from "$app/paths";
|
||||
|
||||
export let incident;
|
||||
export let variant = "title+body+comments+monitor";
|
||||
export let state = "open";
|
||||
export let monitor;
|
||||
export let incident;
|
||||
export let variant = "title+body+comments+monitor";
|
||||
export let state = "open";
|
||||
export let monitor;
|
||||
export let lang;
|
||||
|
||||
let blinker = "bg-transparent";
|
||||
let incidentPriority = "";
|
||||
let incidentDuration = 0;
|
||||
if (incident.labels.includes("incident-down")) {
|
||||
blinker = "bg-red-500";
|
||||
incidentPriority = "DOWN";
|
||||
} else if (incident.labels.includes("incident-degraded")) {
|
||||
blinker = "bg-yellow-500";
|
||||
incidentPriority = "DEGRADED";
|
||||
}
|
||||
let incidentState = incident.state;
|
||||
let incidentClosedAt = incident.incident_end_time;
|
||||
let incidentCreatedAt = incident.incident_start_time;
|
||||
let incidentMessage = "";
|
||||
if (!!incidentClosedAt && !!incidentCreatedAt) {
|
||||
//incidentDuration between closed_at and created_at
|
||||
incidentDuration = moment(incidentClosedAt * 1000)
|
||||
.add(1, "minutes")
|
||||
.diff(moment(incidentCreatedAt * 1000), "minutes");
|
||||
} else if (!!incidentCreatedAt) {
|
||||
//incidentDuration between now and created_at
|
||||
incidentDuration = moment().diff(moment(incidentCreatedAt * 1000), "minutes");
|
||||
}
|
||||
|
||||
//find a replace /\[start_datetime:(\d+)\]/ empty in incident.body
|
||||
//find a replace /\[end_datetime:(\d+)\]/ empty in incident.body
|
||||
incident.body = incident.body.replace(/\[start_datetime:(\d+)\]/g, "");
|
||||
incident.body = incident.body.replace(/\[end_datetime:(\d+)\]/g, "");
|
||||
|
||||
let blinker = "bg-transparent";
|
||||
let incidentPriority = "";
|
||||
let incidentDuration = 0;
|
||||
if (incident.labels.includes("incident-down")) {
|
||||
blinker = "bg-red-500";
|
||||
incidentPriority = "DOWN";
|
||||
} else if (incident.labels.includes("incident-degraded")) {
|
||||
blinker = "bg-yellow-500";
|
||||
incidentPriority = "DEGRADED";
|
||||
}
|
||||
let incidentState = incident.state;
|
||||
let incidentClosedAt = incident.incident_end_time;
|
||||
let incidentCreatedAt = incident.incident_start_time;
|
||||
let incidentMessage = "";
|
||||
if (!!incidentClosedAt && !!incidentCreatedAt) {
|
||||
//incidentDuration between closed_at and created_at
|
||||
incidentDuration = moment(incidentClosedAt * 1000)
|
||||
.add(1, "minutes")
|
||||
.diff(moment(incidentCreatedAt * 1000), "minutes");
|
||||
} else if (!!incidentCreatedAt) {
|
||||
//incidentDuration between now and created_at
|
||||
incidentDuration = moment().diff(moment(incidentCreatedAt * 1000), "minutes");
|
||||
}
|
||||
|
||||
//find a replace /\[start_datetime:(\d+)\]/ empty in incident.body
|
||||
//find a replace /\[end_datetime:(\d+)\]/ empty in incident.body
|
||||
incident.body = incident.body.replace(/\[start_datetime:(\d+)\]/g, "");
|
||||
incident.body = incident.body.replace(/\[end_datetime:(\d+)\]/g, "");
|
||||
|
||||
//fetch comments
|
||||
incident.comments = [];
|
||||
let commentsLoading = true
|
||||
let commentsLoading = true;
|
||||
|
||||
function getComments(){
|
||||
state = (state=='open'? 'close':'open');
|
||||
if(incident.comments.length > 0) return;
|
||||
if(commentsLoading === false) return;
|
||||
axios.get(`${base}/incident/${incident.number}/comments`).then((response) => {
|
||||
incident.comments = response.data;
|
||||
commentsLoading = false;
|
||||
}).catch((error) => {
|
||||
// console.log(error);
|
||||
});
|
||||
function getComments() {
|
||||
state = state == "open" ? "close" : "open";
|
||||
if (incident.comments.length > 0) return;
|
||||
if (commentsLoading === false) return;
|
||||
axios
|
||||
.get(`${base}/incident/${incident.number}/comments`)
|
||||
.then((response) => {
|
||||
incident.comments = response.data;
|
||||
commentsLoading = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
// console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 mb-8 w-full incident-div">
|
||||
<div class="col-span-3">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title class="relative">
|
||||
<div class="incident-div mb-8 grid w-full grid-cols-3 gap-4">
|
||||
<div class="col-span-3">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title class="relative">
|
||||
{#if incidentPriority != "" && incidentDuration > 0}
|
||||
<p class="leading-10 absolute -top-11 -translate-y-1">
|
||||
<Badge class="text-[rgba(0,0,0,.6)] -ml-3 bg-card text-sm font-semibold text-{StatusObj[incidentPriority]} "> {incidentPriority} for {incidentDuration} Minute{incidentDuration > 1 ? "s" : ""} </Badge>
|
||||
</p>
|
||||
|
||||
{/if}
|
||||
{#if variant.includes("monitor")}
|
||||
<div class="pb-4">
|
||||
<div class="scroll-m-20 text-2xl font-semibold tracking-tight">
|
||||
{#if monitor.image}
|
||||
<img src="{monitor.image}" class="w-6 h-6 inline" alt="" srcset="" />
|
||||
{/if} {monitor.name}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/if} {#if variant.includes("title")} {incident.title} {/if}
|
||||
{#if incidentState == 'open'}
|
||||
<span class="animate-ping absolute -left-[24px] -top-[24px] w-[8px] h-[8px] inline-flex rounded-full {blinker} opacity-75"></span>
|
||||
{/if}
|
||||
<p class="absolute -top-11 -translate-y-1 leading-10">
|
||||
<Badge
|
||||
class="-ml-3 bg-card text-sm font-semibold text-[rgba(0,0,0,.6)] text-{StatusObj[
|
||||
incidentPriority
|
||||
]} "
|
||||
>
|
||||
{incidentPriority} for {incidentDuration} Minute{incidentDuration >
|
||||
1
|
||||
? "s"
|
||||
: ""}
|
||||
</Badge>
|
||||
</p>
|
||||
{/if}
|
||||
{#if variant.includes("monitor")}
|
||||
<div class="pb-4">
|
||||
<div class="scroll-m-20 text-2xl font-semibold tracking-tight">
|
||||
{#if monitor.image}
|
||||
<img
|
||||
src={monitor.image}
|
||||
class="inline h-6 w-6"
|
||||
alt=""
|
||||
srcset=""
|
||||
/>
|
||||
{/if}
|
||||
{monitor.name}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if variant.includes("title")}
|
||||
{incident.title}
|
||||
{/if}
|
||||
{#if incidentState == "open"}
|
||||
<span
|
||||
class="absolute -left-[24px] -top-[24px] inline-flex h-[8px] w-[8px] animate-ping rounded-full {blinker} opacity-75"
|
||||
></span>
|
||||
{/if}
|
||||
{#if variant.includes("body") || variant.includes("comments")}
|
||||
<div class="absolute right-4 toggle {state}">
|
||||
<Button variant="outline" class="rounded-full" size="icon" on:click="{getComments}">
|
||||
<ChevronDown class="text-muted-foreground" size="{24}" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Title>
|
||||
<Card.Description>
|
||||
{moment(incidentCreatedAt * 1000).format("MMMM Do YYYY, h:mm:ss a")}
|
||||
|
||||
<div class="toggle absolute right-4 {state}">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="rounded-full"
|
||||
size="icon"
|
||||
on:click={getComments}
|
||||
>
|
||||
<ChevronDown class="text-muted-foreground" size={24} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Title>
|
||||
<Card.Description>
|
||||
{moment(incidentCreatedAt * 1000).format("MMMM Do YYYY, h:mm:ss a")}
|
||||
|
||||
<p class="mt-2 leading-8">
|
||||
{#if incident.labels.includes("identified")}
|
||||
<span class="mt-1 text-xs font-semibold me-2 px-2.5 py-1 uppercase leading-3 inline-block rounded tag-indetified">
|
||||
{l(lang,'incident.identified')}
|
||||
</span>
|
||||
{/if} {#if incident.labels.includes("resolved")}
|
||||
<span class=" text-xs font-semibold me-2 px-2.5 py-1 leading-3 inline-block rounded uppercase tag-resolved">
|
||||
{l(lang,'incident.resolved')}
|
||||
</span>
|
||||
{/if} {#if incident.labels.includes("maintenance")}
|
||||
<span class="text-xs font-semibold me-2 px-2.5 py-1 leading-3 inline-block rounded uppercase tag-maintenance">
|
||||
{l(lang,'incident.maintenance')}
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
{#if (variant.includes("body") || variant.includes("comments")) && state == "open"}
|
||||
<Card.Content>
|
||||
{#if variant.includes("body")}
|
||||
<div class="prose prose-stone dark:prose-invert max-w-none prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:rounded">
|
||||
{@html incident.body}
|
||||
</div>
|
||||
{/if}
|
||||
{#if variant.includes("comments") && incident.comments?.length > 0}
|
||||
<div class="ml-4 mt-8">
|
||||
<ol class="relative border-s border-secondary">
|
||||
{#each incident.comments as comment}
|
||||
<li class="mb-10 ms-4">
|
||||
<div class="absolute w-3 h-3 rounded-full mt-1.5 -start-1.5 border bg-secondary border-secondary"></div>
|
||||
<time class="mb-1 text-sm font-normal leading-none text-muted-foreground"> {moment(comment.created_at).format("MMMM Do YYYY, h:mm:ss a")} </time>
|
||||
<div
|
||||
class="mb-4 text-base font-normal wysiwyg dark:prose-invert prose prose-stone max-w-none prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm prose-code:rounded"
|
||||
>
|
||||
{@html comment.body}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
{:else if commentsLoading}
|
||||
<Skeleton class="w-[100px] h-[20px] rounded-full" />
|
||||
{/if}
|
||||
</Card.Content>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
</div>
|
||||
<p class="mt-2 leading-8">
|
||||
{#if incident.labels.includes("identified")}
|
||||
<span
|
||||
class="tag-indetified me-2 mt-1 inline-block rounded px-2.5 py-1 text-xs font-semibold uppercase leading-3"
|
||||
>
|
||||
{l(lang, "incident.identified")}
|
||||
</span>
|
||||
{/if}
|
||||
{#if incident.labels.includes("resolved")}
|
||||
<span
|
||||
class=" tag-resolved me-2 inline-block rounded px-2.5 py-1 text-xs font-semibold uppercase leading-3"
|
||||
>
|
||||
{l(lang, "incident.resolved")}
|
||||
</span>
|
||||
{/if}
|
||||
{#if incident.labels.includes("maintenance")}
|
||||
<span
|
||||
class="tag-maintenance me-2 inline-block rounded px-2.5 py-1 text-xs font-semibold uppercase leading-3"
|
||||
>
|
||||
{l(lang, "incident.maintenance")}
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
{#if (variant.includes("body") || variant.includes("comments")) && state == "open"}
|
||||
<Card.Content>
|
||||
{#if variant.includes("body")}
|
||||
<div
|
||||
class="prose prose-stone max-w-none dark:prose-invert prose-code:rounded prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm"
|
||||
>
|
||||
{@html incident.body}
|
||||
</div>
|
||||
{/if}
|
||||
{#if variant.includes("comments") && incident.comments?.length > 0}
|
||||
<div class="ml-4 mt-8">
|
||||
<ol class="relative border-s border-secondary">
|
||||
{#each incident.comments as comment}
|
||||
<li class="mb-10 ms-4">
|
||||
<div
|
||||
class="absolute -start-1.5 mt-1.5 h-3 w-3 rounded-full border border-secondary bg-secondary"
|
||||
></div>
|
||||
<time
|
||||
class="mb-1 text-sm font-normal leading-none text-muted-foreground"
|
||||
>
|
||||
{moment(comment.created_at).format(
|
||||
"MMMM Do YYYY, h:mm:ss a"
|
||||
)}
|
||||
</time>
|
||||
<div
|
||||
class="wysiwyg prose prose-stone mb-4 max-w-none text-base font-normal dark:prose-invert prose-code:rounded prose-code:px-[0.3rem] prose-code:py-[0.2rem] prose-code:font-mono prose-code:text-sm"
|
||||
>
|
||||
{@html comment.body}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
{:else if commentsLoading}
|
||||
<Skeleton class="h-[20px] w-[100px] rounded-full" />
|
||||
{/if}
|
||||
</Card.Content>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toggle {
|
||||
display: none;
|
||||
}
|
||||
.toggle{
|
||||
.toggle {
|
||||
display: none;
|
||||
}
|
||||
.toggle {
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
.toggle.open{
|
||||
.toggle.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.incident-div:hover .toggle {
|
||||
display: block;
|
||||
}
|
||||
.incident-div:hover .toggle {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,87 +1,78 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { Languages } from "lucide-svelte";
|
||||
import { base } from '$app/paths';
|
||||
export let data;
|
||||
let defaultLocaleKey = data.selectedLang;
|
||||
const allLocales = data.site.i18n?.locales;
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { Languages } from "lucide-svelte";
|
||||
import { base } from "$app/paths";
|
||||
export let data;
|
||||
let defaultLocaleKey = data.selectedLang;
|
||||
const allLocales = data.site.i18n?.locales;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let defaultLocaleValue;
|
||||
if (!allLocales) {
|
||||
defaultLocaleValue = "English";
|
||||
} else {
|
||||
defaultLocaleValue = allLocales[defaultLocaleKey];
|
||||
}
|
||||
/**
|
||||
* @param {string} locale
|
||||
*/
|
||||
function setLanguage(locale) {
|
||||
document.cookie = `localLang=${locale};max-age=${60 * 60 * 24 * 365 * 30}`;
|
||||
if (locale === defaultLocaleKey) return;
|
||||
defaultLocaleValue = allLocales[locale];
|
||||
location.reload();
|
||||
}
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let defaultLocaleValue;
|
||||
if (!allLocales) {
|
||||
defaultLocaleValue = "English";
|
||||
} else {
|
||||
defaultLocaleValue = allLocales[defaultLocaleKey];
|
||||
}
|
||||
/**
|
||||
* @param {string} locale
|
||||
*/
|
||||
function setLanguage(locale) {
|
||||
document.cookie = `localLang=${locale};max-age=${60 * 60 * 24 * 365 * 30}`;
|
||||
if (locale === defaultLocaleKey) return;
|
||||
defaultLocaleValue = allLocales[locale];
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="one"></div>
|
||||
|
||||
<header class="relative z-50 w-full">
|
||||
<div class="container flex h-14 items-center">
|
||||
<div class="mr-4 flex blurry-bg w-full justify-between">
|
||||
<a
|
||||
href={data.site.home ? data.site.home : base}
|
||||
class="mr-6 flex items-center space-x-2"
|
||||
>
|
||||
{#if data.site.logo}
|
||||
<img
|
||||
src={data.site.logo}
|
||||
class="h-8"
|
||||
alt={data.site.title}
|
||||
srcset=""
|
||||
/>
|
||||
{/if}
|
||||
{#if data.site.title}
|
||||
<span
|
||||
class="hidden font-bold md:inline-block text-[15px] lg:text-base"
|
||||
>
|
||||
{data.site.title}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
{#if data.site.nav}
|
||||
<nav
|
||||
class="flex flex-wrap items-center space-x-6 text-sm font-medium"
|
||||
>
|
||||
{#each data.site.nav as navItem}
|
||||
<a href={navItem.url}> {navItem.name} </a>
|
||||
{/each}
|
||||
{#if data.site.i18n && data.site.i18n.locales && Object.keys(data.site.i18n.locales).length > 1}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="outline" size="sm">
|
||||
<Languages size={14} class="mr-2" />
|
||||
{defaultLocaleValue}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Group>
|
||||
{#each Object.entries(allLocales) as [key, value]}
|
||||
<DropdownMenu.Item
|
||||
on:click={(e) => {
|
||||
setLanguage(key);
|
||||
}}>{value}</DropdownMenu.Item
|
||||
>
|
||||
{/each}
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container flex h-14 items-center">
|
||||
<div class="blurry-bg mr-4 flex w-full justify-between">
|
||||
<a
|
||||
href={data.site.home ? data.site.home : base}
|
||||
class="mr-6 flex items-center space-x-2"
|
||||
>
|
||||
{#if data.site.logo}
|
||||
<img src={data.site.logo} class="h-8" alt={data.site.title} srcset="" />
|
||||
{/if}
|
||||
{#if data.site.title}
|
||||
<span class="hidden text-[15px] font-bold md:inline-block lg:text-base">
|
||||
{data.site.title}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
{#if data.site.nav}
|
||||
<nav class="flex flex-wrap items-center space-x-6 text-sm font-medium">
|
||||
{#each data.site.nav as navItem}
|
||||
<a href={navItem.url}> {navItem.name} </a>
|
||||
{/each}
|
||||
{#if data.site.i18n && data.site.i18n.locales && Object.keys(data.site.i18n.locales).length > 1}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="outline" size="sm">
|
||||
<Languages size={14} class="mr-2" />
|
||||
{defaultLocaleValue}
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Group>
|
||||
{#each Object.entries(allLocales) as [key, value]}
|
||||
<DropdownMenu.Item
|
||||
on:click={(e) => {
|
||||
setLanguage(key);
|
||||
}}>{value}</DropdownMenu.Item
|
||||
>
|
||||
{/each}
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Item
|
||||
{value}
|
||||
class={cn("border-b", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<AccordionPrimitive.Item {value} class={cn("border-b", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</AccordionPrimitive.Item>
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(alertVariants({ variant }), className)}
|
||||
{...$$restProps}
|
||||
role="alert"
|
||||
>
|
||||
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,7 @@ export const badgeVariants = tv({
|
||||
base: "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none select-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||
default: "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||
destructive:
|
||||
|
||||
@@ -7,12 +7,10 @@ const buttonVariants = tv({
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline"
|
||||
},
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
class={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
bind:checked
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<DropdownMenuPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none mt-3",
|
||||
"z-50 mt-3 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<input
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-foreground file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{disabled}
|
||||
{label}
|
||||
class={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
|
||||
@@ -8,7 +8,4 @@
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Separator
|
||||
class={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...$$restProps}
|
||||
/>
|
||||
<SelectPrimitive.Separator class={cn("-mx-1 my-1 h-px bg-muted", className)} {...$$restProps} />
|
||||
|
||||
@@ -8,7 +8,4 @@
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...$$restProps}
|
||||
/>
|
||||
<div class={cn("animate-pulse rounded-md bg-muted", className)} {...$$restProps} />
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
// @ts-nocheck
|
||||
const StatusObj = {
|
||||
UP: "api-up",
|
||||
DEGRADED: "api-degraded",
|
||||
DOWN: "api-down",
|
||||
NO_DATA: "api-nodata",
|
||||
UP: "api-up",
|
||||
DEGRADED: "api-degraded",
|
||||
DOWN: "api-down",
|
||||
NO_DATA: "api-nodata"
|
||||
};
|
||||
const StatusColor = {
|
||||
UP: "00dfa2",
|
||||
DEGRADED: "ffb84c",
|
||||
DOWN: "ff0060",
|
||||
NO_DATA: "b8bcbe",
|
||||
UP: "00dfa2",
|
||||
DEGRADED: "ffb84c",
|
||||
DOWN: "ff0060",
|
||||
NO_DATA: "b8bcbe"
|
||||
};
|
||||
// @ts-ignore
|
||||
const ParseUptime = function (up, all) {
|
||||
if (all === 0) return String("-");
|
||||
if (up == 0) return String("0");
|
||||
if (up == all) {
|
||||
return String(((up / all) * parseFloat(100)).toFixed(0));
|
||||
}
|
||||
//return 50% as 50% and not 50.0000%
|
||||
if (((up / all) * 100) % 10 == 0) {
|
||||
return String(((up / all) * parseFloat(100)).toFixed(0));
|
||||
}
|
||||
return String(((up / all) * parseFloat(100)).toFixed(4));
|
||||
if (all === 0) return String("-");
|
||||
if (up == 0) return String("0");
|
||||
if (up == all) {
|
||||
return String(((up / all) * parseFloat(100)).toFixed(0));
|
||||
}
|
||||
//return 50% as 50% and not 50.0000%
|
||||
if (((up / all) * 100) % 10 == 0) {
|
||||
return String(((up / all) * parseFloat(100)).toFixed(0));
|
||||
}
|
||||
return String(((up / all) * parseFloat(100)).toFixed(4));
|
||||
};
|
||||
const ParsePercentage = function (n) {
|
||||
if (isNaN(n)) return "-";
|
||||
if (n == 0) {
|
||||
return "0";
|
||||
}
|
||||
if (n == 100) {
|
||||
return "100";
|
||||
}
|
||||
return n.toFixed(4);
|
||||
if (isNaN(n)) return "-";
|
||||
if (n == 0) {
|
||||
return "0";
|
||||
}
|
||||
if (n == 100) {
|
||||
return "100";
|
||||
}
|
||||
return n.toFixed(4);
|
||||
};
|
||||
export { StatusObj, StatusColor, ParseUptime, ParsePercentage };
|
||||
|
||||
@@ -1,213 +1,204 @@
|
||||
const l = function (
|
||||
/** @type {any} */ sessionLangMap,
|
||||
/** @type {string} */ key,
|
||||
) {
|
||||
const keys = key.split(".");
|
||||
let obj = sessionLangMap;
|
||||
for (const key of keys) {
|
||||
// @ts-ignore
|
||||
obj = obj[key];
|
||||
if (!obj) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return obj || key;
|
||||
const l = function (/** @type {any} */ sessionLangMap, /** @type {string} */ key) {
|
||||
const keys = key.split(".");
|
||||
let obj = sessionLangMap;
|
||||
for (const key of keys) {
|
||||
// @ts-ignore
|
||||
obj = obj[key];
|
||||
if (!obj) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return obj || key;
|
||||
};
|
||||
|
||||
const summaryTime = function (
|
||||
/** @type {any} */ sessionLangMap,
|
||||
/** @type {string} */ message,
|
||||
) {
|
||||
if (message == "No Data") {
|
||||
return sessionLangMap.monitor.status_no_data;
|
||||
}
|
||||
if (message == "Status OK") {
|
||||
return sessionLangMap.monitor.status_ok;
|
||||
}
|
||||
const summaryTime = function (/** @type {any} */ sessionLangMap, /** @type {string} */ message) {
|
||||
if (message == "No Data") {
|
||||
return sessionLangMap.monitor.status_no_data;
|
||||
}
|
||||
if (message == "Status OK") {
|
||||
return sessionLangMap.monitor.status_ok;
|
||||
}
|
||||
|
||||
const expectedStrings = [
|
||||
{
|
||||
regexes: [/^(DEGRADED|DOWN)/g, /\d+ minute$/g],
|
||||
s: sessionLangMap.monitor.status_x_minute,
|
||||
output: function (
|
||||
/** @type {string} */ message,
|
||||
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap,
|
||||
) {
|
||||
let match = message.match(this.regexes[0]);
|
||||
const status = match ? match[0] : null;
|
||||
if (!status) {
|
||||
return message;
|
||||
}
|
||||
match = message.match(this.regexes[1]);
|
||||
const duration = match ? match[0] : null;
|
||||
if (!duration) {
|
||||
return message;
|
||||
}
|
||||
const expectedStrings = [
|
||||
{
|
||||
regexes: [/^(DEGRADED|DOWN)/g, /\d+ minute$/g],
|
||||
s: sessionLangMap.monitor.status_x_minute,
|
||||
output: function (
|
||||
/** @type {string} */ message,
|
||||
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap
|
||||
) {
|
||||
let match = message.match(this.regexes[0]);
|
||||
const status = match ? match[0] : null;
|
||||
if (!status) {
|
||||
return message;
|
||||
}
|
||||
match = message.match(this.regexes[1]);
|
||||
const duration = match ? match[0] : null;
|
||||
if (!duration) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const digits = duration
|
||||
.replace(" minute", "")
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
return this.s
|
||||
.replace(/%status/g, sessionLangMap.statuses[status])
|
||||
.replace(/%minute/, digits);
|
||||
},
|
||||
},
|
||||
{
|
||||
regexes: [/^(DEGRADED|DOWN)/g, /\d+ minutes$/g],
|
||||
s: sessionLangMap.monitor.status_x_minutes,
|
||||
output: function (
|
||||
/** @type {string} */ message,
|
||||
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap,
|
||||
) {
|
||||
let match = message.match(this.regexes[0]);
|
||||
const status = match ? match[0] : null;
|
||||
if (!status) {
|
||||
return message;
|
||||
}
|
||||
const digits = duration
|
||||
.replace(" minute", "")
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
return this.s
|
||||
.replace(/%status/g, sessionLangMap.statuses[status])
|
||||
.replace(/%minute/, digits);
|
||||
}
|
||||
},
|
||||
{
|
||||
regexes: [/^(DEGRADED|DOWN)/g, /\d+ minutes$/g],
|
||||
s: sessionLangMap.monitor.status_x_minutes,
|
||||
output: function (
|
||||
/** @type {string} */ message,
|
||||
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap
|
||||
) {
|
||||
let match = message.match(this.regexes[0]);
|
||||
const status = match ? match[0] : null;
|
||||
if (!status) {
|
||||
return message;
|
||||
}
|
||||
|
||||
match = message.match(this.regexes[1]);
|
||||
const duration = match ? match[0] : null;
|
||||
match = message.match(this.regexes[1]);
|
||||
const duration = match ? match[0] : null;
|
||||
|
||||
if (!duration) {
|
||||
return message;
|
||||
}
|
||||
if (!duration) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const digits = duration
|
||||
.replace(" minutes", "")
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
const digits = duration
|
||||
.replace(" minutes", "")
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
|
||||
let res = this.s
|
||||
.replace(/%status/g, sessionLangMap.statuses[status])
|
||||
.replace(/%minutes/, digits);
|
||||
return res;
|
||||
},
|
||||
},
|
||||
{
|
||||
regexes: [/^(DEGRADED|DOWN)/g, /\d+h/g, /\d+m$/g],
|
||||
s: sessionLangMap.monitor.status_x_hour_y_minute,
|
||||
output: function (
|
||||
/** @type {string} */ message,
|
||||
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap,
|
||||
) {
|
||||
let match = message.match(this.regexes[0]);
|
||||
const status = match ? match[0] : null;
|
||||
if (!status) {
|
||||
return message;
|
||||
}
|
||||
match = message.match(this.regexes[1]);
|
||||
const hour = match ? match[0] : null;
|
||||
if (!hour) {
|
||||
return message;
|
||||
}
|
||||
match = message.match(this.regexes[2]);
|
||||
const minute = match ? match[0] : null;
|
||||
if (!minute) {
|
||||
return message;
|
||||
}
|
||||
let res = this.s
|
||||
.replace(/%status/g, sessionLangMap.statuses[status])
|
||||
.replace(/%minutes/, digits);
|
||||
return res;
|
||||
}
|
||||
},
|
||||
{
|
||||
regexes: [/^(DEGRADED|DOWN)/g, /\d+h/g, /\d+m$/g],
|
||||
s: sessionLangMap.monitor.status_x_hour_y_minute,
|
||||
output: function (
|
||||
/** @type {string} */ message,
|
||||
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap
|
||||
) {
|
||||
let match = message.match(this.regexes[0]);
|
||||
const status = match ? match[0] : null;
|
||||
if (!status) {
|
||||
return message;
|
||||
}
|
||||
match = message.match(this.regexes[1]);
|
||||
const hour = match ? match[0] : null;
|
||||
if (!hour) {
|
||||
return message;
|
||||
}
|
||||
match = message.match(this.regexes[2]);
|
||||
const minute = match ? match[0] : null;
|
||||
if (!minute) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const digits = hour
|
||||
.replace("h", "")
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
const digits2 = minute
|
||||
.replace("m", "")
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
const digits = hour
|
||||
.replace("h", "")
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
const digits2 = minute
|
||||
.replace("m", "")
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
|
||||
return this.s
|
||||
.replace(/%status/g, sessionLangMap.statuses[status])
|
||||
.replace(/%hours/g, digits)
|
||||
.replace(/%minutes/, digits2);
|
||||
},
|
||||
},
|
||||
{
|
||||
regexes: [/^Last \d+ hours$/g],
|
||||
s: sessionLangMap.root.last_x_hours,
|
||||
output: function (
|
||||
/** @type {string} */ message,
|
||||
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap,
|
||||
) {
|
||||
//extract the number out of message
|
||||
let match = message.match(/\d+/g);
|
||||
const hours = match ? match[0] : null;
|
||||
if (!hours) {
|
||||
return message;
|
||||
}
|
||||
const digits = hours
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
return this.s
|
||||
.replace(/%status/g, sessionLangMap.statuses[status])
|
||||
.replace(/%hours/g, digits)
|
||||
.replace(/%minutes/, digits2);
|
||||
}
|
||||
},
|
||||
{
|
||||
regexes: [/^Last \d+ hours$/g],
|
||||
s: sessionLangMap.root.last_x_hours,
|
||||
output: function (
|
||||
/** @type {string} */ message,
|
||||
/** @type {{ statuses: { [x: string]: any; }; numbers: { [x: string]: any; }; }} */ sessionLangMap
|
||||
) {
|
||||
//extract the number out of message
|
||||
let match = message.match(/\d+/g);
|
||||
const hours = match ? match[0] : null;
|
||||
if (!hours) {
|
||||
return message;
|
||||
}
|
||||
const digits = hours
|
||||
.split("")
|
||||
.map((elem) => {
|
||||
return sessionLangMap.numbers[String(elem)];
|
||||
})
|
||||
.join("");
|
||||
|
||||
return this.s.replace(/%hours/g, digits);
|
||||
},
|
||||
},
|
||||
];
|
||||
return this.s.replace(/%hours/g, digits);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
//loop through the expectedStrings array and find the matching string
|
||||
let selectedIndex = -1;
|
||||
for (let i = 0; i < expectedStrings.length; i++) {
|
||||
let matchCount = 0;
|
||||
for (let j = 0; j < expectedStrings[i].regexes.length; j++) {
|
||||
if (message.match(expectedStrings[i].regexes[j])) {
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
if (matchCount == expectedStrings[i].regexes.length) {
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedIndex < 0) {
|
||||
return message;
|
||||
}
|
||||
//loop through the expectedStrings array and find the matching string
|
||||
let selectedIndex = -1;
|
||||
for (let i = 0; i < expectedStrings.length; i++) {
|
||||
let matchCount = 0;
|
||||
for (let j = 0; j < expectedStrings[i].regexes.length; j++) {
|
||||
if (message.match(expectedStrings[i].regexes[j])) {
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
if (matchCount == expectedStrings[i].regexes.length) {
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedIndex < 0) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const selectedReplace = expectedStrings[selectedIndex];
|
||||
return selectedReplace.output(message, sessionLangMap);
|
||||
const selectedReplace = expectedStrings[selectedIndex];
|
||||
return selectedReplace.output(message, sessionLangMap);
|
||||
};
|
||||
|
||||
const n = function (
|
||||
/** @type {{ numbers: { [x: string]: any; }; }} */ sessionLangMap,
|
||||
/** @type {string} */ inputString,
|
||||
/** @type {{ numbers: { [x: string]: any; }; }} */ sessionLangMap,
|
||||
/** @type {string} */ inputString
|
||||
) {
|
||||
const translations = sessionLangMap.numbers;
|
||||
const translations = sessionLangMap.numbers;
|
||||
|
||||
// @ts-ignore
|
||||
return inputString.replace(
|
||||
/\d/g,
|
||||
(/** @type {string | number} */ match) => translations[match] || match,
|
||||
);
|
||||
// @ts-ignore
|
||||
return inputString.replace(
|
||||
/\d/g,
|
||||
(/** @type {string | number} */ match) => translations[match] || match
|
||||
);
|
||||
};
|
||||
const ampm = function (
|
||||
/** @type {{ monitor: { [x: string]: any; }; }} */ sessionLangMap,
|
||||
/** @type {string} */ inputString,
|
||||
/** @type {{ monitor: { [x: string]: any; }; }} */ sessionLangMap,
|
||||
/** @type {string} */ inputString
|
||||
) {
|
||||
const translations = sessionLangMap.monitor;
|
||||
const translations = sessionLangMap.monitor;
|
||||
|
||||
// @ts-ignore
|
||||
let resp = inputString.replace(
|
||||
/(am|pm)/g,
|
||||
function (/** @type {string | number} */ match) {
|
||||
return translations[match] || match;
|
||||
},
|
||||
);
|
||||
// @ts-ignore
|
||||
let resp = inputString.replace(/(am|pm)/g, function (/** @type {string | number} */ match) {
|
||||
return translations[match] || match;
|
||||
});
|
||||
|
||||
return resp;
|
||||
return resp;
|
||||
};
|
||||
|
||||
export { l, summaryTime, n, ampm };
|
||||
|
||||
@@ -8,32 +8,30 @@ const langMap = {};
|
||||
|
||||
const files = fs.readdirSync("./locales");
|
||||
for (const file of files) {
|
||||
|
||||
if(!file.endsWith(".json")){
|
||||
if (!file.endsWith(".json")) {
|
||||
continue;
|
||||
}
|
||||
const lang = file.split(".")[0];
|
||||
const data = fs.readFileSync(`./locales/${file}`, "utf8");
|
||||
const lang = file.split(".")[0];
|
||||
const data = fs.readFileSync(`./locales/${file}`, "utf8");
|
||||
try {
|
||||
langMap[lang] = JSON.parse(data);
|
||||
} catch(err){
|
||||
} catch (err) {
|
||||
console.log(`Error parsing ${file}: ${err}`);
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* @param {{ [x: string]: any; }} language
|
||||
* @param {{ [x: string]: any; }} english
|
||||
*/
|
||||
function mergeEnglish(language, english) {
|
||||
for (let key in english) {
|
||||
if (language[key] === undefined) {
|
||||
language[key] = english[key];
|
||||
}
|
||||
if (typeof language[key] === "object") {
|
||||
mergeEnglish(language[key], english[key]);
|
||||
}
|
||||
}
|
||||
for (let key in english) {
|
||||
if (language[key] === undefined) {
|
||||
language[key] = english[key];
|
||||
}
|
||||
if (typeof language[key] === "object") {
|
||||
mergeEnglish(language[key], english[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const defaultLang = "en";
|
||||
|
||||
@@ -46,9 +44,8 @@ const init = (/** @type {string} */ lang) => {
|
||||
}
|
||||
|
||||
mergeEnglish(language, english);
|
||||
|
||||
|
||||
return language;
|
||||
return language;
|
||||
};
|
||||
|
||||
export default init;
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
import fs from "fs-extra";
|
||||
|
||||
const FetchData = async function (monitor) {
|
||||
return fs.readJsonSync(monitor.path90Day);
|
||||
return fs.readJsonSync(monitor.path90Day);
|
||||
};
|
||||
export { FetchData };
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
import fs from "fs-extra";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import { ParseUptime } from "$lib/helpers.js";
|
||||
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC, GetMinuteStartTimestampUTC } from "../../../scripts/tool.js";
|
||||
import {
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
GetMinuteStartTimestampUTC
|
||||
} from "../../../scripts/tool.js";
|
||||
import { GetStartTimeFromBody, GetEndTimeFromBody } from "../../../scripts/github.js";
|
||||
import Randomstring from "randomstring";
|
||||
const API_TOKEN = process.env.API_TOKEN;
|
||||
const API_IP = process.env.API_IP;
|
||||
|
||||
|
||||
const GetAllTags = function () {
|
||||
let tags = [];
|
||||
let monitors = [];
|
||||
@@ -21,184 +24,191 @@ const GetAllTags = function () {
|
||||
return tags;
|
||||
};
|
||||
const CheckIfValidTag = function (tag) {
|
||||
let tags = [];
|
||||
let monitors = [];
|
||||
try {
|
||||
monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
tags = monitors.map((monitor) => monitor.tag);
|
||||
if (tags.indexOf(tag) == -1) {
|
||||
throw new Error("not a valid tag");
|
||||
}
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
let tags = [];
|
||||
let monitors = [];
|
||||
try {
|
||||
monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
tags = monitors.map((monitor) => monitor.tag);
|
||||
if (tags.indexOf(tag) == -1) {
|
||||
throw new Error("not a valid tag");
|
||||
}
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auth = function (request) {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
const authToken = authHeader.replace("Bearer ", "");
|
||||
let ip = "";
|
||||
try {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
const authToken = authHeader.replace("Bearer ", "");
|
||||
let ip = "";
|
||||
try {
|
||||
//ip can be in x-forwarded-for or x-real-ip or remoteAddress
|
||||
if(request.headers.get("x-forwarded-for") !== null){
|
||||
if (request.headers.get("x-forwarded-for") !== null) {
|
||||
ip = request.headers.get("x-forwarded-for").split(",")[0];
|
||||
} else if(request.headers.get("x-real-ip") !== null){
|
||||
} else if (request.headers.get("x-real-ip") !== null) {
|
||||
ip = request.headers.get("x-real-ip");
|
||||
} else if (request.connection && request.connection.remoteAddress !== null) {
|
||||
ip = request.connection.remoteAddress;
|
||||
} else if (request.socket && request.socket.remoteAddress !== null) {
|
||||
ip = request.socket.remoteAddress;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("IP Not Found " + err.message);
|
||||
}
|
||||
if (authToken !== API_TOKEN) {
|
||||
return new Error("invalid token");
|
||||
}
|
||||
if (API_IP !== undefined && ip != "" && ip !== API_IP) {
|
||||
return new Error("invalid ip");
|
||||
}
|
||||
return null;
|
||||
ip = request.connection.remoteAddress;
|
||||
} else if (request.socket && request.socket.remoteAddress !== null) {
|
||||
ip = request.socket.remoteAddress;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("IP Not Found " + err.message);
|
||||
}
|
||||
if (authToken !== API_TOKEN) {
|
||||
return new Error("invalid token");
|
||||
}
|
||||
if (API_IP !== undefined && ip != "" && ip !== API_IP) {
|
||||
return new Error("invalid ip");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const store = function (data) {
|
||||
const tag = data.tag;
|
||||
//remove Bearer from start in authHeader
|
||||
const tag = data.tag;
|
||||
//remove Bearer from start in authHeader
|
||||
|
||||
const resp = {};
|
||||
if (data.status === undefined || ["UP", "DOWN", "DEGRADED"].indexOf(data.status) === -1) {
|
||||
return { error: "status missing", status: 400 };
|
||||
}
|
||||
if (data.latency === undefined || isNaN(data.latency)) {
|
||||
return { error: "latency missing or not a number", status: 400 };
|
||||
}
|
||||
if (data.timestampInSeconds !== undefined && isNaN(data.timestampInSeconds)) {
|
||||
return { error: "timestampInSeconds not a number", status: 400 };
|
||||
}
|
||||
if (data.timestampInSeconds === undefined) {
|
||||
data.timestampInSeconds = GetNowTimestampUTC();
|
||||
}
|
||||
data.timestampInSeconds = GetMinuteStartTimestampUTC(data.timestampInSeconds);
|
||||
resp.status = data.status;
|
||||
resp.latency = data.latency;
|
||||
resp.type = "webhook";
|
||||
let timestamp = GetMinuteStartNowTimestampUTC();
|
||||
try {
|
||||
//throw error if timestamp is future or older than 90days
|
||||
if (data.timestampInSeconds > timestamp) {
|
||||
throw new Error("timestampInSeconds is in future");
|
||||
}
|
||||
//past 90 days only
|
||||
if (timestamp - data.timestampInSeconds > 90 * 24 * 60 * 60) {
|
||||
throw new Error("timestampInSeconds is older than 90days");
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: err.message, status: 400 };
|
||||
}
|
||||
//check if tag is valid
|
||||
if (!CheckIfValidTag(tag)) {
|
||||
return { error: "invalid tag", status: 400 };
|
||||
}
|
||||
const resp = {};
|
||||
if (data.status === undefined || ["UP", "DOWN", "DEGRADED"].indexOf(data.status) === -1) {
|
||||
return { error: "status missing", status: 400 };
|
||||
}
|
||||
if (data.latency === undefined || isNaN(data.latency)) {
|
||||
return { error: "latency missing or not a number", status: 400 };
|
||||
}
|
||||
if (data.timestampInSeconds !== undefined && isNaN(data.timestampInSeconds)) {
|
||||
return { error: "timestampInSeconds not a number", status: 400 };
|
||||
}
|
||||
if (data.timestampInSeconds === undefined) {
|
||||
data.timestampInSeconds = GetNowTimestampUTC();
|
||||
}
|
||||
data.timestampInSeconds = GetMinuteStartTimestampUTC(data.timestampInSeconds);
|
||||
resp.status = data.status;
|
||||
resp.latency = data.latency;
|
||||
resp.type = "webhook";
|
||||
let timestamp = GetMinuteStartNowTimestampUTC();
|
||||
try {
|
||||
//throw error if timestamp is future or older than 90days
|
||||
if (data.timestampInSeconds > timestamp) {
|
||||
throw new Error("timestampInSeconds is in future");
|
||||
}
|
||||
//past 90 days only
|
||||
if (timestamp - data.timestampInSeconds > 90 * 24 * 60 * 60) {
|
||||
throw new Error("timestampInSeconds is older than 90days");
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: err.message, status: 400 };
|
||||
}
|
||||
//check if tag is valid
|
||||
if (!CheckIfValidTag(tag)) {
|
||||
return { error: "invalid tag", status: 400 };
|
||||
}
|
||||
|
||||
//get the monitor object matching the tag
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const monitor = monitors.find((monitor) => monitor.tag === tag);
|
||||
//get the monitor object matching the tag
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const monitor = monitors.find((monitor) => monitor.tag === tag);
|
||||
|
||||
//read the monitor.path0Day file
|
||||
let day0 = {};
|
||||
//read the monitor.path0Day file
|
||||
let day0 = {};
|
||||
|
||||
day0[data.timestampInSeconds] = resp;
|
||||
//sort the keys
|
||||
day0[data.timestampInSeconds] = resp;
|
||||
//sort the keys
|
||||
|
||||
//create a random string with high cardinlity
|
||||
//to avoid cache
|
||||
//create a random string with high cardinlity
|
||||
//to avoid cache
|
||||
|
||||
//write the monitor.path0Day file
|
||||
fs.writeFileSync(env.PUBLIC_KENER_FOLDER + `/${monitor.folderName}.webhook.${Randomstring.generate()}.json`, JSON.stringify(day0, null, 2));
|
||||
//write the monitor.path0Day file
|
||||
fs.writeFileSync(
|
||||
env.PUBLIC_KENER_FOLDER + `/${monitor.folderName}.webhook.${Randomstring.generate()}.json`,
|
||||
JSON.stringify(day0, null, 2)
|
||||
);
|
||||
|
||||
return { status: 200, message: "success at " + data.timestampInSeconds };
|
||||
return { status: 200, message: "success at " + data.timestampInSeconds };
|
||||
};
|
||||
const GHIssueToKenerIncident = function (issue) {
|
||||
let issueLabels = issue.labels.map((label) => {
|
||||
return label.name;
|
||||
});
|
||||
let tagsAvailable = GetAllTags();
|
||||
let issueLabels = issue.labels.map((label) => {
|
||||
return label.name;
|
||||
});
|
||||
let tagsAvailable = GetAllTags();
|
||||
|
||||
//get common tags as array
|
||||
let commonTags = tagsAvailable.filter((tag) => issueLabels.includes(tag));
|
||||
let commonTags = tagsAvailable.filter((tag) => issueLabels.includes(tag));
|
||||
|
||||
let resp = {
|
||||
createdAt: Math.floor(new Date(issue.created_at).getTime() / 1000), //in seconds
|
||||
closedAt: issue.closed_at ? Math.floor(new Date(issue.closed_at).getTime() / 1000) : null,
|
||||
title: issue.title,
|
||||
tags: commonTags,
|
||||
incidentNumber: issue.number,
|
||||
};
|
||||
resp.startDatetime = GetStartTimeFromBody(issue.body);
|
||||
resp.endDatetime = GetEndTimeFromBody(issue.body);
|
||||
let resp = {
|
||||
createdAt: Math.floor(new Date(issue.created_at).getTime() / 1000), //in seconds
|
||||
closedAt: issue.closed_at ? Math.floor(new Date(issue.closed_at).getTime() / 1000) : null,
|
||||
title: issue.title,
|
||||
tags: commonTags,
|
||||
incidentNumber: issue.number
|
||||
};
|
||||
resp.startDatetime = GetStartTimeFromBody(issue.body);
|
||||
resp.endDatetime = GetEndTimeFromBody(issue.body);
|
||||
|
||||
let body = issue.body;
|
||||
body = body.replace(/\[start_datetime:(\d+)\]/g, "");
|
||||
body = body.replace(/\[end_datetime:(\d+)\]/g, "");
|
||||
resp.body = body.trim();
|
||||
let body = issue.body;
|
||||
body = body.replace(/\[start_datetime:(\d+)\]/g, "");
|
||||
body = body.replace(/\[end_datetime:(\d+)\]/g, "");
|
||||
resp.body = body.trim();
|
||||
|
||||
resp.impact = null;
|
||||
if (issueLabels.includes("incident-down")) {
|
||||
resp.impact = "DOWN";
|
||||
} else if (issueLabels.includes("incident-degraded")) {
|
||||
resp.impact = "DEGRADED";
|
||||
}
|
||||
resp.isMaintenance = false;
|
||||
if (issueLabels.includes("maintenance")) {
|
||||
resp.isMaintenance = true;
|
||||
}
|
||||
resp.impact = null;
|
||||
if (issueLabels.includes("incident-down")) {
|
||||
resp.impact = "DOWN";
|
||||
} else if (issueLabels.includes("incident-degraded")) {
|
||||
resp.impact = "DEGRADED";
|
||||
}
|
||||
resp.isMaintenance = false;
|
||||
if (issueLabels.includes("maintenance")) {
|
||||
resp.isMaintenance = true;
|
||||
}
|
||||
resp.isIdentified = false;
|
||||
resp.isResolved = false;
|
||||
|
||||
if(issueLabels.includes("identified")){
|
||||
if (issueLabels.includes("identified")) {
|
||||
resp.isIdentified = true;
|
||||
}
|
||||
if (issueLabels.includes("resolved")){
|
||||
if (issueLabels.includes("resolved")) {
|
||||
resp.isResolved = true;
|
||||
}
|
||||
return resp;
|
||||
return resp;
|
||||
};
|
||||
const ParseIncidentPayload = function (payload) {
|
||||
let startDatetime = payload.startDatetime; //in utc seconds optional
|
||||
let endDatetime = payload.endDatetime; //in utc seconds optional
|
||||
let title = payload.title; //string required
|
||||
let body = payload.body || ""; //string optional
|
||||
let tags = payload.tags; //string and required
|
||||
let impact = payload.impact; //string and optional
|
||||
let isMaintenance = payload.isMaintenance; //boolean and optional
|
||||
let isIdentified = payload.isIdentified; //string and optional and if present can be resolved or identified
|
||||
let isResolved = payload.isResolved; //string and optional and if present can be resolved or identified
|
||||
let endDatetime = payload.endDatetime; //in utc seconds optional
|
||||
let title = payload.title; //string required
|
||||
let body = payload.body || ""; //string optional
|
||||
let tags = payload.tags; //string and required
|
||||
let impact = payload.impact; //string and optional
|
||||
let isMaintenance = payload.isMaintenance; //boolean and optional
|
||||
let isIdentified = payload.isIdentified; //string and optional and if present can be resolved or identified
|
||||
let isResolved = payload.isResolved; //string and optional and if present can be resolved or identified
|
||||
|
||||
// Perform validations
|
||||
// Perform validations
|
||||
|
||||
if (startDatetime && typeof startDatetime !== "number") {
|
||||
return { error: "Invalid startDatetime" };
|
||||
}
|
||||
if (endDatetime && (typeof endDatetime !== "number" || endDatetime <= startDatetime)) {
|
||||
return { error: "Invalid endDatetime" };
|
||||
}
|
||||
if (startDatetime && typeof startDatetime !== "number") {
|
||||
return { error: "Invalid startDatetime" };
|
||||
}
|
||||
if (endDatetime && (typeof endDatetime !== "number" || endDatetime <= startDatetime)) {
|
||||
return { error: "Invalid endDatetime" };
|
||||
}
|
||||
|
||||
if (!title || typeof title !== "string") {
|
||||
return { error: "Invalid title" };
|
||||
}
|
||||
if (!title || typeof title !== "string") {
|
||||
return { error: "Invalid title" };
|
||||
}
|
||||
//tags should be an array of string with atleast one element
|
||||
if (!tags || !Array.isArray(tags) || tags.length === 0 || tags.some((tag) => typeof tag !== "string")) {
|
||||
if (
|
||||
!tags ||
|
||||
!Array.isArray(tags) ||
|
||||
tags.length === 0 ||
|
||||
tags.some((tag) => typeof tag !== "string")
|
||||
) {
|
||||
return { error: "Invalid tags" };
|
||||
}
|
||||
|
||||
|
||||
// Optional validation for body and impact
|
||||
if (body && typeof body !== "string") {
|
||||
return { error: "Invalid body" };
|
||||
}
|
||||
// Optional validation for body and impact
|
||||
if (body && typeof body !== "string") {
|
||||
return { error: "Invalid body" };
|
||||
}
|
||||
|
||||
if (impact && (typeof impact !== "string" || ["DOWN", "DEGRADED"].indexOf(impact) === -1)) {
|
||||
return { error: "Invalid impact" };
|
||||
}
|
||||
if (impact && (typeof impact !== "string" || ["DOWN", "DEGRADED"].indexOf(impact) === -1)) {
|
||||
return { error: "Invalid impact" };
|
||||
}
|
||||
//check if tags are valid
|
||||
const allTags = GetAllTags();
|
||||
if (tags.some((tag) => allTags.indexOf(tag) === -1)) {
|
||||
@@ -213,58 +223,65 @@ const ParseIncidentPayload = function (payload) {
|
||||
tags.forEach((tag) => {
|
||||
githubLabels.push(tag);
|
||||
});
|
||||
if (impact) {
|
||||
githubLabels.push("incident-" + impact.toLowerCase());
|
||||
}
|
||||
if (isMaintenance) {
|
||||
githubLabels.push("maintenance");
|
||||
}
|
||||
if (isResolved !== undefined && isResolved === true) {
|
||||
githubLabels.push("resolved");
|
||||
}
|
||||
if (impact) {
|
||||
githubLabels.push("incident-" + impact.toLowerCase());
|
||||
}
|
||||
if (isMaintenance) {
|
||||
githubLabels.push("maintenance");
|
||||
}
|
||||
if (isResolved !== undefined && isResolved === true) {
|
||||
githubLabels.push("resolved");
|
||||
}
|
||||
if (isIdentified !== undefined && isIdentified === true) {
|
||||
githubLabels.push("identified");
|
||||
}
|
||||
githubLabels.push("identified");
|
||||
}
|
||||
|
||||
|
||||
if (startDatetime) body = body + " " + `[start_datetime:${startDatetime}]`;
|
||||
if (endDatetime) body = body + " " + `[end_datetime:${endDatetime}]`;
|
||||
if (startDatetime) body = body + " " + `[start_datetime:${startDatetime}]`;
|
||||
if (endDatetime) body = body + " " + `[end_datetime:${endDatetime}]`;
|
||||
|
||||
return { title, body, githubLabels };
|
||||
}
|
||||
const GetMonitorStatusByTag = function (tag) {
|
||||
if (!CheckIfValidTag(tag)) {
|
||||
return { error: "invalid tag", status: 400 };
|
||||
}
|
||||
const resp = {
|
||||
status: null,
|
||||
uptime: null,
|
||||
lastUpdatedAt: null,
|
||||
};
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const { path0Day } = monitors.find((monitor) => monitor.tag === tag);
|
||||
const dayData = JSON.parse(fs.readFileSync(path0Day, "utf8"));
|
||||
const lastUpdatedAt = Object.keys(dayData)[Object.keys(dayData).length - 1]
|
||||
const lastObj = dayData[lastUpdatedAt];
|
||||
resp.status = lastObj.status;
|
||||
//add all status up, degraded, down
|
||||
let ups = 0;
|
||||
let downs = 0;
|
||||
let degradeds = 0;
|
||||
|
||||
for (const timestamp in dayData) {
|
||||
const obj = dayData[timestamp];
|
||||
if (obj.status == "UP") {
|
||||
ups++;
|
||||
} else if (obj.status == "DEGRADED") {
|
||||
degradeds++;
|
||||
} else if (obj.status == "DOWN") {
|
||||
downs++;
|
||||
}
|
||||
}
|
||||
|
||||
resp.uptime = ParseUptime(ups + degradeds, ups + degradeds + downs) ;
|
||||
resp.lastUpdatedAt = Number(lastUpdatedAt);
|
||||
return { status: 200, ...resp };
|
||||
};
|
||||
export { store, auth, CheckIfValidTag, GHIssueToKenerIncident, ParseIncidentPayload, GetAllTags, GetMonitorStatusByTag };
|
||||
const GetMonitorStatusByTag = function (tag) {
|
||||
if (!CheckIfValidTag(tag)) {
|
||||
return { error: "invalid tag", status: 400 };
|
||||
}
|
||||
const resp = {
|
||||
status: null,
|
||||
uptime: null,
|
||||
lastUpdatedAt: null
|
||||
};
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const { path0Day } = monitors.find((monitor) => monitor.tag === tag);
|
||||
const dayData = JSON.parse(fs.readFileSync(path0Day, "utf8"));
|
||||
const lastUpdatedAt = Object.keys(dayData)[Object.keys(dayData).length - 1];
|
||||
const lastObj = dayData[lastUpdatedAt];
|
||||
resp.status = lastObj.status;
|
||||
//add all status up, degraded, down
|
||||
let ups = 0;
|
||||
let downs = 0;
|
||||
let degradeds = 0;
|
||||
|
||||
for (const timestamp in dayData) {
|
||||
const obj = dayData[timestamp];
|
||||
if (obj.status == "UP") {
|
||||
ups++;
|
||||
} else if (obj.status == "DEGRADED") {
|
||||
degradeds++;
|
||||
} else if (obj.status == "DOWN") {
|
||||
downs++;
|
||||
}
|
||||
}
|
||||
|
||||
resp.uptime = ParseUptime(ups + degradeds, ups + degradeds + downs);
|
||||
resp.lastUpdatedAt = Number(lastUpdatedAt);
|
||||
return { status: 200, ...resp };
|
||||
};
|
||||
export {
|
||||
store,
|
||||
auth,
|
||||
CheckIfValidTag,
|
||||
GHIssueToKenerIncident,
|
||||
ParseIncidentPayload,
|
||||
GetAllTags,
|
||||
GetMonitorStatusByTag
|
||||
};
|
||||
|
||||
@@ -4,59 +4,57 @@ import { cubicOut } from "svelte/easing";
|
||||
import type { TransitionConfig } from "svelte/transition";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
type FlyAndScaleParams = {
|
||||
y?: number;
|
||||
x?: number;
|
||||
start?: number;
|
||||
duration?: number;
|
||||
y?: number;
|
||||
x?: number;
|
||||
start?: number;
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
export const flyAndScale = (
|
||||
node: Element,
|
||||
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
|
||||
node: Element,
|
||||
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
|
||||
): TransitionConfig => {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === "none" ? "" : style.transform;
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === "none" ? "" : style.transform;
|
||||
|
||||
const scaleConversion = (
|
||||
valueA: number,
|
||||
scaleA: [number, number],
|
||||
scaleB: [number, number]
|
||||
) => {
|
||||
const [minA, maxA] = scaleA;
|
||||
const [minB, maxB] = scaleB;
|
||||
const scaleConversion = (
|
||||
valueA: number,
|
||||
scaleA: [number, number],
|
||||
scaleB: [number, number]
|
||||
) => {
|
||||
const [minA, maxA] = scaleA;
|
||||
const [minB, maxB] = scaleB;
|
||||
|
||||
const percentage = (valueA - minA) / (maxA - minA);
|
||||
const valueB = percentage * (maxB - minB) + minB;
|
||||
const percentage = (valueA - minA) / (maxA - minA);
|
||||
const valueB = percentage * (maxB - minB) + minB;
|
||||
|
||||
return valueB;
|
||||
};
|
||||
return valueB;
|
||||
};
|
||||
|
||||
const styleToString = (
|
||||
style: Record<string, number | string | undefined>
|
||||
): string => {
|
||||
return Object.keys(style).reduce((str, key) => {
|
||||
if (style[key] === undefined) return str;
|
||||
return str + `${key}:${style[key]};`;
|
||||
}, "");
|
||||
};
|
||||
const styleToString = (style: Record<string, number | string | undefined>): string => {
|
||||
return Object.keys(style).reduce((str, key) => {
|
||||
if (style[key] === undefined) return str;
|
||||
return str + `${key}:${style[key]};`;
|
||||
}, "");
|
||||
};
|
||||
|
||||
return {
|
||||
duration: params.duration ?? 200,
|
||||
delay: 0,
|
||||
css: (t) => {
|
||||
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
||||
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
||||
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
||||
return {
|
||||
duration: params.duration ?? 200,
|
||||
delay: 0,
|
||||
css: (t) => {
|
||||
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
||||
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
||||
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
||||
|
||||
return styleToString({
|
||||
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||
opacity: t
|
||||
});
|
||||
},
|
||||
easing: cubicOut
|
||||
};
|
||||
};
|
||||
return styleToString({
|
||||
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||
opacity: t
|
||||
});
|
||||
},
|
||||
easing: cubicOut
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import fs from "fs-extra";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import i18n from "$lib/i18n/server";
|
||||
import i18n from "$lib/i18n/server";
|
||||
|
||||
export async function load({ params, route, url, cookies, request }) {
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
const headers = request.headers;
|
||||
const userAgent = headers.get("user-agent");
|
||||
let localTz = "GMT";
|
||||
const localTzCookie = cookies.get("localTz");
|
||||
let localTz = "GMT";
|
||||
const localTzCookie = cookies.get("localTz");
|
||||
if (!!localTzCookie) {
|
||||
localTz = localTzCookie;
|
||||
}
|
||||
let showNav = true
|
||||
if(url.pathname.startsWith('/embed')) {
|
||||
showNav = false
|
||||
localTz = localTzCookie;
|
||||
}
|
||||
let showNav = true;
|
||||
if (url.pathname.startsWith("/embed")) {
|
||||
showNav = false;
|
||||
}
|
||||
// if the user agent is lighthouse, then we are running a lighthouse test
|
||||
//if bot also set localTz to -1 to avoid reload
|
||||
let isBot = false
|
||||
let isBot = false;
|
||||
if (userAgent?.includes("Chrome-Lighthouse") || userAgent?.includes("bot")) {
|
||||
isBot = true;
|
||||
}
|
||||
}
|
||||
|
||||
//load all files from lib locales folder
|
||||
let selectedLang = "en";
|
||||
const localLangCookie = cookies.get("localLang");
|
||||
if (!!localLangCookie && site.i18n?.locales[localLangCookie]) {
|
||||
selectedLang = localLangCookie;
|
||||
} else if (site.i18n?.defaultLocale && site.i18n?.locales[site.i18n.defaultLocale]) {
|
||||
selectedLang = site.i18n.defaultLocale;
|
||||
}
|
||||
return {
|
||||
site: site,
|
||||
localTz: localTz,
|
||||
showNav,
|
||||
isBot,
|
||||
lang: i18n(String(selectedLang)),
|
||||
selectedLang: selectedLang,
|
||||
};
|
||||
selectedLang = localLangCookie;
|
||||
} else if (site.i18n?.defaultLocale && site.i18n?.locales[site.i18n.defaultLocale]) {
|
||||
selectedLang = site.i18n.defaultLocale;
|
||||
}
|
||||
return {
|
||||
site: site,
|
||||
localTz: localTz,
|
||||
showNav,
|
||||
isBot,
|
||||
lang: i18n(String(selectedLang)),
|
||||
selectedLang: selectedLang
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
<script>
|
||||
import "../app.postcss";
|
||||
import "../kener.css";
|
||||
import Nav from "$lib/components/nav.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from '$app/paths';
|
||||
|
||||
|
||||
export let data;
|
||||
import "../app.postcss";
|
||||
import "../kener.css";
|
||||
import Nav from "$lib/components/nav.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { base } from "$app/paths";
|
||||
|
||||
export let data;
|
||||
|
||||
onMount(() => {
|
||||
|
||||
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (localTz != data.localTz) {
|
||||
|
||||
if(data.isBot === false) {
|
||||
onMount(() => {
|
||||
let localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (localTz != data.localTz) {
|
||||
if (data.isBot === false) {
|
||||
document.cookie = "localTz=" + localTz + ";max-age=" + 60 * 60 * 24 * 365 * 30;
|
||||
location.reload();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if data.showNav}
|
||||
<Nav {data} />
|
||||
<Nav {data} />
|
||||
{/if}
|
||||
<svelte:head>
|
||||
<title>{data.site.title}</title>
|
||||
<title>{data.site.title}</title>
|
||||
<link rel="icon" id="kener-app-favicon" href="{base}/logo96.png" />
|
||||
{#each Object.entries(data.site.metaTags) as [key, value]}
|
||||
<meta name="{key}" content="{value}" />
|
||||
{/each}
|
||||
{#each Object.entries(data.site.metaTags) as [key, value]}
|
||||
<meta name={key} content={value} />
|
||||
{/each}
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
{#if data.showNav && !!data.site.footerHTML}
|
||||
<footer class="py-6 z-10 md:px-8 md:py-0">
|
||||
<div class="container relative flex flex-col pl-0 items-center justify-center max-w-[890px] gap-4 md:h-24 md:flex-row">
|
||||
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
|
||||
<p class="text-center text-sm leading-loose text-muted-foreground md:text-left">
|
||||
{@html data.site.footerHTML}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<footer class="z-10 py-6 md:px-8 md:py-0">
|
||||
<div
|
||||
class="container relative flex max-w-[890px] flex-col items-center justify-center gap-4 pl-0 md:h-24 md:flex-row"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
|
||||
<p class="text-center text-sm leading-loose text-muted-foreground md:text-left">
|
||||
{@html data.site.footerHTML}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
{/if}
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
// @ts-nocheck
|
||||
import { Mapper, GetOpenIncidents, FilterAndInsertMonitorInIncident } from "../../scripts/github.js";
|
||||
import {
|
||||
Mapper,
|
||||
GetOpenIncidents,
|
||||
FilterAndInsertMonitorInIncident
|
||||
} from "../../scripts/github.js";
|
||||
import { FetchData } from "$lib/server/page";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import fs from "fs-extra";
|
||||
|
||||
export async function load({ parent }) {
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
const github = siteData.github;
|
||||
const monitorsActive = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
//skip hidden monitors
|
||||
if (monitors[i].hidden !== undefined && monitors[i].hidden === true) {
|
||||
continue;
|
||||
}
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (monitors[i].category !== undefined && monitors[i].category !== "home") {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].defaultStatus;
|
||||
let data = await FetchData(monitors[i], parentData.localTz);
|
||||
monitors[i].pageData = data;
|
||||
monitors[i].activeIncidents = [];
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
let openIncidents = await GetOpenIncidents(github);
|
||||
let openIncidentsReduced = openIncidents.map(Mapper);
|
||||
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
const github = siteData.github;
|
||||
const monitorsActive = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
//skip hidden monitors
|
||||
if (monitors[i].hidden !== undefined && monitors[i].hidden === true) {
|
||||
continue;
|
||||
}
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (monitors[i].category !== undefined && monitors[i].category !== "home") {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].defaultStatus;
|
||||
let data = await FetchData(monitors[i], parentData.localTz);
|
||||
monitors[i].pageData = data;
|
||||
monitors[i].activeIncidents = [];
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
let openIncidents = await GetOpenIncidents(github);
|
||||
let openIncidentsReduced = openIncidents.map(Mapper);
|
||||
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
openIncidents: FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive),
|
||||
};
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
openIncidents: FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,99 +1,140 @@
|
||||
<script>
|
||||
import Monitor from "$lib/components/monitor.svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button, buttonVariants } from "$lib/components/ui/button";
|
||||
import Incident from "$lib/components/incident.svelte";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import Monitor from "$lib/components/monitor.svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button, buttonVariants } from "$lib/components/ui/button";
|
||||
import Incident from "$lib/components/incident.svelte";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { l } from "$lib/i18n/client";
|
||||
import { base } from '$app/paths';
|
||||
import { base } from "$app/paths";
|
||||
|
||||
export let data;
|
||||
let hasActiveIncidents = data.openIncidents.length > 0;
|
||||
|
||||
export let data;
|
||||
let hasActiveIncidents = data.openIncidents.length > 0;
|
||||
</script>
|
||||
|
||||
<div class="mt-32"></div>
|
||||
{#if data.site.hero}
|
||||
<section class="mx-auto mb-8 flex w-full max-w-4xl flex-1 flex-col items-start justify-center">
|
||||
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
|
||||
<div class="blurry-bg mx-auto max-w-3xl text-center">
|
||||
{#if data.site.hero.image}
|
||||
<img src="{data.site.hero.image}" class="m-auto h-16 w-16" alt="" srcset="" />
|
||||
{/if} {#if data.site.hero.title}
|
||||
<h1 class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-snug text-transparent">{data.site.hero.title}</h1>
|
||||
{/if} {#if data.site.hero.subtitle}
|
||||
<p class="mx-auto mt-4 max-w-xl sm:text-xl">{data.site.hero.subtitle}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if} {#if hasActiveIncidents}
|
||||
<section class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent" id="">
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-left">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, 'root.ongoing_incidents')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]" id="">
|
||||
{#each data.openIncidents as incident, i}
|
||||
<Incident {incident} state="close" variant="title+body+comments+monitor" monitor="{incident.monitor}" lang="{data.lang}" />
|
||||
{/each}
|
||||
</section>
|
||||
{/if} {#if data.monitors.length > 0}
|
||||
<section class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent" id="">
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-left" >
|
||||
<Badge class="" variant="outline">
|
||||
{l(data.lang, 'root.availability_per_component')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-right">
|
||||
<Badge variant="outline">
|
||||
<span class="bg-api-up mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, 'statuses.UP')}
|
||||
</span>
|
||||
|
||||
<span class="bg-api-degraded mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, 'statuses.DEGRADED')}
|
||||
</span>
|
||||
|
||||
<span class="bg-api-down mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, 'statuses.DOWN')}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]">
|
||||
<Card.Root>
|
||||
<Card.Content class="monitors-card p-0">
|
||||
{#each data.monitors as monitor}
|
||||
<Monitor {monitor} localTz="{data.localTz}" lang="{data.lang}" />
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{/if} {#if data.site.categories}
|
||||
<section class="mx-auto mb-8 w-full max-w-[890px] flex-1 flex-col items-start backdrop-blur-[2px]">
|
||||
<h2 class="mb-2 mt-2 px-2 text-xl font-semibold">
|
||||
{l(data.lang, 'root.other_monitors')}
|
||||
</h2>
|
||||
{#each data.site.categories as category}
|
||||
<Card.Root class="mb-2 w-full">
|
||||
<Card.Header>
|
||||
<Card.Title>{category.name}</Card.Title>
|
||||
<Card.Description class="relative pr-[100px]">
|
||||
{#if category.description} {category.description} {/if}
|
||||
<a href="{base}/category-{category.name}" class="{buttonVariants({ variant: 'secondary', })} absolute -top-4 right-2"> View </a>
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
</section>
|
||||
<section class="mx-auto mb-8 flex w-full max-w-4xl flex-1 flex-col items-start justify-center">
|
||||
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
|
||||
<div class="blurry-bg mx-auto max-w-3xl text-center">
|
||||
{#if data.site.hero.image}
|
||||
<img src={data.site.hero.image} class="m-auto h-16 w-16" alt="" srcset="" />
|
||||
{/if}
|
||||
{#if data.site.hero.title}
|
||||
<h1
|
||||
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-snug text-transparent"
|
||||
>
|
||||
{data.site.hero.title}
|
||||
</h1>
|
||||
{/if}
|
||||
{#if data.site.hero.subtitle}
|
||||
<p class="mx-auto mt-4 max-w-xl sm:text-xl">{data.site.hero.subtitle}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{#if hasActiveIncidents}
|
||||
<section
|
||||
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
|
||||
id=""
|
||||
>
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-left">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, "root.ongoing_incidents")}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
|
||||
id=""
|
||||
>
|
||||
{#each data.openIncidents as incident, i}
|
||||
<Incident
|
||||
{incident}
|
||||
state="close"
|
||||
variant="title+body+comments+monitor"
|
||||
monitor={incident.monitor}
|
||||
lang={data.lang}
|
||||
/>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{#if data.monitors.length > 0}
|
||||
<section
|
||||
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
|
||||
id=""
|
||||
>
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-left">
|
||||
<Badge class="" variant="outline">
|
||||
{l(data.lang, "root.availability_per_component")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-right">
|
||||
<Badge variant="outline">
|
||||
<span class="bg-api-up mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "statuses.UP")}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bg-api-degraded mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "statuses.DEGRADED")}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bg-api-down mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "statuses.DOWN")}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
|
||||
>
|
||||
<Card.Root>
|
||||
<Card.Content class="monitors-card p-0">
|
||||
{#each data.monitors as monitor}
|
||||
<Monitor {monitor} localTz={data.localTz} lang={data.lang} />
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{/if}
|
||||
{#if data.site.categories}
|
||||
<section
|
||||
class="mx-auto mb-8 w-full max-w-[890px] flex-1 flex-col items-start backdrop-blur-[2px]"
|
||||
>
|
||||
<h2 class="mb-2 mt-2 px-2 text-xl font-semibold">
|
||||
{l(data.lang, "root.other_monitors")}
|
||||
</h2>
|
||||
{#each data.site.categories as category}
|
||||
<Card.Root class="mb-2 w-full">
|
||||
<Card.Header>
|
||||
<Card.Title>{category.name}</Card.Title>
|
||||
<Card.Description class="relative pr-[100px]">
|
||||
{#if category.description}
|
||||
{category.description}
|
||||
{/if}
|
||||
<a
|
||||
href="{base}/category-{category.name}"
|
||||
class="{buttonVariants({
|
||||
variant: 'secondary'
|
||||
})} absolute -top-4 right-2"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -7,56 +7,55 @@ import { env } from "$env/dynamic/public";
|
||||
import fs from "fs-extra";
|
||||
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
const payload = await request.json();
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let { title, body, githubLabels, error } = ParseIncidentPayload(payload);
|
||||
let { title, body, githubLabels, error } = ParseIncidentPayload(payload);
|
||||
if (error) {
|
||||
return json(
|
||||
{ error },
|
||||
{
|
||||
status: 400,
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
let resp = await CreateIssue(github, title, body, githubLabels);
|
||||
if (resp === null) {
|
||||
|
||||
return json(
|
||||
{ error: "github error" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return json(GHIssueToKenerIncident(resp), {
|
||||
status: 200,
|
||||
});
|
||||
let github = site.github;
|
||||
let resp = await CreateIssue(github, title, body, githubLabels);
|
||||
if (resp === null) {
|
||||
return json(
|
||||
{ error: "github error" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return json(GHIssueToKenerIncident(resp), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET({ request, url }) {
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const query = url.searchParams;
|
||||
const query = url.searchParams;
|
||||
|
||||
const state = query.get("state") || "open";
|
||||
const tags = query.get("tags") || ""; //comma separated list of tags
|
||||
@@ -65,51 +64,50 @@ export async function GET({ request, url }) {
|
||||
const createdAfter = query.get("created_after_utc") || "";
|
||||
const createdBefore = query.get("created_before_utc") || "";
|
||||
const titleLike = query.get("title_like") || "";
|
||||
|
||||
|
||||
|
||||
//if state is not open or closed, return 400
|
||||
if (state !== "open" && state !== "closed") {
|
||||
return json(
|
||||
{ error: "state must be open or closed" },
|
||||
{
|
||||
status: 400,
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
let site = JSON.parse(
|
||||
fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8")
|
||||
);
|
||||
let github = site.github;
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
const repo = `${github.owner}/${github.repo}`;
|
||||
const is = "issue";
|
||||
|
||||
|
||||
const filterArray = [
|
||||
`repo:${repo}`,
|
||||
`is:${is}`,
|
||||
`state:${state}`,
|
||||
`label:incident`,
|
||||
`sort:created-desc`,
|
||||
`label:${tags.split(",").map((tag) => tag.trim()).join(",")}`
|
||||
|
||||
];
|
||||
`repo:${repo}`,
|
||||
`is:${is}`,
|
||||
`state:${state}`,
|
||||
`label:incident`,
|
||||
`sort:created-desc`,
|
||||
`label:${tags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.join(",")}`
|
||||
];
|
||||
//if createdAfter and createdBefore are both set, use the range filter
|
||||
if (createdBefore && createdAfter) {
|
||||
let dateFilter = "";
|
||||
let iso = new Date(createdAfter * 1000).toISOString();
|
||||
dateFilter += `created:${iso}`;
|
||||
iso = new Date(createdBefore * 1000).toISOString();
|
||||
dateFilter += `..${iso}`;
|
||||
dateFilter += `created:${iso}`;
|
||||
iso = new Date(createdBefore * 1000).toISOString();
|
||||
dateFilter += `..${iso}`;
|
||||
filterArray.push(dateFilter);
|
||||
} else if(createdAfter){ //if only createdAfter is set, use the greater than or equal to filter
|
||||
} else if (createdAfter) {
|
||||
//if only createdAfter is set, use the greater than or equal to filter
|
||||
let iso = new Date(createdAfter * 1000).toISOString();
|
||||
filterArray.push(`created:>=${iso}`);
|
||||
} else if(createdBefore){//if only createdBefore is set, use the less than or equal to filter
|
||||
} else if (createdBefore) {
|
||||
//if only createdBefore is set, use the less than or equal to filter
|
||||
let iso = new Date(createdBefore * 1000).toISOString();
|
||||
filterArray.push(`created:<=${iso}`);
|
||||
}
|
||||
if(titleLike){
|
||||
if (titleLike) {
|
||||
filterArray.unshift(`${titleLike} in:title`);
|
||||
}
|
||||
|
||||
@@ -117,13 +115,7 @@ export async function GET({ request, url }) {
|
||||
|
||||
const incidents = resp.items.map((issue) => GHIssueToKenerIncident(issue));
|
||||
|
||||
return json(
|
||||
incidents,
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
return json(incidents, {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,40 +2,45 @@
|
||||
// @ts-ignore
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { auth, ParseIncidentPayload, GHIssueToKenerIncident } from "$lib/server/webhook";
|
||||
import { GetIncidentByNumber, GetStartTimeFromBody, GetEndTimeFromBody, UpdateIssue } from "../../../../../scripts/github";
|
||||
import {
|
||||
GetIncidentByNumber,
|
||||
GetStartTimeFromBody,
|
||||
GetEndTimeFromBody,
|
||||
UpdateIssue
|
||||
} from "../../../../../scripts/github";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import fs from "fs-extra";
|
||||
|
||||
export async function PATCH({ request, params }) {
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401
|
||||
}
|
||||
);
|
||||
}
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
const payload = await request.json();
|
||||
if (!incidentNumber || isNaN(incidentNumber)) {
|
||||
return json(
|
||||
{ error: "Invalid incidentNumber" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(
|
||||
{ error: "Invalid incidentNumber" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
let { title, body, githubLabels, error } = ParseIncidentPayload(payload);
|
||||
if (error) {
|
||||
return json(
|
||||
{ error },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return json(
|
||||
{ error },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
let resp = await UpdateIssue(github, incidentNumber, title, body, githubLabels);
|
||||
@@ -43,42 +48,41 @@ export async function PATCH({ request, params }) {
|
||||
return json(
|
||||
{ error: "github error" },
|
||||
{
|
||||
status: 400,
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(GHIssueToKenerIncident(resp), {
|
||||
status: 200,
|
||||
});
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
// const headers = await request.headers();
|
||||
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
let issue = await GetIncidentByNumber(github, incidentNumber);
|
||||
if(issue === null){
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: "incident not found" },
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 404,
|
||||
status: 401
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
// const headers = await request.headers();
|
||||
|
||||
return json(GHIssueToKenerIncident(issue), {
|
||||
status: 200,
|
||||
});
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
let issue = await GetIncidentByNumber(github, incidentNumber);
|
||||
if (issue === null) {
|
||||
return json(
|
||||
{ error: "incident not found" },
|
||||
{
|
||||
status: 404
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return json(GHIssueToKenerIncident(issue), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,95 +8,93 @@ import fs from "fs-extra";
|
||||
|
||||
export async function GET({ request, params }) {
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401
|
||||
}
|
||||
);
|
||||
}
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
// Perform validations
|
||||
if (!incidentNumber || isNaN(incidentNumber)) {
|
||||
return json(
|
||||
{ error: "Invalid incidentNumber" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
// Perform validations
|
||||
if (!incidentNumber || isNaN(incidentNumber)) {
|
||||
return json(
|
||||
{ error: "Invalid incidentNumber" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
let resp = await GetCommentsForIssue(incidentNumber, github);
|
||||
return json(
|
||||
resp.map((comment) => {
|
||||
return {
|
||||
commentID: comment.id,
|
||||
body: comment.body,
|
||||
createdAt: Math.floor(new Date(comment.created_at).getTime() / 1000),
|
||||
};
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
resp.map((comment) => {
|
||||
return {
|
||||
commentID: comment.id,
|
||||
body: comment.body,
|
||||
createdAt: Math.floor(new Date(comment.created_at).getTime() / 1000)
|
||||
};
|
||||
}),
|
||||
{
|
||||
status: 200
|
||||
}
|
||||
);
|
||||
}
|
||||
export async function POST({ request, params }) {
|
||||
|
||||
|
||||
// const headers = await request.headers();
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
// Perform validations
|
||||
if (!incidentNumber || isNaN(incidentNumber)) {
|
||||
return json(
|
||||
{ error: "Invalid incidentNumber" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
// const headers = await request.headers();
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401
|
||||
}
|
||||
);
|
||||
}
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
// Perform validations
|
||||
if (!incidentNumber || isNaN(incidentNumber)) {
|
||||
return json(
|
||||
{ error: "Invalid incidentNumber" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
const payload = await request.json();
|
||||
let body = payload.body; //string required
|
||||
if (!body || typeof body !== "string") {
|
||||
return json(
|
||||
{ error: "Invalid body" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
let body = payload.body; //string required
|
||||
if (!body || typeof body !== "string") {
|
||||
return json(
|
||||
{ error: "Invalid body" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
|
||||
let resp = await AddComment(github, incidentNumber, body);
|
||||
if (resp === null) {
|
||||
return json(
|
||||
{ error: "github error" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(
|
||||
{
|
||||
commentID: resp.id,
|
||||
body: resp.body,
|
||||
createdAt: Math.floor(new Date(resp.created_at).getTime() / 1000),
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
}
|
||||
let resp = await AddComment(github, incidentNumber, body);
|
||||
if (resp === null) {
|
||||
return json(
|
||||
{ error: "github error" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(
|
||||
{
|
||||
commentID: resp.id,
|
||||
body: resp.body,
|
||||
createdAt: Math.floor(new Date(resp.created_at).getTime() / 1000)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,87 +7,87 @@ import { env } from "$env/dynamic/public";
|
||||
import fs from "fs-extra";
|
||||
|
||||
export async function POST({ request, params }) {
|
||||
const payload = await request.json();
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
// const headers = await request.headers();
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
const payload = await request.json();
|
||||
const incidentNumber = params.incidentNumber; //number required
|
||||
// const headers = await request.headers();
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let isIdentified = payload.isIdentified; //string required and can be resolved or identified
|
||||
let isResolved = payload.isResolved; //string required and can be resolved or identified
|
||||
let endDatetime = payload.endDatetime; //in utc seconds optional
|
||||
let isIdentified = payload.isIdentified; //string required and can be resolved or identified
|
||||
let isResolved = payload.isResolved; //string required and can be resolved or identified
|
||||
let endDatetime = payload.endDatetime; //in utc seconds optional
|
||||
|
||||
// Perform validations
|
||||
if (!incidentNumber || isNaN(incidentNumber)) {
|
||||
return json(
|
||||
{ error: "Invalid incidentNumber" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
// Perform validations
|
||||
if (!incidentNumber || isNaN(incidentNumber)) {
|
||||
return json(
|
||||
{ error: "Invalid incidentNumber" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (endDatetime && typeof endDatetime !== "number") {
|
||||
return json(
|
||||
{ error: "Invalid endDatetime" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (endDatetime && typeof endDatetime !== "number") {
|
||||
return json(
|
||||
{ error: "Invalid endDatetime" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let github = site.github;
|
||||
|
||||
let issue = await GetIncidentByNumber(github, incidentNumber);
|
||||
if (issue === null) {
|
||||
return json(
|
||||
{ error: "github error" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
let labels = issue.labels.map((label) => {
|
||||
let issue = await GetIncidentByNumber(github, incidentNumber);
|
||||
if (issue === null) {
|
||||
return json(
|
||||
{ error: "github error" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
let labels = issue.labels.map((label) => {
|
||||
return label.name;
|
||||
});
|
||||
if(isIdentified !== undefined) {
|
||||
if (isIdentified !== undefined) {
|
||||
labels = labels.filter((label) => label !== "identified");
|
||||
if(isIdentified === true){
|
||||
if (isIdentified === true) {
|
||||
labels.push("identified");
|
||||
}
|
||||
}
|
||||
if(isResolved !== undefined) {
|
||||
if (isResolved !== undefined) {
|
||||
labels = labels.filter((label) => label !== "resolved");
|
||||
if(isResolved === true){
|
||||
if (isResolved === true) {
|
||||
labels.push("resolved");
|
||||
}
|
||||
}
|
||||
|
||||
let body = issue.body;
|
||||
|
||||
let body = issue.body;
|
||||
if (endDatetime) {
|
||||
body = body.replace(/\[end_datetime:(\d+)\]/g, "");
|
||||
body = body.trim();
|
||||
body = body.trim();
|
||||
body = body + " " + `[end_datetime:${endDatetime}]`;
|
||||
};
|
||||
}
|
||||
|
||||
let resp = await UpdateIssueLabels(github, incidentNumber, labels, body);
|
||||
if (resp === null) {
|
||||
return json(
|
||||
{ error: "github error" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(GHIssueToKenerIncident(resp), {
|
||||
status: 200,
|
||||
});
|
||||
let resp = await UpdateIssueLabels(github, incidentNumber, labels, body);
|
||||
if (resp === null) {
|
||||
return json(
|
||||
{ error: "github error" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(GHIssueToKenerIncident(resp), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,42 +3,42 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { store, auth, GetMonitorStatusByTag } from "$lib/server/webhook";
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
let resp = store(payload);
|
||||
return json(resp, {
|
||||
status: resp.status,
|
||||
});
|
||||
const payload = await request.json();
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401
|
||||
}
|
||||
);
|
||||
}
|
||||
let resp = store(payload);
|
||||
return json(resp, {
|
||||
status: resp.status
|
||||
});
|
||||
}
|
||||
export async function GET({ request, url }) {
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
const query = url.searchParams;
|
||||
const tag = query.get("tag");
|
||||
if (!!!tag) {
|
||||
return json(
|
||||
{ error: "tag missing" },
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(GetMonitorStatusByTag(tag), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
const authError = auth(request);
|
||||
if (authError !== null) {
|
||||
return json(
|
||||
{ error: authError.message },
|
||||
{
|
||||
status: 401
|
||||
}
|
||||
);
|
||||
}
|
||||
const query = url.searchParams;
|
||||
const tag = query.get("tag");
|
||||
if (!!!tag) {
|
||||
return json(
|
||||
{ error: "tag missing" },
|
||||
{
|
||||
status: 400
|
||||
}
|
||||
);
|
||||
}
|
||||
return json(GetMonitorStatusByTag(tag), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,37 +5,35 @@ import { json } from "@sveltejs/kit";
|
||||
import { GetMinuteStartNowTimestampUTC, BeginningOfDay } from "../../../../scripts/tool.js";
|
||||
import { StatusObj } from "$lib/helpers.js";
|
||||
|
||||
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
const monitor = payload.monitor;
|
||||
const localTz = payload.localTz;
|
||||
let _0Day = {};
|
||||
let _0Day = {};
|
||||
|
||||
const now = GetMinuteStartNowTimestampUTC();
|
||||
const midnight = BeginningOfDay({ timeZone: localTz });
|
||||
const now = GetMinuteStartNowTimestampUTC();
|
||||
const midnight = BeginningOfDay({ timeZone: localTz });
|
||||
|
||||
for (let i = midnight; i <= now; i += 60) {
|
||||
_0Day[i] = {
|
||||
timestamp: i,
|
||||
status: "NO_DATA",
|
||||
cssClass: StatusObj.NO_DATA,
|
||||
index: (i - midnight) / 60,
|
||||
};
|
||||
}
|
||||
for (let i = midnight; i <= now; i += 60) {
|
||||
_0Day[i] = {
|
||||
timestamp: i,
|
||||
status: "NO_DATA",
|
||||
cssClass: StatusObj.NO_DATA,
|
||||
index: (i - midnight) / 60
|
||||
};
|
||||
}
|
||||
|
||||
let day0 = JSON.parse(fs.readFileSync(monitor.path0Day, "utf8"));
|
||||
let day0 = JSON.parse(fs.readFileSync(monitor.path0Day, "utf8"));
|
||||
|
||||
for (const timestamp in day0) {
|
||||
const element = day0[timestamp];
|
||||
let status = element.status;
|
||||
//0 Day data
|
||||
if (_0Day[timestamp] !== undefined) {
|
||||
_0Day[timestamp].status = status;
|
||||
_0Day[timestamp].cssClass = StatusObj[status];
|
||||
for (const timestamp in day0) {
|
||||
const element = day0[timestamp];
|
||||
let status = element.status;
|
||||
//0 Day data
|
||||
if (_0Day[timestamp] !== undefined) {
|
||||
_0Day[timestamp].status = status;
|
||||
_0Day[timestamp].cssClass = StatusObj[status];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return json(_0Day);
|
||||
};
|
||||
return json(_0Day);
|
||||
}
|
||||
|
||||
@@ -5,29 +5,29 @@ import { StatusColor } from "$lib/helpers.js";
|
||||
import { makeBadge } from "badge-maker";
|
||||
const monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
export async function GET({ params, setHeaders, url }) {
|
||||
// @ts-ignore
|
||||
const { path0Day, name } = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
const dayData = JSON.parse(fs.readFileSync(path0Day, "utf8"));
|
||||
const lastObj = dayData[Object.keys(dayData)[Object.keys(dayData).length - 1]];
|
||||
// @ts-ignore
|
||||
const { path0Day, name } = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
const dayData = JSON.parse(fs.readFileSync(path0Day, "utf8"));
|
||||
const lastObj = dayData[Object.keys(dayData)[Object.keys(dayData).length - 1]];
|
||||
|
||||
//read query params
|
||||
const query = url.searchParams;
|
||||
const labelColor = query.get("labelColor") || "#333";
|
||||
const color = query.get("color") || StatusColor[lastObj.status];
|
||||
const style = query.get("style") || "flat";
|
||||
//read query params
|
||||
const query = url.searchParams;
|
||||
const labelColor = query.get("labelColor") || "#333";
|
||||
const color = query.get("color") || StatusColor[lastObj.status];
|
||||
const style = query.get("style") || "flat";
|
||||
|
||||
const format = {
|
||||
label: name,
|
||||
message: lastObj.status,
|
||||
color: color,
|
||||
labelColor: labelColor,
|
||||
style: style,
|
||||
};
|
||||
const svg = makeBadge(format);
|
||||
const format = {
|
||||
label: name,
|
||||
message: lastObj.status,
|
||||
color: color,
|
||||
labelColor: labelColor,
|
||||
style: style
|
||||
};
|
||||
const svg = makeBadge(format);
|
||||
|
||||
return new Response(svg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
},
|
||||
});
|
||||
return new Response(svg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,19 +6,18 @@ import { makeBadge } from "badge-maker";
|
||||
|
||||
const monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
export async function GET({ params, url }) {
|
||||
// @ts-ignore
|
||||
const { path0Day, name } = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
// @ts-ignore
|
||||
const { path0Day, name } = monitors.find((monitor) => monitor.tag === params.tag);
|
||||
const dayData = JSON.parse(fs.readFileSync(path0Day, "utf8"));
|
||||
const query = url.searchParams;
|
||||
const rangeInSeconds = query.get("sinceLast") || 90 * 24 * 60 * 60;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const since = now - rangeInSeconds;
|
||||
|
||||
|
||||
//add all status up, degraded, down
|
||||
let ups = 0;
|
||||
let downs = 0;
|
||||
let degradeds = 0;
|
||||
|
||||
|
||||
for (const timestamp in dayData) {
|
||||
if (timestamp < since) {
|
||||
@@ -35,25 +34,23 @@ export async function GET({ params, url }) {
|
||||
}
|
||||
|
||||
let uptime = ParseUptime(ups + degradeds, ups + degradeds + downs) + "%";
|
||||
|
||||
|
||||
const labelColor = query.get("labelColor") || "#333";
|
||||
const color = query.get("color") || "#0079FF";
|
||||
const style = query.get("style") || "flat";
|
||||
|
||||
const format = {
|
||||
label: name,
|
||||
message: uptime,
|
||||
color: color,
|
||||
labelColor: labelColor,
|
||||
style: style,
|
||||
};
|
||||
const svg = makeBadge(format);
|
||||
|
||||
|
||||
const labelColor = query.get("labelColor") || "#333";
|
||||
const color = query.get("color") || "#0079FF";
|
||||
const style = query.get("style") || "flat";
|
||||
|
||||
const format = {
|
||||
label: name,
|
||||
message: uptime,
|
||||
color: color,
|
||||
labelColor: labelColor,
|
||||
style: style
|
||||
};
|
||||
const svg = makeBadge(format);
|
||||
|
||||
return new Response(svg, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
},
|
||||
});
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
// @ts-nocheck
|
||||
import { Mapper, GetOpenIncidents, FilterAndInsertMonitorInIncident } from "../../../scripts/github.js";
|
||||
import {
|
||||
Mapper,
|
||||
GetOpenIncidents,
|
||||
FilterAndInsertMonitorInIncident
|
||||
} from "../../../scripts/github.js";
|
||||
import { FetchData } from "$lib/server/page";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import fs from "fs-extra";
|
||||
|
||||
export async function load({ params, route, url, parent }) {
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + '/monitors.json', "utf8"));
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
const github = siteData.github;
|
||||
const monitorsActive = [];
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
const github = siteData.github;
|
||||
const monitorsActive = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
if (monitors[i].hidden !== undefined && monitors[i].hidden === true) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (monitors[i].category === undefined || monitors[i].category !== params.category) {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].defaultStatus;
|
||||
let data = await FetchData(monitors[i], parentData.localTz);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].defaultStatus;
|
||||
let data = await FetchData(monitors[i], parentData.localTz);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
let openIncidents = await GetOpenIncidents(github);
|
||||
let openIncidentsReduced = openIncidents.map(Mapper);
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
openIncidents: FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive),
|
||||
};
|
||||
let openIncidentsReduced = openIncidents.map(Mapper);
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
openIncidents: FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,105 +1,143 @@
|
||||
<script>
|
||||
import Monitor from "$lib/components/monitor.svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import Incident from "$lib/components/incident.svelte";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { page } from "$app/stores";
|
||||
import Monitor from "$lib/components/monitor.svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import Incident from "$lib/components/incident.svelte";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { page } from "$app/stores";
|
||||
import { l } from "$lib/i18n/client";
|
||||
|
||||
export let data;
|
||||
export let data;
|
||||
|
||||
let category = data.site.categories.find((c) => c.name === $page.params.category);
|
||||
let hasActiveIncidents = data.openIncidents.length > 0;
|
||||
let category = data.site.categories.find((c) => c.name === $page.params.category);
|
||||
let hasActiveIncidents = data.openIncidents.length > 0;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if category}
|
||||
<title>{category.name} {l(data.lang, 'root.category')}</title>
|
||||
{#if category.description}
|
||||
<meta name="description" content="{category.description}" />
|
||||
<title>{category.name} {l(data.lang, "root.category")}</title>
|
||||
{#if category.description}
|
||||
<meta name="description" content={category.description} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
</svelte:head>
|
||||
<div class="mt-32"></div>
|
||||
{#if category}
|
||||
<section class="mx-auto flex w-full max-w-4xl mb-8 flex-1 flex-col items-start justify-center">
|
||||
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
|
||||
<div class="mx-auto max-w-3xl text-center blurry-bg">
|
||||
{#if category.name}
|
||||
<h1 class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold text-transparent leading-snug">{category.name}</h1>
|
||||
{/if} {#if category.description}
|
||||
<p class="mx-auto mt-4 max-w-xl sm:text-xl">{category.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if} {#if hasActiveIncidents}
|
||||
<section class="mx-auto bg-transparent mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center" id="">
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 md:col-span-1 text-center md:text-left">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, 'root.ongoing_incidents')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mx-auto backdrop-blur-[2px] mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center" id="">
|
||||
{#each data.openIncidents as incident, i}
|
||||
<Incident {incident} state="close" variant="title+body+comments+monitor" monitor="{incident.monitor}" lang="{data.lang}" />
|
||||
{/each}
|
||||
</section>
|
||||
{/if} {#if data.monitors.length > 0}
|
||||
<section class="mx-auto bg-transparent mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center" id="">
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 md:col-span-1 text-center md:text-left">
|
||||
<Badge class="" variant="outline">
|
||||
{l(data.lang, 'root.availability_per_component')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-1 text-center md:text-right">
|
||||
<Badge variant="outline">
|
||||
<span class="w-[8px] h-[8px] inline-flex rounded-full bg-api-up opacity-75 mr-1"></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, 'statuses.UP')}
|
||||
</span>
|
||||
|
||||
<span class="w-[8px] h-[8px] inline-flex rounded-full bg-api-degraded opacity-75 mr-1"></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, 'statuses.DEGRADED')}
|
||||
</span>
|
||||
|
||||
<span class="w-[8px] h-[8px] inline-flex rounded-full bg-api-down opacity-75 mr-1"></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, 'statuses.DOWN')}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mx-auto backdrop-blur-[2px] mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center">
|
||||
<Card.Root class="w-full">
|
||||
<Card.Content class="p-0 monitors-card">
|
||||
{#each data.monitors as monitor}
|
||||
<Monitor {monitor} localTz="{data.localTz}" lang="{data.lang}" />
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{:else}
|
||||
<section class="mx-auto bg-transparent mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center" id="">
|
||||
<Card.Root class="mx-auto">
|
||||
<Card.Content class="pt-4">
|
||||
<h1 class="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-2xl text-center">
|
||||
{l(data.lang, 'root.no_monitors')}
|
||||
</h1>
|
||||
<p class="mt-3 text-center">
|
||||
{l(data.lang, 'root.read_doc_monitor')}
|
||||
<a href="https://kener.ing/docs#h1add-monitors" target="_blank" class="underline">
|
||||
{l(data.lang, 'root.here')}
|
||||
</a>
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
<section class="mx-auto mb-8 flex w-full max-w-4xl flex-1 flex-col items-start justify-center">
|
||||
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
|
||||
<div class="blurry-bg mx-auto max-w-3xl text-center">
|
||||
{#if category.name}
|
||||
<h1
|
||||
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-snug text-transparent"
|
||||
>
|
||||
{category.name}
|
||||
</h1>
|
||||
{/if}
|
||||
{#if category.description}
|
||||
<p class="mx-auto mt-4 max-w-xl sm:text-xl">{category.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{#if hasActiveIncidents}
|
||||
<section
|
||||
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
|
||||
id=""
|
||||
>
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-left">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, "root.ongoing_incidents")}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
|
||||
id=""
|
||||
>
|
||||
{#each data.openIncidents as incident, i}
|
||||
<Incident
|
||||
{incident}
|
||||
state="close"
|
||||
variant="title+body+comments+monitor"
|
||||
monitor={incident.monitor}
|
||||
lang={data.lang}
|
||||
/>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{#if data.monitors.length > 0}
|
||||
<section
|
||||
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
|
||||
id=""
|
||||
>
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-left">
|
||||
<Badge class="" variant="outline">
|
||||
{l(data.lang, "root.availability_per_component")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-right">
|
||||
<Badge variant="outline">
|
||||
<span class="bg-api-up mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "statuses.UP")}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bg-api-degraded mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "statuses.DEGRADED")}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bg-api-down mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "statuses.DOWN")}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
|
||||
>
|
||||
<Card.Root class="w-full">
|
||||
<Card.Content class="monitors-card p-0">
|
||||
{#each data.monitors as monitor}
|
||||
<Monitor {monitor} localTz={data.localTz} lang={data.lang} />
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{:else}
|
||||
<section
|
||||
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
|
||||
id=""
|
||||
>
|
||||
<Card.Root class="mx-auto">
|
||||
<Card.Content class="pt-4">
|
||||
<h1
|
||||
class="scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-2xl"
|
||||
>
|
||||
{l(data.lang, "root.no_monitors")}
|
||||
</h1>
|
||||
<p class="mt-3 text-center">
|
||||
{l(data.lang, "root.read_doc_monitor")}
|
||||
<a
|
||||
href="https://kener.ing/docs#h1add-monitors"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
{l(data.lang, "root.here")}
|
||||
</a>
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import axios from 'axios'
|
||||
import axios from "axios";
|
||||
export async function load({ params, route, url, parent }) {
|
||||
const { data } = await axios.get('https://raw.githubusercontent.com/rajnandan1/kener/main/docs.md')
|
||||
const { data } = await axios.get(
|
||||
"https://raw.githubusercontent.com/rajnandan1/kener/main/docs.md"
|
||||
);
|
||||
return {
|
||||
md: data,
|
||||
};
|
||||
}
|
||||
md: data
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<script>
|
||||
import { marked } from 'marked';
|
||||
import { onMount } from "svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { marked } from "marked";
|
||||
import { onMount } from "svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import * as Accordion from "$lib/components/ui/accordion";
|
||||
export let data;
|
||||
|
||||
let html = marked.parse(data.md);
|
||||
let sideBar = [];
|
||||
function locationHashChanged() {
|
||||
const allSideAs = document.querySelectorAll(".sidebar-a");
|
||||
export let data;
|
||||
|
||||
let html = marked.parse(data.md);
|
||||
let sideBar = [];
|
||||
function locationHashChanged() {
|
||||
const allSideAs = document.querySelectorAll(".sidebar-a");
|
||||
allSideAs.forEach((sideA) => {
|
||||
sideA.classList.remove("active");
|
||||
});
|
||||
document.querySelector(`[href="${window.location.hash}"]`).classList.add("active");
|
||||
}
|
||||
onMount(async () => {
|
||||
const headings = document.querySelectorAll("#markdown h1");
|
||||
headings.forEach((heading) => {
|
||||
const id = "h1" + heading.textContent.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
heading.id = id;
|
||||
heading.setAttribute("sider", "sidemenu");
|
||||
heading.setAttribute("sider-t", "h1");
|
||||
});
|
||||
}
|
||||
onMount(async () => {
|
||||
const headings = document.querySelectorAll("#markdown h1");
|
||||
headings.forEach((heading) => {
|
||||
const id = "h1" + heading.textContent.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
heading.id = id;
|
||||
heading.setAttribute("sider", "sidemenu");
|
||||
heading.setAttribute("sider-t", "h1");
|
||||
});
|
||||
|
||||
const headings2 = document.querySelectorAll("#markdown h2");
|
||||
headings2.forEach((heading) => {
|
||||
const id = "h2" + heading.textContent.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
heading.id = id;
|
||||
heading.setAttribute("sider", "sidemenu");
|
||||
heading.setAttribute("sider-t", "h2");
|
||||
});
|
||||
const headings2 = document.querySelectorAll("#markdown h2");
|
||||
headings2.forEach((heading) => {
|
||||
const id = "h2" + heading.textContent.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
||||
heading.id = id;
|
||||
heading.setAttribute("sider", "sidemenu");
|
||||
heading.setAttribute("sider-t", "h2");
|
||||
});
|
||||
|
||||
const sidemenuHeadings = document.querySelectorAll("#markdown [sider='sidemenu']");
|
||||
//iterate over all headings and create nexted sidebar, if h2 the add to last h1
|
||||
const sidemenuHeadings = document.querySelectorAll("#markdown [sider='sidemenu']");
|
||||
//iterate over all headings and create nexted sidebar, if h2 the add to last h1
|
||||
let lastH1 = null;
|
||||
let lastH2 = null;
|
||||
sidemenuHeadings.forEach((heading) => {
|
||||
@@ -41,69 +41,70 @@
|
||||
id: heading.id,
|
||||
text: heading.textContent,
|
||||
type: heading.getAttribute("sider-t"),
|
||||
children: [],
|
||||
children: []
|
||||
};
|
||||
sideBar = [...sideBar, lastH1];
|
||||
} else if (heading.getAttribute("sider-t") == "h2") {
|
||||
lastH2 = {
|
||||
id: heading.id,
|
||||
text: heading.textContent,
|
||||
type: heading.getAttribute("sider-t"),
|
||||
type: heading.getAttribute("sider-t")
|
||||
};
|
||||
lastH1.children = [...lastH1.children, lastH2];
|
||||
}
|
||||
});
|
||||
|
||||
window.onhashchange = locationHashChanged;
|
||||
|
||||
|
||||
});
|
||||
window.onhashchange = locationHashChanged;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
Kener Documentation
|
||||
</title>
|
||||
<title>Kener Documentation</title>
|
||||
</svelte:head>
|
||||
<section class="mx-auto md:container rounded-3xl scroll-smooth mt-8">
|
||||
<Card.Root>
|
||||
<Card.Content class="px-1">
|
||||
<div class="grid grid-cols-5 gap-4">
|
||||
<div class="col-span-5 md:col-span-1 pt-2 hidden md:block pr-1 border-r-2 border-secondary sticky top-0 overflow-y-auto max-h-screen">
|
||||
<section class="mx-auto mt-8 scroll-smooth rounded-3xl md:container">
|
||||
<Card.Root>
|
||||
<Card.Content class="px-1">
|
||||
<div class="grid grid-cols-5 gap-4">
|
||||
<div
|
||||
class="sticky top-0 col-span-5 hidden max-h-screen overflow-y-auto border-r-2 border-secondary pr-1 pt-2 md:col-span-1 md:block"
|
||||
>
|
||||
{#each sideBar as item}
|
||||
<Accordion.Root>
|
||||
<Accordion.Item value="{item.id}">
|
||||
<Accordion.Trigger class="text-sm font-semibold pl-2 pr-3">
|
||||
<a href="#{item.id}" class="sidebar-a">{item.text}</a>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
<!-- <ul class="w-full text-sm font-medium sticky top-0 overflow-y-auto max-h-screen"> -->
|
||||
<ul class=" text-sm font-medium pl-6">
|
||||
{#each item.children as child}
|
||||
<li class="w-full py-2">
|
||||
<a href="#{child.id}" class="sidebar-a">{child.text}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
<Accordion.Root>
|
||||
<Accordion.Item value={item.id}>
|
||||
<Accordion.Trigger class="pl-2 pr-3 text-sm font-semibold">
|
||||
<a href="#{item.id}" class="sidebar-a">{item.text}</a>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
<!-- <ul class="w-full text-sm font-medium sticky top-0 overflow-y-auto max-h-screen"> -->
|
||||
<ul class=" pl-6 text-sm font-medium">
|
||||
{#each item.children as child}
|
||||
<li class="w-full py-2">
|
||||
<a href="#{child.id}" class="sidebar-a"
|
||||
>{child.text}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
{/each}
|
||||
|
||||
</div>
|
||||
<div class="col-span-5 md:col-span-4">
|
||||
<div class="pt-6 p-0 md:p-10 ">
|
||||
<article
|
||||
id="markdown"
|
||||
class="prose prose-stone max-w-none dark:prose-invert dark:prose-pre:bg-neutral-900 prose-code:py-[0.2rem] prose-code:font-normal prose-code:font-mono prose-code:text-sm prose-code:rounded"
|
||||
>
|
||||
{@html html}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
<div class="col-span-5 md:col-span-4">
|
||||
<div class="p-0 pt-6 md:p-10">
|
||||
<article
|
||||
id="markdown"
|
||||
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 dark:prose-pre:bg-neutral-900"
|
||||
>
|
||||
{@html html}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.h1.inactive ~ .h2 {
|
||||
display: none;
|
||||
@@ -115,8 +116,7 @@
|
||||
font-size: 0.833em;
|
||||
color: #000;
|
||||
}
|
||||
.sidebar-a.active{
|
||||
.sidebar-a.active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,28 +4,28 @@ import { env } from "$env/dynamic/public";
|
||||
import fs from "fs-extra";
|
||||
|
||||
export async function load({ params, route, url, parent }) {
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const parentData = await parent();
|
||||
|
||||
const monitorsActive = [];
|
||||
const query = url.searchParams;
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const parentData = await parent();
|
||||
|
||||
const monitorsActive = [];
|
||||
const query = url.searchParams;
|
||||
const theme = query.get("theme");
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (monitors[i].tag !== params.tag) {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].defaultStatus;
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (monitors[i].tag !== params.tag) {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].defaultStatus;
|
||||
monitors[i].embed = true;
|
||||
let data = await FetchData(monitors[i], parentData.localTz);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
let data = await FetchData(monitors[i], parentData.localTz);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
theme,
|
||||
openIncidents: [],
|
||||
};
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
theme,
|
||||
openIncidents: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
<script>
|
||||
import Monitor from "$lib/components/monitor.svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { page } from "$app/stores";
|
||||
import { onMount, afterUpdate, onDestroy } from "svelte";
|
||||
import Monitor from "$lib/components/monitor.svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { page } from "$app/stores";
|
||||
import { onMount, afterUpdate, onDestroy } from "svelte";
|
||||
import { l } from "$lib/i18n/client";
|
||||
|
||||
let element;
|
||||
let previousHeight = 0;
|
||||
let previousWidth = 0;
|
||||
export let data;
|
||||
|
||||
let element;
|
||||
let previousHeight = 0;
|
||||
let previousWidth = 0;
|
||||
export let data;
|
||||
|
||||
function handleHeightChange(event) {
|
||||
//use window.postMessage to send the height to the parent
|
||||
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
height: element.offsetHeight,
|
||||
width: element.offsetWidth,
|
||||
slug: $page.params.tag,
|
||||
slug: $page.params.tag
|
||||
},
|
||||
"*"
|
||||
);
|
||||
@@ -34,35 +33,47 @@
|
||||
document.documentElement.classList.remove("dark");
|
||||
document.documentElement.classList.remove("dark:bg-background");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if data.monitors.length > 0}
|
||||
<section class="w-fit p-0" bind:this="{element}">
|
||||
<Card.Root class="w-[580px] border-0 shadow-none">
|
||||
<Card.Content class="p-0 monitors-card ">
|
||||
{#each data.monitors as monitor}
|
||||
<Monitor {monitor} localTz="{data.localTz}" lang="{data.lang}" on:heightChange={handleHeightChange}/>
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
<section class="w-fit p-0" bind:this={element}>
|
||||
<Card.Root class="w-[580px] border-0 shadow-none">
|
||||
<Card.Content class="monitors-card p-0 ">
|
||||
{#each data.monitors as monitor}
|
||||
<Monitor
|
||||
{monitor}
|
||||
localTz={data.localTz}
|
||||
lang={data.lang}
|
||||
on:heightChange={handleHeightChange}
|
||||
/>
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{:else}
|
||||
<section class="mx-auto bg-transparent mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center" id="">
|
||||
<Card.Root class="mx-auto">
|
||||
<Card.Content class="pt-4">
|
||||
<h1 class="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-2xl text-center">No Monitor Found.</h1>
|
||||
<p class="mt-3 text-center">
|
||||
{l(data.lang, 'root.read_doc_monitor')}
|
||||
<a href="https://kener.ing/docs#h1add-monitors" target="_blank" class="underline">
|
||||
{l(data.lang, 'root.here')}
|
||||
</a>
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
<section
|
||||
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
|
||||
id=""
|
||||
>
|
||||
<Card.Root class="mx-auto">
|
||||
<Card.Content class="pt-4">
|
||||
<h1
|
||||
class="scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-2xl"
|
||||
>
|
||||
No Monitor Found.
|
||||
</h1>
|
||||
<p class="mt-3 text-center">
|
||||
{l(data.lang, "root.read_doc_monitor")}
|
||||
<a
|
||||
href="https://kener.ing/docs#h1add-monitors"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
{l(data.lang, "root.here")}
|
||||
</a>
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { env } from "$env/dynamic/public";
|
||||
import fs from "fs-extra";
|
||||
const siteData = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
export async function GET({ url, params }) {
|
||||
const { tag } = params;
|
||||
const { tag } = params;
|
||||
const query = url.searchParams;
|
||||
const uriEmbedded = query.get("monitor");
|
||||
const theme = query.get("theme") || "light";
|
||||
@@ -67,9 +67,9 @@ export async function GET({ url, params }) {
|
||||
}).call(this);
|
||||
|
||||
`;
|
||||
return new Response(js, {
|
||||
headers: {
|
||||
"Content-Type": "application/javascript",
|
||||
},
|
||||
});
|
||||
return new Response(js, {
|
||||
headers: {
|
||||
"Content-Type": "application/javascript"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,22 +11,24 @@ import fs from "fs-extra";
|
||||
// @ts-ignore
|
||||
export async function load({ params, route, url, parent }) {
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const siteData = await parent();
|
||||
const github = siteData.site.github;
|
||||
// @ts-ignore
|
||||
const { description, name, tag, image } = monitors.find((monitor) => monitor.folderName === params.id);
|
||||
const siteData = await parent();
|
||||
const github = siteData.site.github;
|
||||
// @ts-ignore
|
||||
const { description, name, tag, image } = monitors.find(
|
||||
(monitor) => monitor.folderName === params.id
|
||||
);
|
||||
const allIncidents = await GetIncidents(tag, github, "all");
|
||||
const gitHubActiveIssues = allIncidents.filter((issue) => {
|
||||
const gitHubActiveIssues = allIncidents.filter((issue) => {
|
||||
return issue.state === "open";
|
||||
});
|
||||
const gitHubPastIssues = allIncidents.filter((issue) => {
|
||||
return issue.state === "closed";
|
||||
});
|
||||
return {
|
||||
issues: params.id,
|
||||
githubConfig: github,
|
||||
monitor: { description, name, image },
|
||||
activeIncidents: await Promise.all(gitHubActiveIssues.map(Mapper, { github })),
|
||||
pastIncidents: await Promise.all(gitHubPastIssues.map(Mapper, { github })),
|
||||
};
|
||||
}
|
||||
const gitHubPastIssues = allIncidents.filter((issue) => {
|
||||
return issue.state === "closed";
|
||||
});
|
||||
return {
|
||||
issues: params.id,
|
||||
githubConfig: github,
|
||||
monitor: { description, name, image },
|
||||
activeIncidents: await Promise.all(gitHubActiveIssues.map(Mapper, { github })),
|
||||
pastIncidents: await Promise.all(gitHubPastIssues.map(Mapper, { github }))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,93 +1,89 @@
|
||||
<script>
|
||||
import Incident from "$lib/components/incident.svelte";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { l, summaryTime } from "$lib/i18n/client";
|
||||
import Incident from "$lib/components/incident.svelte";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { l, summaryTime } from "$lib/i18n/client";
|
||||
|
||||
export let data;
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{data.monitor.name} - {l(data.lang, "root.incidents")}
|
||||
</title>
|
||||
<title>
|
||||
{data.monitor.name} - {l(data.lang, "root.incidents")}
|
||||
</title>
|
||||
</svelte:head>
|
||||
<section
|
||||
class="mx-auto flex w-full max-w-4xl flex-1 flex-col items-start justify-center"
|
||||
>
|
||||
<div
|
||||
class="mx-auto max-w-screen-xl px-4 pt-32 pb-16 lg:flex lg:items-center"
|
||||
>
|
||||
<div class="mx-auto max-w-3xl text-center blurry-bg">
|
||||
<h1
|
||||
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold text-transparent leading-snug"
|
||||
>
|
||||
{data.monitor.name}
|
||||
</h1>
|
||||
<section class="mx-auto flex w-full max-w-4xl flex-1 flex-col items-start justify-center">
|
||||
<div class="mx-auto max-w-screen-xl px-4 pb-16 pt-32 lg:flex lg:items-center">
|
||||
<div class="blurry-bg mx-auto max-w-3xl text-center">
|
||||
<h1
|
||||
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-snug text-transparent"
|
||||
>
|
||||
{data.monitor.name}
|
||||
</h1>
|
||||
|
||||
<p class="mx-auto mt-4 max-w-xl sm:text-xl">
|
||||
{@html data.monitor.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mx-auto mt-4 max-w-xl sm:text-xl">
|
||||
{@html data.monitor.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mx-auto flex-1 mt-8 flex-col mb-4 flex w-full">
|
||||
<div class="container">
|
||||
<h1 class="mb-4 text-2xl font-bold leading-none">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, "root.active_incidents")}
|
||||
</Badge>
|
||||
</h1>
|
||||
<section class="mx-auto mb-4 mt-8 flex w-full flex-1 flex-col">
|
||||
<div class="container">
|
||||
<h1 class="mb-4 text-2xl font-bold leading-none">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, "root.active_incidents")}
|
||||
</Badge>
|
||||
</h1>
|
||||
|
||||
{#if data.activeIncidents.length > 0}
|
||||
{#each data.activeIncidents as incident, i}
|
||||
<Incident
|
||||
{incident}
|
||||
state={i == 0 ? "open" : "close"}
|
||||
variant="title+body+comments"
|
||||
monitor={data.monitor}
|
||||
lang="{data.lang}"
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex items-center justify-left">
|
||||
<p class="text-base font-semibold">
|
||||
{l(data.lang, "root.no_active_incident")}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if data.activeIncidents.length > 0}
|
||||
{#each data.activeIncidents as incident, i}
|
||||
<Incident
|
||||
{incident}
|
||||
state={i == 0 ? "open" : "close"}
|
||||
variant="title+body+comments"
|
||||
monitor={data.monitor}
|
||||
lang={data.lang}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="justify-left flex items-center">
|
||||
<p class="text-base font-semibold">
|
||||
{l(data.lang, "root.no_active_incident")}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator class="container mb-4 w-[400px]" />
|
||||
|
||||
<section class="mx-auto flex-1 mt-8 flex-col mb-4 flex w-full">
|
||||
<div class="container">
|
||||
<h1 class="mb-4 text-2xl font-bold leading-none">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, "root.recent_incidents")} - {summaryTime(
|
||||
data.lang,
|
||||
`Last ${data.site.github.incidentSince} hours`,
|
||||
)}
|
||||
</Badge>
|
||||
</h1>
|
||||
<section class="mx-auto mb-4 mt-8 flex w-full flex-1 flex-col">
|
||||
<div class="container">
|
||||
<h1 class="mb-4 text-2xl font-bold leading-none">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, "root.recent_incidents")} - {summaryTime(
|
||||
data.lang,
|
||||
`Last ${data.site.github.incidentSince} hours`
|
||||
)}
|
||||
</Badge>
|
||||
</h1>
|
||||
|
||||
{#if data.pastIncidents.length > 0}
|
||||
{#each data.pastIncidents as incident}
|
||||
<Incident
|
||||
{incident}
|
||||
state="close"
|
||||
variant="title+body+comments"
|
||||
monitor={data.monitor}
|
||||
lang="{data.lang}"
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex items-center justify-left">
|
||||
<p class="text-base font-semibold">
|
||||
{l(data.lang, "root.no_recent_incident")}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if data.pastIncidents.length > 0}
|
||||
{#each data.pastIncidents as incident}
|
||||
<Incident
|
||||
{incident}
|
||||
state="close"
|
||||
variant="title+body+comments"
|
||||
monitor={data.monitor}
|
||||
lang={data.lang}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="justify-left flex items-center">
|
||||
<p class="text-base font-semibold">
|
||||
{l(data.lang, "root.no_recent_incident")}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -6,19 +6,23 @@ import fs from "fs-extra";
|
||||
import { GetCommentsForIssue } from "../../../../../scripts/github.js";
|
||||
import { marked } from "marked";
|
||||
|
||||
export async function GET({ params, }) {
|
||||
const incidentNumber = params.id;
|
||||
export async function GET({ params }) {
|
||||
const incidentNumber = params.id;
|
||||
let siteData = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
let comments = await GetCommentsForIssue(incidentNumber, siteData.github);
|
||||
comments = comments.map((/** @type {{ body: string | import("markdown-it/lib/token")[]; created_at: any; updated_at: any; html_url: any; }} */ comment) => {
|
||||
const html = marked.parse(comment.body);
|
||||
return {
|
||||
body: html,
|
||||
created_at: comment.created_at,
|
||||
updated_at: comment.updated_at,
|
||||
html_url: comment.html_url,
|
||||
};
|
||||
});
|
||||
let comments = await GetCommentsForIssue(incidentNumber, siteData.github);
|
||||
comments = comments.map(
|
||||
(
|
||||
/** @type {{ body: string | import("markdown-it/lib/token")[]; created_at: any; updated_at: any; html_url: any; }} */ comment
|
||||
) => {
|
||||
const html = marked.parse(comment.body);
|
||||
return {
|
||||
body: html,
|
||||
created_at: comment.created_at,
|
||||
updated_at: comment.updated_at,
|
||||
html_url: comment.html_url
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return json(comments);
|
||||
return json(comments);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
// @ts-nocheck
|
||||
import { Mapper, GetOpenIncidents, FilterAndInsertMonitorInIncident } from "../../../scripts/github.js";
|
||||
import {
|
||||
Mapper,
|
||||
GetOpenIncidents,
|
||||
FilterAndInsertMonitorInIncident
|
||||
} from "../../../scripts/github.js";
|
||||
import { FetchData } from "$lib/server/page";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import fs from "fs-extra";
|
||||
|
||||
export async function load({ params, route, url, parent }) {
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
const github = siteData.github;
|
||||
const monitorsActive = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (monitors[i].tag !== params.tag) {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].defaultStatus;
|
||||
let data = await FetchData(monitors[i], parentData.localTz);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
let openIncidents = await GetOpenIncidents(github);
|
||||
let openIncidentsReduced = openIncidents.map(Mapper);
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
openIncidents: FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive),
|
||||
};
|
||||
let monitors = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/monitors.json", "utf8"));
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
const github = siteData.github;
|
||||
const monitorsActive = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
//only return monitors that have category as home or category is not present
|
||||
if (monitors[i].tag !== params.tag) {
|
||||
continue;
|
||||
}
|
||||
delete monitors[i].api;
|
||||
delete monitors[i].defaultStatus;
|
||||
let data = await FetchData(monitors[i], parentData.localTz);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
}
|
||||
let openIncidents = await GetOpenIncidents(github);
|
||||
let openIncidentsReduced = openIncidents.map(Mapper);
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
openIncidents: FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,89 +1,121 @@
|
||||
<script>
|
||||
import Monitor from "$lib/components/monitor.svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import Incident from "$lib/components/incident.svelte";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { page } from "$app/stores";
|
||||
import Monitor from "$lib/components/monitor.svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import Incident from "$lib/components/incident.svelte";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { page } from "$app/stores";
|
||||
import { l } from "$lib/i18n/client";
|
||||
|
||||
export let data;
|
||||
export let data;
|
||||
|
||||
let hasActiveIncidents = data.openIncidents.length > 0;
|
||||
let hasActiveIncidents = data.openIncidents.length > 0;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if data.monitors.length > 0}
|
||||
<title>{data.monitors[0].name} Monitor Page</title>
|
||||
<title>{data.monitors[0].name} Monitor Page</title>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
<div class="mt-32"></div>
|
||||
{#if hasActiveIncidents}
|
||||
<section class="mx-auto bg-transparent mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center" id="">
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 md:col-span-1 text-center md:text-left">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, 'root.ongoing_incidents')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center" id="">
|
||||
{#each data.openIncidents as incident, i}
|
||||
<Incident {incident} state="close" variant="title+body+comments+monitor" monitor="{incident.monitor}" lang="{data.lang}" />
|
||||
{/each}
|
||||
</section>
|
||||
{/if} {#if data.monitors.length > 0}
|
||||
<section class="mx-auto bg-transparent mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center" id="">
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 md:col-span-1 text-center md:text-left">
|
||||
<Badge class="" variant="outline">
|
||||
{l(data.lang, 'root.availability_per_component')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-1 text-center md:text-right">
|
||||
<Badge variant="outline">
|
||||
<span class="w-[8px] h-[8px] inline-flex rounded-full bg-api-up opacity-75 mr-1"></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, 'statuses.UP')}
|
||||
</span>
|
||||
|
||||
<span class="w-[8px] h-[8px] inline-flex rounded-full bg-api-degraded opacity-75 mr-1"></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, 'statuses.DEGRADED')}
|
||||
</span>
|
||||
|
||||
<span class="w-[8px] h-[8px] inline-flex rounded-full bg-api-down opacity-75 mr-1"></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, 'statuses.DOWN')}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mx-auto backdrop-blur-[2px] mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center">
|
||||
<Card.Root class="w-full">
|
||||
<Card.Content class="p-0 monitors-card">
|
||||
{#each data.monitors as monitor}
|
||||
<Monitor {monitor} localTz="{data.localTz}" lang="{data.lang}" />
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{:else}
|
||||
<section class="mx-auto bg-transparent mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center" id="">
|
||||
<Card.Root class="mx-auto">
|
||||
<Card.Content class="pt-4">
|
||||
<h1 class="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-2xl text-center">
|
||||
{l(data.lang, 'root.no_monitors')}
|
||||
</h1>
|
||||
<p class="mt-3 text-center">
|
||||
{l(data.lang, 'root.read_doc_monitor')}
|
||||
<a href="https://kener.ing/docs#h1add-monitors" target="_blank" class="underline">
|
||||
{l(data.lang, 'root.here')}
|
||||
</a>
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
<section
|
||||
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
|
||||
id=""
|
||||
>
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-left">
|
||||
<Badge variant="outline">
|
||||
{l(data.lang, "root.ongoing_incidents")}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center"
|
||||
id=""
|
||||
>
|
||||
{#each data.openIncidents as incident, i}
|
||||
<Incident
|
||||
{incident}
|
||||
state="close"
|
||||
variant="title+body+comments+monitor"
|
||||
monitor={incident.monitor}
|
||||
lang={data.lang}
|
||||
/>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{#if data.monitors.length > 0}
|
||||
<section
|
||||
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
|
||||
id=""
|
||||
>
|
||||
<div class="grid w-full grid-cols-2 gap-4">
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-left">
|
||||
<Badge class="" variant="outline">
|
||||
{l(data.lang, "root.availability_per_component")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="col-span-2 text-center md:col-span-1 md:text-right">
|
||||
<Badge variant="outline">
|
||||
<span class="bg-api-up mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "statuses.UP")}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bg-api-degraded mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "statuses.DEGRADED")}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bg-api-down mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
|
||||
></span>
|
||||
<span class="mr-3">
|
||||
{l(data.lang, "statuses.DOWN")}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="mx-auto mb-8 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
|
||||
>
|
||||
<Card.Root class="w-full">
|
||||
<Card.Content class="monitors-card p-0">
|
||||
{#each data.monitors as monitor}
|
||||
<Monitor {monitor} localTz={data.localTz} lang={data.lang} />
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{:else}
|
||||
<section
|
||||
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
|
||||
id=""
|
||||
>
|
||||
<Card.Root class="mx-auto">
|
||||
<Card.Content class="pt-4">
|
||||
<h1
|
||||
class="scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-2xl"
|
||||
>
|
||||
{l(data.lang, "root.no_monitors")}
|
||||
</h1>
|
||||
<p class="mt-3 text-center">
|
||||
{l(data.lang, "root.read_doc_monitor")}
|
||||
<a
|
||||
href="https://kener.ing/docs#h1add-monitors"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
{l(data.lang, "root.here")}
|
||||
</a>
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -3,14 +3,14 @@ import adapter from "@sveltejs/adapter-node";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
paths: {
|
||||
base: process.env.KENER_BASE_PATH || "",
|
||||
},
|
||||
},
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
paths: {
|
||||
base: process.env.KENER_BASE_PATH || ""
|
||||
}
|
||||
},
|
||||
|
||||
preprocess: [vitePreprocess({})],
|
||||
preprocess: [vitePreprocess({})]
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -2,67 +2,67 @@ import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
safelist: ["dark"],
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
safelist: ["dark"],
|
||||
plugins: [
|
||||
require("@tailwindcss/typography"),
|
||||
// ...
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border) / <alpha-value>)",
|
||||
input: "hsl(var(--input) / <alpha-value>)",
|
||||
ring: "hsl(var(--ring) / <alpha-value>)",
|
||||
background: "hsl(var(--background) / <alpha-value>)",
|
||||
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
||||
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
||||
foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
||||
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
||||
foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
||||
foreground: "hsl(var(--card-foreground) / <alpha-value>)",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [...fontFamily.sans],
|
||||
},
|
||||
},
|
||||
},
|
||||
require("@tailwindcss/typography")
|
||||
// ...
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px"
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border) / <alpha-value>)",
|
||||
input: "hsl(var(--input) / <alpha-value>)",
|
||||
ring: "hsl(var(--ring) / <alpha-value>)",
|
||||
background: "hsl(var(--background) / <alpha-value>)",
|
||||
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
||||
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
||||
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
||||
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
||||
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
||||
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
||||
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)"
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [...fontFamily.sans]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
|
||||
]
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user