clear notification by toggling them off and back on

This commit is contained in:
KingKDot
2026-05-15 18:57:23 -05:00
parent 5aa3a304a8
commit b68aa4a4c7
6 changed files with 72 additions and 4 deletions
@@ -2,6 +2,7 @@ import {
startNotificationClient, startNotificationClient,
subscribeNotifications, subscribeNotifications,
subscribeClientEvents, subscribeClientEvents,
subscribeNotificationsCleared,
subscribeReady, subscribeReady,
subscribeStatus, subscribeStatus,
markAllNotificationsRead, markAllNotificationsRead,
@@ -338,6 +339,18 @@ function clearTable() {
applyTableView(); applyTableView();
} }
function removeEntriesForClient(clientId) {
if (!clientId) return;
let removed = false;
for (const [uid, entry] of tableState.entries) {
if (entry.item?.clientId === clientId) {
tableState.entries.delete(uid);
removed = true;
}
}
if (removed) applyTableView();
}
// ── Client ID cell (collapsed by default, click to expand) ────────────────── // ── Client ID cell (collapsed by default, click to expand) ──────────────────
const CLIENT_ID_SHORT_LEN = 8; const CLIENT_ID_SHORT_LEN = 8;
@@ -718,6 +731,7 @@ function connect() {
subscribeNotifications((item) => addNotification(item, true)); subscribeNotifications((item) => addNotification(item, true));
subscribeClientEvents((item) => addClientEvent(item)); subscribeClientEvents((item) => addClientEvent(item));
subscribeNotificationsCleared((clientId) => removeEntriesForClient(clientId));
} }
const CLIENT_EVENT_BADGE = { const CLIENT_EVENT_BADGE = {
@@ -34,6 +34,7 @@ let notificationHandlers = new Set();
let statusHandlers = new Set(); let statusHandlers = new Set();
let unreadHandlers = new Set(); let unreadHandlers = new Set();
let clientEventHandlers = new Set(); let clientEventHandlers = new Set();
let notificationsClearedHandlers = new Set();
let lastHistory = []; let lastHistory = [];
function emitStatus(status) { function emitStatus(status) {
@@ -69,6 +70,17 @@ function emitClientEvent(item) {
} }
} }
function emitNotificationsCleared(clientId) {
if (Array.isArray(lastHistory) && lastHistory.length) {
lastHistory = lastHistory.filter((item) => item.clientId !== clientId);
}
for (const handler of notificationsClearedHandlers) {
try {
handler(clientId);
} catch {}
}
}
function shouldNotify() { function shouldNotify() {
return localStorage.getItem(STORAGE_KEY) === "1"; return localStorage.getItem(STORAGE_KEY) === "1";
} }
@@ -319,6 +331,10 @@ function handleMessage(payload) {
emitClientEvent(payload); emitClientEvent(payload);
showClientEventDesktopNotification(payload); showClientEventDesktopNotification(payload);
} }
if (payload.type === "notifications_cleared" && payload.clientId) {
console.log("[notifications] cleared for", payload.clientId);
emitNotificationsCleared(payload.clientId);
}
} }
let msgpackLoadPromise = null; let msgpackLoadPromise = null;
@@ -439,6 +455,11 @@ export function subscribeClientEvents(handler) {
return () => clientEventHandlers.delete(handler); return () => clientEventHandlers.delete(handler);
} }
export function subscribeNotificationsCleared(handler) {
notificationsClearedHandlers.add(handler);
return () => notificationsClearedHandlers.delete(handler);
}
export function subscribeUnread(handler) { export function subscribeUnread(handler) {
unreadHandlers.add(handler); unreadHandlers.add(handler);
try { try {
+6
View File
@@ -1945,6 +1945,12 @@ export function clearNotifications() {
console.log("[db] cleared all notifications and screenshots"); console.log("[db] cleared all notifications and screenshots");
} }
export function deleteNotificationsForClient(clientId: string): number {
db.run(`DELETE FROM notification_screenshots WHERE client_id = ?`, clientId);
const result = db.run(`DELETE FROM notifications WHERE client_id = ?`, clientId);
return (result as any)?.changes || 0;
}
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 }>(
+1
View File
@@ -512,6 +512,7 @@ async function startServer() {
CORS_HEADERS, CORS_HEADERS,
pendingScripts, pendingScripts,
pendingCommandReplies, pendingCommandReplies,
broadcastNotificationsCleared: notificationPluginHandlers.broadcastNotificationsCleared,
}, },
wsUpgrade: { wsUpgrade: {
isAuthorizedAgentRequest: isAuthorizedAgent, isAuthorizedAgentRequest: isAuthorizedAgent,
@@ -25,6 +25,7 @@ import {
updateGroup, updateGroup,
deleteGroup, deleteGroup,
setClientGroup, setClientGroup,
deleteNotificationsForClient,
} from "../../db"; } from "../../db";
import { metrics } from "../../metrics"; import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol"; import { encodeMessage } from "../../protocol";
@@ -58,6 +59,7 @@ type ClientRouteDeps = {
CORS_HEADERS: Record<string, string>; CORS_HEADERS: Record<string, string>;
pendingScripts: Map<string, PendingScript>; pendingScripts: Map<string, PendingScript>;
pendingCommandReplies: Map<string, PendingCommandReply>; pendingCommandReplies: Map<string, PendingCommandReply>;
broadcastNotificationsCleared: (clientId: string) => void;
}; };
export async function handleClientRoutes( export async function handleClientRoutes(
@@ -496,15 +498,18 @@ export async function handleClientRoutes(
const muted = !!body?.muted; const muted = !!body?.muted;
let updated = 0; let updated = 0;
let cleared = 0;
for (const cid of clientIds) { for (const cid of clientIds) {
if (!canUserAccessClient(user.userId, user.role, cid)) continue; if (!canUserAccessClient(user.userId, user.role, cid)) continue;
if (setClientNotificationsMuted(cid, muted)) updated++; if (setClientNotificationsMuted(cid, muted)) updated++;
cleared += deleteNotificationsForClient(cid);
deps.broadcastNotificationsCleared(cid);
} }
notifyDashboardViewers(); notifyDashboardViewers();
const ip = server.requestIP(req)?.address || "unknown"; const ip = server.requestIP(req)?.address || "unknown";
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, details: `bulk_set_notifications_muted:${muted}:${updated}/${clientIds.length}`, success: true }); logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, details: `bulk_set_notifications_muted:${muted}:${updated}/${clientIds.length} (cleared ${cleared})`, success: true });
return Response.json({ ok: true, updated, total: clientIds.length, muted }, { headers: deps.CORS_HEADERS }); return Response.json({ ok: true, updated, total: clientIds.length, muted, cleared }, { headers: deps.CORS_HEADERS });
} }
const muteMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/notifications-muted$/); const muteMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/notifications-muted$/);
@@ -534,6 +539,8 @@ export async function handleClientRoutes(
if (!updated) { if (!updated) {
return Response.json({ error: "Client not found" }, { status: 404 }); return Response.json({ error: "Client not found" }, { status: 404 });
} }
const cleared = deleteNotificationsForClient(targetId);
deps.broadcastNotificationsCleared(targetId);
notifyDashboardViewers(); notifyDashboardViewers();
const ip = server.requestIP(req)?.address || "unknown"; const ip = server.requestIP(req)?.address || "unknown";
@@ -543,11 +550,11 @@ export async function handleClientRoutes(
ip, ip,
action: AuditAction.COMMAND, action: AuditAction.COMMAND,
targetClientId: targetId, targetClientId: targetId,
details: muted ? "mute_notifications" : "unmute_notifications", details: `${muted ? "mute_notifications" : "unmute_notifications"} (cleared ${cleared})`,
success: true, success: true,
}); });
return Response.json({ ok: true, muted }, { headers: deps.CORS_HEADERS }); return Response.json({ ok: true, muted, cleared }, { headers: deps.CORS_HEADERS });
} }
const bookmarkMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/bookmark$/); const bookmarkMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/bookmark$/);
@@ -362,6 +362,25 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
} }
}, },
broadcastNotificationsCleared(clientId: string) {
for (const key of Array.from(deps.notificationRate.keys())) {
if (key === clientId || key.startsWith(`${clientId}:`)) {
deps.notificationRate.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 ?? "";
const sUserId = session.userId ?? session.viewer.data.userId;
if (sRole !== "admin") {
if (sUserId === undefined || !deps.canUserAccessClient(sUserId, sRole, clientId)) {
continue;
}
}
safeSendViewer(session.viewer, item);
}
},
broadcastClientLifecycleEvent( broadcastClientLifecycleEvent(
event: "client_online" | "client_offline" | "client_purgatory", event: "client_online" | "client_offline" | "client_purgatory",
info: { id: string; host?: string; user?: string; os?: string; ip?: string; country?: string }, info: { id: string; host?: string; user?: string; os?: string; ip?: string; country?: string },