persistent notifications + save scripts not tied to local storage.

This commit is contained in:
KingKDot
2026-05-06 10:09:38 -05:00
parent 7a47090447
commit c28de6db32
7 changed files with 335 additions and 62 deletions
+53 -44
View File
@@ -22,7 +22,6 @@ const autoTaskList = document.getElementById("auto-task-list");
let allClients = []; let allClients = [];
let filteredClients = []; let filteredClients = [];
const selectedClients = new Set(); const selectedClients = new Set();
const SAVED_SCRIPTS_KEY = "overlord_saved_scripts";
let autoTasks = []; let autoTasks = [];
let autoTaskEditingId = null; let autoTaskEditingId = null;
let editorInstance = null; let editorInstance = null;
@@ -232,27 +231,23 @@ function escapeHtml(text) {
return div.innerHTML; return div.innerHTML;
} }
function getSavedScripts() { let cachedSavedScripts = [];
async function loadSavedScripts() {
try { try {
const items = JSON.parse(localStorage.getItem(SAVED_SCRIPTS_KEY) || "[]"); const res = await fetch("/api/saved-scripts");
return Array.isArray(items) ? items : []; if (!res.ok) throw new Error("Failed to load saved scripts");
const data = await res.json();
cachedSavedScripts = Array.isArray(data.items) ? data.items : [];
} catch (err) { } catch (err) {
console.error("Failed to load saved scripts:", err); console.error("Failed to load saved scripts:", err);
return []; cachedSavedScripts = [];
}
}
function setSavedScripts(scripts) {
try {
const trimmed = scripts.slice(0, 50);
localStorage.setItem(SAVED_SCRIPTS_KEY, JSON.stringify(trimmed));
} catch (err) {
console.error("Failed to save scripts:", err);
} }
return cachedSavedScripts;
} }
function renderSavedScripts() { function renderSavedScripts() {
const scripts = getSavedScripts().sort((a, b) => b.updatedAt - a.updatedAt); const scripts = cachedSavedScripts.slice().sort((a, b) => b.updatedAt - a.updatedAt);
if (scripts.length === 0) { if (scripts.length === 0) {
savedScriptsList.innerHTML = '<div class="text-slate-500 text-sm">No saved scripts yet.</div>'; savedScriptsList.innerHTML = '<div class="text-slate-500 text-sm">No saved scripts yet.</div>';
@@ -264,7 +259,7 @@ function renderSavedScripts() {
<div class="flex items-center justify-between gap-2 px-3 py-2 rounded-lg border border-slate-700 bg-slate-800/50"> <div class="flex items-center justify-between gap-2 px-3 py-2 rounded-lg border border-slate-700 bg-slate-800/50">
<div class="min-w-0"> <div class="min-w-0">
<div class="font-semibold text-slate-100 truncate">${escapeHtml(s.name)}</div> <div class="font-semibold text-slate-100 truncate">${escapeHtml(s.name)}</div>
<div class="text-xs text-slate-400">${escapeHtml(s.type)} • ${new Date(s.updatedAt).toLocaleString()}</div> <div class="text-xs text-slate-400">${escapeHtml(s.scriptType)} • ${new Date(s.updatedAt).toLocaleString()}</div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button class="load-saved-script px-2 py-1 text-xs rounded bg-emerald-600 hover:bg-emerald-700 text-white" data-id="${escapeHtml(s.id)}"> <button class="load-saved-script px-2 py-1 text-xs rounded bg-emerald-600 hover:bg-emerald-700 text-white" data-id="${escapeHtml(s.id)}">
@@ -281,29 +276,34 @@ function renderSavedScripts() {
savedScriptsList.querySelectorAll(".load-saved-script").forEach((btn) => { savedScriptsList.querySelectorAll(".load-saved-script").forEach((btn) => {
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
const id = btn.dataset.id; const id = btn.dataset.id;
const scripts = getSavedScripts(); const script = cachedSavedScripts.find((s) => s.id === id);
const script = scripts.find((s) => s.id === id);
if (!script) return; if (!script) return;
setEditorValue(script.content); setEditorValue(script.content);
scriptType.value = script.type; scriptType.value = script.scriptType;
scriptSaveName.value = script.name; scriptSaveName.value = script.name;
setEditorMode(script.type); setEditorMode(script.scriptType);
showToast("Script loaded", "success", 3000); showToast("Script loaded", "success", 3000);
}); });
}); });
savedScriptsList.querySelectorAll(".delete-saved-script").forEach((btn) => { savedScriptsList.querySelectorAll(".delete-saved-script").forEach((btn) => {
btn.addEventListener("click", () => { btn.addEventListener("click", async () => {
const id = btn.dataset.id; const id = btn.dataset.id;
const scripts = getSavedScripts().filter((s) => s.id !== id); try {
setSavedScripts(scripts); const res = await fetch(`/api/saved-scripts/${encodeURIComponent(id)}`, { method: "DELETE" });
renderSavedScripts(); if (!res.ok) throw new Error("Delete failed");
showToast("Saved script deleted", "info", 3000); cachedSavedScripts = cachedSavedScripts.filter((s) => s.id !== id);
renderSavedScripts();
showToast("Saved script deleted", "info", 3000);
} catch (err) {
console.error("Failed to delete script:", err);
showToast("Failed to delete script", "error", 3000);
}
}); });
}); });
} }
function saveCurrentScript() { async function saveCurrentScript() {
const name = scriptSaveName.value.trim(); const name = scriptSaveName.value.trim();
const content = getEditorValue().trim(); const content = getEditorValue().trim();
const type = scriptType.value; const type = scriptType.value;
@@ -318,29 +318,38 @@ function saveCurrentScript() {
return; return;
} }
const scripts = getSavedScripts(); const existing = cachedSavedScripts.find((s) => s.name.toLowerCase() === name.toLowerCase());
const existing = scripts.find((s) => s.name.toLowerCase() === name.toLowerCase());
if (existing) { if (existing) {
const ok = confirm("A script with this name already exists. Overwrite it?"); const ok = confirm("A script with this name already exists. Overwrite it?");
if (!ok) return; if (!ok) return;
existing.content = content;
existing.type = type;
existing.updatedAt = Date.now();
} else {
scripts.push({
id: `script-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name,
content,
type,
createdAt: Date.now(),
updatedAt: Date.now(),
});
} }
setSavedScripts(scripts); try {
renderSavedScripts(); const res = await fetch("/api/saved-scripts", {
showToast("Script saved", "success", 3000); method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: existing ? existing.id : undefined,
name,
content,
scriptType: type,
}),
});
if (!res.ok) throw new Error("Save failed");
const data = await res.json();
if (existing) {
const idx = cachedSavedScripts.findIndex((s) => s.id === existing.id);
if (idx !== -1) cachedSavedScripts[idx] = data.item;
} else {
cachedSavedScripts.push(data.item);
}
renderSavedScripts();
showToast("Script saved", "success", 3000);
} catch (err) {
console.error("Failed to save script:", err);
showToast("Failed to save script", "error", 3000);
}
} }
function triggerLabel(trigger) { function triggerLabel(trigger) {
@@ -739,7 +748,7 @@ function renderTemplatePalette() {
checkAuth(); checkAuth();
loadClients(); loadClients();
renderSavedScripts(); loadSavedScripts().then(() => renderSavedScripts());
loadAutoTasks(); loadAutoTasks();
renderTemplatePalette(); renderTemplatePalette();
+183
View File
@@ -216,6 +216,20 @@ db.run(`
`); `);
db.run(`CREATE INDEX IF NOT EXISTS idx_build_profiles_user_updated ON build_profiles(user_id, updated_at DESC);`); db.run(`CREATE INDEX IF NOT EXISTS idx_build_profiles_user_updated ON build_profiles(user_id, updated_at DESC);`);
db.run(`
CREATE TABLE IF NOT EXISTS saved_scripts (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
content TEXT NOT NULL,
script_type TEXT NOT NULL DEFAULT 'powershell',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`);
db.run(`CREATE INDEX IF NOT EXISTS idx_saved_scripts_user ON saved_scripts(user_id, updated_at DESC);`);
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS notification_screenshots ( CREATE TABLE IF NOT EXISTS notification_screenshots (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@@ -235,6 +249,26 @@ db.run(
`CREATE INDEX IF NOT EXISTS idx_notification_screenshots_ts ON notification_screenshots(ts DESC);`, `CREATE INDEX IF NOT EXISTS idx_notification_screenshots_ts ON notification_screenshots(ts DESC);`,
); );
db.run(`
CREATE TABLE IF NOT EXISTS notifications (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
host TEXT,
user TEXT,
os TEXT,
title TEXT NOT NULL,
process TEXT,
process_path TEXT,
pid INTEGER,
keyword TEXT,
category TEXT NOT NULL DEFAULT 'active_window',
ts INTEGER NOT NULL,
screenshot_id TEXT
);
`);
db.run(`CREATE INDEX IF NOT EXISTS idx_notifications_ts ON notifications(ts DESC);`);
db.run(`CREATE INDEX IF NOT EXISTS idx_notifications_client_id ON notifications(client_id);`);
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS push_subscriptions ( CREATE TABLE IF NOT EXISTS push_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1534,6 +1568,72 @@ export function deleteBuildProfileForUser(userId: number, name: string): boolean
return ((result as any)?.changes || 0) > 0; return ((result as any)?.changes || 0) > 0;
} }
export interface SavedScriptRecord {
id: string;
userId: number;
name: string;
content: string;
scriptType: string;
createdAt: number;
updatedAt: number;
}
export function listSavedScriptsForUser(userId: number): SavedScriptRecord[] {
return db
.query<any>(
`SELECT id, user_id, name, content, script_type, created_at, updated_at
FROM saved_scripts
WHERE user_id = ?
ORDER BY updated_at DESC`,
)
.all(userId)
.map((row: any) => ({
id: row.id,
userId: row.user_id,
name: row.name,
content: row.content,
scriptType: row.script_type,
createdAt: row.created_at,
updatedAt: row.updated_at,
}));
}
export function saveSavedScript(
id: string,
userId: number,
name: string,
content: string,
scriptType: string,
): SavedScriptRecord {
const now = Date.now();
db.run(
`INSERT INTO saved_scripts (id, user_id, name, content, script_type, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
content = excluded.content,
script_type = excluded.script_type,
updated_at = excluded.updated_at`,
id,
userId,
name,
content,
scriptType,
now,
now,
);
return { id, userId, name, content, scriptType, createdAt: now, updatedAt: now };
}
export function deleteSavedScript(userId: number, scriptId: string): boolean {
const result = db.run(
`DELETE FROM saved_scripts WHERE id = ? AND user_id = ?`,
scriptId,
userId,
);
return ((result as any)?.changes || 0) > 0;
}
export interface BuildRecord { export interface BuildRecord {
id: string; id: string;
status: string; status: string;
@@ -1735,6 +1835,89 @@ export function clearNotificationScreenshots() {
console.log("[db] cleared notification screenshots"); console.log("[db] cleared notification screenshots");
} }
export type NotificationRow = {
id: string;
clientId: string;
host?: string;
user?: string;
os?: string;
title: string;
process?: string;
processPath?: string;
pid?: number;
keyword?: string;
category: string;
ts: number;
screenshotId?: string;
};
export function saveNotification(record: NotificationRow) {
db.run(
`INSERT OR REPLACE INTO notifications
(id, client_id, host, user, os, title, process, process_path, pid, keyword, category, ts, screenshot_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
record.id,
record.clientId,
record.host ?? null,
record.user ?? null,
record.os ?? null,
record.title,
record.process ?? null,
record.processPath ?? null,
record.pid ?? null,
record.keyword ?? null,
record.category,
record.ts,
record.screenshotId ?? null,
);
}
export function updateNotificationScreenshotId(notificationId: string, screenshotId: string) {
db.run(
`UPDATE notifications SET screenshot_id = ? WHERE id = ?`,
screenshotId,
notificationId,
);
}
export function getNotificationHistory(limit: number = 500): NotificationRow[] {
const rows = db
.query<any>(`SELECT * FROM notifications ORDER BY ts DESC LIMIT ?`)
.all(limit);
return rows.map((row: any) => ({
id: row.id,
clientId: row.client_id,
host: row.host ?? undefined,
user: row.user ?? undefined,
os: row.os ?? undefined,
title: row.title,
process: row.process ?? undefined,
processPath: row.process_path ?? undefined,
pid: row.pid ?? undefined,
keyword: row.keyword ?? undefined,
category: row.category,
ts: row.ts,
screenshotId: row.screenshot_id ?? undefined,
}));
}
const NOTIFICATION_RETENTION_MS = 3 * 24 * 60 * 60 * 1000;
export function pruneOldNotifications() {
const cutoff = Date.now() - NOTIFICATION_RETENTION_MS;
db.run(`DELETE FROM notification_screenshots WHERE notification_id IN (SELECT id FROM notifications WHERE ts < ?)`, cutoff);
const result = db.run(`DELETE FROM notifications WHERE ts < ?`, cutoff);
if (result.changes > 0) {
console.log(`[db] pruned ${result.changes} notifications older than 3 days`);
}
}
export function clearNotifications() {
db.run(`DELETE FROM notifications`);
db.run(`DELETE FROM notification_screenshots`);
console.log("[db] cleared all notifications and screenshots");
}
export function getClientEnrollmentStatus(id: string): string | null { export function getClientEnrollmentStatus(id: string): string | null {
const row = db const row = db
.query<{ enrollment_status: string }>( .query<{ enrollment_status: string }>(
+7 -4
View File
@@ -5,7 +5,7 @@ import { fileURLToPath } from "url";
import path from "path"; import path from "path";
import fs from "fs/promises"; import fs from "fs/promises";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { upsertClientRow, setOnlineState, listClients, markAllClientsOffline, getBuild, getBuildByTag, getAllBuilds, deleteExpiredBuilds, deleteBuild, getNotificationScreenshot, clearNotificationScreenshots, deleteClientRow, getClientIp, banIp, isIpBanned, clientExists, deleteExpiredSharedFiles, getChatHistory, insertChatMessage, getOnlineClientCountForUser, deleteExpiredChatMessages, recordBuildClaim, getClientMetricsSummary } from "./db"; import { upsertClientRow, setOnlineState, listClients, markAllClientsOffline, getBuild, getBuildByTag, getAllBuilds, deleteExpiredBuilds, deleteBuild, getNotificationScreenshot, clearNotificationScreenshots, deleteClientRow, getClientIp, banIp, isIpBanned, clientExists, deleteExpiredSharedFiles, getChatHistory, insertChatMessage, getOnlineClientCountForUser, deleteExpiredChatMessages, recordBuildClaim, getClientMetricsSummary, pruneOldNotifications } from "./db";
import { handleFrame, handleHello, handlePing, handlePong } from "./wsHandlers"; import { handleFrame, handleHello, handlePing, handlePong } from "./wsHandlers";
import { getMessageByteLength, getMaxPayloadLimit, isAllowedClientMessageType } from "./wsValidation"; import { getMessageByteLength, getMaxPayloadLimit, isAllowedClientMessageType } from "./wsValidation";
import { ClientInfo, ClientRole } from "./types"; import { ClientInfo, ClientRole } from "./types";
@@ -19,6 +19,7 @@ import { metrics } from "./metrics";
import { ensureDataDir } from "./paths"; import { ensureDataDir } from "./paths";
import { handleAuthRoutes } from "./server/routes/auth-routes"; import { handleAuthRoutes } from "./server/routes/auth-routes";
import { handleAutoScriptsRoutes } from "./server/routes/auto-scripts-routes"; import { handleAutoScriptsRoutes } from "./server/routes/auto-scripts-routes";
import { handleSavedScriptsRoutes } from "./server/routes/saved-scripts-routes";
import { handleEnrollmentRoutes, setPostApproveHook } from "./server/routes/enrollment-routes"; import { handleEnrollmentRoutes, setPostApproveHook } from "./server/routes/enrollment-routes";
import { handleChatRoutes } from "./server/routes/chat-routes"; import { handleChatRoutes } from "./server/routes/chat-routes";
import { handleBuildRoutes } from "./server/routes/build-routes"; import { handleBuildRoutes } from "./server/routes/build-routes";
@@ -285,7 +286,6 @@ type NotificationRateState = {
lastWarned: number; lastWarned: number;
}; };
const notificationHistory: NotificationRecord[] = [];
const notificationRate = new Map<string, NotificationRateState>(); const notificationRate = new Map<string, NotificationRateState>();
const getNotificationConfig = () => getConfig().notifications; const getNotificationConfig = () => getConfig().notifications;
@@ -298,7 +298,10 @@ const storeNotificationScreenshotForPending = (
format: string, format: string,
width?: number, width?: number,
height?: number, height?: number,
) => storeNotificationScreenshot(notificationHistory, pending, bytes, format, width, height); ) => storeNotificationScreenshot(pending, bytes, format, width, height);
pruneOldNotifications();
setInterval(pruneOldNotifications, 60 * 60 * 1000);
const deliverNotificationWithScreenshotForRecord = (record: NotificationRecord) => { const deliverNotificationWithScreenshotForRecord = (record: NotificationRecord) => {
const getUserDeliveryTargets = (clientId: string): UserDeliveryTarget[] => { const getUserDeliveryTargets = (clientId: string): UserDeliveryTarget[] => {
return getUsersForNotificationDeliveryByClient(clientId) return getUsersForNotificationDeliveryByClient(clientId)
@@ -321,7 +324,6 @@ const deliverNotificationWithScreenshotForRecord = (record: NotificationRecord)
}; };
const notificationPluginHandlers = createNotificationPluginHandlers({ const notificationPluginHandlers = createNotificationPluginHandlers({
notificationHistory,
notificationRate, notificationRate,
pendingNotificationScreenshots, pendingNotificationScreenshots,
pluginLoadedByClient, pluginLoadedByClient,
@@ -711,6 +713,7 @@ async function startServer() {
handleAuthRoutes, handleAuthRoutes,
handleNotificationsConfigRoutes, handleNotificationsConfigRoutes,
handleAutoScriptsRoutes, handleAutoScriptsRoutes,
handleSavedScriptsRoutes,
handleAutoDeployRoutes, handleAutoDeployRoutes,
handleEnrollmentRoutes, handleEnrollmentRoutes,
handleChatRoutes, handleChatRoutes,
@@ -41,6 +41,7 @@ type HttpDispatchDeps<
handleAuthRoutes: RouteHandlerWithServer<TServer>; handleAuthRoutes: RouteHandlerWithServer<TServer>;
handleNotificationsConfigRoutes: RouteHandlerWithServerDeps<TServer, TNotificationsConfig>; handleNotificationsConfigRoutes: RouteHandlerWithServerDeps<TServer, TNotificationsConfig>;
handleAutoScriptsRoutes: RouteHandler; handleAutoScriptsRoutes: RouteHandler;
handleSavedScriptsRoutes: RouteHandler;
handleAutoDeployRoutes: RouteHandlerWithDeps<TAutoDeploy>; handleAutoDeployRoutes: RouteHandlerWithDeps<TAutoDeploy>;
handleEnrollmentRoutes: RouteHandler; handleEnrollmentRoutes: RouteHandler;
handleChatRoutes: RouteHandler; handleChatRoutes: RouteHandler;
@@ -131,6 +132,9 @@ export function createHttpFetchHandler<
const autoScriptsResponse = await deps.handleAutoScriptsRoutes(req, url); const autoScriptsResponse = await deps.handleAutoScriptsRoutes(req, url);
if (autoScriptsResponse) return autoScriptsResponse; if (autoScriptsResponse) return autoScriptsResponse;
const savedScriptsResponse = await deps.handleSavedScriptsRoutes(req, url);
if (savedScriptsResponse) return savedScriptsResponse;
const autoDeployResponse = await deps.handleAutoDeployRoutes(req, url, deps.routeDeps.autoDeploy); const autoDeployResponse = await deps.handleAutoDeployRoutes(req, url, deps.routeDeps.autoDeploy);
if (autoDeployResponse) return autoDeployResponse; if (autoDeployResponse) return autoDeployResponse;
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from "uuid";
import { import {
getNotificationScreenshot, getNotificationScreenshot,
saveNotificationScreenshot, saveNotificationScreenshot,
updateNotificationScreenshotId,
getAllPushSubscriptions, getAllPushSubscriptions,
deletePushSubscription, deletePushSubscription,
type NotificationScreenshotRecord, type NotificationScreenshotRecord,
@@ -133,7 +134,6 @@ export function takePendingNotificationScreenshot(
} }
export function storeNotificationScreenshot( export function storeNotificationScreenshot(
notificationHistory: NotificationRecord[],
pending: PendingNotificationScreenshot, pending: PendingNotificationScreenshot,
bytes: Uint8Array, bytes: Uint8Array,
format: string, format: string,
@@ -154,10 +154,7 @@ export function storeNotificationScreenshot(
bytes, bytes,
}); });
const record = notificationHistory.find((item) => item.id === pending.notificationId); updateNotificationScreenshotId(pending.notificationId, screenshotId);
if (record) {
record.screenshotId = screenshotId;
}
} }
async function deliverToUserWebhook( async function deliverToUserWebhook(
@@ -0,0 +1,81 @@
import { authenticateRequest } from "../../auth";
import {
listSavedScriptsForUser,
saveSavedScript,
deleteSavedScript,
} from "../../db";
import { ALLOWED_SCRIPT_TYPES } from "../validation-constants";
import { v4 as uuidv4 } from "uuid";
export async function handleSavedScriptsRoutes(
req: Request,
url: URL,
): Promise<Response | null> {
if (req.method === "GET" && url.pathname === "/api/saved-scripts") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
return Response.json({ items: listSavedScriptsForUser(user.userId) });
}
if (req.method === "POST" && url.pathname === "/api/saved-scripts") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
let body: any = {};
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const name = String(body?.name || "").trim();
const content = String(body?.content || "");
const scriptTypeRaw = String(body?.scriptType || "powershell").trim();
const id = body?.id ? String(body.id) : uuidv4();
if (!name) {
return Response.json({ error: "Name is required" }, { status: 400 });
}
if (!content.trim()) {
return Response.json({ error: "Script content is required" }, { status: 400 });
}
const scriptType = ALLOWED_SCRIPT_TYPES.has(scriptTypeRaw)
? scriptTypeRaw
: "powershell";
const item = saveSavedScript(id, user.userId, name, content, scriptType);
return Response.json({ ok: true, item });
}
const savedScriptMatch = url.pathname.match(/^\/api\/saved-scripts\/(.+)$/);
if (savedScriptMatch && req.method === "DELETE") {
const user = await authenticateRequest(req);
if (!user) {
return new Response(JSON.stringify({ error: "Not authenticated" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const scriptId = decodeURIComponent(savedScriptMatch[1]);
const deleted = deleteSavedScript(user.userId, scriptId);
if (!deleted) {
return Response.json({ error: "Not found" }, { status: 404 });
}
return Response.json({ ok: true });
}
return null;
}
@@ -2,6 +2,7 @@ import type { ServerWebSocket } from "bun";
import { encode as msgpackEncode } from "@msgpack/msgpack"; import { encode as msgpackEncode } from "@msgpack/msgpack";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import * as clientManager from "../clientManager"; import * as clientManager from "../clientManager";
import { saveNotification, getNotificationHistory } from "../db";
import { logger } from "../logger"; import { logger } from "../logger";
import { metrics } from "../metrics"; import { metrics } from "../metrics";
import { encodeMessage } from "../protocol"; import { encodeMessage } from "../protocol";
@@ -47,7 +48,6 @@ type NotificationConfigShape = {
}; };
type CreateDeps = { type CreateDeps = {
notificationHistory: NotificationRecord[];
notificationRate: Map<string, NotificationRateState>; notificationRate: Map<string, NotificationRateState>;
pendingNotificationScreenshots: Map<string, PendingNotificationScreenshot>; pendingNotificationScreenshots: Map<string, PendingNotificationScreenshot>;
pluginLoadedByClient: Map<string, Set<string>>; pluginLoadedByClient: Map<string, Set<string>>;
@@ -241,10 +241,11 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
ws.data.sessionId = sessionId; ws.data.sessionId = sessionId;
logger.info(`[notify] viewer connected session=${sessionId} userId=${userId ?? "?"} role=${userRole}`); logger.info(`[notify] viewer connected session=${sessionId} userId=${userId ?? "?"} role=${userRole}`);
const allHistory = getNotificationHistory(500);
const visibleHistory = const visibleHistory =
userRole === "admin" userRole === "admin"
? deps.notificationHistory ? allHistory
: deps.notificationHistory.filter( : allHistory.filter(
(item) => (item) =>
userId !== undefined && userId !== undefined &&
deps.canUserAccessClient(userId, userRole, item.clientId), deps.canUserAccessClient(userId, userRole, item.clientId),
@@ -281,12 +282,7 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
ts, ts,
}; };
deps.notificationHistory.unshift(record); saveNotification(record);
const notificationConfig = deps.getNotificationConfig();
const limit = Math.max(50, notificationConfig.historyLimit || 200);
if (deps.notificationHistory.length > limit) {
deps.notificationHistory.splice(limit);
}
requestNotificationScreenshot(info, record); requestNotificationScreenshot(info, record);