diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..3d192092 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Raj Nandan Sharma + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/lib/server/cache/cache.ts b/src/lib/server/cache/cache.ts index cb134861..33d69b6b 100644 --- a/src/lib/server/cache/cache.ts +++ b/src/lib/server/cache/cache.ts @@ -13,6 +13,11 @@ export async function setCache(key: string, value: T | null | undefined, ttlS await redis.set(getCacheKey(key), payload, "EX", ttl); } +export async function deleteCache(key: string): Promise { + const redis = redisIOConnection(); + await redis.del(getCacheKey(key)); +} + export async function getCache( key: string, fetcher?: () => Promise | T | null | undefined, diff --git a/src/lib/server/cache/setGet.ts b/src/lib/server/cache/setGet.ts index 009a6ad8..a554e9f7 100644 --- a/src/lib/server/cache/setGet.ts +++ b/src/lib/server/cache/setGet.ts @@ -1,5 +1,5 @@ import type { MonitoringData } from "../types/db.js"; -import { setCache, getCache } from "./cache.js"; +import { setCache, getCache, deleteCache } from "./cache.js"; export async function SetLastMonitoringValue(tag: string, value: MonitoringData): Promise { await setCache(tag + ":last_status", value, 86400); //set ttl to 1 day } @@ -20,3 +20,8 @@ export async function SetLastHeartbeat(tag: string, timestamp: number): Promise< export async function GetLastHeartbeat(tag: string): Promise<{ timestamp: number } | null> { return await getCache<{ timestamp: number }>("last_heartbeat:" + tag, undefined, 45 * 86400); } + +export async function DeleteMonitorCaches(tag: string): Promise { + await deleteCache(tag + ":last_status"); + await deleteCache("last_heartbeat:" + tag); +} diff --git a/src/lib/server/controllers/monitorsController.ts b/src/lib/server/controllers/monitorsController.ts index 8ccd3e6a..03f7edbd 100644 --- a/src/lib/server/controllers/monitorsController.ts +++ b/src/lib/server/controllers/monitorsController.ts @@ -25,8 +25,8 @@ import type { DayWiseStatus, NumberWithChange } from "../../types/monitor.js"; import GC, { getBadgeStyle, type BadgeStyle } from "../../global-constants.js"; import { makeBadge } from "badge-maker"; import { ErrorSvg } from "../../anywhere.js"; -import { GetLastMonitoringValue, SetLastHeartbeat } from "../cache/setGet.js"; -import type { HeartbeatMonitor } from "../types/monitor.js"; +import { GetLastMonitoringValue, SetLastHeartbeat, DeleteMonitorCaches } from "../cache/setGet.js"; +import type { HeartbeatMonitor, GroupMonitorTypeData } from "../types/monitor.js"; interface GroupUpdateData { monitor_tag: string; @@ -376,12 +376,71 @@ export const RegisterHeartbeat = async (tag: string, secret: string): Promise { + const groupMonitors = await GetMonitorsParsed({ monitor_type: "GROUP" }); + + for (const group of groupMonitors) { + const typeData = group.type_data as GroupMonitorTypeData; + if (!typeData.monitors || !Array.isArray(typeData.monitors)) continue; + + const hasMember = typeData.monitors.some((m) => m.tag === tag); + if (!hasMember) continue; + + // Remove the deleted tag + const remaining = typeData.monitors.filter((m) => m.tag !== tag); + + // Rebalance weights equally across remaining monitors + if (remaining.length > 0) { + const weight = Math.round((1 / remaining.length) * 1000) / 1000; + for (let i = 0; i < remaining.length; i++) { + remaining[i].weight = + i === remaining.length - 1 + ? Math.round((1 - weight * (remaining.length - 1)) * 1000) / 1000 + : weight; + } + } + + typeData.monitors = remaining; + + const updateData: Record = { + id: group.id, + tag: group.tag, + name: group.name, + description: group.description, + image: group.image, + cron: group.cron, + default_status: group.default_status, + status: remaining.length < 2 ? "INACTIVE" : group.status, + category_name: group.category_name, + monitor_type: group.monitor_type, + type_data: JSON.stringify(typeData), + day_degraded_minimum_count: group.day_degraded_minimum_count, + day_down_minimum_count: group.day_down_minimum_count, + include_degraded_in_downtime: group.include_degraded_in_downtime, + is_hidden: group.is_hidden, + monitor_settings_json: + typeof group.monitor_settings_json === "string" + ? group.monitor_settings_json + : JSON.stringify(group.monitor_settings_json), + external_url: group.external_url, + }; + + await db.updateMonitor(updateData as unknown as MonitorRecord); + } +} + export const DeleteMonitorCompletelyUsingTag = async (tag: string): Promise => { await db.deleteMonitorDataByTag(tag); await db.deleteIncidentMonitorsByTag(tag); await db.deleteMonitorAlertsByTag(tag); await db.deletePageMonitorsByTag(tag); await db.deleteMaintenanceMonitorsByTag(tag); + await removeTagFromGroupMonitors(tag); + await DeleteMonitorCaches(tag); return await db.deleteMonitorsByTag(tag); }; diff --git a/src/routes/(docs)/docs/+page.svelte b/src/routes/(docs)/docs/+page.svelte index 056532aa..1895b1b9 100644 --- a/src/routes/(docs)/docs/+page.svelte +++ b/src/routes/(docs)/docs/+page.svelte @@ -283,25 +283,22 @@
-
-
- +
+
+ Production-ready status page platform

- Build trust with - {data.config.name} - documentation that actually gets used + Build stunning status pages with + Kener

-

+

From quick setup to advanced operations, Kener gives you open-source monitoring, incident workflows, notifications, maintenance scheduling, embeds, and automation APIsโ€”all in one modern platform.

-
+
{#each getCtaButtons() as button (button.title)}