mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 07:14:56 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f33c02064 | |||
| 46c1a392b8 | |||
| c6880c5df6 | |||
| 646da94ef9 | |||
| 36ede93dce | |||
| 9ed35589f7 | |||
| e946dd18e0 | |||
| 51cad598c1 | |||
| 0595f23c18 | |||
| b88816706b | |||
| 80edaae023 | |||
| 883c46b064 | |||
| 4248cc31f2 | |||
| 5f81c6d61a | |||
| 00d82cc817 | |||
| a9edfde162 | |||
| 468d7f445d | |||
| 4cc74deece | |||
| f9831490af | |||
| a055a616eb | |||
| 8009a2cbc4 | |||
| 693cc149cf | |||
| d126fe0bce | |||
| 4cc49cce9e | |||
| f7cb4cd805 | |||
| a2bd9883d1 | |||
| 10a079c304 | |||
| 65f12f1082 | |||
| c15f855c72 | |||
| 3d38483022 | |||
| 57dc1e7175 | |||
| ead68c929f |
@@ -5,6 +5,7 @@ KENER_BASE_PATH=""
|
||||
RESEND_API_KEY=
|
||||
ORIGIN=http://localhost:3000
|
||||
DATABASE_URL=sqlite://./database/kener.sqlite.db
|
||||
TZ=UTC
|
||||
RESEND_SENDER_EMAIL=Accounts <accounts@resend.dev>
|
||||
|
||||
# DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydatabase
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Contributing to Kener
|
||||
|
||||
Thank you for considering contributing to our project! Here are some guidelines to help you get started.
|
||||
|
||||
---
|
||||
|
||||
## How to Contribute
|
||||
|
||||
1. Fork the repository and clone it locally.
|
||||
2. Create a new branch for your feature or bug fix:
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
3. Make your changes and commit them:
|
||||
```bash
|
||||
git commit -m 'Describe your changes'
|
||||
```
|
||||
4. Push your changes to your fork:
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
5. Create a pull request to the `main` branch.
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
2. Create a `.env` file in the root of the project and add the following:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
2. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
3. Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is available in the `docs` folder. You can view it by going to [http://localhost:3000/docs/home](http://localhost:3000/docs/home) in your browser.
|
||||
|
||||
## Where to Start
|
||||
|
||||
1. Check out the [roadmap items](https://kener.ing/docs/roadmap/)
|
||||
2. Add language support by following the [i18n guide](https://kener.ing/docs/i18n/)
|
||||
+6
-1
@@ -15,8 +15,10 @@ RUN apt-get update && apt-get install -y \
|
||||
iputils-ping && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the timezone environment variable
|
||||
# Set the timezone environment variable and the application environment
|
||||
ARG KENER_BASE_PATH=
|
||||
ENV TZ=Etc/UTC
|
||||
ENV KENER_BASE_PATH=${KENER_BASE_PATH}
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
@@ -28,6 +30,9 @@ RUN npm install && npm cache clean --force
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# remove dir src/routes/(docs)
|
||||
RUN rm -rf src/routes/\(docs\)
|
||||
|
||||
# Ensure /app/uploads and /app/database have rw permissions
|
||||
RUN mkdir -p /app/uploads /app/database && \
|
||||
chmod -R 777 /app/uploads /app/database
|
||||
|
||||
@@ -103,6 +103,10 @@ If you are using Kener and want to support me, you can do so by sponsoring me on
|
||||
|
||||

|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to contribute to Kener, please read the [Contributing Guide](https://github.com/rajnandan1/kener/blob/main/.github/CONTRIBUTING.md).
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#rajnandan1/kener&Date)
|
||||
|
||||
@@ -27,6 +27,10 @@ DNS Monitor is used to check the status of a DNS server. It can be configured to
|
||||
|
||||
Ping Monitor is used to check the status of a server. It can be configured to check the status of the server at different intervals.
|
||||
|
||||
### Data Interpolation
|
||||
|
||||
Kener interpolates the data for missing intervals. For example, suppose if you have a cron monitor of 5 minutes and it has run twice at 10AM and 10:05AM and the monitor is `DOWN` at 10AM. Kener will interpolate the data for 10:01AM, 10:02AM, 10:03AM and 10:04AM as `DOWN`. Similarly, if the monitor is UP at 10:05AM, Kener will interpolate the data for 10:06AM, 10:07AM, 10:08AM and 10:09AM as UP. Similarly for missing data points it will consider the previous data point. If there is no previous data point, it will consider it as `UP`.
|
||||
|
||||
---
|
||||
|
||||
## Triggers
|
||||
|
||||
@@ -29,4 +29,14 @@ Do not forget to add the base path if you are using a subpath. For example, if y
|
||||
|
||||
### Inline CSS
|
||||
|
||||
To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS there. Do not include `<style>` tags.
|
||||
To add inline css go to Manage kener -> Theme -> Custom CSS and add your CSS there.
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
<div class="note danger">
|
||||
Do not include <style> tags.
|
||||
</div>
|
||||
|
||||
+15
-13
@@ -90,10 +90,6 @@ docker run \
|
||||
|
||||
Or use **Docker Compose** with the example [docker-compose.yaml](https://raw.githubusercontent.com/rajnandan1/kener/main/docker-compose.yml)
|
||||
|
||||
#### Base path
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path. Read more about it [here](/docs/environment-vars/#kener-base-path)
|
||||
|
||||
#### Postgres
|
||||
|
||||
```bash
|
||||
@@ -118,17 +114,23 @@ docker run \
|
||||
rajnandan1/kener
|
||||
```
|
||||
|
||||
#### Base Path
|
||||
#### Base path
|
||||
|
||||
By default kener runs on `/` but you can change it to `/status` or any other path. Read more about it [here](/docs/environment-vars/#kener-base-path).
|
||||
|
||||
<div class="note info">
|
||||
|
||||
- Important: The base path should _**NOT**_ have a trailing slash and should start with `/`
|
||||
- Important: This env variable should be present during both build and run time
|
||||
|
||||
</div>
|
||||
|
||||
Let us say you are running kener on a subpath `/status`. You can set the base path like this:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-p 3000:3000 \
|
||||
-v $(pwd)/uploads:/app/uploads \
|
||||
-e "KENER_BASE_PATH=/status" \
|
||||
-e "KENER_SECRET_KEY=somesecretkey" \
|
||||
-e "DATABASE_URL=mysql://root:password@mysql-container-2.orb.local:3306/kener-2" \
|
||||
-e "ORIGIN=https://www.example.com" \
|
||||
rajnandan1/kener
|
||||
docker build --build-arg KENER_BASE_PATH=/status -t kener .
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 --env-file .env -v $(pwd)/database:/app/uploads -v $(pwd)/database:/app/database kener
|
||||
```
|
||||
|
||||
+4
-4
@@ -33,8 +33,8 @@ You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `monitor`: The monitor url
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
|
||||
Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
|
||||
@@ -79,8 +79,8 @@ Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
|
||||
You can pass the following parameters to the embed code
|
||||
|
||||
- `theme`: You can pass `light` or `dark` theme
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings
|
||||
- `bgc`: Background color of the monitor. Only supports hex color codes. DO NOT include the `#` symbol. Example: `ff0000`
|
||||
- `locale`: The locale of the monitor. You can pass the code of the locale you have enabled in your kener settings. Example: `en`, `fr` etc
|
||||
|
||||
### Demo
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Kener - A Sveltekit NodeJS Status Page System
|
||||
description: Kener is an 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.
|
||||
title: Kener Documentation
|
||||
description: Kener is a feature-rich and modern status page system built with SvelteKit and NodeJS. It is open-source and free to use.
|
||||
---
|
||||
|
||||
# Kener - A Feature Rich & Modern Status Page
|
||||
|
||||
+4
-4
@@ -59,7 +59,7 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
"MONITORING": "MONITORING",
|
||||
"No Data": "No Data",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Incident On %date": "No Incident On %date",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
@@ -69,8 +69,8 @@ Copy and update the translation file in the `locales` folder. The translation fi
|
||||
"Share": "Share",
|
||||
"Share this monitor using a link with others": "Share this monitor using a link with others",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Started about %startedAt ago, lasted for about %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Started about %startedAt ago, still ongoing",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "Started about %startedAt, lasted for about %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Started about %startedAt, still ongoing",
|
||||
"Starts in %startedAt": "Starts in %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Starts in %startedAt, will last for about %lastedFor",
|
||||
"Status": "Status",
|
||||
@@ -129,7 +129,7 @@ Open `src/lib/i18n/client.js` and add the locale code to the `locales` array.
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale";
|
||||
```
|
||||
|
||||
This should be valid for the date-fns library.
|
||||
This should be valid for the date-fns library. You can find the list of locales [here](https://github.com/date-fns/date-fns/blob/9bb51691f201c3ec05ab832acbc5d478f2e5c47a/docs/i18nLocales.md).
|
||||
|
||||
Next add a mapping in the `locales` object.
|
||||
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ The image is used to display a logo or an image for the monitor. It is displayed
|
||||
|
||||
## Description
|
||||
|
||||
The description is used to describe the monitor. It is displayed on the status page and in the admin panel. It is optional.
|
||||
The description is used to describe the monitor. It is displayed on the status page and in the admin panel. It is optional. You can also write html in the description and can use tailwind classes.
|
||||
|
||||
## Cron
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ description: Roadmap for Kener
|
||||
|
||||
☑ ~~Handle uploaded images for docker containers~~
|
||||
|
||||
☐ Introduce sitemap.xml
|
||||
☑ ~~Introduce sitemap.xml~~
|
||||
|
||||
☑ ~~Migrate from moment to date-fns for status page~~ (moment remains in dashboard. Will remove later)
|
||||
|
||||
|
||||
@@ -90,3 +90,17 @@ Add font url
|
||||
### Font Family
|
||||
|
||||
Add font family name
|
||||
|
||||
### Custom CSS
|
||||
|
||||
You can add custom CSS to your status page. This will be added to the head of the page. You can add custom CSS to your status page. This will be added to the head of the page.
|
||||
|
||||
```css
|
||||
.my-class {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
<div class="note danger">
|
||||
Do not include <style> tags.
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import express from "express";
|
||||
import Startup from "./src/lib/server/startup.js";
|
||||
import { GetSiteMap } from "./src/lib/server/controllers/controller.js";
|
||||
import fs from "fs-extra";
|
||||
import knex from "knex";
|
||||
import knexOb from "./knexfile.js";
|
||||
@@ -24,6 +25,10 @@ app.use((req, res, next) => {
|
||||
app.get(base + "/healthcheck", (req, res) => {
|
||||
res.end("ok");
|
||||
});
|
||||
app.get(base + "/sitemap.xml", async (req, res) => {
|
||||
res.header("Content-Type", "application/xml");
|
||||
res.send(await GetSiteMap());
|
||||
});
|
||||
//part /uploads server static files from static/uploads
|
||||
|
||||
//set env variable for upload path
|
||||
|
||||
Generated
+13
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kener",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
@@ -65,6 +65,7 @@
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-awesome-color-picker": "^3.1.4",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte-dnd-action": "^0.9.55",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
@@ -7015,6 +7016,16 @@
|
||||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-dnd-action": {
|
||||
"version": "0.9.55",
|
||||
"resolved": "https://registry.npmjs.org/svelte-dnd-action/-/svelte-dnd-action-0.9.55.tgz",
|
||||
"integrity": "sha512-ljAp1VFwigYCGb+eqnjt+kIkzawGtpUJ/KjapXYkzVZMkoofOWtnU3DVnnx0fo7CpcS4iPfPR7gM6cixh7iQfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"svelte": ">=3.23.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-hmr": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.4",
|
||||
"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.",
|
||||
@@ -59,6 +59,7 @@
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-awesome-color-picker": "^3.1.4",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte-dnd-action": "^0.9.55",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
|
||||
+128
-1
@@ -149,7 +149,132 @@ function IsValidNameServer(nameServer) {
|
||||
const IsValidURL = function (url) {
|
||||
return /^(http|https):\/\/[^ "]+$/.test(url);
|
||||
};
|
||||
function ValidateCronExpression(cronExp) {
|
||||
// Check if expression is provided and is a string
|
||||
if (!cronExp || typeof cronExp !== "string") {
|
||||
return { isValid: false, message: "Cron expression must be a non-empty string" };
|
||||
}
|
||||
|
||||
// Split the expression into its components
|
||||
const fields = cronExp.trim().split(/\s+/);
|
||||
|
||||
// Standard cron should have 5 or 6 fields
|
||||
// minute hour day-of-month month day-of-week [year]
|
||||
if (fields.length < 5 || fields.length > 6) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "Cron expression must have 5 or 6 fields"
|
||||
};
|
||||
}
|
||||
|
||||
// Define field constraints
|
||||
const fieldConstraints = [
|
||||
{ name: "minute", min: 0, max: 59 },
|
||||
{ name: "hour", min: 0, max: 23 },
|
||||
{ name: "day", min: 1, max: 31 },
|
||||
{ name: "month", min: 1, max: 12 },
|
||||
{ name: "weekday", min: 0, max: 6 },
|
||||
{ name: "year", min: 1970, max: 2099 } // Optional field
|
||||
];
|
||||
|
||||
// Valid characters in cron expressions
|
||||
const validChars = /^[\d/*,\-]+$/;
|
||||
|
||||
// Validate each field
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i];
|
||||
const constraint = fieldConstraints[i];
|
||||
|
||||
// Check for valid characters
|
||||
if (!validChars.test(field)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid characters in ${constraint.name} field`
|
||||
};
|
||||
}
|
||||
|
||||
// Handle special characters
|
||||
if (field === "*") {
|
||||
continue; // Asterisk is valid for all fields
|
||||
}
|
||||
|
||||
// Handle lists (comma-separated values)
|
||||
if (field.includes(",")) {
|
||||
const values = field.split(",");
|
||||
for (const value of values) {
|
||||
if (!isValidRange(value, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${value}`
|
||||
};
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle ranges (with hyphens)
|
||||
if (field.includes("-")) {
|
||||
const [start, end] = field.split("-").map(Number);
|
||||
if (
|
||||
start == null ||
|
||||
end == null ||
|
||||
start < constraint.min ||
|
||||
end > constraint.max ||
|
||||
start > end
|
||||
) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${field}`
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle steps (with forward slash)
|
||||
if (field.includes("/")) {
|
||||
const [range, step] = field.split("/");
|
||||
if (range !== "*" && !isValidRange(range, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid range in ${constraint.name} field: ${range}`
|
||||
};
|
||||
}
|
||||
if (!isValidRange(step, 1, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid step value in ${constraint.name} field: ${step}`
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle plain numbers
|
||||
if (!isValidRange(field, constraint.min, constraint.max)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `Invalid value in ${constraint.name} field: ${field}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, message: "Valid cron expression" };
|
||||
}
|
||||
|
||||
function isValidRange(value, min, max) {
|
||||
const num = Number(value);
|
||||
return !isNaN(num) && num >= min && num <= max;
|
||||
}
|
||||
function SortMonitor(monitorSort, resp) {
|
||||
let monitors = [];
|
||||
if (!!monitorSort && monitorSort.length > 0) {
|
||||
monitors = monitorSort.map((id) => resp.find((m) => m.id == id)).filter((m) => !!m);
|
||||
//append any new monitors
|
||||
monitors = [...monitors, ...resp.filter((m) => !monitorSort.includes(m.id))];
|
||||
} else {
|
||||
monitors = resp;
|
||||
}
|
||||
return monitors;
|
||||
}
|
||||
export {
|
||||
siteDataExtractFromDb,
|
||||
storeSiteData,
|
||||
@@ -157,5 +282,7 @@ export {
|
||||
ValidateIpAddress,
|
||||
IsValidHost,
|
||||
IsValidNameServer,
|
||||
IsValidURL
|
||||
IsValidURL,
|
||||
ValidateCronExpression,
|
||||
SortMonitor
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
>
|
||||
{#if !isFuture && incident.state != "RESOLVED"}
|
||||
<span>
|
||||
{l(lang, "Started about %startedAt ago, still ongoing", {
|
||||
{l(lang, "Started about %startedAt, still ongoing", {
|
||||
startedAt
|
||||
})}
|
||||
</span>
|
||||
@@ -53,7 +53,7 @@
|
||||
<span>
|
||||
{l(
|
||||
lang,
|
||||
"Started about %startedAt ago, lasted for about %lastedFor",
|
||||
"Started about %startedAt, lasted for about %lastedFor",
|
||||
{ startedAt, lastedFor }
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import {
|
||||
allRecordTypes,
|
||||
ValidateIpAddress,
|
||||
ValidateCronExpression,
|
||||
IsValidHost,
|
||||
IsValidNameServer
|
||||
} from "$lib/clientTools.js";
|
||||
@@ -130,13 +131,11 @@
|
||||
|
||||
//validating cron
|
||||
const cron = newMonitor.cron;
|
||||
const cronRegex =
|
||||
/^((\*|([0-5]?\d)(-[0-5]?\d)?(\/[1-9]\d*)?|([0-5]?\d(,[0-5]?\d)*))\s+){4}(\*|([0-7](,[0-7])*|([0-7]-[0-7])))$/;
|
||||
if (!cronRegex.test(cron)) {
|
||||
invalidFormMessage = "Invalid Cron";
|
||||
let cronValidation = ValidateCronExpression(cron);
|
||||
if (cronValidation.isValid === false) {
|
||||
invalidFormMessage = "Cron invalid: " + cronValidation.message;
|
||||
return;
|
||||
}
|
||||
|
||||
//if monitor type is API
|
||||
if (newMonitor.monitor_type === "API") {
|
||||
//validating url
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Plus, X, Settings, Bell, Loader } from "lucide-svelte";
|
||||
import { Plus, X, Settings, Bell, Loader, ArrowDownUp, Grip } from "lucide-svelte";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { base } from "$app/paths";
|
||||
@@ -8,10 +8,14 @@
|
||||
import { onMount } from "svelte";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { storeSiteData, SortMonitor } from "$lib/clientTools.js";
|
||||
import { dndzone } from "svelte-dnd-action";
|
||||
import { flip } from "svelte/animate";
|
||||
|
||||
export let categories = [];
|
||||
export let colorDown = "#777";
|
||||
export let colorDegraded = "#777";
|
||||
export let monitorSort = [];
|
||||
let monitors = [];
|
||||
let status = "ACTIVE";
|
||||
let showAddMonitor = false;
|
||||
@@ -88,11 +92,13 @@
|
||||
body: JSON.stringify({ action: "getMonitors", data: { status: status } })
|
||||
});
|
||||
let resp = await apiResp.json();
|
||||
monitors = resp.map((m) => {
|
||||
resp = resp.map((m) => {
|
||||
m.down_trigger = JSON.parse(m.down_trigger);
|
||||
m.degraded_trigger = JSON.parse(m.degraded_trigger);
|
||||
return m;
|
||||
});
|
||||
|
||||
monitors = SortMonitor(monitorSort, resp);
|
||||
} catch (error) {
|
||||
alert("Err2or: " + error);
|
||||
} finally {
|
||||
@@ -178,6 +184,19 @@
|
||||
}
|
||||
shareMenusToggle = true;
|
||||
}
|
||||
const flipDurationMs = 200;
|
||||
function handleSort(e) {
|
||||
dropTargetStyle = {
|
||||
border: "1px solid transparent"
|
||||
};
|
||||
monitors = e.detail.items;
|
||||
monitorSort = monitors.map((m) => m.id);
|
||||
storeSiteData({
|
||||
monitorSort: JSON.stringify(monitorSort)
|
||||
});
|
||||
}
|
||||
let dropTargetStyle;
|
||||
let draggableMenu = false;
|
||||
</script>
|
||||
|
||||
{#if showAddMonitor}
|
||||
@@ -190,7 +209,51 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if draggableMenu}
|
||||
<div
|
||||
class="moldal-container fixed left-0 top-0 z-50 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
|
||||
>
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-fit w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={() => {
|
||||
draggableMenu = false;
|
||||
}}
|
||||
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
|
||||
>
|
||||
<X class="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<div class="content px-4 py-4">
|
||||
<h2 class="text-lg font-semibold">Rearrange Monitors</h2>
|
||||
<div
|
||||
class="mt-4"
|
||||
use:dndzone={{ items: monitors, flipDurationMs, dropTargetStyle }}
|
||||
on:consider={handleSort}
|
||||
on:finalize={handleSort}
|
||||
>
|
||||
{#each monitors as monitor (monitor.id)}
|
||||
<div
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
class="mb-2 rounded-md bg-card p-2"
|
||||
>
|
||||
<Grip class="mr-2 inline h-4 w-4" />
|
||||
{#if !!monitor.image}
|
||||
<img
|
||||
src={base + monitor.image}
|
||||
alt={monitor.name}
|
||||
class="mr-1 inline-block h-4 w-4"
|
||||
/>
|
||||
{/if}
|
||||
{monitor.name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4 flex justify-between">
|
||||
<div class="flex w-40">
|
||||
<Select.Root
|
||||
@@ -219,10 +282,21 @@
|
||||
<Loader class="ml-2 mt-2 inline h-6 w-6 animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
<Button on:click={showAddMonitorSheet}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
Add Monitor
|
||||
</Button>
|
||||
<div>
|
||||
{#if status == "ACTIVE"}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
on:click={() => (draggableMenu = !draggableMenu)}
|
||||
>
|
||||
<ArrowDownUp class=" " />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={showAddMonitorSheet}>
|
||||
<Plus class="mr-2 inline h-6 w-6" />
|
||||
Add Monitor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -240,7 +314,7 @@
|
||||
{monitor.name}
|
||||
</Card.Title>
|
||||
{#if !!monitor.description}
|
||||
<Card.Description>{monitor.description}</Card.Description>
|
||||
<Card.Description>{@html monitor.description}</Card.Description>
|
||||
{/if}
|
||||
<div class="absolute right-2 top-0.5">
|
||||
<Button
|
||||
|
||||
@@ -40,16 +40,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// function handleFileChangeLogo(event) {
|
||||
// const file = event.target.files[0];
|
||||
// if (file) {
|
||||
// const reader = new FileReader();
|
||||
// reader.onload = () => {
|
||||
// siteInformation.logo = reader.result;
|
||||
// };
|
||||
// reader.readAsDataURL(file);
|
||||
// }
|
||||
// }
|
||||
function handleFileChangeFavicon(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
@@ -89,7 +79,7 @@
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
siteInformation.logo = base + "/uploads/" + result.filename;
|
||||
siteInformation.logo = "/uploads/" + result.filename;
|
||||
} else {
|
||||
uploadLogoStatus = "Failed to upload file.";
|
||||
}
|
||||
|
||||
@@ -277,10 +277,7 @@
|
||||
class="text-api-up truncate text-xs font-semibold text-{monitor.pageData
|
||||
.summaryColorClass}"
|
||||
>
|
||||
{l(lang, summaryTime(monitor.pageData.summaryStatus), {
|
||||
status: monitor.pageData.summaryStatus,
|
||||
duration: monitor.pageData.summaryDuration
|
||||
})}
|
||||
{monitor.pageData.summaryStatus}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +301,6 @@
|
||||
on:click={(e) => {
|
||||
dailyDataGetter(e, bar, incidents[ts]);
|
||||
}}
|
||||
style="transition: opacity {bar.ij * 2 + 100}ms ease-in;"
|
||||
href="#"
|
||||
class="oneline h-[34px] w-[6px]
|
||||
{bar.border ? 'opacity-100' : 'opacity-20'} pb-1"
|
||||
@@ -335,10 +331,7 @@
|
||||
selectedLang
|
||||
)}
|
||||
-
|
||||
{l(lang, summaryTime(bar.summaryStatus), {
|
||||
status: bar.summaryStatus,
|
||||
duration: bar.summaryDuration
|
||||
})}
|
||||
{bar.summaryStatus}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -388,9 +381,12 @@
|
||||
>
|
||||
<p>
|
||||
<span class="text-{bar.cssClass}"> ● </span>
|
||||
{new Date(
|
||||
bar.timestamp * 1000
|
||||
).toLocaleTimeString()}
|
||||
|
||||
{f(
|
||||
new Date(bar.timestamp * 1000),
|
||||
"hh:mm a",
|
||||
selectedLang
|
||||
)}
|
||||
</p>
|
||||
{#if bar.status != "NO_DATA"}
|
||||
<p class="pl-2">
|
||||
|
||||
@@ -169,10 +169,7 @@
|
||||
{l(lang, "Embed")}
|
||||
</h2>
|
||||
<p class="mb-1 text-xs text-muted-foreground">
|
||||
{@html l(
|
||||
lang,
|
||||
"Embed this moni2tor using <script> or <iframe> in your app."
|
||||
)}
|
||||
{l(lang, "Embed this monitor using <script> or <iframe> in your app.")}
|
||||
</p>
|
||||
<div class="mb-4 grid grid-cols-2 gap-2">
|
||||
<div class="col-span-1">
|
||||
|
||||
+4
-13
@@ -1,9 +1,9 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko } from "date-fns/locale";
|
||||
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR } from "date-fns/locale";
|
||||
|
||||
const locales = { ru, en: enUS, hi, de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko };
|
||||
const locales = { ru, en: enUS, hi, de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko, "pt-BR": ptBR };
|
||||
|
||||
const f = function (date, formatStr, locale) {
|
||||
return format(date, formatStr, {
|
||||
@@ -19,7 +19,7 @@ const fdn = function (start, locale) {
|
||||
};
|
||||
const fdm = function (duration, locale) {
|
||||
return formatDuration(duration, {
|
||||
format: ["hours", "minutes"],
|
||||
format: ["days", "hours", "minutes"],
|
||||
zero: false,
|
||||
delimiter: " ",
|
||||
locale: locales[locale]
|
||||
@@ -27,15 +27,7 @@ const fdm = function (duration, locale) {
|
||||
};
|
||||
|
||||
const l = function (sessionLangMap, key, args = {}) {
|
||||
const keys = key.split(".");
|
||||
let obj = sessionLangMap;
|
||||
|
||||
for (const keyPart of keys) {
|
||||
obj = obj?.[keyPart];
|
||||
if (!obj) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let obj = sessionLangMap[key];
|
||||
|
||||
// Replace placeholders in the string using the args object
|
||||
if (obj && typeof obj === "string") {
|
||||
@@ -44,7 +36,6 @@ const l = function (sessionLangMap, key, args = {}) {
|
||||
return args[argKey] !== undefined ? args[argKey] : placeholder;
|
||||
});
|
||||
}
|
||||
|
||||
return obj || key;
|
||||
};
|
||||
const summaryTime = function (summaryStatus) {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "ÜBERWACHUNG",
|
||||
"No Data": "Keine Daten",
|
||||
"No Incidents": "Keine Vorfälle",
|
||||
"No Incident On %date": "Kein Vorfall am %date",
|
||||
"No Incident in %date": "Kein Vorfall am %date",
|
||||
"No Monitor Found": "Kein Monitor gefunden",
|
||||
"No Updates Yet": "Noch keine Updates",
|
||||
"Ongoing Incidents": "Laufende Vorfälle",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "Teilen",
|
||||
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "Begann vor etwa %startedAt und dauerte etwa %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Begann vor etwa %startedAt und dauert noch an",
|
||||
"Starts in %startedAt": "Beginnt in %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Beginnt in %startedAt und wird etwa %lastedFor dauern",
|
||||
"Status": "Status",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "OVERVÅGNING",
|
||||
"No Data": "Ingen Data",
|
||||
"No Incidents": "Ingen Hændelser",
|
||||
"No Incident On %date": "Ingen Hændelse Den %date",
|
||||
"No Incident in %date": "Ingen Hændelse Den %date",
|
||||
"No Monitor Found": "Ingen Monitor Fundet",
|
||||
"No Updates Yet": "Ingen Opdateringer Endnu",
|
||||
"Ongoing Incidents": "Igangværende Hændelser",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "Del",
|
||||
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "Startede for cirka %startedAt siden, varede i cirka %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Startede for cirka %startedAt siden, stadig igangværende",
|
||||
"Starts in %startedAt": "Starter om %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Starter om %startedAt, vil vare i cirka %lastedFor",
|
||||
"Status": "Status",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "MONITORING",
|
||||
"No Data": "No Data",
|
||||
"No Incidents": "No Incidents",
|
||||
"No Incident On %date": "No Incident On %date",
|
||||
"No Incident in %date": "No Incident in %date",
|
||||
"No Monitor Found": "No Monitor Found",
|
||||
"No Updates Yet": "No Updates Yet",
|
||||
"Ongoing Incidents": "Ongoing Incidents",
|
||||
@@ -40,10 +40,10 @@
|
||||
"Share": "Share",
|
||||
"Share this monitor using a link with others": "Share this monitor using a link with others",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Started about %startedAt ago, lasted for about %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Started about %startedAt ago, still ongoing",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "Started %startedAt, lasted for about %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Started %startedAt, still ongoing",
|
||||
"Starts in %startedAt": "Starts in %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Starts in %startedAt, will last for about %lastedFor",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Starts in %startedAt, will last for %lastedFor",
|
||||
"Status": "Status",
|
||||
"Status OK": "Status OK",
|
||||
"Theme": "Theme",
|
||||
@@ -52,4 +52,4 @@
|
||||
"Updates": "Updates",
|
||||
"Uptime": "Uptime",
|
||||
"%status for %duration": "%status for %duration"
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "En surveillance",
|
||||
"No Data": "Pas de données",
|
||||
"No Incidents": "Pas d'incidents",
|
||||
"No Incident On %date": "Pas d'incident le %date",
|
||||
"No Incident in %date": "Pas d'incident le %date",
|
||||
"No Monitor Found": "Aucun moniteur trouvé",
|
||||
"No Updates Yet": "Pas encore de mises à jour",
|
||||
"Ongoing Incidents": "Incidents en cours",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "Partager",
|
||||
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
|
||||
"Standard": "Standard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "Commencé il y a environ %startedAt, a duré environ %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Commencé il y a environ %startedAt, toujours en cours",
|
||||
"Starts in %startedAt": "Commence dans %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Commence dans %startedAt, durera environ %lastedFor",
|
||||
"Status": "Statut",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "निगरानी",
|
||||
"No Data": "कोई डेटा नहीं",
|
||||
"No Incidents": "कोई घटना नहीं",
|
||||
"No Incident On %date": "%date को कोई घटना नहीं",
|
||||
"No Incident in %date": "%date को कोई घटना नहीं",
|
||||
"No Monitor Found": "कोई मॉनिटर नहीं मिला",
|
||||
"No Updates Yet": "अभी तक कोई अपडेट नहीं",
|
||||
"Ongoing Incidents": "चल रही घटनाएँ",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "साझा करें",
|
||||
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
|
||||
"Standard": "मानक",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "लगभग %startedAt पहले शुरू हुआ, लगभग %lastedFor तक चला",
|
||||
"Started about %startedAt ago, still ongoing": "लगभग %startedAt पहले शुरू हुआ, अभी भी जारी है",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "लगभग %startedAt पहले शुरू हुआ, लगभग %lastedFor तक चला",
|
||||
"Started about %startedAt, still ongoing": "लगभग %startedAt पहले शुरू हुआ, अभी भी जारी है",
|
||||
"Starts in %startedAt": "%startedAt में शुरू होगा",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "%startedAt में शुरू होगा, लगभग %lastedFor तक चलेगा",
|
||||
"Status": "स्थिति",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "モニタリング中",
|
||||
"No Data": "データなし",
|
||||
"No Incidents": "インシデントなし",
|
||||
"No Incident On %date": "%date にインシデントなし",
|
||||
"No Incident in %date": "%date にインシデントなし",
|
||||
"No Monitor Found": "モニターが見つかりません",
|
||||
"No Updates Yet": "まだ更新はありません",
|
||||
"Ongoing Incidents": "進行中のインシデント",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "共有",
|
||||
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
|
||||
"Standard": "標準",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
|
||||
"Started about %startedAt ago, still ongoing": "%startedAt 前に開始し、まだ進行中です",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "%startedAt 前に開始し、約 %lastedFor 続きました",
|
||||
"Started about %startedAt, still ongoing": "%startedAt 前に開始し、まだ進行中です",
|
||||
"Starts in %startedAt": "%startedAt 後に開始",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 後に開始し、約 %lastedFor 続きます",
|
||||
"Status": "ステータス",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "감시 중",
|
||||
"No Data": "데이터 없음",
|
||||
"No Incidents": "사건 없음",
|
||||
"No Incident On %date": "%date 에는 아무 사건도 없었음",
|
||||
"No Incident in %date": "%date 에는 아무 사건도 없었음",
|
||||
"No Monitor Found": "모니터링 항목을 찾을 수 없음",
|
||||
"No Updates Yet": "아직 아무 업데이트도 없음",
|
||||
"Ongoing Incidents": "진행중 사건",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "공유",
|
||||
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
|
||||
"Standard": "기본",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
|
||||
"Started about %startedAt ago, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 동안 지속되었습니다",
|
||||
"Started about %startedAt, still ongoing": "%startedAt 전에 시작되었으며, 아직 진행중입니다",
|
||||
"Starts in %startedAt": "%startedAt 에 시작되었습니다",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 전에 시작되었으며, %lastedFor 까지 지속될 예정입니다",
|
||||
"Status": "상태",
|
||||
|
||||
@@ -42,5 +42,9 @@
|
||||
{
|
||||
"code": "ko",
|
||||
"name": "한국어"
|
||||
},
|
||||
{
|
||||
"code": "pt-BR",
|
||||
"name": "Português Brasileiro"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "MONITOREN",
|
||||
"No Data": "Geen gegevens",
|
||||
"No Incidents": "Geen incidenten",
|
||||
"No Incident On %date": "Geen incident op %date",
|
||||
"No Incident in %date": "Geen incident op %date",
|
||||
"No Monitor Found": "Geen monitor gevonden",
|
||||
"No Updates Yet": "Nog geen updates",
|
||||
"Ongoing Incidents": "Lopende incidenten",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "Delen",
|
||||
"Share this monitor using a link with others": "Deel deze monitor met anderen via een link",
|
||||
"Standard": "Standaard",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "Begonnen ongeveer %startedAt geleden, duurde ongeveer %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Begonnen ongeveer %startedAt geleden, nog steeds bezig",
|
||||
"Starts in %startedAt": "Begint over %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Begint over %startedAt, zal ongeveer %lastedFor duren",
|
||||
"Status": "Status",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"14 Days": "14 Dias",
|
||||
"30 Days": "30 Dias",
|
||||
"60 Days": "60 Dias",
|
||||
"7 Days": "7 Dias",
|
||||
"90 Days": "90 Dias",
|
||||
"Availability per Component": "Disponibilidade por Componente",
|
||||
"Back": "Voltar",
|
||||
"Badge": "Emblema",
|
||||
"Badge Copied": "Emblema Copiado",
|
||||
"Code Copied": "Código Copiado",
|
||||
"Copy Code": "Copiar Código",
|
||||
"Copy Link": "Copiar Link",
|
||||
"Dark": "Escuro",
|
||||
"Days": "Dias",
|
||||
"DEGRADED": "FALHA",
|
||||
"DOWN": "INDISPONÍVEL",
|
||||
"Embed": "Incorporar",
|
||||
"Embed this monitor using <script> or <iframe> in your app.": "Incorporar este monitor ao seu aplicativo utilizando <script> ou <iframe>.",
|
||||
"Get SVG badge for this monitor": "Obter emblema SVG deste monitor",
|
||||
"Get a LIVE Status for this monitor": "Obter indicador de status AO VIVO",
|
||||
"IDENTIFIED": "IDENTIFICADO",
|
||||
"Incident Updates": "Atualizações do Incidente",
|
||||
"INVESTIGATING": "INVESTIGANDO",
|
||||
"Lasted for about %lastedFor": "Durou aproximadamente %lastedFor",
|
||||
"Light": "Claro",
|
||||
"Link Copied": "Link Copiado",
|
||||
"LIVE Status": "Status AO VIVO",
|
||||
"Mode": "Modo",
|
||||
"MONITORING": "MONITORANDO",
|
||||
"No Data": "Sem dados",
|
||||
"No Incidents": "Sem Incidentes",
|
||||
"No Incident in %date": "Nenhum incidente em %date",
|
||||
"No Monitor Found": "Nenhum monitor encontrado",
|
||||
"No Updates Yet": "Nenhuma atualização ainda",
|
||||
"Ongoing Incidents": "Incidentes em Andamento",
|
||||
"Pinging": "Animado",
|
||||
"Recent Incidents": "Incidentes Recentes",
|
||||
"RESOLVED": "RESOLVIDO",
|
||||
"Share": "Compartilhar",
|
||||
"Share this monitor using a link with others": "Compartilhar este monitor através do link",
|
||||
"Standard": "Padrão",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "Iniciado há cerca de %startedAt e durou aproximadamente %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Iniciado há cerca de %startedAt, ainda em andamento",
|
||||
"Starts in %startedAt": "Inicia em %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Inícia em %startedAt, com duração de aproximadamente %lastedFor",
|
||||
"Status": "Status",
|
||||
"Status OK": "Status OK",
|
||||
"Theme": "Tema",
|
||||
"Today": "Hoje",
|
||||
"UP": "OPERACIONAL",
|
||||
"Updates": "Atualizações",
|
||||
"Uptime": "Tempo de Atividade",
|
||||
"%status for %duration": "%status por %duration"
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "МОНИТОРИНГ",
|
||||
"No Data": "Нет данных",
|
||||
"No Incidents": "Нет инцидентов",
|
||||
"No Incident On %date": "Нет инцидентов на %date",
|
||||
"No Incident in %date": "Нет инцидентов на %date",
|
||||
"No Monitor Found": "Монитор не найден",
|
||||
"No Updates Yet": "Пока нет обновлений",
|
||||
"Ongoing Incidents": "Текущие инциденты",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "Поделиться",
|
||||
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
|
||||
"Standard": "Стандартная",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Началось около %startedAt назад, все еще продолжается",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "Началось около %startedAt назад, длилось около %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Началось около %startedAt назад, все еще продолжается",
|
||||
"Starts in %startedAt": "Начнется через %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Начнется через %startedAt, продлится около %lastedFor",
|
||||
"Status": "Статус",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "Đang giám sát",
|
||||
"No Data": "Không có dữ liệu",
|
||||
"No Incidents": "Không có sự cố",
|
||||
"No Incident On %date": "Không có sự cố vào %date",
|
||||
"No Incident in %date": "Không có sự cố vào %date",
|
||||
"No Monitor Found": "Không tìm thấy trình theo dõi",
|
||||
"No Updates Yet": "Chưa có cập nhật",
|
||||
"Ongoing Incidents": "Sự cố đang diễn ra",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "Chia sẻ",
|
||||
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
|
||||
"Standard": "Tiêu chuẩn",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "Bắt đầu khoảng %startedAt trước, kéo dài khoảng %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "Bắt đầu khoảng %startedAt trước, vẫn đang diễn ra",
|
||||
"Starts in %startedAt": "Bắt đầu trong %startedAt",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "Bắt đầu trong %startedAt, sẽ kéo dài khoảng %lastedFor",
|
||||
"Status": "Trạng thái",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"MONITORING": "正在监控",
|
||||
"No Data": "无数据",
|
||||
"No Incidents": "无事件",
|
||||
"No Incident On %date": "%date 无事件",
|
||||
"No Incident in %date": "%date 无事件",
|
||||
"No Monitor Found": "未找到监控",
|
||||
"No Updates Yet": "尚无更新",
|
||||
"Ongoing Incidents": "正在处理的事件",
|
||||
@@ -40,8 +40,8 @@
|
||||
"Share": "分享",
|
||||
"Share this monitor using a link with others": "通过链接与他人分享此监控",
|
||||
"Standard": "标准",
|
||||
"Started about %startedAt ago, lasted for about %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
|
||||
"Started about %startedAt ago, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
|
||||
"Started about %startedAt, lasted for about %lastedFor": "大约 %startedAt 前开始,持续了约 %lastedFor",
|
||||
"Started about %startedAt, still ongoing": "大约 %startedAt 前开始,目前仍在进行中",
|
||||
"Starts in %startedAt": "%startedAt 后开始",
|
||||
"Starts in %startedAt, will last for about %lastedFor": "%startedAt 后开始,将持续约 %lastedFor",
|
||||
"Status": "状态",
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
IsValidI18n,
|
||||
IsValidAnalytics,
|
||||
IsValidColors,
|
||||
IsValidJSONString
|
||||
IsValidJSONString,
|
||||
IsValidJSONArray
|
||||
} from "./validators.js";
|
||||
import db from "../db/db.js";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
import { format, subMonths, addMonths, startOfMonth } from "date-fns";
|
||||
|
||||
const saltRounds = 10;
|
||||
const DUMMY_SECRET = "DUMMY_SECRET";
|
||||
@@ -133,6 +135,11 @@ const siteDataKeys = [
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object"
|
||||
},
|
||||
{
|
||||
key: "monitorSort",
|
||||
isValid: IsValidJSONArray,
|
||||
data_type: "object"
|
||||
},
|
||||
{
|
||||
key: "categories",
|
||||
isValid: IsValidJSONString,
|
||||
@@ -381,6 +388,41 @@ export const HashString = (str) => {
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
export const InterpolateData = (data, start, anchorStatus, e) => {
|
||||
let finalData = [];
|
||||
let status = anchorStatus || "UP";
|
||||
let end = start;
|
||||
if (!!data && data.length > 0) {
|
||||
end = data[data.length - 1].timestamp;
|
||||
}
|
||||
|
||||
if (e) {
|
||||
end = e;
|
||||
}
|
||||
let dataMap = data.reduce((acc, d) => {
|
||||
acc[d.timestamp] = d;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (let i = start; i <= end; i += 60) {
|
||||
let nowData = dataMap[i];
|
||||
if (!!nowData) {
|
||||
status = nowData.status;
|
||||
}
|
||||
|
||||
finalData.push({ timestamp: i, status: status });
|
||||
}
|
||||
return finalData;
|
||||
};
|
||||
|
||||
export const GetLastStatusBefore = async (monitor_tag, timestamp) => {
|
||||
let data = await db.getLastStatusBefore(monitor_tag, timestamp);
|
||||
if (data) {
|
||||
return data.status;
|
||||
}
|
||||
return "UP";
|
||||
};
|
||||
|
||||
export const GetDataGroupByDayAlternative = async (
|
||||
monitor_tag,
|
||||
start,
|
||||
@@ -394,7 +436,10 @@ export const GetDataGroupByDayAlternative = async (
|
||||
|
||||
const offsetSeconds = offsetMinutes * 60;
|
||||
|
||||
const rawData = await db.getDataGroupByDayAlternative(monitor_tag, start, end);
|
||||
let rawData = await db.getDataGroupByDayAlternative(monitor_tag, start, end);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor_tag, start);
|
||||
rawData = InterpolateData(rawData, start, anchorStatus, rawData.length == 0 ? end - 60 : null);
|
||||
|
||||
const groupedData = rawData.reduce((acc, row) => {
|
||||
// Calculate day group considering timezone offset
|
||||
const dayGroup = Math.floor((row.timestamp + offsetSeconds) / 86400);
|
||||
@@ -700,6 +745,9 @@ export const InsertNewAlert = async (data) => {
|
||||
};
|
||||
|
||||
export const CookieConfig = () => {
|
||||
//get base path from env
|
||||
let cookiePath = !!process.env.KENER_BASE_PATH ? process.env.KENER_BASE_PATH : "/";
|
||||
|
||||
let isSecuredDomain = false;
|
||||
if (!!process.env.ORIGIN) {
|
||||
isSecuredDomain = process.env.ORIGIN.startsWith("https://");
|
||||
@@ -709,13 +757,13 @@ export const CookieConfig = () => {
|
||||
secure: isSecuredDomain,
|
||||
maxAge: 365 * 24 * 60 * 60, // 1 year in seconds
|
||||
httpOnly: true,
|
||||
sameSite: "lax"
|
||||
sameSite: "lax",
|
||||
path: cookiePath
|
||||
};
|
||||
};
|
||||
export const GetLocaleFromCookie = (site, cookies) => {
|
||||
let selectedLang = "en";
|
||||
let selectedLang = site.i18n?.defaultLocale || "en";
|
||||
const localLangCookie = cookies.get("localLang");
|
||||
|
||||
if (!!localLangCookie && site.i18n?.locales.find((l) => l.code === localLangCookie)) {
|
||||
selectedLang = localLangCookie;
|
||||
} else if (site.i18n?.defaultLocale && site.i18n?.locales[site.i18n.defaultLocale]) {
|
||||
@@ -758,3 +806,93 @@ export const IsLoggedInSession = async (cookies) => {
|
||||
user: userDB
|
||||
};
|
||||
};
|
||||
|
||||
export const GetSiteMap = async (cookies) => {
|
||||
let siteMapData = [];
|
||||
let siteURLData = await GetSiteDataByKey("siteURL");
|
||||
let categories = await GetSiteDataByKey("categories");
|
||||
let navs = await GetSiteDataByKey("nav");
|
||||
if (!!siteURLData) {
|
||||
siteMapData.push({
|
||||
url: siteURLData,
|
||||
lastmod: new Date().toISOString(),
|
||||
priority: 1
|
||||
});
|
||||
}
|
||||
|
||||
//get today's date in January-2025 format date-fns
|
||||
const today = format(new Date(), "MMMM-yyyy");
|
||||
//last month
|
||||
const lastMonth = format(subMonths(new Date(), 1), "MMMM-yyyy", { addMonths: -1 });
|
||||
const nextMonth = format(addMonths(new Date(), 1), "MMMM-yyyy", { addMonths: -1 });
|
||||
|
||||
siteMapData.push({
|
||||
url: siteURLData + "/incidents/" + today,
|
||||
lastmod: startOfMonth(new Date()).toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
siteMapData.push({
|
||||
url: siteURLData + "/incidents/" + lastMonth,
|
||||
lastmod: startOfMonth(new Date()).toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
siteMapData.push({
|
||||
url: siteURLData + "/incidents/" + nextMonth,
|
||||
lastmod: startOfMonth(new Date()).toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
|
||||
if (!!categories) {
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
if (categories[i].name !== "Home") {
|
||||
siteMapData.push({
|
||||
url: siteURLData + "?category=" + categories[i].name,
|
||||
lastmod: new Date().toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!!navs) {
|
||||
for (let i = 0; i < navs.length; i++) {
|
||||
if (navs[i].url.startsWith(siteURLData)) {
|
||||
siteMapData.push({
|
||||
url: navs[i].url,
|
||||
lastmod: new Date().toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
} else if (navs[i].url.startsWith("/")) {
|
||||
siteMapData.push({
|
||||
url: siteURLData + navs[i].url,
|
||||
lastmod: new Date().toISOString(),
|
||||
priority: 0.9
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
siteMapData.push({
|
||||
url: siteURLData + "?monitor=" + monitors[i].tag,
|
||||
lastmod: new Date(monitors[i].updated_at).toISOString(),
|
||||
priority: 0.8
|
||||
});
|
||||
}
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${siteMapData
|
||||
.map(
|
||||
(page) => `
|
||||
<url>
|
||||
<loc>${page.url}</loc>
|
||||
<lastmod>${page.lastmod}</lastmod>
|
||||
<priority>${page.priority}</priority>
|
||||
</url>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</urlset>`;
|
||||
};
|
||||
|
||||
@@ -37,6 +37,16 @@ export function IsValidJSONString(data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//IsValidJSONArray
|
||||
export function IsValidJSONArray(data) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return Array.isArray(data);
|
||||
}
|
||||
|
||||
export function IsValidNav(nav) {
|
||||
try {
|
||||
nav = JSON.parse(nav);
|
||||
|
||||
@@ -5,7 +5,8 @@ import { UP, DOWN, DEGRADED } from "./constants.js";
|
||||
import {
|
||||
GetMinuteStartNowTimestampUTC,
|
||||
ReplaceAllOccurrences,
|
||||
GetRequiredSecrets
|
||||
GetRequiredSecrets,
|
||||
Wait
|
||||
} from "./tool.js";
|
||||
|
||||
import alerting from "./alerting.js";
|
||||
@@ -28,6 +29,11 @@ const alertingQueue = new Queue({
|
||||
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
|
||||
autostart: true // Automatically start the queue (optional)
|
||||
});
|
||||
const apiQueue = new Queue({
|
||||
concurrency: 10, // Number of tasks that can run concurrently
|
||||
timeout: 10000, // Timeout in ms after which a task will be considered as failed (optional)
|
||||
autostart: true // Automatically start the queue (optional)
|
||||
});
|
||||
|
||||
const defaultEval = `(function (statusCode, responseTime, responseData) {
|
||||
let statusCodeShort = Math.floor(statusCode/100);
|
||||
@@ -137,7 +143,7 @@ const pingCall = async (hostsV4, hostsV6) => {
|
||||
};
|
||||
const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorEval) => {
|
||||
let axiosHeaders = {};
|
||||
axiosHeaders["User-Agent"] = "Kener/2.0.0";
|
||||
axiosHeaders["User-Agent"] = "Kener/3.0.2";
|
||||
axiosHeaders["Accept"] = "*/*";
|
||||
const start = Date.now();
|
||||
//replace all secrets
|
||||
@@ -183,7 +189,7 @@ const apiCall = async (envSecrets, url, method, headers, body, timeout, monitorE
|
||||
statusCode = data.status;
|
||||
resp = data.data;
|
||||
} catch (err) {
|
||||
console.log("Error in apiCall", err.message);
|
||||
console.log(`Error in apiCall ${url}`, err.message);
|
||||
if (err.message.startsWith("timeout of") && err.message.endsWith("exceeded")) {
|
||||
timeoutError = true;
|
||||
}
|
||||
@@ -334,6 +340,36 @@ const Minuter = async (monitor) => {
|
||||
);
|
||||
|
||||
realTimeData[startOfMinute] = apiResponse;
|
||||
if (apiResponse.type === TIMEOUT) {
|
||||
apiQueue.push(async (cb) => {
|
||||
await Wait(500); //wait for 500ms
|
||||
console.log(
|
||||
"Retrying api call for " +
|
||||
monitor.name +
|
||||
" at " +
|
||||
startOfMinute +
|
||||
" due to timeout"
|
||||
);
|
||||
apiCall(
|
||||
envSecrets,
|
||||
monitor.type_data.url,
|
||||
monitor.type_data.method,
|
||||
JSON.stringify(monitor.type_data.headers),
|
||||
monitor.type_data.body,
|
||||
monitor.type_data.timeout,
|
||||
monitor.type_data.eval
|
||||
).then(async (data) => {
|
||||
await db.insertMonitoringData({
|
||||
monitor_tag: monitor.tag,
|
||||
timestamp: startOfMinute,
|
||||
status: data.status,
|
||||
latency: data.latency,
|
||||
type: data.type
|
||||
});
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (monitor.monitor_type === "PING") {
|
||||
let pingResponse = await pingCall(monitor.type_data.hostsV4, monitor.type_data.hostsV6);
|
||||
realTimeData[startOfMinute] = pingResponse;
|
||||
@@ -395,4 +431,10 @@ alertingQueue.start((err) => {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
apiQueue.start((err) => {
|
||||
if (err) {
|
||||
console.error("Error occurred:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
export { Minuter };
|
||||
|
||||
@@ -56,6 +56,16 @@ class DbImpl {
|
||||
.first();
|
||||
}
|
||||
|
||||
//get the last status before the timestamp given monitor_tag and start timestamp
|
||||
async getLastStatusBefore(monitor_tag, timestamp) {
|
||||
return await this.knex("monitoring_data")
|
||||
.where("monitor_tag", monitor_tag)
|
||||
.where("timestamp", "<", timestamp)
|
||||
.orderBy("timestamp", "desc")
|
||||
.limit(1)
|
||||
.first();
|
||||
}
|
||||
|
||||
async getDataGroupByDayAlternative(monitor_tag, start, end) {
|
||||
// Fetch all raw data
|
||||
//{ timestamp: 1732900380, status: 'UP', latency: 42 }
|
||||
|
||||
+51
-47
@@ -9,8 +9,12 @@ import {
|
||||
GetDayStartTimestampUTC
|
||||
} from "$lib/server/tool.js";
|
||||
import { formatDuration, intervalToDuration } from "date-fns";
|
||||
import { fdm } from "$lib/i18n/client";
|
||||
import { GetDataGroupByDayAlternative } from "$lib/server/controllers/controller.js";
|
||||
import { fdm, l, summaryTime } from "$lib/i18n/client";
|
||||
import {
|
||||
GetDataGroupByDayAlternative,
|
||||
InterpolateData,
|
||||
GetLastStatusBefore
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
|
||||
function getSummaryDuration(numOfMinute, selectedLang) {
|
||||
// Convert minutes to milliseconds and create duration object
|
||||
@@ -33,12 +37,12 @@ function getTimezoneOffset(timeZone) {
|
||||
return parseInt(hours) * 60 + parseInt(minutes);
|
||||
}
|
||||
|
||||
function returnStatusClass(val, up, c, barStyle) {
|
||||
function returnStatusClass(val, total, c, barStyle) {
|
||||
// return "api-degraded-10";
|
||||
if (barStyle === undefined || barStyle == "FULL" || up == 0) {
|
||||
if (barStyle === undefined || barStyle == "FULL") {
|
||||
return c;
|
||||
} else if (barStyle == "PARTIAL") {
|
||||
let totalHeight = 24 * 60;
|
||||
let totalHeight = total;
|
||||
let cl = `api-up`;
|
||||
if (val > 0 && val <= 0.1 * totalHeight) {
|
||||
cl = c + "-10";
|
||||
@@ -72,10 +76,9 @@ function getCountOfSimilarStatuesEnd(arr, statusType) {
|
||||
return count;
|
||||
}
|
||||
|
||||
const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
|
||||
const secondsInDay = 24 * 60 * 60;
|
||||
//get offset from utc in minutes
|
||||
|
||||
const now = GetMinuteStartNowTimestampUTC() + 60;
|
||||
const midnight = BeginningOfDay({ timeZone: localTz });
|
||||
const midnight90DaysAgo = midnight - 90 * 24 * 60 * 60;
|
||||
@@ -84,24 +87,6 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
let offsetInMinutes = parseInt((GetDayStartTimestampUTC(now) - midnight) / 60);
|
||||
const _90Day = {};
|
||||
let latestTimestamp = 0;
|
||||
let ij = 0;
|
||||
for (let i = midnight90DaysAgo; i < midnightTomorrow; i += secondsInDay) {
|
||||
_90Day[i] = {
|
||||
UP: 0,
|
||||
DEGRADED: 0,
|
||||
DOWN: 0,
|
||||
timestamp: i,
|
||||
cssClass: StatusObj.NO_DATA,
|
||||
textClass: StatusObj.NO_DATA,
|
||||
summaryDuration: 0,
|
||||
summaryStatus: NO_DATA,
|
||||
message: NO_DATA,
|
||||
border: true,
|
||||
ij: ij
|
||||
};
|
||||
ij++;
|
||||
latestTimestamp = i;
|
||||
}
|
||||
|
||||
let dbData = await GetDataGroupByDayAlternative(
|
||||
monitor.tag,
|
||||
@@ -113,18 +98,13 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
let totalDownCount = 0;
|
||||
let totalUpCount = 0;
|
||||
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = "UP";
|
||||
|
||||
let summaryColorClass = "api-nodata";
|
||||
for (let i = 0; i < dbData.length; i++) {
|
||||
let dayData = dbData[i];
|
||||
let ts = dayData.timestamp;
|
||||
let cssClass = StatusObj.UP;
|
||||
let message = "Status OK";
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = "UP";
|
||||
|
||||
latestTimestamp = ts;
|
||||
totalDegradedCount += dayData.DEGRADED;
|
||||
totalDownCount += dayData.DOWN;
|
||||
totalUpCount += dayData.UP;
|
||||
@@ -132,7 +112,7 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
if (dayData.DEGRADED >= monitor.day_degraded_minimum_count) {
|
||||
cssClass = returnStatusClass(
|
||||
dayData.DEGRADED,
|
||||
dayData.UP,
|
||||
dayData.total,
|
||||
StatusObj.DEGRADED,
|
||||
site.barStyle
|
||||
);
|
||||
@@ -140,18 +120,30 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
summaryStatus = "DEGRADED";
|
||||
}
|
||||
if (dayData.DOWN >= monitor.day_down_minimum_count) {
|
||||
cssClass = returnStatusClass(dayData.DOWN, dayData.UP, StatusObj.DOWN, site.barStyle);
|
||||
cssClass = returnStatusClass(
|
||||
dayData.DOWN,
|
||||
dayData.total,
|
||||
StatusObj.DOWN,
|
||||
site.barStyle
|
||||
);
|
||||
|
||||
summaryDuration = getSummaryDuration(dayData.DOWN, selectedLang);
|
||||
summaryStatus = "DOWN";
|
||||
}
|
||||
|
||||
if (!!_90Day[ts]) {
|
||||
_90Day[ts].timestamp = ts;
|
||||
_90Day[ts].cssClass = cssClass;
|
||||
_90Day[ts].summaryDuration = summaryDuration;
|
||||
_90Day[ts].summaryStatus = summaryStatus;
|
||||
_90Day[ts].textClass = cssClass.replace(/-\d+$/, "");
|
||||
}
|
||||
_90Day[ts] = {
|
||||
border: true
|
||||
};
|
||||
|
||||
_90Day[ts].timestamp = ts;
|
||||
_90Day[ts].cssClass = cssClass;
|
||||
|
||||
_90Day[ts].summaryStatus = l(lang, summaryTime(summaryStatus), {
|
||||
status: l(lang, summaryStatus),
|
||||
duration: summaryDuration
|
||||
});
|
||||
|
||||
_90Day[ts].textClass = cssClass.replace(/-\d+$/, "");
|
||||
}
|
||||
let uptime90DayNumerator = totalUpCount + totalDegradedCount;
|
||||
let uptime90DayDenominator = totalUpCount + totalDownCount + totalDegradedCount;
|
||||
@@ -162,13 +154,22 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
}
|
||||
// return _90Day;
|
||||
let uptime90Day = ParseUptime(uptime90DayNumerator, uptime90DayDenominator);
|
||||
if (site.summaryStyle === "CURRENT") {
|
||||
let todayDataDb = await db.getMonitoringData(
|
||||
monitor.tag,
|
||||
latestTimestamp,
|
||||
latestTimestamp + secondsInDay
|
||||
);
|
||||
|
||||
let summaryDuration = 0;
|
||||
let summaryStatus = "UP";
|
||||
|
||||
let summaryColorClass = "api-nodata";
|
||||
|
||||
let todayDataDb = await db.getMonitoringData(monitor.tag, midnight, midnight + secondsInDay);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, midnight);
|
||||
todayDataDb = InterpolateData(
|
||||
todayDataDb,
|
||||
midnight,
|
||||
anchorStatus,
|
||||
todayDataDb.length == 0 ? midnight + secondsInDay : null
|
||||
);
|
||||
|
||||
if (site.summaryStyle === "CURRENT") {
|
||||
summaryColorClass = "api-up";
|
||||
|
||||
let lastRow = todayDataDb[todayDataDb.length - 1];
|
||||
@@ -189,6 +190,10 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
summaryStatus = "DOWN";
|
||||
summaryColorClass = "api-down";
|
||||
}
|
||||
summaryStatus = l(lang, summaryTime(summaryStatus), {
|
||||
status: l(lang, summaryStatus),
|
||||
duration: summaryDuration
|
||||
});
|
||||
} else {
|
||||
let lastData = _90Day[latestTimestamp];
|
||||
summaryColorClass = lastData.cssClass.replace(/-\d+$/, "");
|
||||
@@ -198,7 +203,6 @@ const FetchData = async function (site, monitor, localTz, selectedLang) {
|
||||
return {
|
||||
_90Day: _90Day,
|
||||
uptime90Day: uptime90Day,
|
||||
summaryDuration: summaryDuration,
|
||||
summaryStatus: summaryStatus,
|
||||
summaryColorClass: summaryColorClass,
|
||||
barRoundness: site.barRoundness,
|
||||
|
||||
+19
-1
@@ -108,6 +108,17 @@ const BeginningOfDay = (options = {}) => {
|
||||
);
|
||||
return dt.getTime() / 1000;
|
||||
};
|
||||
const BeginningOfMinute = (options = {}) => {
|
||||
const { date = new Date(), timeZone } = options;
|
||||
const parts = Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
hourCycle: "h23",
|
||||
second: "numeric"
|
||||
}).formatToParts(date);
|
||||
const second = parseInt(parts.find((i) => i.type === "second").value);
|
||||
const dt = new Date(1000 * Math.floor((date - second * 1000) / 1000));
|
||||
return dt.getTime() / 1000;
|
||||
};
|
||||
const ValidateIpAddress = function (input) {
|
||||
// Check if input is a valid IPv4 address
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
@@ -276,6 +287,11 @@ function GenerateRandomColor() {
|
||||
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
|
||||
return randomColor;
|
||||
}
|
||||
|
||||
//wait for x ms promise
|
||||
function Wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
export {
|
||||
IsValidURL,
|
||||
IsValidHTTPMethod,
|
||||
@@ -299,5 +315,7 @@ export {
|
||||
ReplaceAllOccurrences,
|
||||
GetRequiredSecrets,
|
||||
ValidateMonitorAlerts,
|
||||
GenerateRandomColor
|
||||
GenerateRandomColor,
|
||||
BeginningOfMinute,
|
||||
Wait
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
|
||||
<svelte:window on:pagechange={pageChange} on:rightbar={updateTableOfContents} />
|
||||
<svelte:head>
|
||||
<title>Kener Documentation</title>
|
||||
<link rel="icon" id="kener-app-favicon" href="{base}/logo96.png" />
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
@@ -86,7 +87,7 @@
|
||||
<img src="https://kener.ing/logo.png" class="h-8 w-8" alt="" />
|
||||
<span class="text-xl font-medium">Kener Documentation</span>
|
||||
<span class="me-2 rounded border px-2.5 py-0.5 text-xs font-medium">
|
||||
v3.0.0
|
||||
v3.0.4
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,8 @@ export async function load({ params, route, url, parent }) {
|
||||
siteData,
|
||||
monitors[i],
|
||||
parentData.localTz,
|
||||
parentData.selectedLang
|
||||
parentData.selectedLang,
|
||||
parentData.lang
|
||||
);
|
||||
monitors[i].pageData = data;
|
||||
monitorsActive.push(monitors[i]);
|
||||
|
||||
@@ -141,10 +141,10 @@
|
||||
{/each}
|
||||
{/if}
|
||||
{#if hasConfiguredAnalytics}
|
||||
<script src="https://unpkg.com/analytics/dist/analytics.min.js" defer></script>
|
||||
<script src="https://unpkg.com/analytics/dist/analytics.min.js"></script>
|
||||
{#each data.site.analytics as { id, type, script }}
|
||||
{#if !!id}
|
||||
<script data-type={type} src={script} defer></script>
|
||||
<script data-type={type} src={script}></script>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// @ts-nocheck
|
||||
import { FetchData } from "$lib/server/page";
|
||||
import { GetMonitors, GetIncidentsOpenHome } from "$lib/server/controllers/controller.js";
|
||||
import { SortMonitor } from "$lib/clientTools.js";
|
||||
import moment from "moment";
|
||||
|
||||
export async function load({ parent, url }) {
|
||||
let monitors = await GetMonitors({ status: "ACTIVE" });
|
||||
|
||||
const query = url.searchParams;
|
||||
const requiredCategory = query.get("category") || "Home";
|
||||
const parentData = await parent();
|
||||
const siteData = parentData.site;
|
||||
let pageTitle = siteData.title;
|
||||
let pageDescription = "";
|
||||
monitors = SortMonitor(siteData.monitorSort, monitors);
|
||||
const monitorsActive = [];
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
//only return monitors that have category as home or category is not present
|
||||
@@ -30,7 +33,8 @@ export async function load({ parent, url }) {
|
||||
siteData,
|
||||
monitors[i],
|
||||
parentData.localTz,
|
||||
parentData.selectedLang
|
||||
parentData.selectedLang,
|
||||
parentData.lang
|
||||
);
|
||||
monitors[i].pageData = data;
|
||||
|
||||
@@ -48,6 +52,19 @@ export async function load({ parent, url }) {
|
||||
//if not home page
|
||||
let isCategoryPage = !!query.get("category") && query.get("category") !== "Home";
|
||||
let isMonitorPage = !!query.get("monitor");
|
||||
if (isMonitorPage && monitorsActive.length > 0) {
|
||||
pageTitle = monitorsActive[0].name + " - " + pageTitle;
|
||||
pageDescription = monitorsActive[0].description;
|
||||
}
|
||||
//if category page
|
||||
if (isCategoryPage) {
|
||||
let allCategories = siteData.categories;
|
||||
let selectedCategory = allCategories.find((category) => category.name === requiredCategory);
|
||||
if (selectedCategory) {
|
||||
pageTitle = selectedCategory.name + " - " + pageTitle;
|
||||
pageDescription = selectedCategory.description;
|
||||
}
|
||||
}
|
||||
if (isCategoryPage || isMonitorPage) {
|
||||
let eligibleTags = monitorsActive.map((monitor) => monitor.tag);
|
||||
//filter incidents that have monitor_tag in monitors
|
||||
@@ -82,11 +99,14 @@ export async function load({ parent, url }) {
|
||||
return incident;
|
||||
});
|
||||
let unresolvedIncidents = allOpenIncidents.filter((incident) => incident.state !== "RESOLVED");
|
||||
|
||||
return {
|
||||
monitors: monitorsActive,
|
||||
unresolvedIncidents: allOpenIncidents,
|
||||
categoryName: requiredCategory,
|
||||
isCategoryPage: isCategoryPage,
|
||||
isMonitorPage: isMonitorPage
|
||||
isMonitorPage: isMonitorPage,
|
||||
pageTitle: pageTitle,
|
||||
pageDescription: pageDescription
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@
|
||||
|
||||
if (data.isCategoryPage) {
|
||||
let category = data.site.categories.find((e) => e.name == data.categoryName);
|
||||
data.site.hero.title = category.name;
|
||||
data.site.hero.subtitle = category.description;
|
||||
if (!!category) {
|
||||
data.site.hero.title = category.name;
|
||||
data.site.hero.subtitle = category.description;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -43,6 +45,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.pageTitle}</title>
|
||||
{#if !!data.pageDescription}
|
||||
<meta name="description" content={data.pageDescription} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
<div class="mt-12"></div>
|
||||
{#if data.site.hero && !data.isMonitorPage}
|
||||
<section
|
||||
@@ -183,7 +191,7 @@
|
||||
window.location.href = `?category=${category.name}`;
|
||||
}}
|
||||
>
|
||||
<Card.Root class="hover:bg-secondary">
|
||||
<Card.Root class="mb-4 hover:bg-secondary">
|
||||
<Card.Header class="bounce-right relative w-full cursor-pointer px-4 ">
|
||||
<Card.Title class="w-full ">
|
||||
{category.name}
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
ParseUptime
|
||||
} from "$lib/server/tool.js";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import { GetAllSiteData, GetIncidentsByIDS } from "$lib/server/controllers/controller.js";
|
||||
import {
|
||||
GetAllSiteData,
|
||||
GetIncidentsByIDS,
|
||||
InterpolateData,
|
||||
GetLastStatusBefore
|
||||
} from "$lib/server/controllers/controller.js";
|
||||
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
@@ -31,6 +36,8 @@ export async function POST({ request }) {
|
||||
}
|
||||
|
||||
let dayData = await db.getMonitoringData(monitor.tag, payload.startTs, end);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
|
||||
dayData = InterpolateData(dayData, payload.startTs, anchorStatus, end);
|
||||
let siteData = await GetAllSiteData();
|
||||
|
||||
let ups = 0;
|
||||
|
||||
@@ -8,18 +8,29 @@ import {
|
||||
ParseUptime
|
||||
} from "$lib/server/tool.js";
|
||||
import db from "$lib/server/db/db.js";
|
||||
import { InterpolateData, GetLastStatusBefore } from "$lib/server/controllers/controller.js";
|
||||
|
||||
export async function POST({ request }) {
|
||||
const payload = await request.json();
|
||||
const monitor = payload.monitor;
|
||||
const start = payload.startTs;
|
||||
let end = GetMinuteStartNowTimestampUTC();
|
||||
let aggregatedData = await db.getAggregatedMonitoringData(monitor.tag, start, end);
|
||||
let rawData = await db.getMonitoringData(monitor.tag, start, end);
|
||||
let anchorStatus = await GetLastStatusBefore(monitor.tag, start);
|
||||
rawData = InterpolateData(rawData, start, anchorStatus, rawData.length == 0 ? end : null);
|
||||
|
||||
let aggregatedData = rawData.reduce(
|
||||
(acc, row) => {
|
||||
acc[row.status] = (acc[row.status] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
UP: 0,
|
||||
DOWN: 0,
|
||||
DEGRADED: 0
|
||||
}
|
||||
);
|
||||
//covert all keys to uppercase
|
||||
aggregatedData = Object.keys(aggregatedData).reduce((acc, key) => {
|
||||
acc[key.toUpperCase()] = aggregatedData[key];
|
||||
return acc;
|
||||
}, {});
|
||||
let ups = Number(aggregatedData.UP);
|
||||
let downs = Number(aggregatedData.DOWN);
|
||||
let degradeds = Number(aggregatedData.DEGRADED);
|
||||
|
||||
@@ -50,6 +50,13 @@
|
||||
let sortedIncidentSmartDates = Object.keys(incidentSmartDates).sort((a, b) => a - b);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{f(parse(data.thisMonthName, "MMMM-yyyy", new Date()), "MMMM, yyyy", selectedLang)}
|
||||
{l(data.lang, "Incident Updates")} |
|
||||
{data.site.title}
|
||||
</title>
|
||||
</svelte:head>
|
||||
<div class="mt-12"></div>
|
||||
<section class="mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
|
||||
<Button
|
||||
@@ -98,7 +105,7 @@
|
||||
</div>
|
||||
|
||||
<h1 class=" text-xl font-semibold leading-tight">
|
||||
{l(data.lang, "No Incident On %date", {
|
||||
{l(data.lang, "No Incident in %date", {
|
||||
date: data.thisMonthName.replace("-", ", ")
|
||||
})}
|
||||
</h1>
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function POST({ request, cookies }) {
|
||||
let token = await GenerateToken(userDB);
|
||||
let cookieConfig = CookieConfig();
|
||||
cookies.set(cookieConfig.name, token, {
|
||||
path: "/" + base,
|
||||
path: cookieConfig.path,
|
||||
maxAge: cookieConfig.maxAge, // 1 year in seconds
|
||||
httpOnly: cookieConfig.httpOnly,
|
||||
secure: cookieConfig.secure,
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function GET({ request, cookies }) {
|
||||
|
||||
//delete cookie from browser
|
||||
cookies.set(cookieConfig.name, "", {
|
||||
path: "/" + base,
|
||||
path: cookieConfig.path,
|
||||
maxAge: 0,
|
||||
httpOnly: cookieConfig.httpOnly,
|
||||
secure: cookieConfig.secure,
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function POST({ request, cookies }) {
|
||||
let cookieConfig = CookieConfig();
|
||||
//set server side cookie with 1 year expiry
|
||||
cookies.set(cookieConfig.name, token, {
|
||||
path: "/" + base,
|
||||
path: cookieConfig.path,
|
||||
maxAge: cookieConfig.maxAge, // 1 year in seconds
|
||||
httpOnly: cookieConfig.httpOnly,
|
||||
secure: cookieConfig.secure,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
<MonitorsAdd
|
||||
monitorSort={data.siteData?.monitorSort}
|
||||
categories={data.categories}
|
||||
colorDown={data.siteData?.colors?.DOWN || "#ca3038"}
|
||||
colorDegraded={data.siteData?.colors?.DEGRADED || "#e6ca61"}
|
||||
|
||||
Reference in New Issue
Block a user