Compare commits

..

32 Commits

Author SHA1 Message Date
Raj Nandan Sharma 5f33c02064 Merge pull request #222 from rajnandan1/release/3.0.4
Release/3.0.4
2025-01-25 21:43:05 +05:30
Raj Nandan Sharma 46c1a392b8 refactor: remove docs from docker build 2025-01-25 21:41:11 +05:30
Raj Nandan Sharma c6880c5df6 feat: sitemap and bug fixes 2025-01-25 21:18:42 +05:30
Raj Nandan Sharma 646da94ef9 feat: sitemap and bug fixes 2025-01-25 21:16:17 +05:30
Raj Nandan Sharma 36ede93dce feat: sitemap and bug fixes 2025-01-25 21:12:41 +05:30
Raj Nandan Sharma 9ed35589f7 Merge pull request #220 from rajnandan1/release/3.0.3
fix: load time #219
2025-01-24 21:45:33 +05:30
Raj Nandan Sharma e946dd18e0 fix: load time #219 2025-01-24 21:44:36 +05:30
Raj Nandan Sharma 51cad598c1 fix: load time #219 2025-01-24 21:42:57 +05:30
Raj Nandan Sharma 0595f23c18 Merge pull request #218 from cosmic-jellyfish/feature/ENG-Locale-Fix
English Locale tweaks
2025-01-24 13:04:58 +05:30
Raj Nandan Sharma b88816706b fix: retry await decrease from 4000 to 500ms 2025-01-24 11:42:06 +05:30
Cake 80edaae023 fix: update localisation strings across the board 2025-01-24 17:08:09 +11:00
Raj Nandan Sharma 883c46b064 Merge pull request #214 from rajnandan1/release/3.0.2
release of 3.0.2
2025-01-24 10:48:15 +05:30
Raj Nandan Sharma 4248cc31f2 chore: updated package.json version 2025-01-24 10:47:56 +05:30
Raj Nandan Sharma 5f81c6d61a fix: daily data local date format 2025-01-24 10:45:50 +05:30
Raj Nandan Sharma 00d82cc817 fix: daily data local date format 2025-01-24 10:44:48 +05:30
Raj Nandan Sharma a9edfde162 fix: bug fix for no data 2025-01-24 10:35:22 +05:30
Cake 468d7f445d fix: update localisation strings further 2025-01-24 15:15:13 +11:00
Cake 4cc74deece fix: update localisation strings 2025-01-24 15:11:50 +11:00
Raj Nandan Sharma f9831490af fix: base path for docker build 2025-01-24 09:08:13 +05:30
Raj Nandan Sharma a055a616eb fix: fix logo url as reported in #213 2025-01-23 23:37:39 +05:30
Raj Nandan Sharma 8009a2cbc4 feat: monitor re-arrange as requested in #215 2025-01-23 22:39:13 +05:30
Raj Nandan Sharma 693cc149cf feat: added retry for api timeouts as requested in #208 2025-01-23 10:33:15 +05:30
Raj Nandan Sharma d126fe0bce fix: fix for #212, data interpolation introduced 2025-01-23 09:36:37 +05:30
Raj Nandan Sharma 4cc49cce9e fix: default locale fix as reported in #209 2025-01-22 09:25:56 +05:30
Raj Nandan Sharma f7cb4cd805 fix: cron validation updated as reported in #206 2025-01-21 22:02:39 +05:30
Raj Nandan Sharma a2bd9883d1 chore: remove old docs.md 2025-01-21 09:18:21 +05:30
Raj Nandan Sharma 10a079c304 fix: fixing #193 base path login was having double slash in cookie 2025-01-21 09:12:10 +05:30
Raj Nandan Sharma 65f12f1082 docs: added contributing file 2025-01-21 08:44:58 +05:30
Raj Nandan Sharma c15f855c72 docs: added contributing file 2025-01-21 08:42:41 +05:30
Raj Nandan Sharma 3d38483022 Merge pull request #205 from matribeiro15/main
Add Brazilian Portuguese
2025-01-21 08:31:33 +05:30
Mateus Ribeiro 57dc1e7175 modified: src/lib/i18n/client.js
modified:   src/lib/locales/locales.json
	new file:   src/lib/locales/pt-BR.json
2025-01-21 01:22:01 +00:00
Raj Nandan Sharma ead68c929f docs: embed docs 2025-01-18 23:19:38 +05:30
56 changed files with 813 additions and 1452 deletions
+1
View File
@@ -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
+48
View File
@@ -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
View File
@@ -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
+4
View File
@@ -103,6 +103,10 @@ If you are using Kener and want to support me, you can do so by sponsoring me on
![image](https://badges.pufler.dev/visits/rajnandan1/kener)
## 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
[![Star History Chart](https://api.star-history.com/svg?repos=rajnandan1/kener&type=Date)](https://star-history.com/#rajnandan1/kener&Date)
-1255
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -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
+11 -1
View File
@@ -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 &#x3C;style&#x3E; tags.
</div>
+15 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+14
View File
@@ -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 &#x3C;style&#x3E; tags.
</div>
+5
View File
@@ -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
+13 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
};
+2 -2
View File
@@ -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
+82 -8
View File
@@ -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
+1 -11
View File
@@ -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.";
}
+8 -12
View 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">
+1 -4
View File
@@ -169,10 +169,7 @@
{l(lang, "Embed")}
</h2>
<p class="mb-1 text-xs text-muted-foreground">
{@html l(
lang,
"Embed this moni2tor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app."
)}
{l(lang, "Embed this monitor using &#x3C;script&#x3E; or &#x3C;iframe&#x3E; in your app.")}
</p>
<div class="mb-4 grid grid-cols-2 gap-2">
<div class="col-span-1">
+4 -13
View File
@@ -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) {
+3 -3
View File
@@ -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",
+3 -3
View File
@@ -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",
+5 -5
View File
@@ -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"
}
}
+3 -3
View File
@@ -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",
+3 -3
View File
@@ -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": "स्थिति",
+3 -3
View File
@@ -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": "ステータス",
+3 -3
View File
@@ -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": "상태",
+4
View File
@@ -42,5 +42,9 @@
{
"code": "ko",
"name": "한국어"
},
{
"code": "pt-BR",
"name": "Português Brasileiro"
}
]
+3 -3
View File
@@ -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",
+55
View File
@@ -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 &#x3C;script&#x3E; or &#x3C;iframe&#x3E; 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"
}
+3 -3
View File
@@ -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": "Статус",
+3 -3
View File
@@ -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",
+3 -3
View File
@@ -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": "状态",
+143 -5
View File
@@ -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>`;
};
+10
View File
@@ -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);
+45 -3
View File
@@ -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 };
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
};
+2 -1
View File
@@ -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]);
+2 -2
View File
@@ -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}
+23 -3
View File
@@ -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
};
}
+11 -3
View File
@@ -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 -1
View File
@@ -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"}