From f257cdc2c4e0f0978bc08e0f5232b6a68f5301cb Mon Sep 17 00:00:00 2001 From: Raj Nandan Sharma Date: Mon, 15 Jun 2026 23:19:17 +0530 Subject: [PATCH 1/3] create heartbeat route handler for GET and POST requests --- .../ext/heartbeat/{[tag]:[secret] => [tag]/[secret]}/+server.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/routes/(ext)/ext/heartbeat/{[tag]:[secret] => [tag]/[secret]}/+server.ts (100%) diff --git a/src/routes/(ext)/ext/heartbeat/[tag]:[secret]/+server.ts b/src/routes/(ext)/ext/heartbeat/[tag]/[secret]/+server.ts similarity index 100% rename from src/routes/(ext)/ext/heartbeat/[tag]:[secret]/+server.ts rename to src/routes/(ext)/ext/heartbeat/[tag]/[secret]/+server.ts From 734b0626260430ead1f8e5809ad944b7cf91a87a Mon Sep 17 00:00:00 2001 From: Raj Nandan Sharma Date: Mon, 15 Jun 2026 23:19:24 +0530 Subject: [PATCH 2/3] refactor: update heartbeat URL format to use path segments and implement legacy URL rewriting --- src/hooks.ts | 22 +++++++++++++++ .../docs/content/v4/monitors/heartbeat.md | 8 ++++-- .../manage/app/incidents/+page.svelte | 28 +++++++++++-------- .../[tag]/types/monitor-heartbeat.svelte | 2 +- 4 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 src/hooks.ts diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 00000000..392bbb11 --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,22 @@ +import type { Reroute } from "@sveltejs/kit"; + +// Back-compat for issue #759: heartbeat URLs used to be `/ext/heartbeat/:`, +// one path segment joined by a colon. A `:` is illegal in Windows file paths, so the +// route is now `/ext/heartbeat//` (two segments). Legacy colon-form URLs +// live forever in external cron jobs / uptime pingers, so rewrite them internally to +// the new path. Returns a 200 (no redirect) — heartbeat clients often don't follow 3xx. +// +// `reroute` is a *universal* hook: it MUST be in src/hooks.ts. A `reroute` exported from +// src/hooks.server.ts is silently ignored by SvelteKit. Keep this file free of +// server-only imports — it is bundled for the client too. Must stay pure/side-effect-free. +// +// The transform is in-place (no path reconstruction), so any KENER_BASE_PATH prefix is +// preserved automatically. `[^/:]+` matches the validated tag charset; only the first +// colon after `/ext/heartbeat/` is rewritten. +const LEGACY_HEARTBEAT = /(\/ext\/heartbeat\/[^/:]+):/; + +export const reroute: Reroute = ({ url }) => { + if (LEGACY_HEARTBEAT.test(url.pathname)) { + return url.pathname.replace(LEGACY_HEARTBEAT, "$1/"); + } +}; diff --git a/src/routes/(docs)/docs/content/v4/monitors/heartbeat.md b/src/routes/(docs)/docs/content/v4/monitors/heartbeat.md index c04890e6..f936dc63 100644 --- a/src/routes/(docs)/docs/content/v4/monitors/heartbeat.md +++ b/src/routes/(docs)/docs/content/v4/monitors/heartbeat.md @@ -10,11 +10,13 @@ Heartbeat monitors are push-based: your job calls a URL, and Kener measures how URL format: ``` -/ext/heartbeat/{tag}:{secret} +/ext/heartbeat/{tag}/{secret} ``` Accepted methods: `GET` and `POST`. +> Older heartbeat URLs used a colon — `/ext/heartbeat/{tag}:{secret}`. Those still work; they are rewritten to the path-separated form automatically, so existing cron jobs need no changes. + ## Minimum setup {#minimum-setup} Set: @@ -53,11 +55,11 @@ Latency is recorded as elapsed time since the last heartbeat (ms). Minimal cron usage pattern: ```bash -*/5 * * * * /path/to/job.sh && curl -s "https://your-kener-host/ext/heartbeat/my-job:my-secret" +*/5 * * * * /path/to/job.sh && curl -s "https://your-kener-host/ext/heartbeat/my-job/my-secret" ``` ## Troubleshooting {#troubleshooting} -- **Always NO_DATA**: endpoint never called or wrong `tag:secret` +- **Always NO_DATA**: endpoint never called or wrong `tag`/`secret` - **Always DOWN/DEGRADED**: thresholds too low for actual job interval - **Signal accepted but stale**: ensure heartbeat is sent only after successful completion diff --git a/src/routes/(manage)/manage/app/incidents/+page.svelte b/src/routes/(manage)/manage/app/incidents/+page.svelte index 07b1cc40..f1191965 100644 --- a/src/routes/(manage)/manage/app/incidents/+page.svelte +++ b/src/routes/(manage)/manage/app/incidents/+page.svelte @@ -2,18 +2,16 @@ import { Button } from "$lib/components/ui/button/index.js"; import { Spinner } from "$lib/components/ui/spinner/index.js"; import { Badge } from "$lib/components/ui/badge/index.js"; - import * as Card from "$lib/components/ui/card/index.js"; import * as Select from "$lib/components/ui/select/index.js"; - import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js"; import * as Table from "$lib/components/ui/table/index.js"; import * as Tooltip from "$lib/components/ui/tooltip/index.js"; import PlusIcon from "@lucide/svelte/icons/plus"; import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left"; import ChevronRightIcon from "@lucide/svelte/icons/chevron-right"; - import ExternalLinkIcon from "@lucide/svelte/icons/external-link"; import PencilIcon from "@lucide/svelte/icons/pencil"; import SirenIcon from "@lucide/svelte/icons/siren"; import { goto } from "$app/navigation"; + import { onMount } from "svelte"; import { format, formatDistanceToNow } from "date-fns"; import GC from "$lib/global-constants"; import { resolve } from "$app/paths"; @@ -104,12 +102,12 @@ // Navigate to incident function openIncident(id: number) { - goto(clientResolver(resolve, `/manage/app/incidents/${id}`)); + goto(resolve(`/manage/app/incidents/${id}`)); } // Create new incident function createNewIncident() { - goto(clientResolver(resolve, "/manage/app/incidents/new")); + goto(resolve("/manage/app/incidents/new")); } // Handle state filter change @@ -127,7 +125,7 @@ fetchData(); } - $effect(() => { + onMount(() => { fetchData(); }); @@ -142,7 +140,7 @@ {stateOptions.find((o) => o.value === stateFilter)?.label || "All States"} - {#each stateOptions as option} + {#each stateOptions as option (option.value)} {option.label} {/each} @@ -166,6 +164,7 @@ ID Title + Started Duration State Affects @@ -175,10 +174,10 @@ {#if incidents.length === 0 && !loading} - No incidents found + No incidents found {:else} - {#each incidents as incident} + {#each incidents as incident (incident.id)} openIncident(incident.id)}> {incident.id} @@ -191,6 +190,11 @@ + + + {format(new Date(incident.start_date_time * 1000), "yyyy-MM-dd HH:mm")} + + @@ -227,7 +231,7 @@
- {#each incident.monitors as monitor} + {#each incident.monitors as monitor (monitor.tag || monitor.monitor_tag)}
{monitor.tag || monitor.monitor_tag}
{#if totalPages <= 7} - {#each Array.from({ length: totalPages }, (_, i) => i + 1) as page} + {#each Array.from({ length: totalPages }, (_, i) => i + 1) as page (page)} @@ -288,7 +292,7 @@ {/if} - {#each Array.from({ length: 3 }, (_, i) => pageNo - 1 + i).filter((p) => p > 1 && p < totalPages) as page} + {#each Array.from({ length: 3 }, (_, i) => pageNo - 1 + i).filter((p) => p > 1 && p < totalPages) as page (page)} diff --git a/src/routes/(manage)/manage/app/monitors/[tag]/types/monitor-heartbeat.svelte b/src/routes/(manage)/manage/app/monitors/[tag]/types/monitor-heartbeat.svelte index 36d8db98..d2ffa00d 100644 --- a/src/routes/(manage)/manage/app/monitors/[tag]/types/monitor-heartbeat.svelte +++ b/src/routes/(manage)/manage/app/monitors/[tag]/types/monitor-heartbeat.svelte @@ -24,7 +24,7 @@ // Generate heartbeat URL let heartbeatUrl = $derived( tag - ? window.location.origin + clientResolve(resolve, `/ext/heartbeat/${tag}:${data.secretString}`) + ? window.location.origin + clientResolve(resolve, `/ext/heartbeat/${tag}/${data.secretString}`) : "Save the monitor first to get the heartbeat URL" ); From 4536bceef65406bf05e6284eb67371668c390f17 Mon Sep 17 00:00:00 2001 From: Raj Nandan Sharma Date: Mon, 15 Jun 2026 23:22:25 +0530 Subject: [PATCH 3/3] refactor: improve incident management page structure and enhance data fetching logic --- .../manage/app/incidents/+page.svelte | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/routes/(manage)/manage/app/incidents/+page.svelte b/src/routes/(manage)/manage/app/incidents/+page.svelte index f1191965..07b1cc40 100644 --- a/src/routes/(manage)/manage/app/incidents/+page.svelte +++ b/src/routes/(manage)/manage/app/incidents/+page.svelte @@ -2,16 +2,18 @@ import { Button } from "$lib/components/ui/button/index.js"; import { Spinner } from "$lib/components/ui/spinner/index.js"; import { Badge } from "$lib/components/ui/badge/index.js"; + import * as Card from "$lib/components/ui/card/index.js"; import * as Select from "$lib/components/ui/select/index.js"; + import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js"; import * as Table from "$lib/components/ui/table/index.js"; import * as Tooltip from "$lib/components/ui/tooltip/index.js"; import PlusIcon from "@lucide/svelte/icons/plus"; import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left"; import ChevronRightIcon from "@lucide/svelte/icons/chevron-right"; + import ExternalLinkIcon from "@lucide/svelte/icons/external-link"; import PencilIcon from "@lucide/svelte/icons/pencil"; import SirenIcon from "@lucide/svelte/icons/siren"; import { goto } from "$app/navigation"; - import { onMount } from "svelte"; import { format, formatDistanceToNow } from "date-fns"; import GC from "$lib/global-constants"; import { resolve } from "$app/paths"; @@ -102,12 +104,12 @@ // Navigate to incident function openIncident(id: number) { - goto(resolve(`/manage/app/incidents/${id}`)); + goto(clientResolver(resolve, `/manage/app/incidents/${id}`)); } // Create new incident function createNewIncident() { - goto(resolve("/manage/app/incidents/new")); + goto(clientResolver(resolve, "/manage/app/incidents/new")); } // Handle state filter change @@ -125,7 +127,7 @@ fetchData(); } - onMount(() => { + $effect(() => { fetchData(); }); @@ -140,7 +142,7 @@ {stateOptions.find((o) => o.value === stateFilter)?.label || "All States"} - {#each stateOptions as option (option.value)} + {#each stateOptions as option} {option.label} {/each} @@ -164,7 +166,6 @@ ID Title - Started Duration State Affects @@ -174,10 +175,10 @@ {#if incidents.length === 0 && !loading} - No incidents found + No incidents found {:else} - {#each incidents as incident (incident.id)} + {#each incidents as incident} openIncident(incident.id)}> {incident.id} @@ -190,11 +191,6 @@ - - - {format(new Date(incident.start_date_time * 1000), "yyyy-MM-dd HH:mm")} - - @@ -231,7 +227,7 @@
- {#each incident.monitors as monitor (monitor.tag || monitor.monitor_tag)} + {#each incident.monitors as monitor}
{monitor.tag || monitor.monitor_tag}
{#if totalPages <= 7} - {#each Array.from({ length: totalPages }, (_, i) => i + 1) as page (page)} + {#each Array.from({ length: totalPages }, (_, i) => i + 1) as page} @@ -292,7 +288,7 @@ {/if} - {#each Array.from({ length: 3 }, (_, i) => pageNo - 1 + i).filter((p) => p > 1 && p < totalPages) as page (page)} + {#each Array.from({ length: 3 }, (_, i) => pageNo - 1 + i).filter((p) => p > 1 && p < totalPages) as page}