clear notification by toggling them off and back on
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 }>(
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
Reference in New Issue
Block a user