diff --git a/Overlord-Server/public/assets/cert-banner.js b/Overlord-Server/public/assets/cert-banner.js
new file mode 100644
index 0000000..c50c8d2
--- /dev/null
+++ b/Overlord-Server/public/assets/cert-banner.js
@@ -0,0 +1,124 @@
+const DISMISS_KEY = "overlord_cert_banner_dismissed";
+
+
+export async function showCertBannerIfNeeded(anchor) {
+ if (sessionStorage.getItem(DISMISS_KEY) === "1") return;
+ if (localStorage.getItem(DISMISS_KEY) === "1") return;
+
+ let source;
+ try {
+ const res = await fetch("/api/cert/info");
+ if (!res.ok) return;
+ const data = await res.json();
+ source = data.source;
+ } catch {
+ return;
+ }
+
+ if (source !== "self-signed") return;
+
+ if ("serviceWorker" in navigator && window.isSecureContext) {
+ try {
+ const reg = await navigator.serviceWorker.getRegistration("/");
+ if (reg && reg.active) {
+ return;
+ }
+ } catch { }
+ }
+
+ const os = detectOS();
+
+ const banner = document.createElement("div");
+ banner.id = "cert-trust-banner";
+ banner.className =
+ "mx-4 mt-3 mb-1 flex items-start gap-3 rounded-lg border border-amber-500/30 " +
+ "bg-amber-950/60 px-4 py-3 text-sm text-amber-200 shadow-lg";
+
+ banner.innerHTML = `
+ ⚠️
+
+
Untrusted self-signed certificate
+
+ This server is using a self-signed TLS certificate. Your browser doesn't
+ trust it, which may block some features (desktop notifications, service workers).
+ Download the certificate and add it to your OS / browser trust store to fix this.
+
+
+
${os.instructions}
+
+ ×
+ `;
+
+ anchor.insertAdjacentElement("afterend", banner);
+
+ document.getElementById("cert-banner-dismiss")?.addEventListener("click", () => {
+ banner.remove();
+ localStorage.setItem(DISMISS_KEY, "1");
+ sessionStorage.setItem(DISMISS_KEY, "1");
+ });
+
+ document.getElementById("cert-banner-how")?.addEventListener("click", () => {
+ const el = document.getElementById("cert-banner-instructions");
+ if (el) el.classList.toggle("hidden");
+ });
+}
+
+function detectOS() {
+ const ua = navigator.userAgent.toLowerCase();
+ if (ua.includes("win")) {
+ return {
+ label: "Windows",
+ instructions:
+`1. Double-click the downloaded overlord-ca.crt file.
+2. Click Install Certificate...
+3. Choose Current User or Local Machine .
+4. Select "Place all certificates in the following store" .
+5. Click Browse → choose Trusted Root Certification Authorities .
+6. Click Next → Finish .
+7. Restart your browser for the change to take effect.`,
+ };
+ }
+ if (ua.includes("mac")) {
+ return {
+ label: "macOS",
+ instructions:
+`1. Double-click the downloaded overlord-ca.crt file — it opens in Keychain Access.
+2. If prompted, add it to the login or System keychain.
+3. Find the certificate in the list, double-click it.
+4. Expand Trust and set "When using this certificate" to Always Trust .
+5. Close the dialog (you'll be prompted for your password).
+6. Restart your browser .`,
+ };
+ }
+ return {
+ label: "Linux",
+ instructions:
+`Debian / Ubuntu:
+ sudo cp overlord-ca.crt /usr/local/share/ca-certificates/overlord-ca.crt
+ sudo update-ca-certificates
+
+Fedora / RHEL:
+ sudo cp overlord-ca.crt /etc/pki/ca-trust/source/anchors/overlord-ca.crt
+ sudo update-ca-trust
+
+Then restart your browser .
+For Chrome/Chromium you may also need: Settings → Privacy → Manage Certificates → Authorities → Import.`,
+ };
+}
diff --git a/Overlord-Server/public/assets/nav.js b/Overlord-Server/public/assets/nav.js
index 1fda5de..9db86f5 100644
--- a/Overlord-Server/public/assets/nav.js
+++ b/Overlord-Server/public/assets/nav.js
@@ -8,10 +8,12 @@ import {
import { mountNav } from "./nav/template.js";
import { createAdaptiveNavController } from "./nav/layout.js";
import { applyUserRoleUI } from "./nav/role-ui.js";
+import { showCertBannerIfNeeded } from "./cert-banner.js";
const host = document.getElementById("top-nav");
if (host) {
const refs = mountNav(host);
+ showCertBannerIfNeeded(host);
const { applyAdaptiveNavLayout } = createAdaptiveNavController(host, refs);
const path = window.location.pathname;
diff --git a/Overlord-Server/public/assets/notification-sw.js b/Overlord-Server/public/assets/notification-sw.js
new file mode 100644
index 0000000..32648a1
--- /dev/null
+++ b/Overlord-Server/public/assets/notification-sw.js
@@ -0,0 +1,48 @@
+self.addEventListener("install", () => self.skipWaiting());
+self.addEventListener("activate", (event) => {
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("notificationclick", (event) => {
+ event.notification.close();
+
+ const targetUrl = (event.notification.data && event.notification.data.url)
+ ? event.notification.data.url
+ : "/notifications";
+
+ event.waitUntil(
+ self.clients
+ .matchAll({ type: "window", includeUncontrolled: true })
+ .then((clientList) => {
+ for (const client of clientList) {
+ try {
+ const clientUrl = new URL(client.url);
+ if (clientUrl.origin === self.location.origin) {
+ if (clientUrl.pathname !== "/notifications") {
+ client.navigate(targetUrl);
+ }
+ return client.focus();
+ }
+ } catch {}
+ }
+ return self.clients.openWindow(targetUrl);
+ }),
+ );
+});
+
+self.addEventListener("message", (event) => {
+ const data = event.data;
+ if (!data || data.type !== "show_notification") return;
+
+ const title = String(data.title || "Overlord Notification");
+ const options = {
+ body: String(data.body || ""),
+ icon: data.icon || "/assets/overlord.png",
+ badge: "/assets/overlord.png",
+ tag: data.tag || `overlord-${Date.now()}`,
+ data: { url: "/notifications" },
+ requireInteraction: false,
+ };
+
+ event.waitUntil(self.registration.showNotification(title, options));
+});
diff --git a/Overlord-Server/public/assets/notifications.js b/Overlord-Server/public/assets/notifications.js
index ee48ea9..1361535 100644
--- a/Overlord-Server/public/assets/notifications.js
+++ b/Overlord-Server/public/assets/notifications.js
@@ -1,9 +1,12 @@
import {
startNotificationClient,
subscribeNotifications,
+ subscribeClientEvents,
subscribeReady,
subscribeStatus,
markAllNotificationsRead,
+ getClientEventNotificationEnabled,
+ setClientEventNotificationEnabled,
} from "./notify-client.js";
// ── DOM refs ──────────────────────────────────────────────────────────────────
@@ -42,6 +45,11 @@ const resetTelegramTemplateBtn = document.getElementById("reset-telegram-templat
const previewTelegramTemplateBtn = document.getElementById("preview-telegram-template");
const telegramTemplatePreview = document.getElementById("telegram-template-preview");
+const eventNotifOnlineInput = document.getElementById("event-notif-online");
+const eventNotifOfflineInput = document.getElementById("event-notif-offline");
+const eventNotifPurgatoryInput = document.getElementById("event-notif-purgatory");
+const saveEventNotifsBtn = document.getElementById("save-event-notifs");
+
// Preview modal
const panel = document.getElementById("notification-panel");
const previewModal = document.getElementById("notification-preview-modal");
@@ -541,6 +549,35 @@ function connect() {
});
subscribeNotifications((item) => renderRow(item, true));
+ subscribeClientEvents((item) => renderClientEventRow(item));
+}
+
+const CLIENT_EVENT_BADGE = {
+ client_online: ` online `,
+ client_offline: ` offline `,
+ client_purgatory: ` purgatory `,
+};
+
+function renderClientEventRow(item) {
+ if (!listEl) return;
+ const badge = CLIENT_EVENT_BADGE[item.event] ||
+ `${escapeHtml(item.event)} `;
+ const row = document.createElement("tr");
+ row.className = "border-t border-slate-800/60";
+ row.innerHTML = `
+ ${formatTime(item.ts)}
+ ${escapeHtml(item.clientId || "-")}
+ ${escapeHtml(item.user || "-")}
+ client event
+ ${escapeHtml(item.os || "-")}
+ -
+ ${badge}
+
+ `;
+ listEl.prepend(row);
+ const rows = listEl.querySelectorAll("tr");
+ if (rows.length > MAX_ROWS) rows[rows.length - 1].remove();
+ if (emptyState) emptyState.classList.add("hidden");
}
// ── Preview modal ─────────────────────────────────────────────────────────────
@@ -564,11 +601,30 @@ document.addEventListener("visibilitychange", clearIfActive);
window.addEventListener("focus", clearIfActive);
clearIfActive();
+// ── Event notification toggles ───────────────────────────────────────────────
+function loadEventNotifPrefs() {
+ if (eventNotifOnlineInput) eventNotifOnlineInput.checked = getClientEventNotificationEnabled("client_online");
+ if (eventNotifOfflineInput) eventNotifOfflineInput.checked = getClientEventNotificationEnabled("client_offline");
+ if (eventNotifPurgatoryInput) eventNotifPurgatoryInput.checked = getClientEventNotificationEnabled("client_purgatory");
+}
+
+function wireEventNotifSave() {
+ if (!saveEventNotifsBtn) return;
+ saveEventNotifsBtn.addEventListener("click", () => {
+ if (eventNotifOnlineInput) setClientEventNotificationEnabled("client_online", eventNotifOnlineInput.checked);
+ if (eventNotifOfflineInput) setClientEventNotificationEnabled("client_offline", eventNotifOfflineInput.checked);
+ if (eventNotifPurgatoryInput) setClientEventNotificationEnabled("client_purgatory", eventNotifPurgatoryInput.checked);
+ window.showToast?.("Event notification preferences saved", "success", 3000);
+ });
+}
+
// ── Bootstrap ─────────────────────────────────────────────────────────────────
initWebhookTemplateEditor();
wireKeywordSave();
wireWebhookSave();
wireTelegramSave();
loadMySettings();
+loadEventNotifPrefs();
+wireEventNotifSave();
initRoleUi();
connect();
diff --git a/Overlord-Server/public/assets/notify-client.js b/Overlord-Server/public/assets/notify-client.js
index c551e82..59a0a99 100644
--- a/Overlord-Server/public/assets/notify-client.js
+++ b/Overlord-Server/public/assets/notify-client.js
@@ -2,6 +2,13 @@ import { decodeMsgpack } from "./msgpack-helpers.js";
const STORAGE_KEY = "overlord_notifications_enabled";
const UNREAD_KEY = "overlord_notifications_unread";
+const DESKTOP_NOTIF_KEY = "overlord_desktop_notifications_enabled";
+const CLIENT_EVENT_KEYS = {
+ client_online: "overlord_desktop_notify_client_online",
+ client_offline: "overlord_desktop_notify_client_offline",
+ client_purgatory: "overlord_desktop_notify_client_purgatory",
+};
+
let enabled = localStorage.getItem(STORAGE_KEY);
if (enabled === null) {
enabled = "1";
@@ -10,12 +17,21 @@ if (enabled === null) {
if (localStorage.getItem(UNREAD_KEY) === null) {
localStorage.setItem(UNREAD_KEY, "0");
}
+
+if (
+ localStorage.getItem(DESKTOP_NOTIF_KEY) === null &&
+ typeof Notification !== "undefined" &&
+ Notification.permission === "granted"
+) {
+ localStorage.setItem(DESKTOP_NOTIF_KEY, "1");
+}
let ws = null;
let started = false;
let readyHandlers = new Set();
let notificationHandlers = new Set();
let statusHandlers = new Set();
let unreadHandlers = new Set();
+let clientEventHandlers = new Set();
let lastHistory = [];
function emitStatus(status) {
@@ -43,6 +59,14 @@ function emitNotification(item) {
}
}
+function emitClientEvent(item) {
+ for (const handler of clientEventHandlers) {
+ try {
+ handler(item);
+ } catch {}
+ }
+}
+
function shouldNotify() {
return localStorage.getItem(STORAGE_KEY) === "1";
}
@@ -65,6 +89,125 @@ function incrementUnread() {
setUnreadCount(getUnreadCount() + 1);
}
+export function getDesktopNotificationsEnabled() {
+ const stored = localStorage.getItem(DESKTOP_NOTIF_KEY);
+ if (stored === null) {
+ return typeof Notification !== "undefined" && Notification.permission === "granted";
+ }
+ return stored === "1";
+}
+
+export function setDesktopNotificationsEnabled(value) {
+ localStorage.setItem(DESKTOP_NOTIF_KEY, value ? "1" : "0");
+}
+
+export function getClientEventNotificationEnabled(eventType) {
+ const key = CLIENT_EVENT_KEYS[eventType];
+ if (!key) return false;
+ const stored = localStorage.getItem(key);
+ return stored === null ? true : stored === "1";
+}
+
+export function setClientEventNotificationEnabled(eventType, value) {
+ const key = CLIENT_EVENT_KEYS[eventType];
+ if (!key) return;
+ localStorage.setItem(key, value ? "1" : "0");
+}
+
+export async function requestDesktopNotificationPermission() {
+ if (!("Notification" in window)) return "denied";
+ if (Notification.permission !== "default") return Notification.permission;
+ try {
+ return await Notification.requestPermission();
+ } catch {
+ return "denied";
+ }
+}
+
+function fireNotification(title, body, tag, clickUrl) {
+ if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
+ navigator.serviceWorker.ready
+ .then((reg) =>
+ reg.showNotification(title, {
+ body,
+ icon: "/assets/overlord.png",
+ tag,
+ data: { url: clickUrl || "/notifications" },
+ requireInteraction: false,
+ }),
+ )
+ .catch(() => fireNotificationFallback(title, body, tag, clickUrl));
+ return;
+ }
+ fireNotificationFallback(title, body, tag, clickUrl);
+}
+
+function fireNotificationFallback(title, body, tag, clickUrl) {
+ try {
+ const n = new Notification(title, {
+ body,
+ icon: "/assets/overlord.png",
+ tag,
+ silent: false,
+ });
+ n.onclick = () => {
+ window.focus();
+ n.close();
+ if (clickUrl && window.location.pathname !== clickUrl) {
+ window.location.href = clickUrl;
+ }
+ };
+ } catch (err) {
+ console.warn("[notifications] desktop notification failed", err);
+ }
+}
+
+function showDesktopNotification(item) {
+ if (!getDesktopNotificationsEnabled()) return;
+ if (!("Notification" in window)) return;
+ if (Notification.permission !== "granted") return;
+
+ const title = item.keyword
+ ? `Overlord \u2014 ${item.keyword}`
+ : "Overlord \u2014 Notification";
+ const lines = [item.title];
+ if (item.user) lines.push(`User: ${item.user}`);
+ if (item.host) lines.push(`Host: ${item.host}`);
+ if (item.process) lines.push(`Process: ${item.process}`);
+ const body = lines.filter(Boolean).join("\n");
+
+ fireNotification(title, body, `overlord-${item.id || Date.now()}`, "/notifications");
+}
+
+const CLIENT_EVENT_LABELS = {
+ client_online: "\u{1F7E2} Client Online",
+ client_offline: "\u{1F534} Client Offline",
+ client_purgatory: "\u{1F7E1} Client Awaiting Approval",
+};
+
+function showClientEventDesktopNotification(item) {
+ if (!getDesktopNotificationsEnabled()) return;
+ if (!getClientEventNotificationEnabled(item.event)) return;
+ if (!("Notification" in window)) return;
+ if (Notification.permission !== "granted") return;
+
+ const title = CLIENT_EVENT_LABELS[item.event] || "Overlord \u2014 Client Event";
+ const lines = [];
+ if (item.host) lines.push(`Host: ${item.host}`);
+ if (item.user) lines.push(`User: ${item.user}`);
+ if (item.os) lines.push(`OS: ${item.os}`);
+ if (item.clientId) lines.push(`ID: ${item.clientId}`);
+ const body = lines.join("\n") || item.clientId || "";
+ const dest = item.event === "client_purgatory" ? "/purgatory" : "/";
+
+ fireNotification(
+ title,
+ body,
+ `overlord-client-${item.event}-${item.clientId || Date.now()}`,
+ dest,
+ );
+}
+
function handleMessage(payload) {
if (!payload || typeof payload.type !== "string") return;
if (payload.type === "ready") {
@@ -77,6 +220,12 @@ function handleMessage(payload) {
if (shouldNotify()) {
incrementUnread();
}
+ showDesktopNotification(payload.item);
+ }
+ if (payload.type === "client_event" && payload.event) {
+ console.log("[notifications] client event", payload.event, payload.clientId);
+ emitClientEvent(payload);
+ showClientEventDesktopNotification(payload);
}
}
@@ -159,6 +308,26 @@ export async function startNotificationClient() {
} catch (err) {
console.warn("[notifications] failed to load msgpackr", err);
}
+
+ if ("serviceWorker" in navigator && window.isSecureContext) {
+ fetch("/assets/notification-sw.js")
+ .then((res) => {
+ if (!res.ok) return;
+ navigator.serviceWorker
+ .register("/assets/notification-sw.js", { scope: "/" })
+ .catch(() => { });
+ })
+ .catch(() => {/* SW script unreachable (e.g. self-signed cert) — skip quietly */});
+ }
+
+ if (getDesktopNotificationsEnabled()) {
+ requestDesktopNotificationPermission().then((perm) => {
+ if (perm !== "granted") {
+ setDesktopNotificationsEnabled(false);
+ }
+ });
+ }
+
connect();
}
@@ -167,6 +336,11 @@ export function subscribeNotifications(handler) {
return () => notificationHandlers.delete(handler);
}
+export function subscribeClientEvents(handler) {
+ clientEventHandlers.add(handler);
+ return () => clientEventHandlers.delete(handler);
+}
+
export function subscribeUnread(handler) {
unreadHandlers.add(handler);
try {
diff --git a/Overlord-Server/public/assets/settings.js b/Overlord-Server/public/assets/settings.js
index fe12630..e8025d6 100644
--- a/Overlord-Server/public/assets/settings.js
+++ b/Overlord-Server/public/assets/settings.js
@@ -1,6 +1,9 @@
import {
getNotificationsEnabled,
setNotificationsEnabled,
+ getDesktopNotificationsEnabled,
+ setDesktopNotificationsEnabled,
+ requestDesktopNotificationPermission,
} from "./notify-client.js";
const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds";
@@ -17,6 +20,8 @@ const passwordPolicyHint = document.getElementById("password-policy-hint");
const prefsForm = document.getElementById("prefs-form");
const prefNotificationsInput = document.getElementById("pref-notifications");
+const prefDesktopNotificationsInput = document.getElementById("pref-desktop-notifications");
+const prefDesktopNotificationsHint = document.getElementById("pref-desktop-notifications-hint");
const prefRefreshSecondsInput = document.getElementById("pref-refresh-seconds");
const myTelegramChatIdInput = document.getElementById("my-telegram-chat-id");
@@ -259,6 +264,9 @@ async function loadTlsSettings() {
function loadPrefs() {
prefNotificationsInput.checked = getNotificationsEnabled();
+ if (prefDesktopNotificationsInput) {
+ prefDesktopNotificationsInput.checked = getDesktopNotificationsEnabled();
+ }
const refreshSeconds = Number(localStorage.getItem(PREF_REFRESH_KEY) || 8);
prefRefreshSecondsInput.value = String(Math.min(120, Math.max(3, refreshSeconds)));
}
@@ -325,7 +333,28 @@ function savePrefs(event) {
localStorage.setItem(PREF_REFRESH_KEY, String(refreshSeconds));
prefRefreshSecondsInput.value = String(refreshSeconds);
- showMessage("Preferences saved.");
+ const wantsDesktop = prefDesktopNotificationsInput
+ ? prefDesktopNotificationsInput.checked
+ : false;
+
+ if (wantsDesktop) {
+ requestDesktopNotificationPermission().then((perm) => {
+ if (perm === "granted") {
+ setDesktopNotificationsEnabled(true);
+ if (prefDesktopNotificationsHint) prefDesktopNotificationsHint.classList.add("hidden");
+ showMessage("Preferences saved. Desktop notifications enabled.");
+ } else {
+ setDesktopNotificationsEnabled(false);
+ if (prefDesktopNotificationsInput) prefDesktopNotificationsInput.checked = false;
+ if (prefDesktopNotificationsHint) prefDesktopNotificationsHint.classList.remove("hidden");
+ showMessage("Desktop notifications require browser permission — not granted.", "error");
+ }
+ });
+ } else {
+ setDesktopNotificationsEnabled(false);
+ if (prefDesktopNotificationsHint) prefDesktopNotificationsHint.classList.add("hidden");
+ showMessage("Preferences saved.");
+ }
}
async function saveSecurityPolicy(event) {
diff --git a/Overlord-Server/public/notifications.html b/Overlord-Server/public/notifications.html
index 348992c..13dbf5f 100644
--- a/Overlord-Server/public/notifications.html
+++ b/Overlord-Server/public/notifications.html
@@ -251,6 +251,48 @@
+
+
+
+
+
Desktop Event Notifications
+
+ Choose which client events trigger a desktop (OS) notification. Desktop notifications must be enabled in Settings first.
+
+
+
+
+ Save
+
+
+
+
+
+
+ 🟢 Client Online
+ Fire when a client connects
+
+
+
+
+
+ 🔴 Client Offline
+ Fire when a client disconnects
+
+
+
+
+
+ 🟡 Client in Purgatory
+ Fire when a client awaits approval
+
+
+
+
+
Enable notification badge updates
+
+
+ Enable desktop (OS) notifications
+
+
+
+ Browser permission was denied. Reset notification permissions for this site in your browser settings, then try again.
+
Client refresh interval (seconds)
({
diff --git a/Overlord-Server/src/server/http-security.ts b/Overlord-Server/src/server/http-security.ts
index 0e2f549..9dc8f9d 100644
--- a/Overlord-Server/src/server/http-security.ts
+++ b/Overlord-Server/src/server/http-security.ts
@@ -5,7 +5,7 @@ export const CORS_HEADERS = {
};
export const SECURITY_HEADERS = {
- "Content-Security-Policy": "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.googleapis.com; img-src 'self' data: blob: https://cdn.jsdelivr.net https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org https://tile.openstreetmap.org https://a.basemaps.cartocdn.com https://b.basemaps.cartocdn.com https://c.basemaps.cartocdn.com https://d.basemaps.cartocdn.com; font-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.gstatic.com; connect-src 'self' wss: ws: https://cdn.jsdelivr.net",
+ "Content-Security-Policy": "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.googleapis.com; img-src 'self' data: blob: https://cdn.jsdelivr.net https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org https://tile.openstreetmap.org https://a.basemaps.cartocdn.com https://b.basemaps.cartocdn.com https://c.basemaps.cartocdn.com https://d.basemaps.cartocdn.com; font-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.gstatic.com; connect-src 'self' wss: ws: https://cdn.jsdelivr.net",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
diff --git a/Overlord-Server/src/server/routes/assets-routes.ts b/Overlord-Server/src/server/routes/assets-routes.ts
index 71959b8..619f36b 100644
--- a/Overlord-Server/src/server/routes/assets-routes.ts
+++ b/Overlord-Server/src/server/routes/assets-routes.ts
@@ -45,7 +45,11 @@ export async function handleAssetsRoutes(
const file = Bun.file(resolvedPath);
if (await file.exists()) {
- return new Response(file, { headers: deps.secureHeaders(deps.mimeType(url.pathname)) });
+ const headers: Record = { ...deps.secureHeaders(deps.mimeType(url.pathname)) };
+ if (relativePath === "notification-sw.js") {
+ headers["Service-Worker-Allowed"] = "/";
+ }
+ return new Response(file, { headers });
}
return new Response("Not found", { status: 404 });
}
diff --git a/Overlord-Server/src/server/routes/misc-routes.ts b/Overlord-Server/src/server/routes/misc-routes.ts
index 1a062a0..99dbf76 100644
--- a/Overlord-Server/src/server/routes/misc-routes.ts
+++ b/Overlord-Server/src/server/routes/misc-routes.ts
@@ -20,6 +20,8 @@ type MiscRouteDeps = {
getRdSessionCount: () => number;
getFileBrowserSessionCount: () => number;
getProcessSessionCount: () => number;
+ tlsCertPath?: string;
+ tlsSource?: "certbot" | "configured" | "self-signed";
};
export async function handleMiscRoutes(
@@ -388,5 +390,28 @@ export async function handleMiscRoutes(
});
}
+ if (req.method === "GET" && url.pathname === "/api/cert/info") {
+ return Response.json(
+ { source: deps.tlsSource || "unknown" },
+ { headers: { "Content-Type": "application/json", ...deps.CORS_HEADERS } },
+ );
+ }
+
+ if (req.method === "GET" && url.pathname === "/api/cert/download" && deps.tlsCertPath) {
+ try {
+ const file = Bun.file(deps.tlsCertPath);
+ if (await file.exists()) {
+ return new Response(file, {
+ headers: {
+ "Content-Type": "application/x-pem-file",
+ "Content-Disposition": 'attachment; filename="overlord-ca.crt"',
+ ...deps.CORS_HEADERS,
+ },
+ });
+ }
+ } catch { }
+ return new Response("Certificate not available", { status: 404 });
+ }
+
return null;
}
diff --git a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts
index 1087ab6..cde379d 100644
--- a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts
+++ b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts
@@ -86,6 +86,10 @@ type WsLifecycleDeps = {
notifyRemoteDesktopStatus: (clientId: string, status: string, reason?: string) => void;
handleBuildTagConnection: (clientId: string, buildTag: string) => void;
notifyDashboard: () => void;
+ broadcastClientEvent: (
+ event: "client_online" | "client_offline" | "client_purgatory",
+ info: { id: string; host?: string; user?: string; os?: string; ip?: string; country?: string },
+ ) => void;
};
const ENROLLMENT_TIMEOUT_MS = 30_000;
@@ -296,6 +300,14 @@ export async function handleWebSocketMessage(
logger.info(`[purgatory] client ${resolvedId} is pending approval`);
ws.send(encodeMessage({ type: "enrollment_status", status: "pending" }));
deps.notifyDashboard();
+ deps.broadcastClientEvent("client_purgatory", {
+ id: resolvedId,
+ host: (payload as any).host || undefined,
+ user: (payload as any).user || undefined,
+ os: (payload as any).os || undefined,
+ ip: ip || undefined,
+ country,
+ });
try { ws.close(4001, "pending"); } catch {}
return;
}
@@ -349,8 +361,14 @@ export async function handleWebSocketMessage(
clientManager.addClient(infoObj.id, infoObj);
deps.dispatchAutoScriptsForConnection(infoObj, ws);
- deps.notifyDashboard();
-
+ deps.notifyDashboard(); deps.broadcastClientEvent("client_online", {
+ id: infoObj.id,
+ host: infoObj.host,
+ user: infoObj.user,
+ os: infoObj.os,
+ ip: infoObj.ip,
+ country: infoObj.country,
+ });
if (infoObj.role === "client") {
deps.notifyRemoteDesktopStatus(resolvedId, "online");
metrics.recordConnection();
@@ -642,6 +660,17 @@ export function handleWebSocketClose(
return;
}
+ if (role === "client" && currentClient) {
+ deps.broadcastClientEvent("client_offline", {
+ id: clientId,
+ host: currentClient.host,
+ user: currentClient.user,
+ os: currentClient.os,
+ ip: currentClient.ip,
+ country: currentClient.country,
+ });
+ }
+
clientManager.deleteClient(clientId);
stopAllProxiesForClient(clientId);
clearClientSyncState(clientId);
diff --git a/Overlord-Server/src/server/tls-bootstrap.ts b/Overlord-Server/src/server/tls-bootstrap.ts
index e757669..4e78a9c 100644
--- a/Overlord-Server/src/server/tls-bootstrap.ts
+++ b/Overlord-Server/src/server/tls-bootstrap.ts
@@ -1,5 +1,6 @@
import { certificatesExist, generateSelfSignedCert, getLocalIPs, isOpenSSLAvailable } from "../certGenerator";
import { logger } from "../logger";
+import { X509Certificate } from "crypto";
import path from "path";
type TlsBootstrapParams = {
@@ -119,6 +120,15 @@ export async function prepareTlsOptions(params: TlsBootstrapParams): Promise