Merge pull request #78 from rajnandan1/pretty

refactor: added prettier config
This commit is contained in:
Raj Nandan Sharma
2024-05-04 12:03:15 +05:30
committed by GitHub
76 changed files with 8670 additions and 8208 deletions

22
.prettierignore Normal file
View 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
View 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" } }]
}

View File

@@ -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
![image](static/marken_tl.png)
![image](static/marken_theme.png)
## 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&amp;A sites" title="profile for Raj Nandan Sharma on Stack Exchange, a network of free, community-driven Q&amp;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>

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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: "日本語"

503
docs.md

File diff suppressed because it is too large Load Diff

View File

@@ -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"]
}

View File

@@ -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": "उप"
}
}

View File

@@ -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"]
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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
View File

@@ -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 + "!");
});

View File

@@ -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 };

View File

@@ -1,4 +1,3 @@
// Define your constants
const FOLDER = process.env.PUBLIC_KENER_FOLDER;
const ENV = process.env.NODE_ENV;

View File

@@ -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 };

View File

@@ -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
};

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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) }));

View File

@@ -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
};

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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:

View File

@@ -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"
},

View File

@@ -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 />

View File

@@ -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}

View File

@@ -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
)}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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

View File

@@ -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}

View File

@@ -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} />

View File

@@ -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} />

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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
};

View File

@@ -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
};
};

View File

@@ -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
};
}

View File

@@ -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}

View File

@@ -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)
};
}

View File

@@ -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}

View File

@@ -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
});
}

View File

@@ -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
});
}

View File

@@ -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
}
);
}

View File

@@ -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
});
}

View File

@@ -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
});
}

View File

@@ -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);
}

View File

@@ -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"
}
});
}

View File

@@ -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"
}
});
}

View File

@@ -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)
};
}

View File

@@ -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}

View File

@@ -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
};
}

View File

@@ -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>

View File

@@ -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: []
};
}

View File

@@ -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}

View File

@@ -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"
}
});
}

View File

@@ -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 }))
};
}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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)
};
}

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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()]
});