convert to utc
@@ -2,7 +2,7 @@ title: "Kener"
|
||||
theme: "dark"
|
||||
siteURL: "https://kener.netlify.app"
|
||||
home: "/"
|
||||
logo: "/logo.svg"
|
||||
logo: "/kener.png"
|
||||
favicon: "/kener.png"
|
||||
github:
|
||||
owner: "rajnandan1"
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lucide-svelte": "^0.292.0",
|
||||
"mode-watcher": "^0.1.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"node-cache": "^5.1.2",
|
||||
@@ -2451,14 +2450,6 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mode-watcher": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.1.2.tgz",
|
||||
"integrity": "sha512-XTdPCdqC3kqSvB+Q262Kor983YJkkB2Z3vj9uqg5IqKQpOdiz+xB99Jihp8sWbyM67drC7KKp0Nt5FzCypZi2g==",
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
@@ -3900,9 +3891,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
|
||||
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
|
||||
@@ -27,13 +27,8 @@
|
||||
"url": "https://github.com/rajnandan1/kener.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"kener:dev": "cross-env PUBLIC_KENER_FOLDER=./static/kener tz=UTC concurrently \"node dev.js\" \"vite dev\"",
|
||||
"kener:dev-monitor": "cross-env PUBLIC_KENER_FOLDER=./static/kener tz=UTC node dev.js",
|
||||
"kener:dev": "cross-env PUBLIC_KENER_FOLDER=./static/kener concurrently \"node dev.js\" \"vite dev\"",
|
||||
"kener:dev-monitor": "cross-env PUBLIC_KENER_FOLDER=./static/kener node dev.js",
|
||||
"kener:dev-build": "cross-env PUBLIC_KENER_FOLDER=./static/kener tz=UTC vite build",
|
||||
"kener:build": "cross-env PUBLIC_KENER_FOLDER=./build/client/kener tz=UTC vite build",
|
||||
"kener": "cross-env PUBLIC_KENER_FOLDER=./build/client/kener tz=UTC node prod.js"
|
||||
@@ -65,7 +60,6 @@
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lucide-svelte": "^0.292.0",
|
||||
"mode-watcher": "^0.1.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"node-cache": "^5.1.2",
|
||||
|
||||
@@ -1,21 +1,89 @@
|
||||
import axios from "axios";
|
||||
import fs from "fs-extra";
|
||||
import { UP, DOWN, DEGRADED } from "./constants.js";
|
||||
import moment from "moment";
|
||||
import { GetIncidentsOpen, GetStartTimeFromBody, GetEndTimeFromBody, GetNowTimestampUTC, GetMinuteStartNowTimestampUTC,GetMinuteStartTimestampUTC, GetDayStartTimestampUTC } from "./tool.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)
|
||||
});
|
||||
|
||||
async function manualIncident(monitor, githubConfig){
|
||||
let incidentsResp = await GetIncidentsOpen(monitor.tag, githubConfig);
|
||||
|
||||
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];
|
||||
let start_time = GetStartTimeFromBody(incident.body);
|
||||
|
||||
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;
|
||||
} else {
|
||||
newIncident.end_time = GetNowTimestampUTC();
|
||||
}
|
||||
|
||||
let allLabels = incident.labels.map((label) => label.name);
|
||||
//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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//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,
|
||||
type: "manual"
|
||||
};
|
||||
}
|
||||
return manualData;
|
||||
}
|
||||
|
||||
function replaceAllOccurrences(originalString, searchString, replacement) {
|
||||
const regex = new RegExp(`\\${searchString}`, "g");
|
||||
const replacedString = originalString.replace(regex, replacement);
|
||||
return replacedString;
|
||||
}
|
||||
const Kener_folder = process.env.PUBLIC_KENER_FOLDER;
|
||||
|
||||
|
||||
const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorEval) => {
|
||||
let axiosHeaders = {};
|
||||
@@ -123,8 +191,8 @@ const getWebhookData = async (monitor) => {
|
||||
try {
|
||||
let fd = fs.readFileSync(Kener_folder + "/" + file, "utf8");
|
||||
webhookData = JSON.parse(fd);
|
||||
for (const timestampISO in webhookData) {
|
||||
originalData[timestampISO] = webhookData[timestampISO];
|
||||
for (const timestamp in webhookData) {
|
||||
originalData[timestamp] = webhookData[timestamp];
|
||||
}
|
||||
//delete the file
|
||||
fs.unlinkSync(Kener_folder + "/" + file);
|
||||
@@ -144,24 +212,22 @@ const getDayData = async (monitor) => {
|
||||
}
|
||||
return originalData;
|
||||
};
|
||||
const updateDayData = async (mergedData, startOfMinute, monitor) => {
|
||||
let today = moment(startOfMinute).subtract(1, "days").startOf("day").toISOString();
|
||||
let _0Day = {};
|
||||
const updateDayData = async (mergedData, startOfMinute, monitor, since) => {
|
||||
let mxBackDate = startOfMinute - (since * 3600);
|
||||
let _0Day = {};
|
||||
for (const ts in mergedData) {
|
||||
const element = mergedData[ts];
|
||||
if (moment(ts).isAfter(moment(today))) {
|
||||
if (ts >= mxBackDate) {
|
||||
_0Day[ts] = element;
|
||||
}
|
||||
}
|
||||
|
||||
//sort the keys
|
||||
let keys = Object.keys(_0Day);
|
||||
keys.sort((a, b) => {
|
||||
return moment(a).isBefore(moment(b)) ? -1 : 1;
|
||||
});
|
||||
keys.sort();
|
||||
let sortedDay0 = {};
|
||||
keys.reverse() //reverse to keep 90days data
|
||||
.slice(0, 2880) //2days data
|
||||
.slice(0, since * 60)
|
||||
.reverse() //reverse to keep 0day data
|
||||
.forEach((key) => {
|
||||
sortedDay0[key] = _0Day[key];
|
||||
@@ -173,7 +239,9 @@ const updateDayData = async (mergedData, startOfMinute, monitor) => {
|
||||
}
|
||||
};
|
||||
|
||||
const update90DayData = async (mergedData, startOfMinute, monitor) => {
|
||||
const update90DayData = async (monitor) => {
|
||||
const mergedData = JSON.parse(fs.readFileSync(monitor.path0Day, "utf8"));
|
||||
|
||||
let _90Day = {};
|
||||
let _90File = monitor.path90Day;
|
||||
try {
|
||||
@@ -183,41 +251,43 @@ const update90DayData = async (mergedData, startOfMinute, monitor) => {
|
||||
fs.ensureFileSync(_90File);
|
||||
fs.writeFileSync(_90File, JSON.stringify({}));
|
||||
}
|
||||
let dayISO = moment(startOfMinute).startOf("day").toISOString();
|
||||
//calculat 90day data from mergedData
|
||||
let up = 0,
|
||||
degraded = 0,
|
||||
down = 0,
|
||||
latency = 0;
|
||||
let temp = {};
|
||||
for (const timestamp in mergedData) {
|
||||
|
||||
let dayTS = GetDayStartTimestampUTC(timestamp);
|
||||
|
||||
for (const timestampISO in mergedData) {
|
||||
//only consider data from last today
|
||||
if (moment(timestampISO).isBefore(moment(dayISO))) {
|
||||
if (temp[dayTS] === undefined) {
|
||||
temp[dayTS] = {
|
||||
timestamp: dayTS,
|
||||
UP: 0,
|
||||
DEGRADED: 0,
|
||||
DOWN: 0,
|
||||
avgLatency: 0,
|
||||
latency: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let _this = mergedData[timestamp];
|
||||
let d = temp[dayTS];
|
||||
|
||||
temp[dayTS].UP = d.UP + (_this.status == UP ? 1 : 0);
|
||||
temp[dayTS].DEGRADED = d.DEGRADED + (_this.status == DEGRADED ? 1 : 0);
|
||||
temp[dayTS].DOWN = d.DOWN + (_this.status == DOWN ? 1 : 0);
|
||||
temp[dayTS].latency = d.latency + _this.latency;
|
||||
}
|
||||
|
||||
for (const dayTS in temp) {
|
||||
let d = temp[dayTS];
|
||||
if (d.UP + d.DEGRADED + d.DOWN === 0) {
|
||||
continue;
|
||||
}
|
||||
const element = mergedData[timestampISO];
|
||||
up += element.status == UP ? 1 : 0;
|
||||
degraded += element.status == DEGRADED ? 1 : 0;
|
||||
down += element.status == DOWN ? 1 : 0;
|
||||
latency += element.latency;
|
||||
let avgLatency = (d.latency / (d.UP + d.DEGRADED + d.DOWN)).toFixed(0);
|
||||
temp[dayTS].avgLatency = avgLatency;
|
||||
}
|
||||
if (up + degraded + down === 0) return;
|
||||
let avgLatency = (latency / (up + degraded + down)).toFixed(0);
|
||||
|
||||
_90Day[dayISO] = {
|
||||
timestamp: dayISO,
|
||||
UP: up,
|
||||
DEGRADED: degraded,
|
||||
DOWN: down,
|
||||
avgLatency: avgLatency,
|
||||
latency: latency,
|
||||
};
|
||||
|
||||
_90Day = {..._90Day, ...temp};
|
||||
//sort the keys
|
||||
let keys = Object.keys(_90Day);
|
||||
keys.sort((a, b) => {
|
||||
return moment(a).isBefore(moment(b)) ? -1 : 1;
|
||||
});
|
||||
keys.sort();
|
||||
let sorted90Day = {};
|
||||
|
||||
keys.reverse() //reverse to keep 90days data
|
||||
@@ -229,17 +299,19 @@ const update90DayData = async (mergedData, startOfMinute, monitor) => {
|
||||
|
||||
fs.writeFileSync(_90File, JSON.stringify(sorted90Day, null, 2));
|
||||
};
|
||||
const Minuter = async (envSecrets, monitor) => {
|
||||
const Minuter = async (envSecrets, monitor, githubConfig) => {
|
||||
if (apiQueue.length > 0) console.log("Queue length is " + apiQueue.length);
|
||||
let apiData = {};
|
||||
let webhookData = {};
|
||||
const startOfMinute = moment().startOf("minute").toISOString();
|
||||
let manualData = {};
|
||||
const startOfMinute = GetMinuteStartNowTimestampUTC();
|
||||
let dayData = {};
|
||||
|
||||
if (monitor.hasAPI) {
|
||||
let apiResponse = await apiCall(envSecrets, monitor.url, monitor.method, JSON.stringify(monitor.headers), monitor.body, monitor.timeout, monitor.eval);
|
||||
apiData[startOfMinute] = apiResponse;
|
||||
if (apiResponse.type === "timeout") {
|
||||
console.log("Retrying api call");
|
||||
console.log("Retrying api call for " + monitor.name + " at " + startOfMinute + " due to timeout");
|
||||
//retry
|
||||
apiQueue.push(async (cb) => {
|
||||
apiCall(envSecrets, monitor.url, monitor.method, JSON.stringify(monitor.headers), monitor.body, monitor.timeout, monitor.eval).then(async (data) => {
|
||||
@@ -250,29 +322,36 @@ const Minuter = async (envSecrets, monitor) => {
|
||||
});
|
||||
});
|
||||
}
|
||||
// apiData[startOfMinute] = await apiCall(envSecrets, startOfMinute, monitor.url, monitor.method, JSON.stringify(monitor.headers), monitor.body, monitor.timeout, monitor.eval);
|
||||
}
|
||||
webhookData = await getWebhookData(monitor);
|
||||
dayData = await getDayData(monitor);
|
||||
manualData = await manualIncident(monitor, githubConfig);
|
||||
|
||||
//merge apiData, webhookData, dayData
|
||||
let mergedData = {};
|
||||
|
||||
for (const timestampISO in dayData) {
|
||||
mergedData[timestampISO] = dayData[timestampISO];
|
||||
// console.log(Object.keys(dayData).length);;
|
||||
console.log(Object.keys(mergedData).length);
|
||||
for (const timestamp in dayData) {
|
||||
mergedData[timestamp] = dayData[timestamp];
|
||||
}
|
||||
for (const timestampISO in apiData) {
|
||||
mergedData[timestampISO] = apiData[timestampISO];
|
||||
console.log(Object.keys(mergedData).length);
|
||||
for (const timestamp in apiData) {
|
||||
mergedData[timestamp] = apiData[timestamp];
|
||||
}
|
||||
for (const timestampISO in webhookData) {
|
||||
mergedData[timestampISO] = webhookData[timestampISO];
|
||||
console.log(Object.keys(mergedData).length);
|
||||
for (const timestamp in webhookData) {
|
||||
mergedData[timestamp] = webhookData[timestamp];
|
||||
}
|
||||
|
||||
//update 90day data
|
||||
await update90DayData(mergedData, startOfMinute, monitor);
|
||||
console.log(Object.keys(mergedData).length);
|
||||
for (const timestamp in manualData) {
|
||||
mergedData[timestamp] = manualData[timestamp];
|
||||
}
|
||||
console.log(Object.keys(mergedData).filter((x) => !Object.keys(mergedData).includes(x)));
|
||||
|
||||
//update day data
|
||||
await updateDayData(mergedData, startOfMinute, monitor);
|
||||
await updateDayData(mergedData, startOfMinute, monitor, githubConfig.incidentSince);
|
||||
//update 90day data
|
||||
await update90DayData(monitor);
|
||||
};
|
||||
apiQueue.start((err) => {
|
||||
if (err) {
|
||||
|
||||
@@ -139,7 +139,15 @@ const Startup = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: 24
|
||||
};
|
||||
}
|
||||
if (checkIfDuplicateExists(monitors.map((monitor) => monitor.folderName)) === true) {
|
||||
console.log("duplicate monitor detected");
|
||||
process.exit(1);
|
||||
@@ -167,9 +175,16 @@ const Startup = async () => {
|
||||
const tagsAndDescription = monitors.map((monitor) => {
|
||||
return { tag: monitor.tag, description: monitor.name };
|
||||
});
|
||||
//add status label if does not exist
|
||||
if (ghlabels.indexOf("status") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "status", "Status of the site");
|
||||
//add incident label if does not exist
|
||||
|
||||
if (ghlabels.indexOf("incident") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "incident", "Status of the site");
|
||||
}
|
||||
if (ghlabels.indexOf("incident-degraded") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "incident-degraded", "Status is degraded of the site");
|
||||
}
|
||||
if (ghlabels.indexOf("incident-down") === -1) {
|
||||
await CreateGHLabel(ghowner, ghrepo, "incident-down", "Status is down of the site");
|
||||
}
|
||||
//add tags if does not exist
|
||||
for (let i = 0; i < tagsAndDescription.length; i++) {
|
||||
@@ -198,23 +213,23 @@ const Startup = async () => {
|
||||
|
||||
|
||||
console.log("Staring One Minute Cron for ", monitor.path0Day);
|
||||
await Minuter(envSecrets, monitor);
|
||||
await Minuter(envSecrets, monitor, site.github);
|
||||
}
|
||||
|
||||
//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);
|
||||
});
|
||||
}
|
||||
// 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);
|
||||
// });
|
||||
// }
|
||||
};
|
||||
|
||||
export { Startup };
|
||||
@@ -1,5 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { MONITOR, SITE } from "./constants.js";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
|
||||
const GH_TOKEN = process.env.GH_TOKEN;
|
||||
const IsValidURL = function (url) {
|
||||
@@ -9,7 +11,7 @@ const IsValidHTTPMethod = function (method) {
|
||||
return /^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH)$/.test(method);
|
||||
};
|
||||
function generateRandomColor() {
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
return randomColor;
|
||||
//random color will be freshly served
|
||||
}
|
||||
@@ -56,18 +58,18 @@ const GetAllGHLabels = async function (owner, repo) {
|
||||
},
|
||||
};
|
||||
|
||||
let labels = [];
|
||||
try {
|
||||
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;
|
||||
return labels;
|
||||
};
|
||||
const CreateGHLabel = async function (owner, repo, label, description) {
|
||||
const options = {
|
||||
const options = {
|
||||
method: "POST",
|
||||
url: `https://api.github.com/repos/${owner}/${repo}/labels`,
|
||||
headers: {
|
||||
@@ -78,16 +80,113 @@ const CreateGHLabel = async function (owner, repo, label, description) {
|
||||
data: {
|
||||
name: label,
|
||||
color: generateRandomColor(),
|
||||
description: description,
|
||||
description: description,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error.response.data);
|
||||
return null;
|
||||
}
|
||||
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 matches = pattern.exec(text);
|
||||
|
||||
if (matches) {
|
||||
const timestamp = matches[1];
|
||||
return parseInt(timestamp);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const GetEndTimeFromBody = function (text) {
|
||||
const pattern = /\[end_datetime:(\d+)\]/;
|
||||
|
||||
const matches = pattern.exec(text);
|
||||
|
||||
if (matches) {
|
||||
const timestamp = matches[1];
|
||||
return parseInt(timestamp);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const GetIncidentsOpen = async function (tagName, githubConfig) {
|
||||
if (tagName === undefined) {
|
||||
return [];
|
||||
}
|
||||
if (githubConfig === undefined) {
|
||||
return [];
|
||||
}
|
||||
const sinceHours = githubConfig.incidentSince || 24;
|
||||
const since = moment().subtract(sinceHours, "hours").toISOString();
|
||||
const options = {
|
||||
method: "GET",
|
||||
url: `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?labels=${tagName},incident&state=open&sort=created&direction=desc&since=${since}`,
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: "Bearer " + GH_TOKEN,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
};
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
//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);
|
||||
};
|
||||
//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);
|
||||
};
|
||||
//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);
|
||||
};
|
||||
//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);
|
||||
};
|
||||
const DurationInMinutes = function (start, end) {
|
||||
return Math.floor((end - start) / 60);
|
||||
}
|
||||
export { IsValidURL, IsValidHTTPMethod, LoadMonitorsPath, LoadSitePath, GetAllGHLabels, CreateGHLabel };
|
||||
export {
|
||||
IsValidURL,
|
||||
IsValidHTTPMethod,
|
||||
LoadMonitorsPath,
|
||||
LoadSitePath,
|
||||
GetAllGHLabels,
|
||||
CreateGHLabel,
|
||||
GetIncidentsOpen,
|
||||
GetStartTimeFromBody,
|
||||
GetEndTimeFromBody,
|
||||
GetMinuteStartTimestampUTC,
|
||||
GetNowTimestampUTC,
|
||||
GetDayStartTimestampUTC,
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
DurationInMinutes,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en" class="dark dark:bg-background">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" id="kener-app-favicon" href="/kener.png" />
|
||||
<link rel="icon" id="kener-app-favicon" href="/k96.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
@@ -11,37 +11,38 @@
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import * as Alert from "$lib/components/ui/alert";
|
||||
|
||||
function getTodayDD() {
|
||||
function getTodayDD() {
|
||||
let yourDate = new Date();
|
||||
|
||||
let yourDate = new Date();
|
||||
const offset = yourDate.getTimezoneOffset();
|
||||
yourDate = new Date(yourDate.getTime() - offset * 60 * 1000);
|
||||
return yourDate.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
const offset = yourDate.getTimezoneOffset()
|
||||
yourDate = new Date(yourDate.getTime() - (offset*60*1000))
|
||||
return yourDate.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
function getminuteFromMidnightTillNow() {
|
||||
var date = new Date();
|
||||
var hours = date.getHours();
|
||||
var minutes = date.getMinutes();
|
||||
var totalMinutes = (hours*60) + minutes ;
|
||||
return totalMinutes
|
||||
}
|
||||
function getminuteFromMidnightTillNow() {
|
||||
var date = new Date();
|
||||
var hours = date.getHours();
|
||||
var minutes = date.getMinutes();
|
||||
var totalMinutes = hours * 60 + minutes;
|
||||
return totalMinutes;
|
||||
}
|
||||
|
||||
export let monitor;
|
||||
let loading90 = true;
|
||||
let todayDD = getTodayDD();
|
||||
|
||||
let uptime90Day = "0";
|
||||
let uptime0Day = "0";
|
||||
let dailyUps = 0;
|
||||
let dailyDown = 0;
|
||||
let dailyDegraded = 0;
|
||||
let avgLatency90Day = 0;
|
||||
let avgLatency0Day = 0;
|
||||
|
||||
let _0Day = monitor.pageData._0Day;
|
||||
let _90Day = monitor.pageData._90Day;
|
||||
let uptime0Day = monitor.pageData.uptime0Day;
|
||||
let uptime90Day = monitor.pageData.uptime90Day;
|
||||
let avgLatency90Day = monitor.pageData.avgLatency90Day;
|
||||
let avgLatency0Day = monitor.pageData.avgLatency0Day;
|
||||
let dailyUps = monitor.pageData.dailyUps;
|
||||
let dailyDown = monitor.pageData.dailyDown;
|
||||
let dailyDegraded = monitor.pageData.dailyDegraded;
|
||||
|
||||
let loading90 = false;
|
||||
let todayDD = getTodayDD();
|
||||
let view = "90day";
|
||||
let _0Day = {};
|
||||
let _90Day = {};
|
||||
|
||||
let statusObj = {
|
||||
UP: "api-up",
|
||||
DEGRADED: "api-degraded",
|
||||
@@ -51,47 +52,16 @@
|
||||
|
||||
let minuteFromMidnightTillNow = getminuteFromMidnightTillNow();
|
||||
|
||||
|
||||
function switchView(s) {
|
||||
view = s;
|
||||
}
|
||||
|
||||
|
||||
const fetchFromFile = async function (day0File, day90File) {
|
||||
//use axios to call POST /api/today
|
||||
let options = {
|
||||
method: "POST",
|
||||
url: "/api/today",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: {
|
||||
day0: day0File,
|
||||
day90: day90File,
|
||||
tz: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
},
|
||||
};
|
||||
let res = await axios(options);
|
||||
const day90 = res.data.day90;
|
||||
_0Day = res.data._0Day;
|
||||
_90Day = res.data._90Day;
|
||||
uptime0Day = res.data.uptime0Day;
|
||||
uptime90Day = res.data.uptime90Day;
|
||||
avgLatency90Day = res.data.avgLatency90Day;
|
||||
avgLatency0Day = res.data.avgLatency0Day;
|
||||
dailyUps = res.data.dailyUps;
|
||||
dailyDown = res.data.dailyDown;
|
||||
dailyDegraded = res.data.dailyDegraded;
|
||||
|
||||
|
||||
loading90 = false;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
fetchFromFile(monitor.path0Day, monitor.path90Day);
|
||||
let xk = Object.keys(_90Day);
|
||||
console.log(">>>>>>---- monitor:66 ", xk[xk.length - 1], xk[0]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<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>
|
||||
@@ -108,7 +78,7 @@
|
||||
<HoverCard.Trigger>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide inline lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Content>
|
||||
<HoverCard.Content class="dark:invert">
|
||||
{monitor.description}
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
@@ -187,9 +157,9 @@
|
||||
<div class="absolute show-hover text-sm bg-background">
|
||||
<div class="text-{bar.cssClass} font-semibold" >
|
||||
{#if bar.message != "No Data"}
|
||||
{new Date(bar.timestamp).toLocaleDateString()} ● {bar.message} ● {bar.avgLatency} ms AVG latency
|
||||
{new Date(bar.timestamp * 1000).toLocaleDateString()} ● {bar.message} ● {bar.avgLatency} ms AVG latency
|
||||
{:else}
|
||||
{new Date(bar.timestamp).toLocaleDateString()} ● {bar.message}
|
||||
{new Date(bar.timestamp * 1000).toLocaleDateString()} ● {bar.message}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,8 +174,8 @@
|
||||
|
||||
</div>
|
||||
<div class="hiddenx relative">
|
||||
<div data-index="{bar.index}" class=" p-2 text-sm rounded font-semibold message bg-black text-white border ">
|
||||
<p><span class="text-{bar.cssClass}">●</span> {new Date(bar.timestamp).toLocaleString()}</p>
|
||||
<div data-index="{ts.index}" class=" p-2 text-sm rounded font-semibold message bg-black text-white border ">
|
||||
<p><span class="text-{bar.cssClass}">●</span> {new Date(bar.timestamp * 1000).toLocaleString()}</p>
|
||||
{#if bar.status != 'NO_DATA'}
|
||||
<p class="pl-4">{bar.status} / {bar.latency}ms</p>
|
||||
{:else}
|
||||
@@ -231,4 +201,4 @@
|
||||
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</section>
|
||||
</section>
|
||||
@@ -1,34 +1,29 @@
|
||||
<script>
|
||||
import { Github } from "lucide-svelte";
|
||||
import { toggleMode } from "mode-watcher";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
export let data;
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
export let data;
|
||||
</script>
|
||||
<div class="one"></div>
|
||||
<header class=" relative z-50 w-full ">
|
||||
|
||||
<header class="relative z-50 w-full">
|
||||
<div class="container flex h-14 items-center">
|
||||
<div class="mr-4 flex blurry-bg ">
|
||||
<a href="{data.site.home}" class="mr-6 flex items-center space-x-2">
|
||||
<img src="{data.site.logo}" class="h-5 w-5" alt="" srcset="">
|
||||
<span class="hidden font-bold sm:inline-block text-[15px] lg:text-base">
|
||||
{data.site.title}
|
||||
</span>
|
||||
<div class="mr-4 flex blurry-bg">
|
||||
<a href="{data.site.home ? data.site.home:'/'}" class="mr-6 flex items-center space-x-2">
|
||||
{#if data.site.logo}
|
||||
<img src="{data.site.logo}" class="h-5 w-5" alt="" srcset="" />
|
||||
{/if}
|
||||
{#if data.site.title}
|
||||
<span class="hidden font-bold sm:inline-block text-[15px] lg:text-base"> {data.site.title} </span>
|
||||
{/if}
|
||||
</a>
|
||||
{#if data.site.nav}
|
||||
<nav class="flex items-center space-x-6 text-sm font-medium">
|
||||
{#each data.site.nav as navItem}
|
||||
{#each data.site.nav as navItem}
|
||||
<a href="{navItem.url}"> {navItem.name} </a>
|
||||
{/each}
|
||||
{/each}
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
{#if data.site.github && data.site.github.visible}
|
||||
<div class="flex flex-1 items-center justify-between space-x-2 sm:space-x-4 md:justify-end">
|
||||
<div class="w-full flex-1 md:w-auto md:flex-none">
|
||||
<a href="https://github.com/{data.site.github.owner}/{data.site.github.repo}" class="{buttonVariants({ variant: "ghost" })} blurry-bg">
|
||||
<Github class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
const GH_TOKEN = process.env.GH_TOKEN;
|
||||
/**
|
||||
* @param {any} url
|
||||
@@ -17,13 +18,27 @@ function getAxiosOptions(url){
|
||||
};
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* @param {any} tagName
|
||||
* @param {{ owner: any; repo: any; }} githubConfig
|
||||
*/
|
||||
async function activeIncident(tagName, githubConfig) {
|
||||
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?labels=${tagName},status&state=open&sort=created&direction=desc`;
|
||||
const sinceHours = githubConfig.incidentSince || 24;
|
||||
const since = moment().subtract(sinceHours, "hours").toISOString();
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?labels=${tagName},incident&state=open&sort=created&direction=desc&since=${since}`;
|
||||
try {
|
||||
const response = await axios.request(getAxiosOptions(url));
|
||||
return response.data;
|
||||
@@ -37,7 +52,9 @@ async function activeIncident(tagName, githubConfig) {
|
||||
* @param {{ owner: any; repo: any; }} githubConfig
|
||||
*/
|
||||
async function pastIncident(tagName, githubConfig) {
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?labels=${tagName},status&state=closed&sort=created&direction=desc`;
|
||||
const sinceHours = githubConfig.incidentSince || 24;
|
||||
const since = moment().subtract(sinceHours, "hours").toISOString();
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?labels=${tagName},incident&state=closed&sort=created&direction=desc&since=${since}`;
|
||||
try {
|
||||
const response = await axios.request(getAxiosOptions(url));
|
||||
return response.data;
|
||||
@@ -51,7 +68,11 @@ async function pastIncident(tagName, githubConfig) {
|
||||
* @param {{ owner: any; repo: any; }} githubConfig
|
||||
*/
|
||||
async function hasActiveIncident(tagName, githubConfig) {
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?labels=${tagName},status&state=open&sort=created&direction=desc&per_page=1`;
|
||||
|
||||
const sinceHours = githubConfig.incidentSince || 24;
|
||||
const since = moment().subtract(sinceHours, "hours").toISOString();
|
||||
|
||||
const url = `https://api.github.com/repos/${githubConfig.owner}/${githubConfig.repo}/issues?labels=${tagName},incident&state=open&sort=created&direction=desc&per_page=1&since=${since}`;
|
||||
try {
|
||||
const response = await axios.request(getAxiosOptions(url));
|
||||
return response.data.length > 0;
|
||||
@@ -74,6 +95,25 @@ async function getCommentsForIssue(issueID, githubConfig) {
|
||||
console.log(error.response.data);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function createIssue(githubConfig, issueTitle, issueBody, issueLabels) {
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
async function updateIssue(githubConfig, issueID) {
|
||||
|
||||
}
|
||||
/*
|
||||
{
|
||||
@@ -87,4 +127,4 @@ async function getCommentsForIssue(issueID, githubConfig) {
|
||||
*/
|
||||
|
||||
|
||||
export { activeIncident, hasActiveIncident, getCommentsForIssue, pastIncident };
|
||||
export { activeIncident, hasActiveIncident, getCommentsForIssue, pastIncident, createIssue };
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// @ts-nocheck
|
||||
// @ts-ignore
|
||||
import fs from "fs-extra";
|
||||
import { GetDayStartTimestampUTC, GetMinuteStartNowTimestampUTC, DurationInMinutes } from "../../../scripts/tool.js";
|
||||
let statusObj = {
|
||||
UP: "api-up",
|
||||
DEGRADED: "api-degraded",
|
||||
DOWN: "api-down",
|
||||
NO_DATA: "api-nodata",
|
||||
};
|
||||
|
||||
function parseUptime(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 String(((up / all) * parseFloat(100)).toFixed(4));
|
||||
}
|
||||
function parsePercentage(n) {
|
||||
if (isNaN(n)) return "-";
|
||||
if (n == 0) {
|
||||
return "0";
|
||||
}
|
||||
if (n == 100) {
|
||||
return "100";
|
||||
}
|
||||
return n.toFixed(4);
|
||||
}
|
||||
const secondsInDay = 24 * 60 * 60;
|
||||
const FetchData = async function (monitor) {
|
||||
let _0Day = {};
|
||||
let _90Day = {};
|
||||
let uptime0Day = "0";
|
||||
let dailyUps = 0;
|
||||
let dailyDown = 0;
|
||||
let percentage90DaysBuildUp = [];
|
||||
let latency90DaysBuildUp = [];
|
||||
let dailyDegraded = 0;
|
||||
let dailyLatencyBuildUp = [];
|
||||
|
||||
const now = GetMinuteStartNowTimestampUTC();
|
||||
const midnight = GetDayStartTimestampUTC(now);
|
||||
const midnightTomorrow = midnight + secondsInDay;
|
||||
const minuteFromMidnightTillNow = DurationInMinutes(midnight, now);
|
||||
const midnight90DaysAgo = GetDayStartTimestampUTC(now - 90 * secondsInDay);
|
||||
|
||||
for (let i = midnight; i <= now; i += 60) {
|
||||
let eachMin = i;
|
||||
_0Day[eachMin] = {
|
||||
timestamp: eachMin,
|
||||
status: "NO_DATA",
|
||||
cssClass: statusObj.NO_DATA,
|
||||
latency: "NA",
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
for (let i = midnight90DaysAgo; i < midnightTomorrow; i += secondsInDay) {
|
||||
let eachDay = i;
|
||||
_90Day[eachDay] = {
|
||||
timestamp: eachDay,
|
||||
UP: 0,
|
||||
DEGRADED: 0,
|
||||
DOWN: 0,
|
||||
uptimePercentage: 0,
|
||||
avgLatency: 0,
|
||||
latency: 0,
|
||||
cssClass: statusObj.NO_DATA,
|
||||
message: "No Data",
|
||||
};
|
||||
}
|
||||
|
||||
let day0 = JSON.parse(fs.readFileSync(monitor.path0Day, "utf8"));
|
||||
let _90DayFileData = JSON.parse(fs.readFileSync(monitor.path90Day, "utf8"));
|
||||
|
||||
//loop 90DayFileData as object
|
||||
for (const timestamp in _90DayFileData) {
|
||||
let cssClass = statusObj.UP;
|
||||
let message = "Status OK";
|
||||
const element = _90DayFileData[timestamp];
|
||||
|
||||
if (element === undefined) continue;
|
||||
|
||||
if (_90Day[timestamp] === undefined) continue;
|
||||
|
||||
_90Day[timestamp].UP = element.UP;
|
||||
_90Day[timestamp].DEGRADED = element.DEGRADED;
|
||||
_90Day[timestamp].DOWN = element.DOWN;
|
||||
_90Day[timestamp].avgLatency = element.avgLatency;
|
||||
_90Day[timestamp].latency = element.latency;
|
||||
|
||||
_90Day[timestamp].uptimePercentage = parseUptime(element.UP + element.DEGRADED, element.UP + element.DEGRADED + element.DOWN);
|
||||
|
||||
if (element.DEGRADED > 0) {
|
||||
cssClass = statusObj.DEGRADED;
|
||||
message = "Degraded for " + element.DEGRADED + " minutes";
|
||||
}
|
||||
|
||||
if (element.DOWN > 0) {
|
||||
cssClass = statusObj.DOWN;
|
||||
message = "Down for " + element.DOWN + " minutes";
|
||||
}
|
||||
|
||||
_90Day[timestamp].cssClass = cssClass;
|
||||
_90Day[timestamp].message = message;
|
||||
}
|
||||
//loop day0 as object
|
||||
for (const timestamp in day0) {
|
||||
const element = day0[timestamp];
|
||||
let status = element.status;
|
||||
let latency = element.latency;
|
||||
|
||||
//0 Day data
|
||||
if (_0Day[timestamp] !== undefined) {
|
||||
_0Day[timestamp].status = status;
|
||||
_0Day[timestamp].cssClass = statusObj[status];
|
||||
_0Day[timestamp].latency = latency;
|
||||
|
||||
dailyUps = status == "UP" ? dailyUps + 1 : dailyUps;
|
||||
dailyDown = status == "DOWN" ? dailyDown + 1 : dailyDown;
|
||||
dailyDegraded = status == "DEGRADED" ? dailyDegraded + 1 : dailyDegraded;
|
||||
dailyLatencyBuildUp.push(latency);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in _90Day) {
|
||||
const element = _90Day[key];
|
||||
if (element.message == "No Data") continue;
|
||||
percentage90DaysBuildUp.push(parseFloat(element.uptimePercentage));
|
||||
latency90DaysBuildUp.push(parseFloat(element.avgLatency));
|
||||
}
|
||||
uptime0Day = parseUptime(dailyUps + dailyDegraded, dailyUps + dailyDown + dailyDegraded);
|
||||
return {
|
||||
_0Day: _0Day,
|
||||
_90Day: _90Day,
|
||||
uptime0Day,
|
||||
uptime90Day: parsePercentage(percentage90DaysBuildUp.reduce((a, b) => a + b, 0) / percentage90DaysBuildUp.length),
|
||||
avgLatency90Day: latency90DaysBuildUp.length > 0 ? (latency90DaysBuildUp.reduce((a, b) => a + b, 0) / latency90DaysBuildUp.length).toFixed(0) : "-",
|
||||
avgLatency0Day: dailyLatencyBuildUp.length > 0 ? (dailyLatencyBuildUp.reduce((a, b) => a + b, 0) / dailyLatencyBuildUp.length).toFixed(0) : "-",
|
||||
dailyUps,
|
||||
dailyDown,
|
||||
dailyDegraded,
|
||||
};
|
||||
};
|
||||
export { FetchData };
|
||||
@@ -6,6 +6,21 @@ import Randomstring from "randomstring";
|
||||
const API_TOKEN = process.env.API_TOKEN;
|
||||
const API_IP = process.env.API_IP;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const store = function(data, authHeader, ip){
|
||||
const tag = data.tag;
|
||||
//remove Bearer from start in authHeader
|
||||
@@ -47,19 +62,12 @@ const store = function(data, authHeader, ip){
|
||||
return { error: err.message, status: 400 };
|
||||
}
|
||||
//check if tag is valid
|
||||
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 { error: err.message, status: 400 };
|
||||
}
|
||||
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);
|
||||
|
||||
//read the monitor.path0Day file
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import fs from "fs-extra";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import { GetDayStartTimestampUTC, GetMinuteStartNowTimestampUTC, DurationInMinutes } from "../../scripts/tool.js";
|
||||
var dt = new Date();
|
||||
let tz = dt.getTimezoneOffset();
|
||||
let startTodayAtTs = GetDayStartTimestampUTC(GetMinuteStartNowTimestampUTC()) + tz * 60;
|
||||
let start90DayAtTs = startTodayAtTs - 90*24*60*60;
|
||||
export async function load({ params, route, url }) {
|
||||
let site = JSON.parse(fs.readFileSync(env.PUBLIC_KENER_FOLDER + "/site.json", "utf8"));
|
||||
return {
|
||||
site: site,
|
||||
timezone: tz,
|
||||
startTodayAtTs: startTodayAtTs,
|
||||
start90DayAtTs: start90DayAtTs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import { hasActiveIncident } from "$lib/server/incident";
|
||||
import { FetchData } from "$lib/server/page";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import fs from "fs-extra";
|
||||
|
||||
@@ -10,10 +11,10 @@ export async function load({ params, route, url, parent }) {
|
||||
const github = siteData.github;
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
monitors[i].hasActiveIncident = await hasActiveIncident(monitors[i].tag, github);
|
||||
let data = await FetchData(monitors[0]);
|
||||
monitors[i].pageData = data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
monitors: monitors,
|
||||
};
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
<div class="mt-32"></div>
|
||||
{#if data.site.hero}
|
||||
<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">
|
||||
<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 data.site.hero.image}
|
||||
<img src="{data.site.hero.image}" class="h-16 w-16 m-auto" alt="" srcset="">
|
||||
|
||||
@@ -12,6 +12,7 @@ let statusObj = {
|
||||
|
||||
function parseUptime(up, all) {
|
||||
if (all === 0) return String("-");
|
||||
if (up == 0) return String("0");
|
||||
if (up == all) {
|
||||
return String(((up / all) * parseFloat(100)).toFixed(0));
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 52 KiB |
@@ -1,6 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" fill="#FF6F61" viewBox="2 6 28 24.1">
|
||||
<g>
|
||||
<path
|
||||
d="M24.66,26a3.83,3.83,0,0,1-2.86-1.32,1.81,1.81,0,0,0-2.95,0,3.76,3.76,0,0,1-5.72,0,1.8,1.8,0,0,0-2.94,0,3.76,3.76,0,0,1-5.72,0A1.9,1.9,0,0,0,3,24a1,1,0,0,1,0-2,3.82,3.82,0,0,1,2.86,1.32A1.9,1.9,0,0,0,7.33,24a1.9,1.9,0,0,0,1.47-.76,3.76,3.76,0,0,1,5.72,0A1.9,1.9,0,0,0,16,24a1.94,1.94,0,0,0,1.48-.76,3.76,3.76,0,0,1,5.72,0,1.81,1.81,0,0,0,2.95,0A3.82,3.82,0,0,1,29,22a1,1,0,0,1,0,2,1.94,1.94,0,0,0-1.48.76A3.82,3.82,0,0,1,24.66,26Zm2.86,2.68A1.94,1.94,0,0,1,29,28a1,1,0,0,0,0-2,3.82,3.82,0,0,0-2.86,1.32,1.81,1.81,0,0,1-2.95,0,3.76,3.76,0,0,0-5.72,0A1.94,1.94,0,0,1,16,28a1.9,1.9,0,0,1-1.47-.76,3.76,3.76,0,0,0-5.72,0A1.9,1.9,0,0,1,7.33,28a1.9,1.9,0,0,1-1.47-.76A3.82,3.82,0,0,0,3,26a1,1,0,0,0,0,2,1.9,1.9,0,0,1,1.47.76,3.76,3.76,0,0,0,5.72,0,1.8,1.8,0,0,1,2.94,0,3.76,3.76,0,0,0,5.72,0,1.81,1.81,0,0,1,2.95,0,3.76,3.76,0,0,0,5.72,0ZM26,6a1,1,0,0,0-1,1c0,.55-1.83,1.08-3.17,1.47-2,.57-4.34,1.25-5.83,2.82C14.51,9.72,12.14,9,10.17,8.47,8.83,8.08,7,7.55,7,7A1,1,0,0,0,5,7a11,11,0,0,0,7.28,10.35,1,1,0,0,1,.72,1V20a1,1,0,0,0,1,1h4a1,1,0,0,0,1-1V18.31a1,1,0,0,1,.72-1A11,11,0,0,0,27,7,1,1,0,0,0,26,6Z"></path>
|
||||
</g>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: SketchAPI 2022.10.18.0 https://api.sketch.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketchjs="https://sketch.io/dtd/" sketchjs:metadata="eyJuYW1lIjoiRHJhd2luZyIsInN1cmZhY2UiOnsiaXNQYWludCI6dHJ1ZSwibWV0aG9kIjoiZmlsbCIsImJsZW5kIjoibm9ybWFsIiwiZW5hYmxlZCI6dHJ1ZSwib3BhY2l0eSI6MSwidHlwZSI6InBhdHRlcm4iLCJwYXR0ZXJuIjp7InR5cGUiOiJwYXR0ZXJuIiwicmVmbGVjdCI6Im5vLXJlZmxlY3QiLCJyZXBlYXQiOiJyZXBlYXQiLCJzbW9vdGhpbmciOmZhbHNlLCJzcmMiOiJ0cmFuc3BhcmVudExpZ2h0Iiwic3giOjEsInN5IjoxLCJ4MCI6MC41LCJ4MSI6MSwieTAiOjAuNSwieTEiOjF9LCJpc0ZpbGwiOnRydWV9LCJjbGlwUGF0aCI6eyJlbmFibGVkIjp0cnVlLCJzdHlsZSI6eyJzdHJva2VTdHlsZSI6ImJsYWNrIiwibGluZVdpZHRoIjoxfX0sImRlc2NyaXB0aW9uIjoiTWFkZSB3aXRoIFNrZXRjaHBhZCIsIm1ldGFkYXRhIjp7fSwiZXhwb3J0RFBJIjo3MiwiZXhwb3J0Rm9ybWF0IjoicG5nIiwiZXhwb3J0UXVhbGl0eSI6MC45NSwidW5pdHMiOiJweCIsIndpZHRoIjo1MTIsImhlaWdodCI6NTEyLCJwYWdlcyI6W3sid2lkdGgiOjUxMiwiaGVpZ2h0Ijo1MTJ9XSwidXVpZCI6IjRiYmQ5NjRmLTdhNDAtNDgzNC04ZDIwLWFiYTVmNTk5ZjcyMiJ9" width="512" height="512" viewBox="0 0 512 512">
|
||||
<path style="fill: #00f030; mix-blend-mode: source-over; fill-opacity: 1; vector-effect: non-scaling-stroke;" sketchjs:tool="rectangle" d="M9.289 0 L25.711 0 Q29.559 0 32.279 2.635 35 5.269 35 8.995 L35 248.005 Q35 251.731 32.279 254.365 29.559 257 25.711 257 L9.289 257 Q5.441 257 2.721 254.365 0 251.731 0 248.005 L0 8.995 Q0 5.269 2.721 2.635 5.441 0 9.289 0 z" transform="matrix(1.4976109878380102,0,0,1.5464877503495498,72,60.25270317732208)"/>
|
||||
<path style="fill: #00f030; mix-blend-mode: source-over; fill-opacity: 1; vector-effect: non-scaling-stroke;" sketchjs:tool="rectangle" d="M9.289 0 L25.711 0 Q29.559 0 32.279 2.635 35 5.269 35 8.995 L35 248.005 Q35 251.731 32.279 254.365 29.559 257 25.711 257 L9.289 257 Q5.441 257 2.721 254.365 0 251.731 0 248.005 L0 8.995 Q0 5.269 2.721 2.635 5.441 0 9.289 0 z" transform="matrix(1.4976109878380102,0,0,1.5464877503495498,180,61.25270317732208)"/>
|
||||
<path style="fill: #00f030; mix-blend-mode: source-over; fill-opacity: 1; vector-effect: non-scaling-stroke;" sketchjs:tool="rectangle" d="M9.289 0 L25.711 0 Q29.559 0 32.279 2.635 35 5.269 35 8.995 L35 248.005 Q35 251.731 32.279 254.365 29.559 257 25.711 257 L9.289 257 Q5.441 257 2.721 254.365 0 251.731 0 248.005 L0 8.995 Q0 5.269 2.721 2.635 5.441 0 9.289 0 z" transform="matrix(1.4976109878380102,0,0,1.5464877503495498,290,60.252703177322076)"/>
|
||||
<path style="fill: #f04800; mix-blend-mode: source-over; fill-opacity: 1; vector-effect: non-scaling-stroke;" sketchjs:tool="rectangle" d="M9.289 0 L25.711 0 Q29.559 0 32.279 2.635 35 5.269 35 8.995 L35 248.005 Q35 251.731 32.279 254.365 29.559 257 25.711 257 L9.289 257 Q5.441 257 2.721 254.365 0 251.731 0 248.005 L0 8.995 Q0 5.269 2.721 2.635 5.441 0 9.289 0 z" transform="matrix(1.4976109878380102,0,0,1.5464877503495498,397,60.252703177322076)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.8 KiB |