persistent notifications + save scripts not tied to local storage.
This commit is contained in:
@@ -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" });
|
||||||
|
if (!res.ok) throw new Error("Delete failed");
|
||||||
|
cachedSavedScripts = cachedSavedScripts.filter((s) => s.id !== id);
|
||||||
renderSavedScripts();
|
renderSavedScripts();
|
||||||
showToast("Saved script deleted", "info", 3000);
|
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 {
|
||||||
|
const res = await fetch("/api/saved-scripts", {
|
||||||
|
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();
|
renderSavedScripts();
|
||||||
showToast("Script saved", "success", 3000);
|
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();
|
||||||
|
|
||||||
|
|||||||
@@ -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 }>(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user