Skip to content

Commit 1fdafa7

Browse files
committed
Adds badges and embed features
Implements badges for monitors, including status, uptime, and liveness, along with a dedicated management page. Adds embed options for various platforms with customizable styles.
1 parent 1f5e683 commit 1fdafa7

File tree

28 files changed

+1387
-139
lines changed

28 files changed

+1387
-139
lines changed

docs/changelogs.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,20 @@ Here are the changelogs for Kener. Changelogs are only published when there are
5151
- Professionally designed HTML email templates
5252

5353
- **Invitation System**
54+
5455
- Token-based invitation workflow for new users
5556
- Secure invitation links with expiration dates
5657
- Admin controls for managing invitations
5758

59+
- **Badges & Embed Features**
60+
- Added dedicated Badges management page in the admin dashboard
61+
- Support for status, uptime, and liveness badges for all monitors
62+
- Site-wide status badges to show overall service health
63+
- Customizable badge styles, colors, and labels
64+
- Advanced embedding options for Svelte, React, Vue, Angular, HTML, and JavaScript
65+
- Color picker for customizing badge and embed appearance
66+
- Responsive embed components with automatic sizing
67+
5868
### Improvements
5969

6070
- **Performance Enhancements**

embed.html

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<title>Document</title>
7-
<style>
8-
body {
9-
background-color: #111;
10-
}
11-
</style>
12-
</head>
13-
<body>
14-
Hello world
15-
<script
16-
async
17-
src="http://localhost:3000/embed/monitor-earth/js?theme=dark&bgc=111&locale=hi&monitor=http://localhost:3000/embed/monitor-earth"
18-
></script>
19-
</body>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Document</title>
7+
<style>
8+
body {
9+
background-color: #111;
10+
}
11+
</style>
12+
</head>
13+
<body>
14+
<script
15+
async
16+
src="http://localhost:3000/embed/monitor-bitbuckettcp/js?bgc=212226&locale=en&theme=dark&monitor=http://localhost:3000/embed/monitor-bitbuckettcp"
17+
></script>
18+
</body>
2019
</html>

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
"js-yaml": "^4.1.0",
9494
"jsonwebtoken": "^9.0.2",
9595
"knex": "^3.1.0",
96-
"lucide-svelte": "^0.292.0",
96+
"lucide-svelte": "^0.483.0",
9797
"marked": "^11.1.1",
9898
"mode-watcher": "^0.4.1",
9999
"moment": "^2.29.4",

src/kener.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,3 +905,37 @@ textarea::placeholder {
905905
content: ".";
906906
}
907907
}
908+
.dark {
909+
--cp-bg-color: hsl(var(--background) / var(--tw-bg-opacity, 1));
910+
--cp-border-color: hsl(var(--border) / var(--tw-border-opacity));
911+
--cp-text-color: hsl(var(--card-foreground) / var(--tw-text-opacity, 1));
912+
}
913+
914+
.color-picker .container {
915+
padding-left: 0px;
916+
padding-right: 0px;
917+
}
918+
.color-picker .color {
919+
border: 1px solid hsl(var(--border) / var(--tw-border-opacity));
920+
}
921+
.color-picker button {
922+
display: none;
923+
}
924+
.color-picker [aria-label="color picker"] input {
925+
color: hsl(var(--foreground) / var(--tw-border-opacity)) !important;
926+
background-color: hsl(var(--input) / var(--tw-text-opacity, 1)) !important;
927+
}
928+
929+
.dark .color-picker {
930+
--cp-bg-color: hsl(var(--background) / var(--tw-bg-opacity, 1));
931+
--cp-border-color: hsl(var(--border) / var(--tw-border-opacity));
932+
--cp-text-color: hsl(var(--card-foreground) / var(--tw-text-opacity, 1));
933+
--cp-input-color: hsl(var(--input) / var(--tw-text-opacity, 1));
934+
--cp-button-hover-color: hsl(var(--input) / var(--tw-text-opacity, 1));
935+
}
936+
.like-input {
937+
background-color: rgba(0, 0, 0, 0.02) !important;
938+
}
939+
.dark .like-input {
940+
background-color: rgba(0, 0, 0, 0.1) !important;
941+
}

