allow for plugin badges

This commit is contained in:
KingKDot
2026-06-25 11:45:28 -05:00
parent a51c465227
commit 4b3fd52bc5
9 changed files with 320 additions and 5 deletions
+27
View File
@@ -2961,6 +2961,33 @@ tbody tr:hover {
.cv-pill-muted { background: rgba(245, 158, 11, 0.12); color: #fcd34d; border-color: rgba(245, 158, 11, 0.3); }
.cv-pill-macperm.is-ok { background: rgba(16, 185, 129, 0.12); color: #86efac; border-color: rgba(16, 185, 129, 0.3); }
.cv-pill-macperm.is-warn { background: rgba(245, 158, 11, 0.12); color: #fcd34d; border-color: rgba(245, 158, 11, 0.3); }
.cv-plugin-badge {
width: auto;
min-width: 18px;
gap: 4px;
padding: 0 5px;
color: #bfdbfe;
background: rgba(59, 130, 246, 0.12);
border-color: rgba(59, 130, 246, 0.3);
text-decoration: none;
cursor: pointer;
}
.cv-plugin-badge span {
max-width: 74px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cv-plugin-badge img {
width: 13px;
height: 13px;
object-fit: contain;
border-radius: 3px;
}
.cv-plugin-badge-good { color: #86efac; background: rgba(16, 185, 129, 0.12); border-color: rgba(16, 185, 129, 0.3); }
.cv-plugin-badge-warn { color: #fcd34d; background: rgba(245, 158, 11, 0.12); border-color: rgba(245, 158, 11, 0.3); }
.cv-plugin-badge-danger { color: #fca5a5; background: rgba(239, 68, 68, 0.12); border-color: rgba(239, 68, 68, 0.3); }
.cv-plugin-badge:hover { filter: brightness(1.18); }
.cv-perm-list {
display: flex;
+72
View File
@@ -139,6 +139,9 @@ const clearContext = () => {
let _pluginCache = null;
let _pluginCacheAge = 0;
const PLUGIN_CACHE_TTL = 30_000;
const pluginDashboardBadges = new Map();
let pluginDashboardDigest = "";
let pluginDashboardRefreshInFlight = false;
async function refreshPluginCache() {
try {
@@ -157,6 +160,71 @@ function getCachedPlugins() {
return null;
}
function dashboardBadgeHref(href, clientId, pluginId) {
const raw = String(href || `/plugins/${encodeURIComponent(pluginId)}?clientId=${encodeURIComponent(clientId)}`);
return raw
.replaceAll("{clientId}", encodeURIComponent(clientId))
.replaceAll("{pluginId}", encodeURIComponent(pluginId));
}
function getDashboardBadgesForClient(client) {
return pluginDashboardBadges.get(client?.id || "") || [];
}
async function refreshDashboardPluginContributions(items = []) {
if (pluginDashboardRefreshInFlight) return;
const clientIds = items.map((item) => item?.id).filter(Boolean);
if (!clientIds.length) {
if (pluginDashboardBadges.size) {
pluginDashboardBadges.clear();
pluginDashboardDigest = "";
renderCachedClients({ force: true, fromPluginDashboard: true });
}
return;
}
pluginDashboardRefreshInFlight = true;
try {
const res = await fetch("/api/plugins/dashboard-contributions", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ clientIds }),
});
if (!res.ok) return;
const data = await res.json();
const pluginNames = new Map((Array.isArray(data.plugins) ? data.plugins : []).map((p) => [p.id, p.name || p.id]));
const next = new Map();
for (const row of Array.isArray(data.contributions) ? data.contributions : []) {
const clientId = String(row?.clientId || "");
const pluginId = String(row?.pluginId || "");
if (!clientId || !pluginId || !clientIds.includes(clientId)) continue;
const badges = Array.isArray(row.badges) ? row.badges : [];
const current = next.get(clientId) || [];
for (const badge of badges) {
const id = String(badge?.id || pluginId || "plugin");
current.push({
...badge,
id: `${pluginId}:${id}`,
pluginId,
pluginName: pluginNames.get(pluginId) || pluginId,
href: dashboardBadgeHref(badge?.href, clientId, pluginId),
});
}
next.set(clientId, current);
}
const nextDigest = JSON.stringify(Array.from(next.entries()).sort(([a], [b]) => a.localeCompare(b)));
if (nextDigest === pluginDashboardDigest) return;
pluginDashboardDigest = nextDigest;
pluginDashboardBadges.clear();
for (const [clientId, badges] of next.entries()) pluginDashboardBadges.set(clientId, badges);
renderCachedClients({ force: true, fromPluginDashboard: true });
} catch {
// Dashboard badges are decorative; leave the last known state in place.
} finally {
pluginDashboardRefreshInFlight = false;
}
}
function detectClientPlatform(clientId) {
if (!clientId) {
return "unknown";
@@ -1031,11 +1099,15 @@ function initializeRenderer() {
userRole: currentUser?.role,
getServerVersion: () => currentServerVersion,
getDisplayFields: () => displayFields,
getDashboardBadges: getDashboardBadgesForClient,
});
rendererSetLayout = rSetLayout;
registerRenderer((data, options) => {
renderMerge(data, options);
updateDashboardStatsFromClients(data);
if (!options?.fromPluginDashboard) {
refreshDashboardPluginContributions(data?.items || []);
}
});
initDashboardStats();
setupDashboardThumbnailLoader();
+54 -1
View File
@@ -328,6 +328,7 @@ export function createRenderer({
userRole,
getServerVersion,
getDisplayFields,
getDashboardBadges,
}) {
const isViewer = userRole === "viewer";
const LARGE_CLIENT_THRESHOLD = 50_000;
@@ -510,6 +511,11 @@ export function createRenderer({
return;
}
if (e.target.closest(".cv-plugin-badge")) {
e.stopPropagation();
return;
}
const thumbImg = e.target.closest(".thumb-img");
if (thumbImg) {
if (thumbImg.src) openModal(thumbImg.src);
@@ -661,8 +667,50 @@ export function createRenderer({
});
}
function dashboardBadges(client) {
if (typeof getDashboardBadges !== "function") return [];
const badges = getDashboardBadges(client) || [];
return Array.isArray(badges) ? badges.slice(0, 8) : [];
}
function dashboardBadgesDigest(client) {
return JSON.stringify(dashboardBadges(client).map((badge) => ({
id: badge.id,
pluginId: badge.pluginId,
label: badge.label,
title: badge.title,
icon: badge.icon,
imageUrl: badge.imageUrl,
href: badge.href,
tone: badge.tone,
priority: badge.priority,
})));
}
function dashboardBadgesHtml(client) {
return dashboardBadges(client)
.sort((a, b) => (Number(b.priority) || 0) - (Number(a.priority) || 0))
.map((badge) => {
const label = String(badge.label || badge.title || badge.pluginId || "Plugin");
const title = String(badge.title || label);
const tone = String(badge.tone || "info").replace(/[^a-z0-9_-]/gi, "").toLowerCase() || "info";
const icon = String(badge.icon || "").replace(/[^a-z0-9_\- ]/gi, "").trim();
const imageUrl = String(badge.imageUrl || "").trim();
const href = String(badge.href || "").trim();
const visual = imageUrl
? `<img src="${escapeHtml(imageUrl)}" alt="">`
: `<i class="${escapeHtml(icon || "fa-solid fa-puzzle-piece")}"></i>`;
const inner = `${visual}<span>${escapeHtml(label)}</span>`;
const attrs = `class="cv-mini-pill cv-plugin-badge cv-plugin-badge-${escapeHtml(tone)}" title="${escapeHtml(title)}"`;
return href
? `<a ${attrs} href="${escapeHtml(href)}" target="_blank" rel="noopener">${inner}</a>`
: `<span ${attrs}>${inner}</span>`;
})
.join("");
}
function cardDigest(c) {
return `${currentDisplayDigest}|${currentLayout}|${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${JSON.stringify(c.permissions || {})}|${c.cpu}|${c.gpu}|${c.ram}|${c.batteryPercent}|${c.batteryCharging}|${!!c.webcamAvailable}|${JSON.stringify(c.webcamDevices || [])}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${c.groupId}|${c.groupName}|${c.groupColor}|${!!c.notificationsMuted}`;
return `${currentDisplayDigest}|${currentLayout}|${dashboardBadgesDigest(c)}|${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${JSON.stringify(c.permissions || {})}|${c.cpu}|${c.gpu}|${c.ram}|${c.batteryPercent}|${c.batteryCharging}|${!!c.webcamAvailable}|${JSON.stringify(c.webcamDevices || [])}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${c.groupId}|${c.groupName}|${c.groupColor}|${!!c.notificationsMuted}`;
}
function cardThumbDigest(c) {
@@ -901,6 +949,7 @@ export function createRenderer({
const cpuHtml = client.cpu ? cpuBadgeHtml(client.cpu) : "—";
const batteryIndicator = showBattery ? batteryHtml(client) : "";
const verLatest = isClientVersionCurrent(client.version, typeof getServerVersion === "function" ? getServerVersion() : "");
const pluginBadges = dashboardBadgesHtml(client);
const metaParts = [
showField("system") ? `<span class="cv-os cv-tone-${os.tone}"><i class="fa ${os.icon}"></i> ${escapeHtml(shortOsLabel(client.os))}</span>` : "",
@@ -935,6 +984,7 @@ export function createRenderer({
${webcamBadgeHtml(client)}
${macPermissionBadgeHtml(client)}
${client.notificationsMuted ? `<span class="cv-mini-pill cv-pill-muted" title="Notifications muted"><i class="fa-solid fa-bell-slash"></i></span>` : ""}
${pluginBadges}
</div>
${showField("user") ? `<div class="cv-user-line"><i class="fa-solid fa-user"></i> ${escapeHtml(userLine)}</div>` : ""}
${meta ? `<div class="cv-meta-line">${meta}</div>` : ""}
@@ -985,6 +1035,7 @@ export function createRenderer({
const nickname = String(client.nickname || "").trim();
const displayName = nickname || client.host || deviceId;
const userLine = client.user || client.host || deviceId;
const pluginBadges = dashboardBadgesHtml(client);
const userMeta = [
showField("user") ? `<i class="fa-solid fa-user"></i> ${escapeHtml(userLine)}` : "",
showField("ip") && client.ip ? `<span class="cv-mid">·</span><i class="fa-solid fa-network-wired"></i> ${escapeHtml(client.ip)}` : "",
@@ -1016,6 +1067,7 @@ export function createRenderer({
${webcamBadgeHtml(client)}
${macPermissionBadgeHtml(client)}
${client.notificationsMuted ? `<span class="cv-mini-pill cv-pill-muted" title="Notifications muted"><i class="fa-solid fa-bell-slash"></i></span>` : ""}
${pluginBadges}
</span>
${userMeta ? `<span class="cv-user-line cv-mono">${userMeta}</span>` : ""}
</div>
@@ -1079,6 +1131,7 @@ export function createRenderer({
webcamBadgeHtml(client),
macPermissionBadgeHtml(client),
client.notificationsMuted ? `<span class="cv-mini-pill cv-pill-muted" title="Notifications muted"><i class="fa-solid fa-bell-slash"></i></span>` : "",
dashboardBadgesHtml(client),
].join("");
article.innerHTML = `