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
@@ -41,6 +41,7 @@ type HttpDispatchDeps<
handleAuthRoutes: RouteHandlerWithServer<TServer>;
handleNotificationsConfigRoutes: RouteHandlerWithServerDeps<TServer, TNotificationsConfig>;
handleAutoScriptsRoutes: RouteHandler;
handleSavedScriptsRoutes: RouteHandler;
handleAutoDeployRoutes: RouteHandlerWithDeps<TAutoDeploy>;
handleEnrollmentRoutes: RouteHandler;
handleChatRoutes: RouteHandler;
@@ -131,6 +132,9 @@ export function createHttpFetchHandler<
const autoScriptsResponse = await deps.handleAutoScriptsRoutes(req, url);
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);
if (autoDeployResponse) return autoDeployResponse;
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from "uuid";
import {
getNotificationScreenshot,
saveNotificationScreenshot,
updateNotificationScreenshotId,
getAllPushSubscriptions,
deletePushSubscription,
type NotificationScreenshotRecord,
@@ -133,7 +134,6 @@ export function takePendingNotificationScreenshot(
}
export function storeNotificationScreenshot(
notificationHistory: NotificationRecord[],
pending: PendingNotificationScreenshot,
bytes: Uint8Array,
format: string,
@@ -154,10 +154,7 @@ export function storeNotificationScreenshot(
bytes,
});
const record = notificationHistory.find((item) => item.id === pending.notificationId);
if (record) {
record.screenshotId = screenshotId;
}
updateNotificationScreenshotId(pending.notificationId, screenshotId);
}
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 { v4 as uuidv4 } from "uuid";
import * as clientManager from "../clientManager";
import { saveNotification, getNotificationHistory } from "../db";
import { logger } from "../logger";
import { metrics } from "../metrics";
import { encodeMessage } from "../protocol";
@@ -47,7 +48,6 @@ type NotificationConfigShape = {
};
type CreateDeps = {
notificationHistory: NotificationRecord[];
notificationRate: Map<string, NotificationRateState>;
pendingNotificationScreenshots: Map<string, PendingNotificationScreenshot>;
pluginLoadedByClient: Map<string, Set<string>>;
@@ -241,10 +241,11 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
ws.data.sessionId = sessionId;
logger.info(`[notify] viewer connected session=${sessionId} userId=${userId ?? "?"} role=${userRole}`);
const allHistory = getNotificationHistory(500);
const visibleHistory =
userRole === "admin"
? deps.notificationHistory
: deps.notificationHistory.filter(
? allHistory
: allHistory.filter(
(item) =>
userId !== undefined &&
deps.canUserAccessClient(userId, userRole, item.clientId),
@@ -281,12 +282,7 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
ts,
};
deps.notificationHistory.unshift(record);
const notificationConfig = deps.getNotificationConfig();
const limit = Math.max(50, notificationConfig.historyLimit || 200);
if (deps.notificationHistory.length > limit) {
deps.notificationHistory.splice(limit);
}
saveNotification(record);
requestNotificationScreenshot(info, record);