src/lib/Embed.svelte

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<script>
2+
import { onMount, onDestroy, beforeUpdate } from "svelte";
3+
import { createEventDispatcher } from "svelte";
4+
5+
const dispatch = createEventDispatcher();
6+
7+
export let monitor;
8+
export let theme = "light";
9+
export let bgc = "transparent";
10+
export let locale = "en";
11+
12+
let iframe, listeners;
13+
let containerId = `embed-container-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
14+
let isMounted = false;
15+
let prevMonitor, prevTheme, prevBgc, prevLocale;
16+
17+
onMount(() => {
18+
if (!monitor) return;
19+
20+
isMounted = true;
21+
prevMonitor = monitor;
22+
prevTheme = theme;
23+
prevBgc = bgc;
24+
prevLocale = locale;
25+
26+
const container = document.getElementById(containerId);
27+
if (!container) return;
28+
29+
const uid = "KENER_" + ~~(new Date().getTime() / 86400000);
30+
const uriEmbedded = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
31+
32+
iframe = document.createElement("iframe");
33+
iframe.src = uriEmbedded;
34+
iframe.id = uid;
35+
iframe.width = "0%";
36+
iframe.height = "0";
37+
iframe.frameBorder = "0";
38+
iframe.allowTransparency = true;
39+
iframe.sandbox =
40+
"allow-modals allow-forms allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation allow-downloads";
41+
iframe.allow = "midi; geolocation; microphone; camera; display-capture; encrypted-media;";
42+
43+
container.appendChild(iframe);
44+
45+
const setHeight = (data) => {
46+
if (data.height !== undefined) {
47+
iframe.height = data.height;
48+
}
49+
};
50+
51+
const setWidth = (data) => {
52+
if (data.width !== undefined) {
53+
iframe.width = data.width;
54+
}
55+
};
56+
57+
listeners = (event) => {
58+
if (event.data && event.data.height !== undefined) {
59+
setHeight(event.data);
60+
}
61+
if (event.data && event.data.width !== undefined) {
62+
setWidth(event.data);
63+
}
64+
};
65+
66+
window.addEventListener("message", listeners, false);
67+
});
68+
69+
beforeUpdate(() => {
70+
if (
71+
isMounted &&
72+
iframe &&
73+
(monitor !== prevMonitor || theme !== prevTheme || bgc !== prevBgc || locale !== prevLocale)
74+
) {
75+
iframe.src = `${monitor}?theme=${theme}&bgc=${bgc}&locale=${locale}`;
76+
77+
// Update previous values
78+
prevMonitor = monitor;
79+
prevTheme = theme;
80+
prevBgc = bgc;
81+
prevLocale = locale;
82+
dispatch("update", {
83+
monitor: prevMonitor,
84+
theme: prevTheme,
85+
bgc: prevBgc,
86+
locale: prevLocale
87+
});
88+
}
89+
});
90+
91+
onDestroy(() => {
92+
if (iframe) {
93+
window.removeEventListener("message", listeners);
94+
iframe.remove();
95+
}
96+
});
97+
</script>
98+
99+
<!-- Dynamic container with unique ID -->
100+
<div id={containerId}></div>

src/lib/anywhere.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,11 @@ export const DefaultTCPEval = `(async function (arrayOfPings) {
184184
latency: latencyTotal / arrayOfPings.length,
185185
}
186186
})`;
187+
188+
export const ErrorSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="60" viewBox="0 0 120 60" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
189+
<circle cx="30" cy="24" r="10"/>
190+
<path d="M26 27h8"/>
191+
<path d="M26 21h2"/>
192+
<path d="M32 21h2"/>
193+
<text x="80" y="29" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" fill="currentColor" font-weight="300">Not Found</text>
194+
</svg>`;

src/lib/server/controllers/controller.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,23 @@ export const VerifyPassword = async (plainTextPassword, hashedPassword) => {
305305
export const GetLatestMonitoringData = async (monitor_tag) => {
306306
return await db.getLatestMonitoringData(monitor_tag);
307307
};
308+
export const GetLatestStatusActiveAll = async (monitor_tags) => {
309+
let latestData = await db.getLatestMonitoringDataAllActive(monitor_tags);
310+
let status = "NO_DATA";
311+
for (let i = 0; i < latestData.length; i++) {
312+
//if any status is down then status = down, if any is degraded then status = degraded, down > degraded > up
313+
if (latestData[i].status === "DOWN") {
314+
status = "DOWN";
315+
} else if (latestData[i].status === "DEGRADED" && status !== "DOWN") {
316+
status = "DEGRADED";
317+
} else if (latestData[i].status === "UP" && status !== "DOWN" && status !== "DEGRADED") {
318+
status = "UP";
319+
}
320+
}
321+
return {
322+
status: status,
323+
};
324+
};
308325
export const GetLastHeartbeat = async (monitor_tag) => {
309326
return await db.getLastHeartbeat(monitor_tag);
310327
};
@@ -482,6 +499,19 @@ export const GetLastStatusBefore = async (monitor_tag, timestamp) => {
482499
}
483500
return NO_DATA;
484501
};
502+
export const GetMonitoringData = async (tag, since, now) => {
503+
return await db.getMonitoringData(tag, since, now);
504+
};
505+
export const GetMonitoringDataAll = async (tags, since, now) => {
506+
return await db.getMonitoringDataAll(tags, since, now);
507+
};
508+
export const GetLastStatusBeforeAll = async (monitor_tags, timestamp) => {
509+
let data = await db.getLastStatusBeforeAll(monitor_tags, timestamp);
510+
if (data) {
511+
return data.status;
512+
}
513+
return NO_DATA;
514+
};
485515

486516
export const AggregateData = (rawData) => {
487517
//data like [{ timestamp: 1732435920, status: 'NO_DATA' }]

src/lib/server/db/dbimpl.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ class DbImpl {
3030
.where("timestamp", "<=", end)
3131
.orderBy("timestamp", "asc");
3232
}
33+
//given monitor_tags array of string, start and end timestamp in utc seconds return data
34+
async getMonitoringDataAll(monitor_tags, start, end) {
35+
return await this.knex("monitoring_data")
36+
.whereIn("monitor_tag", monitor_tags)
37+
.where("timestamp", ">=", start)
38+
.where("timestamp", "<=", end)
39+
.orderBy("timestamp", "asc");
40+
}
3341

3442
//get latest data for a monitor_tag
3543
async getLatestMonitoringData(monitor_tag) {
@@ -40,6 +48,38 @@ class DbImpl {
4048
.first();
4149
}
4250

51+
//get latest data for all active monitors
52+
async getLatestMonitoringDataAllActive(monitor_tags) {
53+
// Find the latest timestamp for each provided monitor tag
54+
const latestTimestamps = await this.knex("monitoring_data")
55+
.select("monitor_tag")
56+
.select(this.knex.raw("MAX(timestamp) as max_timestamp"))
57+
.whereIn("monitor_tag", monitor_tags)
58+
.groupBy("monitor_tag");
59+
60+
// Early exit if no results
61+
if (!latestTimestamps || latestTimestamps.length === 0) {
62+
return [];
63+
}
64+
65+
// Then fetch the complete records using the timestamp pairs
66+
const conditions = latestTimestamps.map((item) => {
67+
return function () {
68+
this.where(function () {
69+
this.where("monitor_tag", item.monitor_tag).andWhere("timestamp", item.max_timestamp);
70+
});
71+
};
72+
});
73+
74+
// Build query with OR conditions
75+
let query = this.knex("monitoring_data").where(conditions[0]);
76+
for (let i = 1; i < conditions.length; i++) {
77+
query = query.orWhere(conditions[i]);
78+
}
79+
80+
return await query;
81+
}
82+
4383
async getLastHeartbeat(monitor_tag) {
4484
return await this.knex("monitoring_data")
4585
.where("monitor_tag", monitor_tag)
@@ -75,6 +115,15 @@ class DbImpl {
75115
.limit(1)
76116
.first();
77117
}
118+
//get the last status before the timestamp given monitor_tag and start timestamp
119+
async getLastStatusBeforeAll(monitor_tags, timestamp) {
120+
return await this.knex("monitoring_data")
121+
.whereIn("monitor_tag", monitor_tags)
122+
.where("timestamp", "<", timestamp)
123+
.orderBy("timestamp", "desc")
124+
.limit(1)
125+
.first();
126+
}
78127

79128
async getDataGroupByDayAlternative(monitor_tag, start, end) {
80129
// Fetch all raw data

0 commit comments

Comments
 (0)